lib/ftw/websocket/parser.rb in ftw-0.0.4 vs lib/ftw/websocket/parser.rb in ftw-0.0.5

- old
+ new

@@ -20,16 +20,33 @@ # +-------------------------------- - - - - - - - - - - - - - - - + # : Payload Data continued ... : # + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + # | Payload Data continued ... | # +---------------------------------------------------------------+ +# +# Example use: +# +# socket = FTW::Connection.new("example.com:80") +# parser = FTW::WebSocket::Parser.new +# # ... do HTTP Upgrade request to websockets +# loop do +# data = socket.sysread(4096) +# payload = parser.feed(data) +# if payload +# # We got a full websocket frame, print the payload. +# p :payload => payload +# end +# end +# class FTW::WebSocket::Parser # XXX: Implement control frames: http://tools.ietf.org/html/rfc6455#section-5.5 # States are based on the minimal unit of 'byte' STATES = [ :flags_and_opcode, :mask_and_payload_init, :payload_length, :payload ] + private + # A new WebSocket protocol parser. def initialize @logger = Cabin::Channel.get($0) @opcode = 0 @masking_key = "" @@ -40,22 +57,23 @@ @buffer = "" @buffer.force_encoding("BINARY") end # def initialize # Transition to a specified state and set the next required read length. - private def transition(state, next_length) @logger.debug("Transitioning", :transition => state, :nextlen => next_length) @state = state need(next_length) end # def transition # Feed data to this parser. # # Currently, it will return the raw payload of websocket messages. # Otherwise, it returns nil if no complete message has yet been consumed. - public + # + # @param [String] the string data to feed into the parser. + # @return [String, nil] the websocket message payload, if any, nil otherwise. def feed(data) @buffer << data while have?(@need) value = send(@state) # Return if our state yields a value. @@ -64,33 +82,29 @@ end return nil end # def << # Do we have at least 'length' bytes in the buffer? - private def have?(length) return length <= @buffer.size end # def have? # Get 'length' string from the buffer. - private def get(length=nil) length = @need if length.nil? data = @buffer[0 ... length] @buffer = @buffer[length .. -1] return data end # def get # Set the minimum number of bytes we need in the buffer for the next read. - private def need(length) @need = length end # def need # State: Flags (fin, etc) and Opcode. # See: http://tools.ietf.org/html/rfc6455#section-5.3 - private def flags_and_opcode # 0 # 0 1 2 3 4 5 6 7 # +-+-+-+-+------- # |F|R|R|R| opcode @@ -110,11 +124,10 @@ return nil end # def flags_and_opcode # State: mask_and_payload_init # See: http://tools.ietf.org/html/rfc6455#section-5.2 - private def mask_and_payload_init byte = get.bytes.first @mask = byte & 0x80 # first bit (msb) @payload_length = byte & 0x7F # remaining bits are the length case @payload_length @@ -134,11 +147,10 @@ # State: payload_length # This is the 'extended payload length' with support for both 16 # and 64 bit lengths. # See: http://tools.ietf.org/html/rfc6455#section-5.2 - private def payload_length # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 # +-+-+-+-+-------+-+-------------+-------------------------------+ # |F|R|R|R| opcode|M| Payload len | Extended payload length | # |I|S|S|S| (4) |A| (7) | (16/64) | @@ -167,17 +179,18 @@ # State: payload # Read the full payload and return it. # See: http://tools.ietf.org/html/rfc6455#section-5.3 # - private def payload # TODO(sissel): Handle massive payload lengths without exceeding memory. # Perhaps if the payload is large (say, larger than 500KB by default), # instead of returning the whole thing, simply return an Enumerable that # yields chunks of the payload. There's no reason to buffer the entire # thing. Have the consumer of this library make that decision. data = get(@need) transition(:flags_and_opcode, 1) return data end # def payload + + public(:feed) end # class FTW::WebSocket::Parser