class Faye::RackAdapter

Constants

ASYNC_RESPONSE
DEFAULT_ENDPOINT
HTTP_X_NO_CONTENT_LENGTH

This header is passed by Rack::Proxy during testing. Rack::Proxy seems to set content-length for you, and setting it in here really slows the tests down. Better suggestions welcome.

SCRIPT_PATH
TYPE_JSON
TYPE_SCRIPT
TYPE_TEXT
VALID_JSONP_CALLBACK

Public Class Methods

new(app = nil, options = nil, &block) click to toggle source
# File lib/faye/adapters/rack_adapter.rb, line 25
def initialize(app = nil, options = nil, &block)
  @app     = app if app.respond_to?(:call)
  @options = [app, options].grep(Hash).first || {}

  ::WebSocket::Driver.validate_options(@options, [:engine, :mount, :ping, :timeout, :extensions, :websocket_extensions])

  @endpoint    = @options[:mount] || DEFAULT_ENDPOINT
  @extensions  = []
  @endpoint_re = Regexp.new('^' + @endpoint.gsub(/\/$/, '') + '(/[^/]*)*(\\.[^\\.]+)?$')
  @server      = Server.new(@options)

  @static = StaticServer.new(File.join(ROOT, '..', 'build', 'client'), /\.(?:js|map)$/)
  @static.map(File.basename(@endpoint) + '.js', SCRIPT_PATH)
  @static.map('client.js', SCRIPT_PATH)

  if extensions = @options[:extensions]
    [*extensions].each { |extension| add_extension(extension) }
  end

  if websocket_extensions = @options[:websocket_extensions]
    [*websocket_extensions].each { |ext| add_websocket_extension(ext) }
  end

  block.call(self) if block
end

Public Instance Methods

add_extension(extension) click to toggle source
# File lib/faye/adapters/rack_adapter.rb, line 55
def add_extension(extension)
  @server.add_extension(extension)
end
add_websocket_extension(extension) click to toggle source
# File lib/faye/adapters/rack_adapter.rb, line 63
def add_websocket_extension(extension)
  @extensions << extension
end
call(env) click to toggle source
# File lib/faye/adapters/rack_adapter.rb, line 75
def call(env)
  Faye.ensure_reactor_running!
  request = Rack::Request.new(env)

  unless request.path_info =~ @endpoint_re
    env['faye.client'] = get_client
    return @app ? @app.call(env) :
                  [404, TYPE_TEXT, ["Sure you're not looking for #{ @endpoint } ?"]]
  end

  return @static.call(env) if @static =~ request.path_info

  # http://groups.google.com/group/faye-users/browse_thread/thread/4a01bb7d25d3636a
  if env['REQUEST_METHOD'] == 'OPTIONS' or env['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] == 'POST'
    return handle_options(request)
  end

  return handle_websocket(request)   if Faye::WebSocket.websocket?(env)
  return handle_eventsource(request) if Faye::EventSource.eventsource?(env)

  handle_request(request)
end
close() click to toggle source
# File lib/faye/adapters/rack_adapter.rb, line 67
def close
  @server.close
end
get_client() click to toggle source
# File lib/faye/adapters/rack_adapter.rb, line 71
def get_client
  @client ||= Client.new(@server)
end
listen(*args) click to toggle source
# File lib/faye/adapters/rack_adapter.rb, line 51
def listen(*args)
  raise 'The listen() method is deprecated - see https://github.com/faye/faye-websocket-ruby#running-your-socket-application for information on running your Faye server'
end
remove_extension(extension) click to toggle source
# File lib/faye/adapters/rack_adapter.rb, line 59
def remove_extension(extension)
  @server.remove_extension(extension)
end

Private Instance Methods

format_request(request) click to toggle source
# File lib/faye/adapters/rack_adapter.rb, line 259
def format_request(request)
  request.body.rewind
  string = "curl -X #{ request.request_method.upcase }"
  string << " '#{ request.url }'"
  if request.post?
    string << " -H 'Content-Type: #{ request.env['CONTENT_TYPE'] }'"
    string << " -d '#{ request.body.read }'"
  end
  string
end
handle_eventsource(request) click to toggle source
# File lib/faye/adapters/rack_adapter.rb, line 225
def handle_eventsource(request)
  es        = Faye::EventSource.new(request.env, :ping => @options[:ping])
  client_id = es.url.split('/').pop

  debug('Opened EventSource connection for ?', client_id)
  @server.open_socket(client_id, es, request)

  es.onclose = lambda do |event|
    @server.close_socket(client_id)
    es = nil
  end

  es.rack_response
end
handle_options(request) click to toggle source
# File lib/faye/adapters/rack_adapter.rb, line 240
def handle_options(request)
  origin = request.env['HTTP_ORIGIN'] || request.env['HTTP_REFERER']

  headers = {
    'Access-Control-Allow-Credentials' => 'true',
    'Access-Control-Allow-Headers'     => 'Accept, Authorization, Content-Type, Pragma, X-Requested-With',
    'Access-Control-Allow-Methods'     => 'POST, GET',
    'Access-Control-Allow-Origin'      => origin || '*',
    'Access-Control-Max-Age'           => '86400'
  }
  [200, headers, []]
end
handle_request(request) click to toggle source
# File lib/faye/adapters/rack_adapter.rb, line 100
def handle_request(request)
  unless json_msg = message_from_request(request)
    error 'Received request with no message: ?', format_request(request)
    return [400, TYPE_TEXT, ['Bad request']]
  end

  unless json_msg.force_encoding('UTF-8').valid_encoding?
    error 'Received request with invalid encoding: ?', format_request(request)
    return [400, TYPE_TEXT, ['Bad request']]
  end

  debug("Received message via HTTP #{ request.request_method }: ?", json_msg)

  message  = parse_json(json_msg)
  jsonp    = request.params['jsonp'] || JSONP_CALLBACK
  headers  = request.get? ? TYPE_SCRIPT.dup : TYPE_JSON.dup
  origin   = request.env['HTTP_ORIGIN']
  callback = request.env['async.callback']

  if jsonp !~ VALID_JSONP_CALLBACK
    error 'Invalid JSON-P callback: ?', jsonp
    return [400, TYPE_TEXT, ['Bad request']]
  end

  headers['Cache-Control'] = 'no-cache, no-store'
  headers['X-Content-Type-Options'] = 'nosniff'

  if origin
    headers['Access-Control-Allow-Credentials'] = 'true'
    headers['Access-Control-Allow-Origin'] = origin
  end

  request.env['rack.hijack'].call if request.env['rack.hijack']
  hijack = request.env['rack.hijack_io']

  EventMachine.next_tick do
    @server.process(message, request) do |replies|
      response = Faye.to_json(replies)

      if request.get?
        response = "/**/#{ jsonp }(#{ jsonp_escape(response) });"
        headers['Content-Disposition'] = 'attachment; filename=f.txt'
      end

      headers['Content-Length'] = response.bytesize.to_s unless request.env[HTTP_X_NO_CONTENT_LENGTH]
      debug('HTTP response: ?', response)
      send_response([200, headers, [response]], hijack, callback)
    end
  end

  ASYNC_RESPONSE
rescue => e
  error "#{ e.message }\nBacktrace:\n#{ e.backtrace * "\n" }"
  [400, TYPE_TEXT, ['Bad request']]
end
handle_websocket(request) click to toggle source
# File lib/faye/adapters/rack_adapter.rb, line 193
def handle_websocket(request)
  options   = { :extensions => @extensions, :ping => @options[:ping] }
  ws        = Faye::WebSocket.new(request.env, [], options)
  client_id = nil

  ws.onmessage = lambda do |event|
    begin
      debug("Received message via WebSocket[#{ ws.version }]: ?", event.data)

      message = parse_json(event.data)
      cid     = Faye.client_id_from_messages(message)

      @server.close_socket(client_id, false) if client_id and cid and cid != client_id
      @server.open_socket(cid, ws, request)
      client_id = cid if cid

      @server.process(message, request) do |replies|
        ws.send(Faye.to_json(replies)) if ws
      end
    rescue => e
      error "#{ e.message }\nBacktrace:\n#{ e.backtrace * "\n" }"
    end
  end

  ws.onclose = lambda do |event|
    @server.close_socket(client_id)
    ws = nil
  end

  ws.rack_response
end
jsonp_escape(json) click to toggle source
# File lib/faye/adapters/rack_adapter.rb, line 172
def jsonp_escape(json)
  json.gsub(/\u2028/, '\u2028').gsub(/\u2029/, '\u2029')
end
message_from_request(request) click to toggle source
# File lib/faye/adapters/rack_adapter.rb, line 156
def message_from_request(request)
  message = request.params['message']
  return message if message

  # Some clients do not send a content-type, e.g.
  # Internet Explorer when using cross-origin-long-polling
  # Some use application/xml when using CORS
  content_type = request.env['CONTENT_TYPE'] || ''

  if content_type.split(';').first == 'application/json'
    request.body.read
  else
    CGI.parse(request.body.read)['message'][0]
  end
end
parse_json(json) click to toggle source
# File lib/faye/adapters/rack_adapter.rb, line 253
def parse_json(json)
  data = MultiJson.load(json)
  return data if Array === data or Hash === data
  raise ArgumentError, 'JSON messages must contain an object or array'
end
send_response(response, hijack, callback) click to toggle source
# File lib/faye/adapters/rack_adapter.rb, line 176
def send_response(response, hijack, callback)
  return callback.call(response) if callback

  buffer = "HTTP/1.1 #{ response[0] } OK\r\n"
  response[1].each do |name, value|
    buffer << "#{ name }: #{ value }\r\n"
  end
  buffer << "\r\n"
  response[2].each do |chunk|
    buffer << chunk
  end

  hijack.write(buffer)
  hijack.flush
  hijack.close_write
end