# class WebSocket::Driver::Hybi

### Constants

BYTE
DEFAULT_ERROR_CODE
ERRORS
ERROR_CODES
FIN
GUID
LENGTH
MAX_RESERVED_ERROR
MESSAGE_OPCODES
MIN_RESERVED_ERROR
OPCODE
OPCODES
OPCODE_CODES
OPENING_OPCODES
PACK_FORMATS
RSV1
RSV2
RSV3
VERSION

### Public Class Methods

generate_accept(key) click to toggle source

    # File lib/websocket/driver/hybi.rb, line 12
    def self.generate_accept(key)
      Base64.strict_encode64(Digest::SHA1.digest(key + GUID))
    end

new(socket, options = {}) click to toggle source
Calls superclass method `WebSocket::Driver::new`

    # File lib/websocket/driver/hybi.rb, line 59
    def initialize(socket, options = {})
      super

      @extensions      = ::WebSocket::Extensions.new
      @stage           = 0
      @masking         = options[:masking]
      @protocols       = options[:protocols] || []
      @protocols       = @protocols.strip.split(/ *, */) if String === @protocols
      @require_masking = options[:require_masking]
      @ping_callbacks  = {}

      @frame = @message = nil

      return unless @socket.respond_to?(:env)

      if protos = @socket.env['HTTP_SEC_WEBSOCKET_PROTOCOL']
        protos = protos.split(/ *, */) if String === protos
        @protocol = protos.find { |p| @protocols.include?(p) }
      else
        @protocol = nil
      end
    end

### Public Instance Methods

add_extension(extension) click to toggle source

    # File lib/websocket/driver/hybi.rb, line 86
    def add_extension(extension)
      @extensions.add(extension)
      true
    end

binary(message) click to toggle source

    # File lib/websocket/driver/hybi.rb, line 129
    def binary(message)
      frame(message, :binary)
    end

close(reason = nil, code = nil) click to toggle source

    # File lib/websocket/driver/hybi.rb, line 142
    def close(reason = nil, code = nil)
      reason ||= ''
      code   ||= ERRORS[:normal_closure]

      if @ready_state <= 0
        @ready_state = 3
        emit(:close, CloseEvent.new(code, reason))
        true
      elsif @ready_state == 1
        frame(reason, :close, code)
        @ready_state = 2
        true
      else
        false
      end
    end

frame(buffer, type = nil, code = nil) click to toggle source

    # File lib/websocket/driver/hybi.rb, line 159
    def frame(buffer, type = nil, code = nil)
      return queue([buffer, type, code]) if @ready_state <= 0
      return false unless @ready_state == 1

      message = Message.new
      frame   = Frame.new

      is_binary = (Array === buffer or buffer.encoding == Encoding::BINARY)
      payload   = Driver.encode(buffer, is_binary ? nil : Encoding::UTF_8)
      payload   = [code, payload].pack('S>a*') if code
      type    ||= is_binary ? :binary : :text

      message.rsv1 = message.rsv2 = message.rsv3 = false
      message.opcode = OPCODES[type]
      message.data = payload

      if MESSAGE_OPCODES.include?(message.opcode)
        message = @extensions.process_outgoing_message(message)
      end

      frame.final       = true
      frame.rsv1        = message.rsv1
      frame.rsv2        = message.rsv2
      frame.rsv3        = message.rsv3
      frame.opcode      = message.opcode
      frame.masked      = !!@masking
      frame.masking_key = SecureRandom.random_bytes(4) if frame.masked
      frame.length      = message.data.bytesize
      frame.payload     = message.data

      send_frame(frame)
      true

    rescue ::WebSocket::Extensions::ExtensionError => error
      fail(:extension_error, error.message)
    end

parse(chunk) click to toggle source

    # File lib/websocket/driver/hybi.rb, line 91
    def parse(chunk)
      @reader.put(chunk)
      buffer = true
      while buffer
        case @stage
          when 0 then
            buffer = @reader.read(1)
            parse_opcode(buffer.getbyte(0)) if buffer

          when 1 then
            buffer = @reader.read(1)
            parse_length(buffer.getbyte(0)) if buffer

          when 2 then
            buffer = @reader.read(@frame.length_bytes)
            parse_extended_length(buffer) if buffer

          when 3 then
            buffer = @reader.read(4)
            if buffer
              @stage = 4
              @frame.masking_key = buffer
            end

          when 4 then
            buffer = @reader.read(@frame.length)

            if buffer
              @stage = 0
              emit_frame(buffer)
            end

          else
            buffer = nil
        end
      end
    end

ping(message = '', &callback) click to toggle source

    # File lib/websocket/driver/hybi.rb, line 133
    def ping(message = '', &callback)
      @ping_callbacks[message] = callback if callback
      frame(message, :ping)
    end

pong(message = '') click to toggle source

    # File lib/websocket/driver/hybi.rb, line 138
    def pong(message = '')
      frame(message, :pong)
    end

version() click to toggle source

    # File lib/websocket/driver/hybi.rb, line 82
    def version
      "hybi-#{ VERSION }"
    end

### Private Instance Methods

check_frame_length() click to toggle source

    # File lib/websocket/driver/hybi.rb, line 338
    def check_frame_length
      length = @message ? @message.data.bytesize : 0

      if length + @frame.length > @max_length
        fail(:too_large, 'WebSocket frame length too large')
        false
      else
        true
      end
    end

emit_frame(buffer) click to toggle source

    # File lib/websocket/driver/hybi.rb, line 349
    def emit_frame(buffer)
      frame    = @frame
      opcode   = frame.opcode
      payload  = frame.payload = Mask.mask(buffer, @frame.masking_key)
      bytesize = payload.bytesize

      @frame = nil

      case opcode
        when OPCODES[:continuation] then
          return fail(:protocol_error, 'Received unexpected continuation frame') unless @message
          @message << frame

        when OPCODES[:text], OPCODES[:binary] then
          @message = Message.new
          @message << frame

        when OPCODES[:close] then
          code, reason = payload.unpack('S>a*') if bytesize >= 2
          reason = Driver.encode(reason || '', Encoding::UTF_8)

          unless (bytesize == 0) or
                 (code && code >= MIN_RESERVED_ERROR && code <= MAX_RESERVED_ERROR) or
                 ERROR_CODES.include?(code)
            code = ERRORS[:protocol_error]
          end

          if bytesize > 125 or !reason.valid_encoding?
            code = ERRORS[:protocol_error]
          end

          shutdown(code || DEFAULT_ERROR_CODE, reason || '')

        when OPCODES[:ping] then
          frame(payload, :pong)
          emit(:ping, PingEvent.new(payload))

        when OPCODES[:pong] then
          message = Driver.encode(payload, Encoding::UTF_8)
          callback = @ping_callbacks[message]
          @ping_callbacks.delete(message)
          callback.call if callback
          emit(:pong, PongEvent.new(payload))
      end

      emit_message if frame.final and MESSAGE_OPCODES.include?(opcode)
    end

emit_message() click to toggle source

    # File lib/websocket/driver/hybi.rb, line 397
    def emit_message
      message  = @extensions.process_incoming_message(@message)
      @message = nil

      payload = message.data

      case message.opcode
        when OPCODES[:text] then
          payload = Driver.encode(payload, Encoding::UTF_8)
          payload = nil unless payload.valid_encoding?
        when OPCODES[:binary]
          if @binary_data_format == :array
            payload = payload.bytes.to_a
          else
            payload = Driver.encode(payload, Encoding::BINARY)
          end
      end

      if payload
        emit(:message, MessageEvent.new(payload))
      else
        fail(:encoding_error, 'Could not decode a text frame as UTF-8')
      end
    rescue ::WebSocket::Extensions::ExtensionError => error
      fail(:extension_error, error.message)
    end

fail(type, message) click to toggle source

    # File lib/websocket/driver/hybi.rb, line 272
    def fail(type, message)
      return if @ready_state > 1
      shutdown(ERRORS[type], message, true)
    end

handshake_response() click to toggle source

    # File lib/websocket/driver/hybi.rb, line 234
    def handshake_response
      sec_key = @socket.env['HTTP_SEC_WEBSOCKET_KEY']
      version = @socket.env['HTTP_SEC_WEBSOCKET_VERSION']

      unless version == VERSION
        raise ProtocolError.new("Unsupported WebSocket version: #{ VERSION }")
      end

      unless sec_key
        raise ProtocolError.new('Missing handshake request header: Sec-WebSocket-Key')
      end

      @headers['Upgrade']              = 'websocket'
      @headers['Connection']           = 'Upgrade'
      @headers['Sec-WebSocket-Accept'] = Hybi.generate_accept(sec_key)

      @headers['Sec-WebSocket-Protocol'] = @protocol if @protocol

      extensions = @extensions.generate_response(@socket.env['HTTP_SEC_WEBSOCKET_EXTENSIONS'])
      @headers['Sec-WebSocket-Extensions'] = extensions if extensions

      start   = 'HTTP/1.1 101 Switching Protocols'
      headers = [start, @headers.to_s, '']
      headers.join("\r\n")
    end

parse_extended_length(buffer) click to toggle source

    # File lib/websocket/driver/hybi.rb, line 327
    def parse_extended_length(buffer)
      @frame.length = buffer.unpack(PACK_FORMATS[buffer.bytesize]).first
      @stage = @frame.masked ? 3 : 4

      unless MESSAGE_OPCODES.include?(@frame.opcode) or @frame.length <= 125
        return fail(:protocol_error, "Received control frame having too long payload: #{ @frame.length }")
      end

      return unless check_frame_length
    end

parse_length(octet) click to toggle source

    # File lib/websocket/driver/hybi.rb, line 310
    def parse_length(octet)
      @frame.masked = (octet & MASK) == MASK
      @frame.length = (octet & LENGTH)

      if @frame.length >= 0 and @frame.length <= 125
        @stage = @frame.masked ? 3 : 4
        return unless check_frame_length
      else
        @stage = 2
        @frame.length_bytes = (@frame.length == 126) ? 2 : 8
      end

      if @require_masking and not @frame.masked
        return fail(:unacceptable, 'Received unmasked frame but masking is required')
      end
    end

parse_opcode(octet) click to toggle source

    # File lib/websocket/driver/hybi.rb, line 277
    def parse_opcode(octet)
      rsvs = [RSV1, RSV2, RSV3].map { |rsv| (octet & rsv) == rsv }

      @frame = Frame.new

      @frame.final  = (octet & FIN) == FIN
      @frame.rsv1   = rsvs[0]
      @frame.rsv2   = rsvs[1]
      @frame.rsv3   = rsvs[2]
      @frame.opcode = (octet & OPCODE)

      @stage = 1

      unless @extensions.valid_frame_rsv?(@frame)
        return fail(:protocol_error,
            "One or more reserved bits are on: reserved1 = #{ @frame.rsv1 ? 1 : 0 }" +
            ", reserved2 = #{ @frame.rsv2 ? 1 : 0 }" +
            ", reserved3 = #{ @frame.rsv3 ? 1 : 0 }")
      end

      unless OPCODES.values.include?(@frame.opcode)
        return fail(:protocol_error, "Unrecognized frame opcode: #{ @frame.opcode }")
      end

      unless MESSAGE_OPCODES.include?(@frame.opcode) or @frame.final
        return fail(:protocol_error, "Received fragmented control frame: opcode = #{ @frame.opcode }")
      end

      if @message and OPENING_OPCODES.include?(@frame.opcode)
        return fail(:protocol_error, 'Received new data frame but previous continuous frame is unfinished')
      end
    end

send_frame(frame) click to toggle source

    # File lib/websocket/driver/hybi.rb, line 198
    def send_frame(frame)
      length = frame.length
      values = []
      format = 'C2'
      masked = frame.masked ? MASK : 0

      values[0] = (frame.final ? FIN : 0) |
                  (frame.rsv1 ? RSV1 : 0) |
                  (frame.rsv2 ? RSV2 : 0) |
                  (frame.rsv3 ? RSV3 : 0) |
                  frame.opcode

      if length <= 125
        values[1] = masked | length
      elsif length <= 65535
        values[1] = masked | 126
        values[2] = length
        format << 'S>'
      else
        values[1] = masked | 127
        values[2] = length
        format << 'Q>'
      end

      if frame.masked
        values << frame.masking_key
        values << Mask.mask(frame.payload, frame.masking_key)
        format << 'a4a*'
      else
        values << frame.payload
        format << 'a*'
      end

      @socket.write(values.pack(format))
    end

shutdown(code, reason, error = false) click to toggle source

    # File lib/websocket/driver/hybi.rb, line 260
    def shutdown(code, reason, error = false)
      @frame = @message = nil
      @stage = 5
      @extensions.close

      frame(reason, :close, code) if @ready_state < 2
      @ready_state = 3

      emit(:error, ProtocolError.new(reason)) if error
      emit(:close, CloseEvent.new(code, reason))
    end