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

Public Class Methods

generate_accept(key) click to toggle source
# File lib/websocket/driver/hybi.rb, line 10
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 56
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)

  sec_key = @socket.env['HTTP_SEC_WEBSOCKET_KEY']
  protos  = @socket.env['HTTP_SEC_WEBSOCKET_PROTOCOL']

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

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

Public Instance Methods

add_extension(extension) click to toggle source
# File lib/websocket/driver/hybi.rb, line 89
def add_extension(extension)
  @extensions.add(extension)
  true
end
binary(message) click to toggle source
# File lib/websocket/driver/hybi.rb, line 132
def binary(message)
  frame(message, :binary)
end
close(reason = nil, code = nil) click to toggle source
# File lib/websocket/driver/hybi.rb, line 145
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 162
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_text = String === buffer

  message.rsv1   = message.rsv2 = message.rsv3 = false
  message.opcode = OPCODES[type || (is_text ? :text : :binary)]

  payload = is_text ? buffer.bytes.to_a : buffer
  payload = [code].pack(PACK_FORMATS[2]).bytes.to_a + payload if code
  message.data = payload.pack('C*')

  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 94
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 136
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 141
def pong(message = '')
  frame(message, :pong)
end
version() click to toggle source
# File lib/websocket/driver/hybi.rb, line 85
def version
  "hybi-#{@socket.env['HTTP_SEC_WEBSOCKET_VERSION']}"
end

Private Instance Methods

check_frame_length() click to toggle source
# File lib/websocket/driver/hybi.rb, line 324
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 335
def emit_frame(buffer)
  frame    = @frame
  opcode   = frame.opcode
  payload  = frame.payload = Mask.mask(buffer, @frame.masking_key)
  bytesize = payload.bytesize
  bytes    = payload.bytes.to_a

  @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   = (bytesize >= 2) ? payload.unpack(PACK_FORMATS[2]).first : nil
      reason = (bytesize > 2)  ? Driver.encode(bytes[2..-1] || [], UNICODE) : nil

      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 (bytesize > 2 and reason.nil?)
        code = ERRORS[:protocol_error]
      end

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

    when OPCODES[:ping] then
      frame(payload, :pong)

    when OPCODES[:pong] then
      message = Driver.encode(payload, UNICODE)
      callback = @ping_callbacks[message]
      @ping_callbacks.delete(message)
      callback.call if callback
  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 382
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, UNICODE)
    when OPCODES[:binary]
      payload = payload.bytes.to_a
  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 258
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 231
def handshake_response
  begin
    extensions = @extensions.generate_response(@socket.env['HTTP_SEC_WEBSOCKET_EXTENSIONS'])
  rescue => error
    fail(:protocol_error, error.message)
    return nil
  end

  @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 313
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 296
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 263
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 200
def send_frame(frame)
  length = frame.length
  buffer = []
  masked = frame.masked ? MASK : 0

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

  if length <= 125
    buffer[1] = masked | length
  elsif length <= 65535
    buffer[1] = masked | 126
    buffer[2..3] = [length].pack(PACK_FORMATS[2]).bytes.to_a
  else
    buffer[1] = masked | 127
    buffer[2..9] = [length].pack(PACK_FORMATS[8]).bytes.to_a
  end

  if frame.masked
    buffer.concat(frame.masking_key.bytes.to_a)
    buffer.concat(Mask.mask(frame.payload, frame.masking_key).bytes.to_a)
  else
    buffer.concat(frame.payload.bytes.to_a)
  end

  @socket.write(buffer.pack('C*'))
end
shutdown(code, reason, error = false) click to toggle source
# File lib/websocket/driver/hybi.rb, line 246
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