314 lines
9.9 KiB
Ruby
314 lines
9.9 KiB
Ruby
module EventMachine
|
|
module WebSocket
|
|
class Connection < EventMachine::Connection
|
|
include Debugger
|
|
|
|
attr_writer :max_frame_size
|
|
|
|
# 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 onbinary(&blk); @onbinary = blk; end
|
|
def onping(&blk); @onping = blk; end
|
|
def onpong(&blk); @onpong = blk; end
|
|
|
|
def trigger_on_message(msg)
|
|
@onmessage.call(msg) if defined? @onmessage
|
|
end
|
|
def trigger_on_binary(msg)
|
|
@onbinary.call(msg) if defined? @onbinary
|
|
end
|
|
def trigger_on_open(handshake)
|
|
@onopen.call(handshake) if defined? @onopen
|
|
end
|
|
def trigger_on_close(event = {})
|
|
@onclose.call(event) if defined? @onclose
|
|
end
|
|
def trigger_on_ping(data)
|
|
@onping.call(data) if defined? @onping
|
|
end
|
|
def trigger_on_pong(data)
|
|
@onpong.call(data) if defined? @onpong
|
|
end
|
|
def trigger_on_error(reason)
|
|
return false unless defined? @onerror
|
|
@onerror.call(reason)
|
|
true
|
|
end
|
|
|
|
def initialize(options)
|
|
@options = options
|
|
@debug = options[:debug] || false
|
|
@secure = options[:secure] || false
|
|
@secure_proxy = options[:secure_proxy] || false
|
|
@tls_options = options[:tls_options] || {}
|
|
@close_timeout = options[:close_timeout]
|
|
|
|
@handler = nil
|
|
|
|
debug [:initialize]
|
|
end
|
|
|
|
# Use this method to close the websocket connection cleanly
|
|
# This sends a close frame and waits for acknowlegement before closing
|
|
# the connection
|
|
def close(code = nil, body = nil)
|
|
if code && !acceptable_close_code?(code)
|
|
raise "Application code may only use codes from 1000, 3000-4999"
|
|
end
|
|
|
|
close_websocket_private(code, body)
|
|
end
|
|
|
|
# Deprecated, to be removed in version 0.6
|
|
alias :close_websocket :close
|
|
|
|
def post_init
|
|
start_tls(@tls_options) if @secure
|
|
end
|
|
|
|
def receive_data(data)
|
|
debug [:receive_data, data]
|
|
|
|
if @handler
|
|
@handler.receive_data(data)
|
|
else
|
|
dispatch(data)
|
|
end
|
|
rescue => e
|
|
debug [:error, e]
|
|
|
|
# There is no code defined for application errors, so use 3000
|
|
# (which is reserved for frameworks)
|
|
close_websocket_private(3000, "Application error")
|
|
|
|
# These are application errors - raise unless onerror defined
|
|
trigger_on_error(e) || raise(e)
|
|
end
|
|
|
|
def unbind
|
|
debug [:unbind, :connection]
|
|
|
|
@handler.unbind if @handler
|
|
rescue => e
|
|
debug [:error, e]
|
|
# These are application errors - raise unless onerror defined
|
|
trigger_on_error(e) || raise(e)
|
|
end
|
|
|
|
def dispatch(data)
|
|
if data.match(/\A<policy-file-request\s*\/>/)
|
|
send_flash_cross_domain_file
|
|
else
|
|
@handshake ||= begin
|
|
handshake = Handshake.new(@secure || @secure_proxy)
|
|
|
|
handshake.callback { |upgrade_response, handler_klass|
|
|
debug [:accepting_ws_version, handshake.protocol_version]
|
|
debug [:upgrade_response, upgrade_response]
|
|
self.send_data(upgrade_response)
|
|
@handler = handler_klass.new(self, @debug)
|
|
@handshake = nil
|
|
trigger_on_open(handshake)
|
|
}
|
|
|
|
handshake.errback { |e|
|
|
debug [:error, e]
|
|
trigger_on_error(e)
|
|
# Handshake errors require the connection to be aborted
|
|
abort
|
|
}
|
|
|
|
handshake
|
|
end
|
|
|
|
@handshake.receive_data(data)
|
|
end
|
|
end
|
|
|
|
def send_flash_cross_domain_file
|
|
file = '<?xml version="1.0"?><cross-domain-policy><allow-access-from domain="*" to-ports="*"/></cross-domain-policy>'
|
|
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
|
|
|
|
# Cache encodings since it's moderately expensive to look them up each time
|
|
ENCODING_SUPPORTED = "string".respond_to?(:force_encoding)
|
|
UTF8 = Encoding.find("UTF-8") if ENCODING_SUPPORTED
|
|
BINARY = Encoding.find("BINARY") if ENCODING_SUPPORTED
|
|
|
|
# Send a WebSocket text frame.
|
|
#
|
|
# A WebSocketError may be raised if the connection is in an opening or a
|
|
# closing state, or if the passed in data is not valid UTF-8
|
|
#
|
|
def send_text(data)
|
|
# If we're using Ruby 1.9, be pedantic about encodings
|
|
if ENCODING_SUPPORTED
|
|
# Also accept ascii only data in other encodings for convenience
|
|
unless (data.encoding == UTF8 && data.valid_encoding?) || data.ascii_only?
|
|
raise WebSocketError, "Data sent to WebSocket must be valid UTF-8 but was #{data.encoding} (valid: #{data.valid_encoding?})"
|
|
end
|
|
# This labels the encoding as binary so that it can be combined with
|
|
# the BINARY framing
|
|
data.force_encoding(BINARY)
|
|
else
|
|
# TODO: Check that data is valid UTF-8
|
|
end
|
|
|
|
if @handler
|
|
@handler.send_text_frame(data)
|
|
else
|
|
raise WebSocketError, "Cannot send data before onopen callback"
|
|
end
|
|
|
|
# Revert data back to the original encoding (which we assume is UTF-8)
|
|
# Doing this to avoid duping the string - there may be a better way
|
|
data.force_encoding(UTF8) if ENCODING_SUPPORTED
|
|
return nil
|
|
end
|
|
|
|
alias :send :send_text
|
|
|
|
# Send a WebSocket binary frame.
|
|
#
|
|
def send_binary(data)
|
|
if @handler
|
|
@handler.send_frame(:binary, data)
|
|
else
|
|
raise WebSocketError, "Cannot send binary before onopen callback"
|
|
end
|
|
end
|
|
|
|
# Send a ping to the client. The client must respond with a pong.
|
|
#
|
|
# In the case that the client is running a WebSocket draft < 01, false
|
|
# is returned since ping & pong are not supported
|
|
#
|
|
def ping(body = '')
|
|
if @handler
|
|
@handler.pingable? ? @handler.send_frame(:ping, body) && true : false
|
|
else
|
|
raise WebSocketError, "Cannot ping before onopen callback"
|
|
end
|
|
end
|
|
|
|
# Send an unsolicited pong message, as allowed by the protocol. The
|
|
# client is not expected to respond to this message.
|
|
#
|
|
# em-websocket automatically takes care of sending pong replies to
|
|
# incoming ping messages, as the protocol demands.
|
|
#
|
|
def pong(body = '')
|
|
if @handler
|
|
@handler.pingable? ? @handler.send_frame(:pong, body) && true : false
|
|
else
|
|
raise WebSocketError, "Cannot ping before onopen callback"
|
|
end
|
|
end
|
|
|
|
# Test whether the connection is pingable (i.e. the WebSocket draft in
|
|
# use is >= 01)
|
|
def pingable?
|
|
if @handler
|
|
@handler.pingable?
|
|
else
|
|
raise WebSocketError, "Cannot test whether pingable before onopen callback"
|
|
end
|
|
end
|
|
|
|
def supports_close_codes?
|
|
if @handler
|
|
@handler.supports_close_codes?
|
|
else
|
|
raise WebSocketError, "Cannot test before onopen callback"
|
|
end
|
|
end
|
|
|
|
def state
|
|
@handler ? @handler.state : :handshake
|
|
end
|
|
|
|
# Returns the maximum frame size which this connection is configured to
|
|
# accept. This can be set globally or on a per connection basis, and
|
|
# defaults to a value of 10MB if not set.
|
|
#
|
|
# The behaviour when a too large frame is received varies by protocol,
|
|
# but in the newest protocols the connection will be closed with the
|
|
# correct close code (1009) immediately after receiving the frame header
|
|
#
|
|
def max_frame_size
|
|
defined?(@max_frame_size) ? @max_frame_size : WebSocket.max_frame_size
|
|
end
|
|
|
|
def close_timeout
|
|
@close_timeout || WebSocket.close_timeout
|
|
end
|
|
|
|
private
|
|
|
|
# As definited in draft 06 7.2.2, some failures require that the server
|
|
# abort the websocket connection rather than close cleanly
|
|
def abort
|
|
close_connection
|
|
end
|
|
|
|
def close_websocket_private(code, body)
|
|
if @handler
|
|
debug [:closing, code]
|
|
@handler.close_websocket(code, body)
|
|
else
|
|
# The handshake hasn't completed - should be safe to terminate
|
|
abort
|
|
end
|
|
end
|
|
|
|
# Allow applications to close with 1000, 1003, 1008, 1011, 3xxx or 4xxx.
|
|
#
|
|
# em-websocket uses a few other codes internally which should not be
|
|
# used by applications
|
|
#
|
|
# Browsers generally allow connections to be closed with code 1000,
|
|
# 3xxx, and 4xxx. em-websocket allows closing with a few other codes
|
|
# which seem reasonable (for discussion see
|
|
# https://github.com/igrigorik/em-websocket/issues/98)
|
|
#
|
|
# Usage from the rfc:
|
|
#
|
|
# 1000 indicates a normal closure
|
|
#
|
|
# 1003 indicates that an endpoint is terminating the connection
|
|
# because it has received a type of data it cannot accept
|
|
#
|
|
# 1008 indicates that an endpoint is terminating the connection because
|
|
# it has received a message that violates its policy
|
|
#
|
|
# 1011 indicates that a server is terminating the connection because it
|
|
# encountered an unexpected condition that prevented it from fulfilling
|
|
# the request
|
|
#
|
|
# Status codes in the range 3000-3999 are reserved for use by libraries,
|
|
# frameworks, and applications
|
|
#
|
|
# Status codes in the range 4000-4999 are reserved for private use and
|
|
# thus can't be registered
|
|
#
|
|
def acceptable_close_code?(code)
|
|
case code
|
|
when 1000, 1003, 1008, 1011, (3000..4999)
|
|
true
|
|
else
|
|
false
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|