Skip to content

Commit

Permalink
updates to our em-websocket to properly handle masking and handshakes
Browse files Browse the repository at this point in the history
  • Loading branch information
Will Ryan committed Dec 14, 2011
1 parent a7d7c11 commit 764a2ba
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 59 deletions.
21 changes: 16 additions & 5 deletions lib/em-websocket/framing07.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/em-websocket/handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
43 changes: 31 additions & 12 deletions lib/em-websocket/handshake04.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions lib/em-websocket/masking04.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
152 changes: 110 additions & 42 deletions spec/unit/framing_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def <<(data)
@data << data
process_data(data)
end

def debug(*args); end
end

Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions spec/unit/message_processor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ def debug(*args); end

before :each do
@mp = MessageProcessorContainer06.new
@mp.connection = Object.new
end

describe "#message" do
Expand Down

0 comments on commit 764a2ba

Please sign in to comment.