class Ribbon::Intercom::Service
Attributes
Public Class Methods
# File lib/ribbon/intercom/service.rb, line 36 def _load_store raise "Store name missing" unless (store_name = @_store_name.to_s) store = Utils.classify(store_name) + "Store" Intercom::Service::Channel::Stores.const_get(store).new(@_store_params) end
The call method is needed here because Rails checks to see if a mounted Rack app can respond_to?(:call). Without it, the Service
will not mount
# File lib/ribbon/intercom/service.rb, line 28 def call(env) instance.call(env) end
# File lib/ribbon/intercom/service.rb, line 13 def instance @instance ||= new(store: _load_store) end
# File lib/ribbon/intercom/service.rb, line 32 def method_missing(meth, *args, &block) instance.public_send(meth, *args, &block) end
# File lib/ribbon/intercom/service.rb, line 17 def mock Client::MockSDK.new(self) end
# File lib/ribbon/intercom/service.rb, line 50 def initialize(opts={}) @_opts = opts.dup end
# File lib/ribbon/intercom/service.rb, line 21 def store(store_name, params={}) @_store_name = store_name @_store_params = params end
Public Instance Methods
# File lib/ribbon/intercom/service.rb, line 84 def call(env) dup.call!(env) end
# File lib/ribbon/intercom/service.rb, line 88 def call!(env) @env = env response = catch(:response) { begin _process_request rescue Exception => error _respond_with_error!(error) end } response.finish end
# File lib/ribbon/intercom/service.rb, line 65 def lookup_channel(token) store.lookup_channel(token) end
# File lib/ribbon/intercom/service.rb, line 58 def open_channel(params={}) # Accept either an array of permissions or a string store.open_channel(params).tap { |channel| channel.may(Utils.method_identifier(self, :rotate_secret)) } end
# File lib/ribbon/intercom/service.rb, line 102 def rotate_secret channel.rotate_secret! end
# File lib/ribbon/intercom/service.rb, line 54 def store @store ||= @_opts[:store] or raise Errors::MissingStoreError end
Check that the channel has sufficient permissions to call the method.
The ‘send` method is forbidden because it breaks the encapsulation guaranteed by intercom (i.e., private methods can’t be called).
In addition to the permissions granted to the channel, all channels have implicit permission to call public methods on basic types.
# File lib/ribbon/intercom/service.rb, line 77 def sufficient_permissions?(base, intercom_method) intercom_method != :send && ( Utils.basic_type?(base) || channel.may?(Utils.method_identifier(base, intercom_method)) ) end
Private Instance Methods
# File lib/ribbon/intercom/service.rb, line 130 def _authenticate_request! unless _request_authenticated? _error!(Errors::AuthenticationError, "invalid channel credentials") end end
Call all the methods in the method queue.
# File lib/ribbon/intercom/service.rb, line 158 def _call_methods method_queue = _load_method_queue intercom_method = nil base = subject method_queue.each { |meth, *args| intercom_method = meth _sufficient_permissions!(base, meth) base = base.public_send(meth, *args) } Package.package(base) rescue NoMethodError => error if error.name == intercom_method _error!(Errors::InvalidMethodError, intercom_method) else raise end end
Decodes the arguments.
It’s very important that this happens after channel authentication is performed. Since ‘args` comes from the client it could contain malicious marshalled data.
# File lib/ribbon/intercom/service.rb, line 280 def _decode_args(args) Utils.sanitize(Marshal.load(Base64.strict_decode64(args))).tap { |args| raise Errors::UnsafeValueError unless args.is_a?(Array) } end
# File lib/ribbon/intercom/service.rb, line 250 def _error!(klass, message=nil) error = message ? klass.new(message) : klass.new _respond_with_error!(error, _error_to_http_code(error)) end
# File lib/ribbon/intercom/service.rb, line 255 def _error_to_http_code(error) case error when Errors::MethodNotAllowedError 405 when Errors::NotFoundError 404 when Errors::ForbiddenError 403 when Errors::AuthenticationError 401 when Errors::RequestError 400 when Errors::ServerError 500 else 500 end end
# File lib/ribbon/intercom/service.rb, line 122 def _init_request @request = Rack::Request.new(env) unless request.put? || request.get? _error!(Errors::MethodNotAllowedError, 'only PUT or GET allowed') end end
# File lib/ribbon/intercom/service.rb, line 178 def _load_method_queue request_packet.method_queue.tap { |mq| raise "No method queue given" unless mq raise "Expected MethodQueue, got: #{mq.inspect}" unless mq.is_a?(Packet::MethodQueue) raise "Empty MethodQueue" if mq.empty? } end
# File lib/ribbon/intercom/service.rb, line 136 def _load_request_packet @request_packet = Packet.decode(request.body.read) end
# File lib/ribbon/intercom/service.rb, line 140 def _load_subject if (encoded_subject=request_packet.subject) && !encoded_subject.empty? @subject = Package.decode_subject(encoded_subject) _error!(Errors::InvalidSubjectSignatureError) unless @subject else @subject = self end end
# File lib/ribbon/intercom/service.rb, line 208 def _perform_health_check if store.healthy? _respond!(200) else _respond!(503) end end
Creates a successful response Packet
to be returned to the client.
# File lib/ribbon/intercom/service.rb, line 188 def _prepare_response_packet(retval) Packet.new.tap { |packet| unless self == subject # Order matters here! See: issue#52 # Need to send subject back in case it was modified by the methods. packet.subject = Package.encode_subject(subject) if subject.is_a?(Packageable::Mixin) # Need to send the package data back in case it changed, too. packet.package_data = Package.package(subject.package_data) end end packet.retval = retval } end
Calls requested methods and returns a response packet for the client.
# File lib/ribbon/intercom/service.rb, line 151 def _process_methods retval = _call_methods _prepare_response_packet(retval) end
# File lib/ribbon/intercom/service.rb, line 108 def _process_request _init_request if request.put? _authenticate_request! _load_request_packet _load_subject response_packet = _process_methods _respond_with_packet(response_packet) elsif request.get? _perform_health_check end end
# File lib/ribbon/intercom/service.rb, line 216 def _request_authenticated? auth = Rack::Auth::Basic::Request.new(env) if auth.provided? && auth.basic? token = auth.credentials[0] secret = auth.credentials[1] # Check if the request is authenticated @channel = lookup_channel(token) channel && channel.valid_secret?(secret) end end
# File lib/ribbon/intercom/service.rb, line 242 def _respond!(status, headers={}, body=EmptyResponse) throw :response, _response(status, headers, body) end
# File lib/ribbon/intercom/service.rb, line 246 def _respond_with_error!(error, status=500) _respond_with_packet(Packet.new(error: error), status) end
# File lib/ribbon/intercom/service.rb, line 204 def _respond_with_packet(packet, status=200) _respond!(status, {}, packet.encode) end
# File lib/ribbon/intercom/service.rb, line 236 def _response(status, headers={}, body=EmptyResponse) body = body == EmptyResponse ? [] : [body] headers = headers.merge("Content-Type" => "text/plain", "Transfer-Encoding" => "gzip") Rack::Response.new(body, status, headers) end
# File lib/ribbon/intercom/service.rb, line 229 def _sufficient_permissions!(base, intercom_method) unless sufficient_permissions?(base, intercom_method) required = Utils.method_identifier(base, intercom_method) _error!(Errors::InsufficientPermissionsError, required) end end