class Skinny::Websocket
We need to be really careful not to throw an exception too high or we’ll kill the server.
Constants
- GUID
- MAX_BUFFER_LENGTH
4mb is almost too generous, imho.
- OPCODE_BINARY
- OPCODE_CLOSE
- OPCODE_CONTINUATION
- OPCODE_PING
- OPCODE_PONG
- OPCODE_TEXT
Attributes
env[R]
location[R]
origin[R]
protocol[R]
version[R]
Public Class Methods
from_env(env, options={})
click to toggle source
Create a new WebSocket from a Thin::Request environment
# File lib/skinny.rb, line 63 def self.from_env env, options={} # Pull the connection out of the env thin_connection = env[Thin::Request::ASYNC_CALLBACK].receiver # Steal the IO fd = thin_connection.detach # EventMachine 1.0.0 needs this to be closable io = IO.for_fd(fd) unless fd.respond_to? :close # We have all the events now, muahaha EM.attach(io, self, env, options) end
new(env, options={})
click to toggle source
# File lib/skinny.rb, line 74 def initialize env, options={} @env = env.dup @buffer = '' @protocol = options.delete :protocol if options.has_key? :protocol [:on_open, :on_start, :on_handshake, :on_message, :on_error, :on_finish, :on_close].each do |name| send name, &options.delete(name) if options.has_key?(name) end raise ArgumentError, "Unknown options: #{options.inspect}" unless options.empty? end
Public Instance Methods
challenge()
click to toggle source
# File lib/skinny.rb, line 170 def challenge if hixie_75? nil elsif hixie_76? [key1, key2].pack("N*") + key3 else key + GUID end end
challenge?()
click to toggle source
# File lib/skinny.rb, line 166 def challenge? env.has_key? 'HTTP_SEC_WEBSOCKET_KEY1' end
challenge_response()
click to toggle source
# File lib/skinny.rb, line 180 def challenge_response if hixie_75? nil elsif hixie_76? Digest::MD5.digest(challenge) else Base64.encode64(Digest::SHA1.digest(challenge)).strip end end
error!(message=nil, callback=true)
click to toggle source
# File lib/skinny.rb, line 408 def error! message=nil, callback=true log message unless message.nil? log_error # Logs the exception itself # Allow error messages to be handled, maybe # but only if this error was not caused by the error callback if callback EM.next_tick { callback(:on_error, self) rescue error! "Error in error callback", true } end # Try to finish and close nicely. EM.next_tick { finish! } unless [:finished, :closed, :error].include? @state # We're closed! @state = :error end
finish!()
click to toggle source
Finish the connection read for closing
# File lib/skinny.rb, line 384 def finish! if hixie_75? or hixie_76? send_data "\xff\x00" else send_frame OPCODE_CLOSE end EM.next_tick { callback(:on_finish, self) rescue error! "Error in finish callback" } EM.next_tick { close_connection_after_writing } @state = :finished rescue error! "Error finishing WebSocket connection" end
handshake()
click to toggle source
Generate the handshake
# File lib/skinny.rb, line 191 def handshake "HTTP/1.1 101 Switching Protocols\r\n" << "Connection: Upgrade\r\n" << "Upgrade: WebSocket\r\n" << if hixie_75? "WebSocket-Location: #{location}\r\n" << "WebSocket-Origin: #{origin}\r\n" elsif hixie_76? "Sec-WebSocket-Location: #{location}\r\n" << "Sec-WebSocket-Origin: #{origin}\r\n" else "Sec-WebSocket-Accept: #{challenge_response}\r\n" end << (protocol ? "Sec-WebSocket-Protocol: #{protocol}\r\n" : "") << "\r\n" << (if hixie_76? then challenge_response else "" end) end
handshake!()
click to toggle source
# File lib/skinny.rb, line 209 def handshake! if hixie_76? [key1, key2].each { |key| raise WebSocketProtocolError, "Invalid key: #{key}" if key >= 2**32 } raise WebSocketProtocolError, "Invalid challenge: #{key3}" if key3.length < 8 end send_data handshake @state = :handshook EM.next_tick { callback :on_handshake, self rescue error! "Error in handshake callback" } rescue error! "Error during WebSocket connection handshake" end
hixie_75?()
click to toggle source
# File lib/skinny.rb, line 136 def hixie_75? @version == "hixie-75" end
hixie_76?()
click to toggle source
# File lib/skinny.rb, line 140 def hixie_76? @version == "hixie-76" end
key()
click to toggle source
# File lib/skinny.rb, line 151 def key @env['HTTP_SEC_WEBSOCKET_KEY'] end
key3()
click to toggle source
# File lib/skinny.rb, line 162 def key3 @key3 ||= @buffer.slice!(0...8) end
mask(payload, mask_key)
click to toggle source
# File lib/skinny.rb, line 232 def mask payload, mask_key payload.unpack("C*").map.with_index do |byte, index| byte ^ mask_key[index % 4] end.pack("C*") end
post_init()
click to toggle source
Connection is now open
# File lib/skinny.rb, line 86 def post_init EM.next_tick { callback :on_open, self rescue error! "Error in open callback" } @state = :open rescue error! "Error opening connection" end
process_frame()
click to toggle source
# File lib/skinny.rb, line 238 def process_frame if hixie_75? or hixie_76? if @buffer.length >= 1 if @buffer[0].ord < 0x7f if ending = @buffer.index("\xff") frame = @buffer.slice! 0..ending message = frame[1..-2] EM.next_tick { receive_message message } # There might be more frames to process EM.next_tick { process_frame } elsif @buffer.length > MAX_BUFFER_LENGTH raise WebSocketProtocolError, "Maximum buffer length (#{MAX_BUFFER_LENGTH}) exceeded: #{@buffer.length}" end elsif @buffer[0] == "\xff" if @buffer.length > 1 if @buffer[1] == "\x00" @buffer.slice! 0..1 EM.next_tick { finish! } else raise WebSocketProtocolError, "Incorrect finish frame length: #{@buffer[1].inspect}" end end else raise WebSocketProtocolError, "Unknown frame type: #{@buffer[0].inspect}" end end else @frame_state ||= :opcode if @frame_state == :opcode return unless @buffer.length >= 2 bytes = @buffer.slice!(0...2).unpack("C*") @opcode = bytes[0] & 0x0f @fin = (bytes[0] & 0x80) != 0 @payload_length = bytes[1] & 0x7f @masked = (bytes[1] & 0x80) != 0 return error! "Received unmasked data" unless @masked if @payload_length == 126 @frame_state = :payload_2 elsif @payload_length == 127 @frame_state = :payload_8 else @frame_state = :payload end elsif @frame_state == :payload_2 return unless @buffer.length >= 2 @payload_length = @buffer.slice!(0...2).unpack("n")[0] @frame_state = :mask elsif @frame_state == :payload_8 return unless @buffer.length >= 8 (high, low) = @buffer.slice!(0...8).unpack("NN") @payload_length = high * (2 ** 32) + low @frame_state = :mask elsif @frame_state == :mask return unless @buffer.length >= 4 bytes = @buffer[(offset)...(offset += 4)] @mask_key = bytes.unpack("C*") @frame_state = :payload elsif @frame_state == :payload return unless @buffer.length >= @payload_length payload = @buffer.slice!(0...@payload_length) payload = mask(payload, @mask_key) if @opcode == OPCODE_TEXT message = payload.force_encoding("UTF-8") if payload.respond_to? :force_encoding EM.next_tick { receive_message payload } elsif @opcode == OPCODE_CLOSE EM.next_tick { finish! } else error! "Unsupported opcode: %d" % @opcode end @frame_state = nil @opcode = @fin = @payload_length = @masked = nil end end rescue error! "Error while processing WebSocket frames" end
receive_data(data)
click to toggle source
# File lib/skinny.rb, line 224 def receive_data data @buffer << data EM.next_tick { process_frame } if @state == :handshook rescue error! "Error while receiving WebSocket data" end
receive_message(message)
click to toggle source
# File lib/skinny.rb, line 336 def receive_message message EM.next_tick { callback :on_message, self, message rescue error! "Error in message callback" } end
response()
click to toggle source
Return an async response – stops Thin doing anything with connection.
# File lib/skinny.rb, line 94 def response Thin::Connection::AsyncResponse end
Also aliased as: to_a
secure?()
click to toggle source
# File lib/skinny.rb, line 144 def secure? @env['HTTPS'] == 'on' or # XXX: This could be faked... do we care? @env['HTTP_X_FORWARDED_PROTO'] == 'https' or @env['rack.url_scheme'] == 'https' end
send_frame(opcode, payload="", masked=false)
click to toggle source
This is for post-hixie-76 versions only
# File lib/skinny.rb, line 341 def send_frame opcode, payload="", masked=false payload = payload.dup.force_encoding("ASCII-8BIT") if payload.respond_to? :force_encoding payload_length = payload.bytesize # We don't support continuations (yet), so always send fin fin_byte = 0x80 send_data [fin_byte | opcode].pack("C") # We shouldn't be sending mask, we're a server only masked_byte = masked ? 0x80 : 0x00 if payload_length <= 125 send_data [masked_byte | payload_length].pack("C") elsif payload_length < 2 ** 16 send_data [masked_byte | 126].pack("C") send_data [payload_length].pack("n") else send_data [masked_byte | 127].pack("C") send_data [payload_length / (2 ** 32), payload_length % (2 ** 32)].pack("NN") end if payload_length if masked mask_key = Array.new(4) { rand(256) }.pack("C*") send_data mask_key payload = mask payload, mask_key end send_data payload end end
send_message(message)
click to toggle source
# File lib/skinny.rb, line 375 def send_message message if hixie_75? or hixie_76? send_data "\x00#{message}\xff" else send_frame OPCODE_TEXT, message end end
start!()
click to toggle source
Start the websocket connection
# File lib/skinny.rb, line 102 def start! # Steal any remaining data from rack.input @buffer = @env[Thin::Request::RACK_INPUT].read + @buffer # Remove references to Thin connection objects, freeing memory @env.delete Thin::Request::RACK_INPUT @env.delete Thin::Request::ASYNC_CALLBACK @env.delete Thin::Request::ASYNC_CLOSE # Figure out which version we're using @version = @env['HTTP_SEC_WEBSOCKET_VERSION'] @version ||= "hixie-76" if @env.has_key?('HTTP_SEC_WEBSOCKET_KEY1') and @env.has_key?('HTTP_SEC_WEBSOCKET_KEY2') @version ||= "hixie-75" # Pull out the details we care about @origin ||= @env['HTTP_SEC_WEBSOCKET_ORIGIN'] || @env['HTTP_ORIGIN'] @location ||= "ws#{secure? ? 's' : ''}://#{@env['HTTP_HOST']}#{@env['REQUEST_PATH']}" @protocol ||= @env['HTTP_SEC_WEBSOCKET_PROTOCOL'] || @env['HTTP_WEBSOCKET_PROTOCOL'] EM.next_tick { callback :on_start, self rescue error! "Error in start callback" } # Queue up the actual handshake EM.next_tick method :handshake! @state = :started # Return self so we can be used as a response self rescue error! "Error starting connection" end
unbind()
click to toggle source
Make sure we call the on_close callbacks when the connection disappears
# File lib/skinny.rb, line 401 def unbind EM.next_tick { callback(:on_close, self) rescue error! "Error in close callback" } @state = :closed rescue error! "Error closing WebSocket connection" end