$:.unshift File.join(File.dirname(__FILE__),'..') require 'sinatra/base' require 'erb' require 'json' require 'fileutils' require 'rhoconnect' require 'rhoconnect/x_domain_session_wrapper' require 'rhoconnect/body_content_type_parser' require 'rhoconnect/cors' module Rhoconnect class ApiException < Exception attr_accessor :error_code def initialize(error_code,message) super(message) @error_code = error_code end end class Server < Sinatra::Base set :static, true set :stats, false # default secret @secret = '' # Setup route and mimetype for bulk data downloads # TODO: Figure out why "mime :data, 'application/octet-stream'" doesn't work Rack::Mime::MIME_TYPES['.data'] = 'application/octet-stream' include Rhoconnect # Set rhoconnect middleware set :use_middleware, Proc.new { return false if @middleware_configured # Middleware might be configured only once! use XDomainSessionWrapper use Rack::Cors do |cfg| cfg.allow do |allow| allow.origins /.*/ allow.resource '/application', :headers => :any, :methods => [:get, :post, :put, :delete], :credentials => true allow.resource '/application/*', :headers => :any, :methods => [:get, :post, :put, :delete], :credentials => true allow.resource '/api/application', :headers => :any, :methods => [:get, :post, :put, :delete], :credentials => true allow.resource '/api/application/*', :headers => :any, :methods => [:get, :post, :put, :delete], :credentials => true end end use Rhoconnect::BodyContentTypeParser use Rhoconnect::Stats::Middleware Rhoconnect::Server.set :secret, @secret unless settings.respond_to?(:secret) use Rack::Session::Cookie, :key => 'rhoconnect_session', :expire_after => Rhoconnect.cookie_expire, :secret => Rhoconnect::Server.secret @middleware_configured ||= true } helpers do def request_action request.env['PATH_INFO'].split('/').last end def check_api_token request_action == 'login' or request_action == 'get_api_token' or (params[:api_token] and ApiToken.is_exist?(params[:api_token])) end def do_login begin login ? status(200) : status(401) rescue LoginException => le throw :halt, [401, le.message] rescue Exception => e throw :halt, [500, e.message] end end def login_required current_user.nil? end def login if params[:login].nil? or params[:login].empty? return false end if params[:login] == 'rhoadmin' user = User.authenticate(params[:login], params[:password]) elsif current_app and current_app.can_authenticate? and params[:login] user = current_app.authenticate(params[:login], params[:password], session) end if user session[:login] = user.login session[:app_name] = APP_NAME else false end end def logout session[:login] = nil end def current_user if @user.nil? and User.is_exist?(session[:login]) @user = User.load(session[:login]) end if @user and (@user.admin == 1 || session[:app_name] == APP_NAME) @user else nil end end def api_user if request_action == 'get_api_token' or request_action == 'login' current_user else u = ApiToken.load(params[:api_token]) raise "Wrong API token - #{params[:api_token].inspect}" unless u u.user end end def current_app App.load(APP_NAME) end def current_source return @source if @source user = current_user if params[:source_name] and user @source = Source.load(params[:source_name], {:user_id => user.login,:app_id => APP_NAME}) # if source does not exist create one for dynamic adapter unless @source sconfig = Rhoconnect.source_config(params[:source_name]) @source = Source.create(sconfig.merge!({:name => params[:source_name]}),{:user_id => user.login, :app_id => APP_NAME}) current_app.sources << @source.name end @source else log "ERROR: Can't load source, no source_name provided.\n" nil end end def current_client if @client.nil? and params[:client_id] @client = Client.load(params[:client_id].to_s, params[:source_name] ? {:source_name => current_source.name} : {:source_name => '*'}) if @client and current_user and @client.user_id != current_user.login @client.switch_user(current_user.login) end @client end end def current_client_sync ClientSync.new(current_source,current_client,params[:p_size]) end def catch_all begin yield rescue ApiException => ae throw :halt, [ae.error_code, ae.message] rescue Exception => e log e.message + e.backtrace.join("\n") throw :halt, [500, e.message] end end def mark_deprecated_call_and_reroute(name, namespace, *params) namespace_val = namespace.nil? ? "" : "#{namespace}" http_method = request.get? ? "GET" : "POST" warning_message = "Use of the #{http_method} #{request.path} is deprecated. Use #{http_method} /api/#{namespace_val}/#{name} instead." response.headers['Warning'] = warning_message Rhoconnect.log warning_message if namespace != nil call env.merge('PATH_INFO' => "/api/#{namespace}/#{name}") else yield *params end end def execute_api_call(client_call = false) if client_call or check_api_token catch_all do res = yield params, (client_call ? current_user : api_user), self if params.has_key? :warning Rhoconnect.log params[:warning] response.headers['Warning'] = params[:warning] end res end else throw :halt, [422, "No API token provided"] end end end private def self._use_async_framework return false if settings.respond_to?(:use_async_model) and settings.use_async_model == false return false if @dispatch_framework_initialized @dispatch_framework_initialized ||=true if RUBY_VERSION =~ /1.9/ and not defined?(JRUBY_VERSION) require 'rhoconnect/async' register Rhoconnect::Synchrony helpers Rhoconnect::AsyncHelpers else settings.use_async_model = false end end def self.new # by default, enable this feature if not settings.respond_to?(:use_async_model) or settings.use_async_model != false set :use_async_model, true end # this must be called first - because # it redefines some of the middleware Rhoconnect::Server._use_async_framework if settings.respond_to?(:stats) and settings.send(:stats) == true Rhoconnect.stats = true else Rhoconnect::Server.disable :stats Rhoconnect.stats = false end settings.use_middleware super end def initialize # Whine about default session secret check_default_secret!(Rhoconnect::Server.secret) super end Rhoconnect.log "Rhoconnect Server v#{Rhoconnect::VERSION} started..." before do cache_control :no_cache headers({'pragma'=>'no-cache'}) if params[:version] and params[:version].to_i < 3 throw :halt, [404, "Server supports version 3 or higher of the protocol."] end end get '/' do redirect "/console/" end %w[get post].each do |verb| send(verb, "/*application*") do unless request_action == 'clientlogin' throw :halt, [401, "Not authenticated"] if login_required end pass end end # old routes post '/login' do mark_deprecated_call_and_reroute(:login, :admin, self, params) end get '/application' do mark_deprecated_call_and_reroute(:query, :application, self, params) end post '/application' do mark_deprecated_call_and_reroute(:queue_updates, :application, self, params) end # confusion routes - only because Rhodes didn't switch # to new style API yet get '/api/application' do mark_deprecated_call_and_reroute(:query, :application, self, params) end post '/api/application' do mark_deprecated_call_and_reroute(:queue_updates, :application, self, params) end def self.api(name, namespace = nil, verb = :post, &block) old_api_prefix = (namespace == :application) ? :application : :api client_call = (namespace == :application) ? true : false send verb, "/#{old_api_prefix}/#{name}" do mark_deprecated_call_and_reroute(name, namespace, params, &block) end send verb, "/api/#{namespace}/#{name}" do execute_api_call(client_call, &block) end end end end include Rhoconnect Dir[File.join(File.dirname(__FILE__),'api','**','*.rb')].each { |api| load api }