class Ribbon::Intercom::Service

Attributes

channel[R]
env[R]
request[R]
request_packet[R]
subject[R]

Public Class Methods

_load_store() click to toggle source
# 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
call(env) click to toggle source

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
instance() click to toggle source
# File lib/ribbon/intercom/service.rb, line 13
def instance
  @instance ||= new(store: _load_store)
end
method_missing(meth, *args, &block) click to toggle source
# File lib/ribbon/intercom/service.rb, line 32
def method_missing(meth, *args, &block)
  instance.public_send(meth, *args, &block)
end
mock() click to toggle source
# File lib/ribbon/intercom/service.rb, line 17
def mock
  Client::MockSDK.new(self)
end
new(opts={}) click to toggle source
# File lib/ribbon/intercom/service.rb, line 50
def initialize(opts={})
  @_opts = opts.dup
end
store(store_name, params={}) click to toggle source
# File lib/ribbon/intercom/service.rb, line 21
def store(store_name, params={})
  @_store_name = store_name
  @_store_params = params
end

Public Instance Methods

call(env) click to toggle source
# File lib/ribbon/intercom/service.rb, line 84
def call(env)
  dup.call!(env)
end
call!(env) click to toggle source
# 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
lookup_channel(token) click to toggle source
# File lib/ribbon/intercom/service.rb, line 65
def lookup_channel(token)
  store.lookup_channel(token)
end
open_channel(params={}) click to toggle source
# 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
rotate_secret() click to toggle source
# File lib/ribbon/intercom/service.rb, line 102
def rotate_secret
  channel.rotate_secret!
end
store() click to toggle source
# File lib/ribbon/intercom/service.rb, line 54
def store
  @store ||= @_opts[:store] or raise Errors::MissingStoreError
end
sufficient_permissions?(base, intercom_method) click to toggle source

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

_authenticate_request!() click to toggle source
# File lib/ribbon/intercom/service.rb, line 130
def _authenticate_request!
  unless _request_authenticated?
    _error!(Errors::AuthenticationError, "invalid channel credentials")
  end
end
_call_methods() click to toggle source

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
_decode_args(args) click to toggle source

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
_error!(klass, message=nil) click to toggle source
# 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
_error_to_http_code(error) click to toggle source
# 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
_init_request() click to toggle source
# 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
_load_method_queue() click to toggle source
# 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
_load_request_packet() click to toggle source
# File lib/ribbon/intercom/service.rb, line 136
def _load_request_packet
  @request_packet = Packet.decode(request.body.read)
end
_load_subject() click to toggle source
# 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
_perform_health_check() click to toggle source
# File lib/ribbon/intercom/service.rb, line 208
def _perform_health_check
  if store.healthy?
    _respond!(200)
  else
    _respond!(503)
  end
end
_prepare_response_packet(retval) click to toggle source

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
_process_methods() click to toggle source

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
_process_request() click to toggle source
# 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
_request_authenticated?() click to toggle source
# 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
_respond!(status, headers={}, body=EmptyResponse) click to toggle source
# File lib/ribbon/intercom/service.rb, line 242
def _respond!(status, headers={}, body=EmptyResponse)
  throw :response, _response(status, headers, body)
end
_respond_with_error!(error, status=500) click to toggle source
# File lib/ribbon/intercom/service.rb, line 246
def _respond_with_error!(error, status=500)
  _respond_with_packet(Packet.new(error: error), status)
end
_respond_with_packet(packet, status=200) click to toggle source
# File lib/ribbon/intercom/service.rb, line 204
def _respond_with_packet(packet, status=200)
  _respond!(status, {}, packet.encode)
end
_response(status, headers={}, body=EmptyResponse) click to toggle source
# 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
_sufficient_permissions!(base, intercom_method) click to toggle source
# 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