Permalink
Browse files

updates to our em-websocket to properly handle masking and handshakes

  • Loading branch information...
1 parent a7d7c11 commit 764a2ba57277e4e8dc6d9a6d4917e2c6515d1ff8 @willryan committed Dec 14, 2011
@@ -4,9 +4,13 @@ module EventMachine
module WebSocket
module Framing07
+ attr_accessor :mask_outbound_messages, :require_masked_inbound_messages
+
def initialize_framing
@data = MaskedString.new
@application_data_buffer = '' # Used for MORE frames
+ @mask_outbound_messages = false
+ @require_masked_inbound_messages = true
end
def process_data(newdata)
@@ -24,7 +28,9 @@ def process_data(newdata)
length = @data.getbyte(pointer) & 0b01111111
pointer += 1
- # raise WebSocketError, 'Data from client must be masked' unless mask
+ if require_masked_inbound_messages
+ raise WebSocketError, 'Data from client must be masked' unless mask
+ end
payload_length = case length
when 127 # Length defined by 8 bytes
@@ -118,19 +124,24 @@ def send_frame(frame_type, application_data)
byte1 = opcode | 0b10000000 # fin bit set, rsv1-3 are 0
frame << byte1
+ mask = mask_outbound_messages ? 0b10000000 : 0b00000000 # must be masked if from client
length = application_data.size
if length <= 125
byte2 = length # since rsv4 is 0
- frame << byte2
+ frame << (mask | byte2)
elsif length < 65536 # write 2 byte length
- frame << 126
+ frame << (mask | 126)
frame << [length].pack('n')
else # write 8 byte length
- frame << 127
+ frame << (mask | 127)
frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
end
- frame << application_data
+ if mask_outbound_messages
+ frame << MaskedString.create_masked_string(application_data)
+ else
+ frame << application_data
+ end
@connection.send_data(frame)
end
@@ -19,6 +19,8 @@ def run_server
end
def run_client
+ self.mask_outbound_messages = true
+ self.require_masked_inbound_messages = false
@connection.send_data handshake_client
end
@@ -4,6 +4,12 @@
module EventMachine
module WebSocket
module Handshake04
+
+ def handshake_key_response(key)
+ string_to_sign = "#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+ Base64.encode64(Digest::SHA1.digest(string_to_sign)).chomp
+ end
+
def handshake_server
# Required
unless key = request['sec-websocket-key']
@@ -15,40 +21,53 @@ def handshake_server
protocols = request['sec-websocket-protocol']
extensions = request['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}"
+ upgrade << "Sec-WebSocket-Accept: #{handshake_key_response(key)}"
# TODO: Support Sec-WebSocket-Protocol
# TODO: Sec-WebSocket-Extensions
- debug [:upgrade_headers, upgrade]
+ [:upgrade_headers, upgrade]
return upgrade.join("\r\n") + "\r\n\r\n"
end
def handshake_client
request = ["GET /websocket HTTP/1.1"]
- request << "Host: #{@request[:host]}:#{@request[:port]}" # TODO: replace with connection ws loc
+ request << "Host: #{@request[:host]}:#{@request[:port]}"
request << "Connection: keep-alive, Upgrade"
request << "Sec-WebSocket-Version: 8" # TODO: supply version somehow
request << "Sec-WebSocket-Origin: null"
- request << "Sec-WebSocket-Key: j3aqDbLsk5fH5dqRrTJU8g==" # TODO: figure out from spec what key should be
+ random16 = (0...16).map{rand(255).chr}.join
+ random16_base64 = Base64.encode64(random16).chomp
+ @correct_response = handshake_key_response random16_base64
+ request << "Sec-WebSocket-Key: #{random16_base64}"
request << "Upgrade: websocket"
# TODO: anything else needed? nothing else parsed anyway
return request.join("\r\n") + "\r\n\r\n"
end
def client_handle_server_handshake_response(data)
- handshake, msg = data.split "\r\n\r\n"
- @state = :connected #TODO - some actual logic would be nice
- @connection.trigger_on_open
- if msg # handle message bundled in with handshake response
- receive_data(msg)
+ header, msg = data.split "\r\n\r\n"
+ lines = header.split("\r\n")
+ accept = false
+ lines.each do |line|
+ h = /^([^:]+):\s*(.+)$/.match(line)
+ if !h.nil? and h[1].strip.downcase == "sec-websocket-accept"
+ accept = (h[2] == @correct_response)
+ break
+ end
+ end
+ if accept
+ @state = :connected #TODO - some actual logic would be nice
+ @connection.trigger_on_open
+ if msg # handle message bundled in with handshake response
+ receive_data(msg)
+ end
+ else
+ close_websocket(1002,nil)
end
end
end
@@ -10,6 +10,25 @@ def read_mask
@masking_key = String.new(self[0..3])
end
+ def self.create_mask
+ MaskedString.new "rAnD" #TODO make random 4 character string
+ end
+
+ def self.create_masked_string(original)
+ masked_string = MaskedString.new
+ masking_key = self.create_mask
+ masked_string << masking_key
+ original.size.times do |i|
+ char = original.getbyte(i)
+ masked_string << (char ^ masking_key.getbyte(i%4))
+ end
+ if masked_string.respond_to?(:force_encoding)
+ masked_string.force_encoding("ASCII-8BIT")
+ end
+ masked_string.read_mask # get input string
+ return masked_string
+ end
+
# Removes the mask, behaves like a normal string again
def unset_mask
@masking_key = nil
View
@@ -8,7 +8,7 @@ def <<(data)
@data << data
process_data(data)
end
-
+
def debug(*args); end
end
@@ -199,59 +199,127 @@ def debug(*args); end
# These examples are straight from the spec
# http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07#section-4.6
- describe "examples from the spec" do
- it "a single-frame unmakedtext message" do
- @f.should_receive(:message).with(:text, '', 'Hello')
- @f << "\x81\x05\x48\x65\x6c\x6c\x6f" # "\x84\x05Hello"
- end
+ # NOTE I modified these to be compliant with the rule that client data must be masked
+ describe "server side" do
+ describe "examples from the spec" do
+ it "rejects a single-frame unmasked text message from the client" do
+ lambda {
+ @f << "\x81\x05\x48\x65\x6c\x6c\x6f" # "\x84\x05Hello"
+ }.should raise_error(EventMachine::WebSocket::WebSocketError, 'Data from client must be masked')
+ end
- it "a single-frame masked text message" do
- @f.should_receive(:message).with(:text, '', 'Hello')
- @f << "\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58" # "\x84\x05Hello"
- end
+ it "a single-frame masked text message" do
+ @f.should_receive(:message).with(:text, '', 'Hello')
+ @f << "\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58" # "\x84\x05Hello"
+ end
- it "a fragmented unmasked text message" do
- @f.should_receive(:message).with(:text, '', 'Hello')
- @f << "\x01\x03Hel"
- @f << "\x80\x02lo"
- end
+ it "a fragmented masked text message" do
+ @f.should_receive(:message).with(:text, '', 'Hello')
+ @f << "\x01\x83\x01\x01\x01\x01Idm" # with mask x01x01x01x01, Hello -> Idmmn
+ @f << "\x80\x82\x01\x01\x01\x01mn"
+ end
- it "Ping request" do
- @f.should_receive(:message).with(:ping, '', 'Hello')
- @f << "\x89\x05Hello"
- end
+ it "Ping request" do
+ @f.should_receive(:message).with(:ping, '', 'Hello')
+ @f << "\x89\x85\x01\x01\x01\x01Idmmn"
+ end
- it "a pong response" do
- @f.should_receive(:message).with(:pong, '', 'Hello')
- @f << "\x8a\x05Hello"
- end
+ it "a pong response" do
+ @f.should_receive(:message).with(:pong, '', 'Hello')
+ @f << "\x8a\x85\x01\x01\x01\x01Idmmn"
+ end
- it "256 bytes binary message in a single unmasked frame" do
- data = "a"*256
- @f.should_receive(:message).with(:binary, '', data)
- @f << "\x82\x7E\x01\x00" + data
+ it "256 bytes binary message in a single masked frame" do
+ data = "b"*256
+ masked_data = "c"*256
+ @f.should_receive(:message).with(:binary, '', data)
+ @f << "\x82\xFE\x01\x00\x01\x01\x01\x01" + masked_data
+ end
+
+ it "64KiB binary message in a single masked frame" do
+ data = "b"*65536
+ masked_data = "c"*65536
+ @f.should_receive(:message).with(:binary, '', data)
+ @f << "\x82\xFF\x00\x00\x00\x00\x00\x01\x00\x00\x01\x01\x01\x01" + masked_data
+ end
end
- it "64KiB binary message in a single unmasked frame" do
- data = "a"*65536
- @f.should_receive(:message).with(:binary, '', data)
- @f << "\x82\x7F\x00\x00\x00\x00\x00\x01\x00\x00" + data
+ describe "other tests" do
+ it "should raise a DataError if an invalid frame type is requested" do
+ lambda {
+ # Opcode 3 is not supported by this draft
+ @f << "\x83\x85\x01\x01\x01\x01Idmmn"
+ }.should raise_error(EventMachine::WebSocket::DataError, "Unknown opcode")
+ end
+
+ it "should accept a fragmented masked text message in 3 frames" do
+ @f.should_receive(:message).with(:text, '', 'Hello world')
+ @f << "\x01\x83\x01\x01\x01\x01Idm"
+ @f << "\x00\x82\x01\x01\x01\x01mn"
+ @f << "\x80\x86\x01\x01\x01\x01\x21vnsme"
+ end
end
end
- describe "other tests" do
- it "should raise a DataError if an invalid frame type is requested" do
- lambda {
- # Opcode 3 is not supported by this draft
- @f << "\x83\x05Hello"
- }.should raise_error(EventMachine::WebSocket::DataError, "Unknown opcode")
+ describe "client side" do
+ before do
+ @f.mask_outbound_messages = true
+ @f.require_masked_inbound_messages = false
+ end
+ describe "examples from the spec" do
+ it "accepts a single-frame unmasked text message from the client" do
+ @f.should_receive(:message).with(:text, '', 'Hello')
+ @f << "\x81\x05\x48\x65\x6c\x6c\x6f" # "\x84\x05Hello"
+ end
+
+ it "a single-frame masked text message" do
+ @f.should_receive(:message).with(:text, '', 'Hello')
+ @f << "\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58" # "\x84\x05Hello"
+ end
+
+ it "a fragmented unmasked text message" do
+ @f.should_receive(:message).with(:text, '', 'Hello')
+ @f << "\x01\x03\Hel"
+ @f << "\x80\x02\lo"
+ end
+
+ it "Ping request" do
+ @f.should_receive(:message).with(:ping, '', 'Hello')
+ @f << "\x89\x05Hello"
+ end
+
+ it "a pong response" do
+ @f.should_receive(:message).with(:pong, '', 'Hello')
+ @f << "\x8a\x05Hello"
+ end
+
+ it "256 bytes binary message in a single unmasked frame" do
+ data = "a"*256
+ @f.should_receive(:message).with(:binary, '', data)
+ @f << "\x82\x7E\x01\x00" + data
+ end
+
+ it "64KiB binary message in a single unmasked frame" do
+ data = "a"*65536
+ @f.should_receive(:message).with(:binary, '', data)
+ @f << "\x82\x7F\x00\x00\x00\x00\x00\x01\x00\x00" + data
+ end
end
- it "should accept a fragmented unmasked text message in 3 frames" do
- @f.should_receive(:message).with(:text, '', 'Hello world')
- @f << "\x01\x03Hel"
- @f << "\x00\x02lo"
- @f << "\x80\x06 world"
+ describe "other tests" do
+ it "should raise a DataError if an invalid frame type is requested" do
+ lambda {
+ # Opcode 3 is not supported by this draft
+ @f << "\x83\x05Hello"
+ }.should raise_error(EventMachine::WebSocket::DataError, "Unknown opcode")
+ end
+
+ it "should accept a fragmented unmasked text message in 3 frames" do
+ @f.should_receive(:message).with(:text, '', 'Hello world')
+ @f << "\x01\x03Hel"
+ @f << "\x00\x02lo"
+ @f << "\x80\x06 world"
+ end
end
end
end
@@ -9,6 +9,7 @@ def debug(*args); end
before :each do
@mp = MessageProcessorContainer06.new
+ @mp.connection = Object.new
end
describe "#message" do

0 comments on commit 764a2ba

Please sign in to comment.