Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Use http_parser.rb and refactor handshake handling

  • Loading branch information...
commit 8dc773bbecc1a4f01a8949cb4bed3e9aeac54445 1 parent 04b0770
Martyn Loughran mloughran authored
1  em-websocket.gemspec
View
@@ -21,6 +21,7 @@ Gem::Specification.new do |s|
s.add_dependency("eventmachine", ">= 0.12.9")
s.add_dependency("addressable", '>= 2.1.1')
+ s.add_dependency("http_parser.rb", '~> 0.5.3')
s.add_development_dependency('em-spec', '~> 0.2.6')
s.add_development_dependency("eventmachine", "~> 0.12.10")
s.add_development_dependency('em-http-request', '~> 0.2.6')
3  lib/em-websocket.rb
View
@@ -4,12 +4,13 @@
%w[
debugger websocket connection
+ handshake
handshake75 handshake76 handshake04
framing76 framing03 framing04 framing05 framing07
close75 close03 close05 close06
masking04
message_processor_03 message_processor_06
- handler_factory handler handler75 handler76 handler03 handler05 handler06 handler07 handler08 handler13
+ handler handler75 handler76 handler03 handler05 handler06 handler07 handler08 handler13
].each do |file|
require "em-websocket/#{file}"
end
41 lib/em-websocket/connection.rb
View
@@ -18,8 +18,8 @@ def onpong(&blk); @onpong = blk; end
def trigger_on_message(msg)
@onmessage.call(msg) if @onmessage
end
- def trigger_on_open
- @onopen.call if @onopen
+ def trigger_on_open(handshake)
+ @onopen.call(handshake) if @onopen
end
def trigger_on_close
@onclose.call if @onclose
@@ -41,7 +41,6 @@ def initialize(options)
@debug = options[:debug] || false
@secure = options[:secure] || false
@tls_options = options[:tls_options] || {}
- @data = ''
debug [:initialize]
end
@@ -72,11 +71,6 @@ def receive_data(data)
else
dispatch(data)
end
- rescue HandshakeError => e
- debug [:error, e]
- trigger_on_error(e)
- # Errors during the handshake require the connection to be aborted
- abort
rescue WSProtocolError => e
debug [:error, e]
trigger_on_error(e)
@@ -103,18 +97,29 @@ def unbind
def dispatch(data)
if data.match(/\A<policy-file-request\s*\/>/)
send_flash_cross_domain_file
- return false
else
- debug [:inbound_headers, data]
- @data << data
- @handler = HandlerFactory.build(self, @data, @secure, @debug)
- unless @handler
- # The whole header has not been received yet.
- return false
+ @handshake ||= begin
+ handshake = Handshake.new(@secure)
+
+ handshake.callback { |upgrade_response, handler_klass|
+ debug [:upgrade_response, upgrade_response]
+ self.send_data(upgrade_response)
+ @handler = handler_klass.new(self, @debug)
+ trigger_on_open(@handshake)
+ @handshake = nil
+ }
+
+ handshake.errback { |e|
+ debug [:error, e]
+ trigger_on_error(e)
+ # Handshake errors require the connection to be aborted
+ abort
+ }
+
+ handshake
end
- @data = nil
- @handler.run
- return true
+
+ @handshake.receive_data(data)
end
end
17 lib/em-websocket/handler.rb
View
@@ -5,22 +5,11 @@ class Handler
attr_reader :request, :state
- def initialize(connection, request, debug = false)
- @connection, @request = connection, request
+ def initialize(connection, debug = false)
+ @connection = connection
@debug = debug
- @state = :handshake
- initialize_framing
- end
-
- def run
- @connection.send_data handshake
@state = :connected
- @connection.trigger_on_open
- end
-
- # Handshake response
- def handshake
- # Implemented in subclass
+ initialize_framing
end
def receive_data(data)
1  lib/em-websocket/handler03.rb
View
@@ -1,7 +1,6 @@
module EventMachine
module WebSocket
class Handler03 < Handler
- include Handshake76
include Framing03
include MessageProcessor03
include Close03
1  lib/em-websocket/handler05.rb
View
@@ -1,7 +1,6 @@
module EventMachine
module WebSocket
class Handler05 < Handler
- include Handshake04
include Framing05
include MessageProcessor03
include Close05
1  lib/em-websocket/handler06.rb
View
@@ -1,7 +1,6 @@
module EventMachine
module WebSocket
class Handler06 < Handler
- include Handshake04
include Framing05
include MessageProcessor06
include Close06
1  lib/em-websocket/handler07.rb
View
@@ -1,7 +1,6 @@
module EventMachine
module WebSocket
class Handler07 < Handler
- include Handshake04
include Framing07
include MessageProcessor06
include Close06
1  lib/em-websocket/handler08.rb
View
@@ -1,7 +1,6 @@
module EventMachine
module WebSocket
class Handler08 < Handler
- include Handshake04
include Framing07
include MessageProcessor06
include Close06
1  lib/em-websocket/handler13.rb
View
@@ -1,7 +1,6 @@
module EventMachine
module WebSocket
class Handler13 < Handler
- include Handshake04
include Framing07
include MessageProcessor06
include Close06
109 lib/em-websocket/handler_factory.rb
View
@@ -1,109 +0,0 @@
-module EventMachine
- module WebSocket
- class HandlerFactory
- PATH = /^(\w+) (\/[^\s]*) HTTP\/1\.1$/
- HEADER = /^([^:]+):\s*(.+)$/
-
- def self.build(connection, data, secure = false, debug = false)
- (header, remains) = data.split("\r\n\r\n", 2)
- unless remains
- # The whole header has not been received yet.
- return nil
- end
-
- request = {}
-
- lines = header.split("\r\n")
-
- raise HandshakeError, "Empty HTTP header" unless lines.size > 0
-
- # extract request path
- first_line = lines.shift.match(PATH)
- raise HandshakeError, "Invalid HTTP header" unless first_line
- request['method'] = first_line[1].strip
- request['path'] = first_line[2].strip
-
- unless request["method"] == "GET"
- raise HandshakeError, "Must be GET request"
- end
-
- # extract query string values
- request['query'] = Addressable::URI.parse(request['path']).query_values ||= {}
- # extract remaining headers
- lines.each do |line|
- h = HEADER.match(line)
- request[h[1].strip.downcase] = h[2].strip if h
- end
-
- build_with_request(connection, request, remains, secure, debug)
- end
-
- def self.build_with_request(connection, request, remains, secure = false, debug = false)
- # Determine version heuristically
- version = if request['sec-websocket-version']
- # Used from drafts 04 onwards
- request['sec-websocket-version'].to_i
- elsif request['sec-websocket-draft']
- # Used in drafts 01 - 03
- request['sec-websocket-draft'].to_i
- elsif request['sec-websocket-key1']
- 76
- else
- 75
- end
-
- # Additional handling of bytes after the header if required
- case version
- when 75
- if !remains.empty?
- raise HandshakeError, "Extra bytes after header"
- end
- when 76, 1..3
- if remains.length < 8
- # The whole third-key has not been received yet.
- return nil
- elsif remains.length > 8
- raise HandshakeError, "Extra bytes after third key"
- end
- request['third-key'] = remains
- end
-
- # Validate that Connection and Upgrade headers
- unless request['connection'] && request['connection'] =~ /Upgrade/ && request['upgrade'] && request['upgrade'].downcase == 'websocket'
- raise HandshakeError, "Connection and Upgrade headers required"
- end
-
- # transform headers
- protocol = (secure ? "wss" : "ws")
- request['host'] = Addressable::URI.parse("#{protocol}://"+request['host'])
-
- case version
- when 75
- Handler75.new(connection, request, debug)
- when 76
- Handler76.new(connection, request, debug)
- when 1..3
- # We'll use handler03 - I believe they're all compatible
- Handler03.new(connection, request, debug)
- when 5
- Handler05.new(connection, request, debug)
- when 6
- Handler06.new(connection, request, debug)
- when 7
- Handler07.new(connection, request, debug)
- when 8
- # drafts 9, 10, 11 and 12 should never change the version
- # number as they are all the same as version 08.
- Handler08.new(connection, request, debug)
- when 13
- # drafts 13 to 17 all identify as version 13 as they are
- # only minor changes or text changes.
- Handler13.new(connection, request, debug)
- else
- # According to spec should abort the connection
- raise HandshakeError, "Protocol version #{version} not supported"
- end
- end
- end
- end
-end
116 lib/em-websocket/handshake.rb
View
@@ -0,0 +1,116 @@
+require "http/parser"
+
+module EventMachine
+ module WebSocket
+
+ # Resposible for creating the server handshake response
+ class Handshake
+ include EM::Deferrable
+
+ # Unfortunately drafts 75 & 76 require knowledge of whether the
+ # connection is being terminated as ws/wss in order to generate the
+ # correct handshake response
+ def initialize(secure)
+ @parser = Http::Parser.new
+ @secure = secure
+
+ @parser.on_headers_complete = proc { |headers|
+ @headers = Hash[headers.map { |k,v| [k.downcase, v] }]
+ }
+ end
+
+ def receive_data(data)
+ @parser << data
+
+ if @headers
+ process(@headers, @parser.upgrade_data)
+ end
+ rescue HTTP::Parser::Error => e
+ fail(HandshakeError.new("Invalid HTTP header"))
+ end
+
+ private
+
+ def process(headers, remains)
+ # Validate Upgrade
+ unless @parser.upgrade? && @headers['upgrade'].downcase == 'websocket'
+ raise HandshakeError, "Connection and Upgrade headers required"
+ end
+
+ # Determine version heuristically
+ version = if @headers['sec-websocket-version']
+ # Used from drafts 04 onwards
+ @headers['sec-websocket-version'].to_i
+ elsif @headers['sec-websocket-draft']
+ # Used in drafts 01 - 03
+ @headers['sec-websocket-draft'].to_i
+ elsif @headers['sec-websocket-key1']
+ 76
+ else
+ 75
+ end
+
+ # Additional handling of bytes after the header if required
+ case version
+ when 75
+ if !remains.empty?
+ raise HandshakeError, "Extra bytes after header"
+ end
+ when 76, 1..3
+ if remains.length < 8
+ # The whole third-key has not been received yet.
+ return nil
+ elsif remains.length > 8
+ raise HandshakeError, "Extra bytes after third key"
+ end
+ @headers['third-key'] = remains
+ end
+
+ handshake_klass = case version
+ when 75
+ Handshake75
+ when 76, 1..3
+ Handshake76
+ when 5, 6, 7, 8, 13
+ Handshake04
+ else
+ # According to spec should abort the connection
+ raise HandshakeError, "Protocol version #{version} not supported"
+ end
+
+ handshake_response = handshake_klass.handshake(@headers, @parser.request_url, @secure)
+
+ handler_klass = case version
+ when 75
+ Handler75
+ when 76
+ Handler76
+ when 1..3
+ # We'll use handler03 - I believe they're all compatible
+ Handler03
+ when 5
+ Handler05
+ when 6
+ Handler06
+ when 7
+ Handler07
+ when 8
+ # drafts 9, 10, 11 and 12 should never change the version
+ # number as they are all the same as version 08.
+ Handler08
+ when 13
+ # drafts 13 to 17 all identify as version 13 as they are
+ # only minor changes or text changes.
+ Handler13
+ else
+ # According to spec should abort the connection
+ raise HandshakeError, "Protocol version #{version} not supported"
+ end
+
+ succeed(handshake_response, handler_klass)
+ rescue HandshakeError => e
+ fail(e)
+ end
+ end
+ end
+end
28 lib/em-websocket/handshake04.rb
View
@@ -4,30 +4,28 @@
module EventMachine
module WebSocket
module Handshake04
- def handshake
+ def self.handshake(headers, _, _)
# Required
- unless key = request['sec-websocket-key']
- raise HandshakeError, "Sec-WebSocket-Key header is required"
+ unless key = headers['sec-websocket-key']
+ raise HandshakeError, "sec-websocket-key header is required"
end
-
+
# Optional
- origin = request['sec-websocket-origin']
- protocols = request['sec-websocket-protocol']
- extensions = request['sec-websocket-extensions']
-
+ origin = headers['sec-websocket-origin']
+ protocols = headers['sec-websocket-protocol']
+ extensions = headers['sec-websocket-extensions']
+
string_to_sign = "#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
signature = Base64.encode64(Digest::SHA1.digest(string_to_sign)).chomp
-
+
upgrade = ["HTTP/1.1 101 Switching Protocols"]
upgrade << "Upgrade: websocket"
upgrade << "Connection: Upgrade"
upgrade << "Sec-WebSocket-Accept: #{signature}"
-
- # TODO: Support Sec-WebSocket-Protocol
- # TODO: Sec-WebSocket-Extensions
-
- debug [:upgrade_headers, upgrade]
-
+
+ # TODO: Support sec-websocket-protocol
+ # TODO: sec-websocket-extensions
+
return upgrade.join("\r\n") + "\r\n\r\n"
end
end
11 lib/em-websocket/handshake75.rb
View
@@ -1,19 +1,16 @@
module EventMachine
module WebSocket
module Handshake75
- def handshake
- location = "#{request['host'].scheme}://#{request['host'].host}"
- location << ":#{request['host'].port}" if request['host'].port
- location << request['path']
+ def self.handshake(headers, path, secure)
+ scheme = (secure ? "wss" : "ws")
+ location = "#{scheme}://#{headers['host']}#{path}"
upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
upgrade << "Upgrade: WebSocket\r\n"
upgrade << "Connection: Upgrade\r\n"
- upgrade << "WebSocket-Origin: #{request['origin']}\r\n"
+ upgrade << "WebSocket-Origin: #{headers['origin']}\r\n"
upgrade << "WebSocket-Location: #{location}\r\n\r\n"
- debug [:upgrade_headers, upgrade]
-
return upgrade
end
end
25 lib/em-websocket/handshake76.rb
View
@@ -1,33 +1,30 @@
require 'digest/md5'
-module EventMachine
- module WebSocket
- module Handshake76
- def handshake
+module EventMachine::WebSocket
+ module Handshake76
+ class << self
+ def handshake(headers, path, secure)
challenge_response = solve_challenge(
- request['sec-websocket-key1'],
- request['sec-websocket-key2'],
- request['third-key']
+ headers['sec-websocket-key1'],
+ headers['sec-websocket-key2'],
+ headers['third-key']
)
- location = "#{request['host'].scheme}://#{request['host'].host}"
- location << ":#{request['host'].port}" if request['host'].port
- location << request['path']
+ scheme = (secure ? "wss" : "ws")
+ location = "#{scheme}://#{headers['host']}#{path}"
upgrade = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
upgrade << "Upgrade: WebSocket\r\n"
upgrade << "Connection: Upgrade\r\n"
upgrade << "Sec-WebSocket-Location: #{location}\r\n"
- upgrade << "Sec-WebSocket-Origin: #{request['origin']}\r\n"
- if protocol = request['sec-websocket-protocol']
+ upgrade << "Sec-WebSocket-Origin: #{headers['origin']}\r\n"
+ if protocol = headers['sec-websocket-protocol']
validate_protocol!(protocol)
upgrade << "Sec-WebSocket-Protocol: #{protocol}\r\n"
end
upgrade << "\r\n"
upgrade << challenge_response
- debug [:upgrade_headers, upgrade]
-
return upgrade
end
Please sign in to comment.
Something went wrong with that request. Please try again.