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