class WebSocket::Driver::Hybi

Constants

BYTE
ERRORS
ERROR_CODES
FIN
FRAGMENTED_OPCODES
GUID
LENGTH
MAX_RESERVED_ERROR
MIN_RESERVED_ERROR
OPCODE
OPCODES
OPCODE_CODES
OPENING_OPCODES
RSV1
RSV2
RSV3

Public Class Methods

generate_accept(key) click to toggle source
# File lib/websocket/driver/hybi.rb, line 8
def self.generate_accept(key)
  Base64.encode64(Digest::SHA1.digest(key + GUID)).strip
end
new(socket, options = {}) click to toggle source
Calls superclass method WebSocket::Driver.new
# File lib/websocket/driver/hybi.rb, line 51
def initialize(socket, options = {})
  super
  reset

  @reader          = StreamReader.new
  @stage           = 0
  @masking         = options[:masking]
  @protocols       = options[:protocols] || []
  @protocols       = @protocols.strip.split(/\s*,\s*/) if String === @protocols
  @require_masking = options[:require_masking]
  @ping_callbacks  = {}

  return unless @socket.respond_to?(:env)

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

Public Instance Methods

binary(message) click to toggle source
# File lib/websocket/driver/hybi.rb, line 171
def binary(message)
  frame(message, :binary)
end
close(reason = nil, code = nil) click to toggle source
# File lib/websocket/driver/hybi.rb, line 180
def close(reason = nil, code = nil)
  reason ||= ''
  code   ||= ERRORS[:normal_closure]

  case @ready_state
    when 0 then
      @ready_state = 3
      emit(:close, CloseEvent.new(code, reason))
      true
    when 1 then
      frame(reason, :close, code)
      @ready_state = 2
      true
    else
      false
  end
end
frame(data, type = nil, code = nil) click to toggle source
# File lib/websocket/driver/hybi.rb, line 114
def frame(data, type = nil, code = nil)
  return queue([data, type, code]) if @ready_state <= 0
  return false unless @ready_state == 1

  data = data.to_s unless Array === data
  data = Driver.encode(data, :utf8) if String === data

  is_text = (String === data)
  opcode  = OPCODES[type || (is_text ? :text : :binary)]
  buffer  = data.respond_to?(:bytes) ? data.bytes.to_a : data
  insert  = code ? 2 : 0
  length  = buffer.size + insert
  header  = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10)
  offset  = header + (@masking ? 4 : 0)
  masked  = @masking ? MASK : 0
  frame   = Array.new(offset)

  frame[0] = FIN | opcode

  if length <= 125
    frame[1] = masked | length
  elsif length <= 65535
    frame[1] = masked | 126
    frame[2] = (length >> 8) & BYTE
    frame[3] = length & BYTE
  else
    frame[1] = masked | 127
    frame[2] = (length >> 56) & BYTE
    frame[3] = (length >> 48) & BYTE
    frame[4] = (length >> 40) & BYTE
    frame[5] = (length >> 32) & BYTE
    frame[6] = (length >> 24) & BYTE
    frame[7] = (length >> 16) & BYTE
    frame[8] = (length >> 8)  & BYTE
    frame[9] = length & BYTE
  end

  if code
    buffer = [(code >> 8) & BYTE, code & BYTE] + buffer
  end

  if @masking
    mask = [rand(256), rand(256), rand(256), rand(256)]
    frame[header...offset] = mask
    buffer = Mask.mask(buffer, mask)
  end

  frame.concat(buffer)

  @socket.write(Driver.encode(frame, :binary))
  true
end
parse(data) click to toggle source
# File lib/websocket/driver/hybi.rb, line 75
def parse(data)
  data = data.bytes.to_a if data.respond_to?(:bytes)
  @reader.put(data)
  buffer = true
  while buffer
    case @stage
      when 0 then
        buffer = @reader.read(1)
        parse_opcode(buffer[0]) if buffer

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

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

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

      when 4 then
        buffer = @reader.read(@length)
        if buffer
          @payload = buffer
          emit_frame(buffer)
          @stage = 0
        end

      else
        buffer = nil
    end
  end
end
ping(message = '', &callback) click to toggle source
# File lib/websocket/driver/hybi.rb, line 175
def ping(message = '', &callback)
  @ping_callbacks[message] = callback if callback
  frame(message, :ping)
end
text(message) click to toggle source
# File lib/websocket/driver/hybi.rb, line 167
def text(message)
  frame(message, :text)
end
version() click to toggle source
# File lib/websocket/driver/hybi.rb, line 71
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 287
def check_frame_length
  if @buffer.size + @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 296
def emit_frame(buffer)
  payload  = Mask.mask(buffer, @mask)
  is_final = @final
  opcode   = @opcode

  @final = @opcode = @length = @length_size = @masked = @mask = nil

  case opcode
    when OPCODES[:continuation] then
      return fail(:protocol_error, 'Received unexpected continuation frame') unless @mode
      @buffer.concat(payload)
      if is_final
        message = @buffer
        message = Driver.encode(message, :utf8) if @mode == :text
        reset
        if message
          emit(:message, MessageEvent.new(message))
        else
          fail(:encoding_error, 'Could not decode a text frame as UTF-8')
        end
      end

    when OPCODES[:text] then
      if is_final
        message = Driver.encode(payload, :utf8)
        if message
          emit(:message, MessageEvent.new(message))
        else
          fail(:encoding_error, 'Could not decode a text frame as UTF-8')
        end
      else
        @mode = :text
        @buffer.concat(payload)
      end

    when OPCODES[:binary] then
      if is_final
        emit(:message, MessageEvent.new(payload))
      else
        @mode = :binary
        @buffer.concat(payload)
      end

    when OPCODES[:close] then
      code = (payload.size >= 2) ? 256 * payload[0] + payload[1] : nil

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

      message = Driver.encode(payload[2..-1] || [], :utf8)

      if payload.size > 125 or message.nil?
        code = ERRORS[:protocol_error]
      end

      reason = (payload.size > 2) ? message : ''
      shutdown(code, reason || '')

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

    when OPCODES[:pong] then
      message = Driver.encode(payload, :utf8)
      callback = @ping_callbacks[message]
      @ping_callbacks.delete(message)
      callback.call if callback
  end
end
fail(type, message) click to toggle source
# File lib/websocket/driver/hybi.rb, line 225
def fail(type, message)
  emit(:error, ProtocolError.new(message))
  shutdown(ERRORS[type], message)
end
handshake_response() click to toggle source
# File lib/websocket/driver/hybi.rb, line 200
def handshake_response
  sec_key = @socket.env['HTTP_SEC_WEBSOCKET_KEY']
  return '' unless String === sec_key

  headers = [
    "HTTP/1.1 101 Switching Protocols",
    "Upgrade: websocket",
    "Connection: Upgrade",
    "Sec-WebSocket-Accept: #{Hybi.generate_accept(sec_key)}"
  ]

  if @protocol
    headers << "Sec-WebSocket-Protocol: #{@protocol}"
  end

  (headers + [@headers.to_s, '']).join("\r\n")
end
integer(bytes) click to toggle source
# File lib/websocket/driver/hybi.rb, line 373
def integer(bytes)
  number = 0
  bytes.each_with_index do |data, i|
    number += data << (8 * (bytes.size - 1 - i))
  end
  number
end
parse_extended_length(buffer) click to toggle source
# File lib/websocket/driver/hybi.rb, line 275
def parse_extended_length(buffer)
  @length = integer(buffer)

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

  return unless check_frame_length

  @stage  = @masked ? 3 : 4
end
parse_length(data) click to toggle source
# File lib/websocket/driver/hybi.rb, line 258
def parse_length(data)
  @masked = (data & MASK) == MASK
  if @require_masking and not @masked
    return fail(:unacceptable, 'Received unmasked frame but masking is required')
  end

  @length = (data & LENGTH)

  if @length >= 0 and @length <= 125
    return unless check_frame_length
    @stage = @masked ? 3 : 4
  else
    @length_size = (@length == 126) ? 2 : 8
    @stage       = 2
  end
end
parse_opcode(data) click to toggle source
# File lib/websocket/driver/hybi.rb, line 230
def parse_opcode(data)
  rsvs = [RSV1, RSV2, RSV3].map { |rsv| (data & rsv) == rsv }

  if rsvs.any?
    return fail(:protocol_error,
        "One or more reserved bits are on: reserved1 = #{rsvs[0] ? 1 : 0}" +
        ", reserved2 = #{rsvs[1] ? 1 : 0 }" +
        ", reserved3 = #{rsvs[2] ? 1 : 0 }")
  end

  @final   = (data & FIN) == FIN
  @opcode  = (data & OPCODE)

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

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

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

  @stage = 1
end
reset() click to toggle source
# File lib/websocket/driver/hybi.rb, line 368
def reset
  @buffer = []
  @mode   = nil
end
shutdown(code, reason) click to toggle source
# File lib/websocket/driver/hybi.rb, line 218
def shutdown(code, reason)
  frame(reason, :close, code)
  @ready_state = 3
  @stage = 5
  emit(:close, CloseEvent.new(code, reason))
end