class WebSocket

Constants

NOISE_CHARS
OPCODE_BINARY
OPCODE_CLOSE
OPCODE_CONTINUATION
OPCODE_PING
OPCODE_PONG
OPCODE_TEXT
WEB_SOCKET_GUID

Attributes

debug[RW]
header[R]
path[R]
server[R]

Public Class Methods

new(arg, params = {}) click to toggle source
# File lib/websocket.rb, line 37
def initialize(arg, params = {})
  if params[:server] # server

    @server = params[:server]
    @socket = arg
    line = gets().chomp()
    if !(line =~ /\AGET (\S+) HTTP\/1.1\z/n)
      raise(WebSocket::Error, "invalid request: #{line}")
    end
    @path = $1
    read_header()
    if @header["sec-websocket-version"]
      @web_socket_version = @header["sec-websocket-version"]
      @key3 = nil
    elsif @header["sec-websocket-key1"] && @header["sec-websocket-key2"]
      @web_socket_version = "hixie-76"
      @key3 = read(8)
    else
      @web_socket_version = "hixie-75"
      @key3 = nil
    end
    if !@server.accepted_origin?(self.origin)
      raise(WebSocket::Error,
        ("Unaccepted origin: %s (server.accepted_domains = %p)\n\n" +
          "To accept this origin, write e.g. \n" +
          "  WebSocketServer.new(..., :accepted_domains => [%p]), or\n" +
          "  WebSocketServer.new(..., :accepted_domains => [\"*\"])\n") %
        [self.origin, @server.accepted_domains, @server.origin_to_domain(self.origin)])
    end
    @handshaked = false

  else # client
    
    @web_socket_version = "hixie-76"
    uri = arg.is_a?(String) ? URI.parse(arg) : arg

    if uri.scheme == "ws"
      default_port = 80
    elsif uri.scheme = "wss"
      default_port = 443
    else
      raise(WebSocket::Error, "unsupported scheme: #{uri.scheme}")
    end

    @path = (uri.path.empty? ? "/" : uri.path) + (uri.query ? "?" + uri.query : "")
    host = uri.host + ((!uri.port || uri.port == default_port) ? "" : ":#{uri.port}")
    origin = params[:origin] || "http://#{uri.host}"
    key1 = generate_key()
    key2 = generate_key()
    key3 = generate_key3()

    socket = TCPSocket.new(uri.host, uri.port || default_port)

    if uri.scheme == "ws"
      @socket = socket
    else
      @socket = ssl_handshake(socket)
    end

    write(
      "GET #{@path} HTTP/1.1\r\n" +
      "Upgrade: WebSocket\r\n" +
      "Connection: Upgrade\r\n" +
      "Host: #{host}\r\n" +
      "Origin: #{origin}\r\n" +
      "Sec-WebSocket-Key1: #{key1}\r\n" +
      "Sec-WebSocket-Key2: #{key2}\r\n" +
      "\r\n" +
      "#{key3}")
    flush()

    line = gets().chomp()
    raise(WebSocket::Error, "bad response: #{line}") if !(line =~ /\AHTTP\/1.1 101 /n)
    read_header()
    if (@header["sec-websocket-origin"] || "").downcase() != origin.downcase()
      raise(WebSocket::Error,
        "origin doesn't match: '#{@header["sec-websocket-origin"]}' != '#{origin}'")
    end
    reply_digest = read(16)
    expected_digest = hixie_76_security_digest(key1, key2, key3)
    if reply_digest != expected_digest
      raise(WebSocket::Error,
        "security digest doesn't match: %p != %p" % [reply_digest, expected_digest])
    end
    @handshaked = true

  end
  @received = []
  @buffer = ""
  @closing_started = false
end

Public Instance Methods

close(code = 1005, reason = "", origin = :self) click to toggle source

Does closing handshake.

# File lib/websocket.rb, line 266
def close(code = 1005, reason = "", origin = :self)
  if !@closing_started
    case @web_socket_version
      when "hixie-75", "hixie-76"
        write("\xff\x00")
      else
        if code == 1005
          payload = ""
        else
          payload = [code].pack("n") + force_encoding(reason.dup(), "ASCII-8BIT")
        end
        send_frame(OPCODE_CLOSE, payload, false)
    end
  end
  @socket.close() if origin == :peer
  @closing_started = true
end
close_socket() click to toggle source
# File lib/websocket.rb, line 284
def close_socket()
  @socket.close()
end
handshake(status = nil, header = {}) click to toggle source
# File lib/websocket.rb, line 131
def handshake(status = nil, header = {})
  if @handshaked
    raise(WebSocket::Error, "handshake has already been done")
  end
  status ||= "101 Switching Protocols"
  def_header = {}
  case @web_socket_version
    when "hixie-75"
      def_header["WebSocket-Origin"] = self.origin
      def_header["WebSocket-Location"] = self.location
      extra_bytes = ""
    when "hixie-76"
      def_header["Sec-WebSocket-Origin"] = self.origin
      def_header["Sec-WebSocket-Location"] = self.location
      extra_bytes = hixie_76_security_digest(
        @header["Sec-WebSocket-Key1"], @header["Sec-WebSocket-Key2"], @key3)
    else
      def_header["Sec-WebSocket-Accept"] = security_digest(@header["sec-websocket-key"])
      extra_bytes = ""
  end
  header = def_header.merge(header)
  header_str = header.map(){ |k, v| "#{k}: #{v}\r\n" }.join("")
  # Note that Upgrade and Connection must appear in this order.
  write(
    "HTTP/1.1 #{status}\r\n" +
    "Upgrade: websocket\r\n" +
    "Connection: Upgrade\r\n" +
    "#{header_str}\r\n#{extra_bytes}")
  flush()
  @handshaked = true
end
host() click to toggle source
# File lib/websocket.rb, line 243
def host
  return @header["host"]
end
location() click to toggle source
# File lib/websocket.rb, line 261
def location
  return "ws://#{self.host}#{@path}"
end
origin() click to toggle source
# File lib/websocket.rb, line 247
def origin
  case @web_socket_version
    when "7", "8"
      name = "sec-websocket-origin"
    else
      name = "origin"
  end
  if @header[name]
    return @header[name]
  else
    raise(WebSocket::Error, "%s header is missing" % name)
  end
end
receive() click to toggle source
# File lib/websocket.rb, line 177
def receive()
  if !@handshaked
    raise(WebSocket::Error, "call WebSocket\#handshake first")
  end
  case @web_socket_version
    
    when "hixie-75", "hixie-76"
      packet = gets("\xff")
      return nil if !packet
      if packet =~ /\A\x00(.*)\xff\z/nm
        return force_encoding($1, "UTF-8")
      elsif packet == "\xff" && read(1) == "\x00" # closing
        close(1005, "", :peer)
        return nil
      else
        raise(WebSocket::Error, "input must be either '\\x00...\\xff' or '\\xff\\x00'")
      end
    
    else
      begin
        bytes = read(2).unpack("C*")
        fin = (bytes[0] & 0x80) != 0
        opcode = bytes[0] & 0x0f
        mask = (bytes[1] & 0x80) != 0
        plength = bytes[1] & 0x7f
        if plength == 126
          bytes = read(2)
          plength = bytes.unpack("n")[0]
        elsif plength == 127
          bytes = read(8)
          (high, low) = bytes.unpack("NN")
          plength = high * (2 ** 32) + low
        end
        if @server && !mask
          # Masking is required.
          @socket.close()
          raise(WebSocket::Error, "received unmasked data")
        end
        mask_key = mask ? read(4).unpack("C*") : nil
        payload = read(plength)
        payload = apply_mask(payload, mask_key) if mask
        case opcode
          when OPCODE_TEXT
            return force_encoding(payload, "UTF-8")
          when OPCODE_BINARY
            raise(WebSocket::Error, "received binary data, which is not supported")
          when OPCODE_CLOSE
            close(1005, "", :peer)
            return nil
          when OPCODE_PING
            raise(WebSocket::Error, "received ping, which is not supported")
          when OPCODE_PONG
          else
            raise(WebSocket::Error, "received unknown opcode: %d" % opcode)
        end
      rescue EOFError
        return nil
      end
    
  end
end
send(data) click to toggle source
# File lib/websocket.rb, line 163
def send(data)
  if !@handshaked
    raise(WebSocket::Error, "call WebSocket\#handshake first")
  end
  case @web_socket_version
    when "hixie-75", "hixie-76"
      data = force_encoding(data.dup(), "ASCII-8BIT")
      write("\x00#{data}\xff")
      flush()
    else
      send_frame(OPCODE_TEXT, data, !@server)
  end
end
tcp_socket() click to toggle source
# File lib/websocket.rb, line 239
def tcp_socket
  return @socket
end

Private Instance Methods

apply_mask(payload, mask_key) click to toggle source
# File lib/websocket.rb, line 384
def apply_mask(payload, mask_key)
  orig_bytes = payload.unpack("C*")
  new_bytes = []
  orig_bytes.each_with_index() do |b, i|
    new_bytes.push(b ^ mask_key[i % 4])
  end
  return new_bytes.pack("C*")
end
flush() click to toggle source
# File lib/websocket.rb, line 366
def flush()
  @socket.flush()
end
force_encoding(str, encoding) click to toggle source
# File lib/websocket.rb, line 419
def force_encoding(str, encoding)
  if str.respond_to?(:force_encoding)
    return str.force_encoding(encoding)
  else
    return str
  end
end
generate_key() click to toggle source
# File lib/websocket.rb, line 393
def generate_key()
  spaces = 1 + rand(12)
  max = 0xffffffff / spaces
  number = rand(max + 1)
  key = (number * spaces).to_s()
  (1 + rand(12)).times() do
    char = NOISE_CHARS[rand(NOISE_CHARS.size)]
    pos = rand(key.size + 1)
    key[pos...pos] = char
  end
  spaces.times() do
    pos = 1 + rand(key.size - 1)
    key[pos...pos] = " "
  end
  return key
end
generate_key3() click to toggle source
# File lib/websocket.rb, line 410
def generate_key3()
  return [rand(0x100000000)].pack("N") + [rand(0x100000000)].pack("N")
end
gets(rs = $/) click to toggle source
# File lib/websocket.rb, line 341
def gets(rs = $/)
  line = @socket.gets(rs)
  $stderr.printf("recv> %p\n", line) if WebSocket.debug
  return line
end
hixie_76_security_digest(key1, key2, key3) click to toggle source
# File lib/websocket.rb, line 378
def hixie_76_security_digest(key1, key2, key3)
  bytes1 = websocket_key_to_bytes(key1)
  bytes2 = websocket_key_to_bytes(key2)
  return Digest::MD5.digest(bytes1 + bytes2 + key3)
end
read(num_bytes) click to toggle source
# File lib/websocket.rb, line 347
def read(num_bytes)
  str = @socket.read(num_bytes)
  $stderr.printf("recv> %p\n", str) if WebSocket.debug
  if str && str.bytesize == num_bytes
    return str
  else
    raise(EOFError)
  end
end
read_header() click to toggle source
# File lib/websocket.rb, line 292
def read_header()
  @header = {}
  while line = gets()
    line = line.chomp()
    break if line.empty?
    if !(line =~ /\A(\S+): (.*)\z/n)
      raise(WebSocket::Error, "invalid request: #{line}")
    end
    @header[$1] = $2
    @header[$1.downcase()] = $2
  end
  if !@header["upgrade"]
    raise(WebSocket::Error, "Upgrade header is missing")
  end
  if !(@header["upgrade"] =~ /\AWebSocket\z/i)
    raise(WebSocket::Error, "invalid Upgrade: " + @header["upgrade"])
  end
  if !@header["connection"]
    raise(WebSocket::Error, "Connection header is missing")
  end
  if @header["connection"].split(/,/).grep(/\A\s*Upgrade\s*\z/i).empty?
    raise(WebSocket::Error, "invalid Connection: " + @header["connection"])
  end
end
security_digest(key) click to toggle source
# File lib/websocket.rb, line 374
def security_digest(key)
  return Base64.encode64(Digest::SHA1.digest(key + WEB_SOCKET_GUID)).gsub(/\n/, "")
end
send_frame(opcode, payload, mask) click to toggle source
# File lib/websocket.rb, line 317
def send_frame(opcode, payload, mask)
  payload = force_encoding(payload.dup(), "ASCII-8BIT")
  # Setting StringIO's encoding to ASCII-8BIT.
  buffer = StringIO.new(force_encoding("", "ASCII-8BIT"))
  write_byte(buffer, 0x80 | opcode)
  masked_byte = mask ? 0x80 : 0x00
  if payload.bytesize <= 125
    write_byte(buffer, masked_byte | payload.bytesize)
  elsif payload.bytesize < 2 ** 16
    write_byte(buffer, masked_byte | 126)
    buffer.write([payload.bytesize].pack("n"))
  else
    write_byte(buffer, masked_byte | 127)
    buffer.write([payload.bytesize / (2 ** 32), payload.bytesize % (2 ** 32)].pack("NN"))
  end
  if mask
    mask_key = Array.new(4){ rand(256) }
    buffer.write(mask_key.pack("C*"))
    payload = apply_mask(payload, mask_key)
  end
  buffer.write(payload)
  write(buffer.string)
end
ssl_handshake(socket) click to toggle source
# File lib/websocket.rb, line 427
def ssl_handshake(socket)
  ssl_context = OpenSSL::SSL::SSLContext.new()
  ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
  ssl_socket.sync_close = true
  ssl_socket.connect()
  return ssl_socket
end
websocket_key_to_bytes(key) click to toggle source
# File lib/websocket.rb, line 414
def websocket_key_to_bytes(key)
  num = key.gsub(/[^\d]/n, "").to_i() / key.scan(/ /).size
  return [num].pack("N")
end
write(data) click to toggle source
# File lib/websocket.rb, line 357
def write(data)
  if WebSocket.debug
    data.scan(/\G(.*?(\n|\z))/n) do
      $stderr.printf("send> %p\n", $&) if !$&.empty?
    end
  end
  @socket.write(data)
end
write_byte(buffer, byte) click to toggle source
# File lib/websocket.rb, line 370
def write_byte(buffer, byte)
  buffer.write([byte].pack("C"))
end