require 'addressable/uri'
module EventMachine
module WebSocket
class Connection < EventMachine::Connection
include Debugger
attr_reader :state, :request
# define WebSocket callbacks
def onopen(&blk); @onopen = blk; end
def onclose(&blk); @onclose = blk; end
def onerror(&blk); @onerror = blk; end
def onmessage(&blk); @onmessage = blk; end
def initialize(options)
@options = options
@debug = options[:debug] || false
@secure = options[:secure] || false
@state = :handshake
@request = {}
@data = ''
debug [:initialize]
end
def post_init
start_tls if @secure
end
def receive_data(data)
debug [:receive_data, data]
@data << data
dispatch
end
def unbind
debug [:unbind, :connection]
@state = :closed
@onclose.call if @onclose
end
def dispatch
case @state
when :handshake
handshake
when :connected
process_message
else raise RuntimeError, "invalid state: #{@state}"
end
end
def handshake
if @data.match(//)
send_flash_cross_domain_file
return false
else
debug [:inbound_headers, @data]
begin
@handler = HandlerFactory.build(@data, @secure, @debug)
@data = ''
send_data @handler.handshake
@request = @handler.request
@state = :connected
@onopen.call if @onopen
return true
rescue => e
debug [:error, e]
process_bad_request(e)
return false
end
end
end
def process_bad_request(reason)
@onerror.call(reason) if @onerror
send_data "HTTP/1.1 400 Bad request\r\n\r\n"
close_connection_after_writing
end
def send_flash_cross_domain_file
file = ''
debug [:cross_domain, file]
send_data file
# handle the cross-domain request transparently
# no need to notify the user about this connection
@onclose = nil
close_connection_after_writing
end
def process_message
debug [:message, @data]
# This algorithm comes straight from the spec
# http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76#section-4.2
error = false
while !error
pointer = 0
frame_type = @data[pointer].to_i
pointer += 1
if (frame_type & 0x80) == 0x80
# If the high-order bit of the /frame type/ byte is set
length = 0
loop do
b = @data[pointer].to_i
return false unless b
pointer += 1
b_v = b & 0x7F
length = length * 128 + b_v
break unless (b & 0x80) == 0x80
end
if @data[pointer+length-1] == nil
debug [:buffer_incomplete, @data.inspect]
# Incomplete data - leave @data to accumulate
error = true
else
# Straight from spec - I'm sure this isn't crazy...
# 6. Read /length/ bytes.
# 7. Discard the read bytes.
@data = @data[(pointer+length)..-1]
# If the /frame type/ is 0xFF and the /length/ was 0, then close
if length == 0
send_data("\xff\x00")
@state = :closing
close_connection_after_writing
else
error = true
end
end
else
# If the high-order bit of the /frame type/ byte is _not_ set
msg = @data.slice!(/^\x00([^\xff]*)\xff/)
if msg
msg.gsub!(/\A\x00|\xff\z/, '')
if @state == :closing
debug [:ignored_message, msg]
else
msg.force_encoding('UTF-8') if msg.respond_to?(:force_encoding)
@onmessage.call(msg) if @onmessage
end
else
error = true
end
end
end
false
end
# should only be invoked after handshake, otherwise it
# will inject data into the header exchange
#
# frames need to start with 0x00-0x7f byte and end with
# an 0xFF byte. Per spec, we can also set the first
# byte to a value betweent 0x80 and 0xFF, followed by
# a leading length indicator
def send(data)
debug [:send, data]
ary = ["\x00", data, "\xff"]
ary.collect{ |s| s.force_encoding('UTF-8') if s.respond_to?(:force_encoding) }
send_data(ary.join)
end
end
end
end