Class: Skinny::Websocket
- Inherits:
-
EventMachine::Connection
- Object
- EventMachine::Connection
- Skinny::Websocket
- Includes:
- Callbacks, Thin::Logging
- Defined in:
- lib/skinny.rb
Overview
We need to be really careful not to throw an exception too high or we’ll kill the server.
Constant Summary collapse
- MAX_BUFFER_LENGTH =
4mb is almost too generous, imho.
2 ** 32
- GUID =
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
- OPCODE_CONTINUATION =
0x00
- OPCODE_TEXT =
0x01
- OPCODE_BINARY =
0x02
- OPCODE_CLOSE =
0x08
- OPCODE_PING =
0x09
- OPCODE_PONG =
0x0a
Instance Attribute Summary collapse
-
#env ⇒ Object
readonly
Returns the value of attribute env.
-
#location ⇒ Object
readonly
Returns the value of attribute location.
-
#origin ⇒ Object
readonly
Returns the value of attribute origin.
-
#protocol ⇒ Object
readonly
Returns the value of attribute protocol.
-
#version ⇒ Object
readonly
Returns the value of attribute version.
Class Method Summary collapse
-
.from_env(env, options = {}) ⇒ Object
Create a new WebSocket from a Thin::Request environment.
Instance Method Summary collapse
- #challenge ⇒ Object
- #challenge? ⇒ Boolean
- #challenge_response ⇒ Object
- #error!(message = nil, callback = true) ⇒ Object
-
#finish! ⇒ Object
Finish the connection read for closing.
-
#handshake ⇒ Object
Generate the handshake.
- #handshake! ⇒ Object
- #hixie_75? ⇒ Boolean
- #hixie_76? ⇒ Boolean
-
#initialize(env, options = {}) ⇒ Websocket
constructor
A new instance of Websocket.
- #key ⇒ Object
- #key3 ⇒ Object
- #mask(payload, mask_key) ⇒ Object
-
#post_init ⇒ Object
Connection is now open.
- #process_frame ⇒ Object
- #receive_data(data) ⇒ Object
- #receive_message(message) ⇒ Object
-
#response ⇒ Object
(also: #to_a)
Return an async response – stops Thin doing anything with connection.
- #secure? ⇒ Boolean
-
#send_frame(opcode, payload = "", masked = false) ⇒ Object
This is for post-hixie-76 versions only.
- #send_message(message) ⇒ Object
-
#start! ⇒ Object
Start the websocket connection.
-
#unbind ⇒ Object
Make sure we call the on_close callbacks when the connection disappears.
Methods included from Callbacks
Constructor Details
#initialize(env, options = {}) ⇒ Websocket
Returns a new instance of Websocket.
74 75 76 77 78 79 80 81 82 83 |
# File 'lib/skinny.rb', line 74 def initialize env, ={} @env = env.dup @buffer = '' @protocol = .delete :protocol if .has_key? :protocol [:on_open, :on_start, :on_handshake, :on_message, :on_error, :on_finish, :on_close].each do |name| send name, &.delete(name) if .has_key?(name) end raise ArgumentError, "Unknown options: #{.inspect}" unless .empty? end |
Instance Attribute Details
#env ⇒ Object (readonly)
Returns the value of attribute env.
134 135 136 |
# File 'lib/skinny.rb', line 134 def env @env end |
#location ⇒ Object (readonly)
Returns the value of attribute location.
134 135 136 |
# File 'lib/skinny.rb', line 134 def location @location end |
#origin ⇒ Object (readonly)
Returns the value of attribute origin.
134 135 136 |
# File 'lib/skinny.rb', line 134 def origin @origin end |
#protocol ⇒ Object (readonly)
Returns the value of attribute protocol.
134 135 136 |
# File 'lib/skinny.rb', line 134 def protocol @protocol end |
#version ⇒ Object (readonly)
Returns the value of attribute version.
134 135 136 |
# File 'lib/skinny.rb', line 134 def version @version end |
Class Method Details
.from_env(env, options = {}) ⇒ Object
Create a new WebSocket from a Thin::Request environment
63 64 65 66 67 68 69 70 71 72 |
# File 'lib/skinny.rb', line 63 def self.from_env env, ={} # 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, ) end |
Instance Method Details
#challenge ⇒ Object
170 171 172 173 174 175 176 177 178 |
# 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? ⇒ Boolean
166 167 168 |
# File 'lib/skinny.rb', line 166 def challenge? env.has_key? 'HTTP_SEC_WEBSOCKET_KEY1' end |
#challenge_response ⇒ Object
180 181 182 183 184 185 186 187 188 |
# 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) ⇒ Object
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 |
# File 'lib/skinny.rb', line 408 def error! =nil, callback=true log unless .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! ⇒ Object
Finish the connection read for closing
384 385 386 387 388 389 390 391 392 393 394 395 396 397 |
# 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 ⇒ Object
Generate the handshake
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# 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! ⇒ Object
209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# 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? ⇒ Boolean
136 137 138 |
# File 'lib/skinny.rb', line 136 def hixie_75? @version == "hixie-75" end |
#hixie_76? ⇒ Boolean
140 141 142 |
# File 'lib/skinny.rb', line 140 def hixie_76? @version == "hixie-76" end |
#key ⇒ Object
151 152 153 |
# File 'lib/skinny.rb', line 151 def key @env['HTTP_SEC_WEBSOCKET_KEY'] end |
#key3 ⇒ Object
162 163 164 |
# File 'lib/skinny.rb', line 162 def key3 @key3 ||= @buffer.slice!(0...8) end |
#mask(payload, mask_key) ⇒ Object
232 233 234 235 236 |
# 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 ⇒ Object
Connection is now open
86 87 88 89 90 91 |
# 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 ⇒ Object
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 |
# 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 = frame[1..-2] EM.next_tick { } # 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 = payload.force_encoding("UTF-8") if payload.respond_to? :force_encoding EM.next_tick { 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) ⇒ Object
224 225 226 227 228 229 230 |
# 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) ⇒ Object
336 337 338 |
# File 'lib/skinny.rb', line 336 def EM.next_tick { callback :on_message, self, rescue error! "Error in message callback" } end |
#response ⇒ Object Also known as: to_a
Return an async response – stops Thin doing anything with connection.
94 95 96 |
# File 'lib/skinny.rb', line 94 def response Thin::Connection::AsyncResponse end |
#secure? ⇒ Boolean
144 145 146 147 148 149 |
# 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) ⇒ Object
This is for post-hixie-76 versions only
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 |
# 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) ⇒ Object
375 376 377 378 379 380 381 |
# File 'lib/skinny.rb', line 375 def if hixie_75? or hixie_76? send_data "\x00#{}\xff" else send_frame OPCODE_TEXT, end end |
#start! ⇒ Object
Start the websocket connection
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# 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 ⇒ Object
Make sure we call the on_close callbacks when the connection disappears
401 402 403 404 405 406 |
# 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 |