lib/faye/adapters/rack_adapter.rb in faye-0.8.11 vs lib/faye/adapters/rack_adapter.rb in faye-1.0.0

- old
+ new

@@ -2,44 +2,46 @@ class RackAdapter include Logging extend Forwardable - def_delegators "@server.engine", :bind, :unbind + def_delegators '@server.engine', *Faye::Publisher.instance_methods ASYNC_RESPONSE = [-1, {}, []].freeze DEFAULT_ENDPOINT = '/bayeux' SCRIPT_PATH = 'faye-browser-min.js' TYPE_JSON = {'Content-Type' => 'application/json; charset=utf-8'} TYPE_SCRIPT = {'Content-Type' => 'text/javascript; charset=utf-8'} TYPE_TEXT = {'Content-Type' => 'text/plain; charset=utf-8'} - VALID_JSONP_CALLBACK = /^[a-z_\$][a-z0-9_\$]*(\.[a-z_\$][a-z0-9_\$]*)*$/i - # 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. HTTP_X_NO_CONTENT_LENGTH = 'HTTP_X_NO_CONTENT_LENGTH' def initialize(app = nil, options = nil) - @app = app if app.respond_to?(:call) - @options = [app, options].grep(Hash).first || {} + @app = app if app.respond_to?(:call) + @options = [app, options].grep(Hash).first || {} @endpoint = @options[:mount] || DEFAULT_ENDPOINT - @endpoint_re = Regexp.new('^' + @endpoint.gsub(/\/$/, '') + '(/[^/]+)*(\\.[^\\.]+)?$') + @endpoint_re = Regexp.new('^' + @endpoint.gsub(/\/$/, '') + '(/[^/]*)*(\\.[^\\.]+)?$') @server = Server.new(@options) @static = StaticServer.new(ROOT, /\.(?:js|map)$/) @static.map(File.basename(@endpoint) + '.js', SCRIPT_PATH) @static.map('client.js', SCRIPT_PATH) return unless extensions = @options[:extensions] [*extensions].each { |extension| add_extension(extension) } end + 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 + def add_extension(extension) @server.add_extension(extension) end def remove_extension(extension) @@ -48,49 +50,29 @@ def get_client @client ||= Client.new(@server) end - def listen(port, ssl_options = nil) - Faye::WebSocket.load_adapter('thin') - handler = Rack::Handler.get('thin') - handler.run(self, :Port => port) do |s| - if ssl_options - s.ssl = true - s.ssl_options = { - :private_key_file => ssl_options[:key], - :cert_chain_file => ssl_options[:cert] - } - end - @thin_server = s - end - end - - def stop - return unless @thin_server - @thin_server.stop - @thin_server = nil - end - 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) + return handle_options end - return @static.call(env) if @static =~ request.path_info - return handle_websocket(env) if Faye::WebSocket.websocket?(env) - return handle_eventsource(env) if Faye::EventSource.eventsource?(env) + return handle_websocket(request) if Faye::WebSocket.websocket?(env) + return handle_eventsource(request) if Faye::EventSource.eventsource?(env) handle_request(request) end private @@ -101,63 +83,92 @@ return [400, TYPE_TEXT, ['Bad request']] end debug "Received message via HTTP #{request.request_method}: ?", json_msg - message = Yajl::Parser.parse(json_msg) + message = MultiJson.load(json_msg) + request.env['rack.hijack'].call if request.env['rack.hijack'] + 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'] + hijack = request.env['rack.hijack_io'] - if jsonp !~ VALID_JSONP_CALLBACK - error 'Invalid JSON-P callback: ?', jsonp - return [400, TYPE_TEXT, ['Bad request']] - end - - @server.flush_connection(message) if request.get? - headers['Access-Control-Allow-Origin'] = origin if origin headers['Cache-Control'] = 'no-cache, no-store' - headers['X-Content-Type-Options'] = 'nosniff' - @server.process(message, false) do |replies| - response = Faye.to_json(replies) - - if request.get? - response = "/**/#{ jsonp }(#{ response });" - headers['Content-Disposition'] = 'attachment; filename=f.txt' + EventMachine.next_tick do + @server.process(message, request) do |replies| + response = Faye.to_json(replies) + response = "#{ jsonp }(#{ jsonp_escape(response) });" if request.get? + headers['Content-Length'] = response.bytesize.to_s unless request.env[HTTP_X_NO_CONTENT_LENGTH] + headers['Connection'] = 'close' + debug 'HTTP response: ?', response + send_response([200, headers, [response]], hijack, callback) end - - headers['Content-Length'] = response.bytesize.to_s unless request.env[HTTP_X_NO_CONTENT_LENGTH] - headers['Connection'] = 'close' - debug 'HTTP response: ?', response - callback.call [200, headers, [response]] end ASYNC_RESPONSE rescue => e error "#{e.message}\nBacktrace:\n#{e.backtrace * "\n"}" [400, TYPE_TEXT, ['Bad request']] end - def handle_websocket(env) - ws = Faye::WebSocket.new(env, nil, :ping => @options[:ping]) + 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 + + def jsonp_escape(json) + json.gsub(/\u2028/, '\u2028').gsub(/\u2029/, '\u2029') + end + + 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 + + def handle_websocket(request) + ws = Faye::WebSocket.new(request.env, nil, :ping => @options[:ping]) client_id = nil ws.onmessage = lambda do |event| begin debug "Received message via WebSocket[#{ws.version}]: ?", event.data - message = Yajl::Parser.parse(event.data) + message = MultiJson.load(event.data) cid = Faye.client_id_from_messages(message) - @server.close_socket(client_id) if client_id and cid != client_id - @server.open_socket(cid, ws) + @server.close_socket(client_id) if client_id and cid and cid != client_id + @server.open_socket(cid, ws, request) client_id = cid - @server.process(message, false) do |replies| + @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 @@ -169,39 +180,34 @@ end ws.rack_response end - def handle_eventsource(env) - es = Faye::EventSource.new(env, :ping => @options[:ping]) + 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) + @server.open_socket(client_id, es, request) es.onclose = lambda do |event| @server.close_socket(client_id) es = nil end es.rack_response end - 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 + def handle_options + headers = { + 'Access-Control-Allow-Credentials' => 'false', + 'Access-Control-Allow-Headers' => 'Accept, Content-Type, Pragma, X-Requested-With', + 'Access-Control-Allow-Methods' => 'POST, GET, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Origin' => '*', + 'Access-Control-Max-Age' => '86400' + } + [200, headers, []] end def format_request(request) request.body.rewind string = "curl -X #{request.request_method.upcase}" @@ -209,20 +215,9 @@ if request.post? string << " -H 'Content-Type: #{request.env['CONTENT_TYPE']}'" string << " -d '#{request.body.read}'" end string - end - - def handle_options(request) - headers = { - 'Access-Control-Allow-Origin' => '*', - 'Access-Control-Allow-Credentials' => 'false', - 'Access-Control-Max-Age' => '86400', - 'Access-Control-Allow-Methods' => 'POST, GET, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers' => 'Accept, Content-Type, Pragma, X-Requested-With' - } - [200, headers, ['']] end end end