aboutsummaryrefslogtreecommitdiff
path: root/third_party/websockify/other/websocket.rb
diff options
context:
space:
mode:
authorAlon Zakai <alonzakai@gmail.com>2012-09-30 10:24:53 -0700
committerAlon Zakai <alonzakai@gmail.com>2012-09-30 10:24:53 -0700
commitb614f2bc5d9fc421565824b1ceb9a3384f26f35f (patch)
tree243c47baa4c6ce4273a5743d79f3c0dbc78789e5 /third_party/websockify/other/websocket.rb
parentc70758e3b49beb016a3d9db7b609c499d55de48b (diff)
parent7eaa78060c34489c7e56193c725641303d520f31 (diff)
Merge branch 'incoming'
Diffstat (limited to 'third_party/websockify/other/websocket.rb')
-rw-r--r--third_party/websockify/other/websocket.rb456
1 files changed, 456 insertions, 0 deletions
diff --git a/third_party/websockify/other/websocket.rb b/third_party/websockify/other/websocket.rb
new file mode 100644
index 00000000..6bf9e63a
--- /dev/null
+++ b/third_party/websockify/other/websocket.rb
@@ -0,0 +1,456 @@
+
+# Python WebSocket library with support for "wss://" encryption.
+# Copyright 2011 Joel Martin
+# Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3)
+#
+# Supports following protocol versions:
+# - http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
+# - http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
+# - http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
+
+require 'gserver'
+require 'stringio'
+require 'digest/md5'
+require 'digest/sha1'
+require 'base64'
+
+class EClose < Exception
+end
+
+class WebSocketServer < GServer
+ @@Buffer_size = 65536
+
+ #
+ # WebSocket constants
+ #
+ @@Server_handshake_hixie = "HTTP/1.1 101 Web Socket Protocol Handshake\r
+Upgrade: WebSocket\r
+Connection: Upgrade\r
+%sWebSocket-Origin: %s\r
+%sWebSocket-Location: %s://%s%s\r
+"
+
+ @@Server_handshake_hybi = "HTTP/1.1 101 Switching Protocols\r
+Upgrade: websocket\r
+Connection: Upgrade\r
+Sec-WebSocket-Accept: %s\r
+"
+ @@GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+
+
+ def initialize(opts)
+ vmsg "in WebSocketServer.initialize"
+ port = opts['listen_port']
+ host = opts['listen_host'] || GServer::DEFAULT_HOST
+
+ super(port, host)
+
+ @@client_id = 0 # Track client number total on class
+
+ @verbose = opts['verbose']
+ @opts = opts
+ end
+
+ def serve(io)
+ @@client_id += 1
+
+ # Initialize per thread state
+ t = Thread.current
+ t[:my_client_id] = @@client_id
+ t[:send_parts] = []
+ t[:recv_part] = nil
+ t[:base64] = nil
+
+ puts "in serve, client: #{t[:client].inspect}"
+
+ begin
+ t[:client] = do_handshake(io)
+ new_client(t[:client])
+ rescue EClose => e
+ msg "Client closed: #{e.message}"
+ return
+ rescue Exception => e
+ msg "Uncaught exception: #{e.message}"
+ msg "Trace: #{e.backtrace}"
+ return
+ end
+
+ msg "Client disconnected"
+ end
+
+ #
+ # WebSocketServer logging/output functions
+ #
+ def traffic(token)
+ if @verbose then print token; STDOUT.flush; end
+ end
+
+ def msg(msg)
+ puts "% 3d: %s" % [Thread.current[:my_client_id], msg]
+ end
+
+ def vmsg(msg)
+ if @verbose then msg(msg) end
+ end
+
+ #
+ # WebSocketServer general support routines
+ #
+ def gen_md5(h)
+ key1 = h['sec-websocket-key1']
+ key2 = h['sec-websocket-key2']
+ key3 = h['key3']
+ spaces1 = key1.count(" ")
+ spaces2 = key2.count(" ")
+ num1 = key1.scan(/[0-9]/).join('').to_i / spaces1
+ num2 = key2.scan(/[0-9]/).join('').to_i / spaces2
+
+ return Digest::MD5.digest([num1, num2, key3].pack('NNa8'))
+ end
+
+ def unmask(buf, hlen, length)
+ pstart = hlen + 4
+ mask = buf[hlen...hlen+4]
+ data = buf[pstart...pstart+length]
+ #data = data.bytes.zip(mask.bytes.cycle(length)).map { |d,m| d^m }
+ for i in (0...data.length) do
+ data[i] ^= mask[i%4]
+ end
+ return data
+ end
+
+ def encode_hybi(buf, opcode, base64=false)
+ if base64
+ buf = Base64.encode64(buf).gsub(/\n/, '')
+ end
+
+ b1 = 0x80 | (opcode & 0x0f) # FIN + opcode
+ payload_len = buf.length
+ if payload_len <= 125
+ header = [b1, payload_len].pack('CC')
+ elsif payload_len > 125 && payload_len < 65536
+ header = [b1, 126, payload_len].pack('CCn')
+ elsif payload_len >= 65536
+ header = [b1, 127, payload_len >> 32,
+ payload_len & 0xffffffff].pack('CCNN')
+ end
+
+ return [header + buf, header.length, 0]
+ end
+
+ def decode_hybi(buf, base64=false)
+ f = {'fin' => 0,
+ 'opcode' => 0,
+ 'hlen' => 2,
+ 'length' => 0,
+ 'payload' => nil,
+ 'left' => 0,
+ 'close_code' => nil,
+ 'close_reason' => nil}
+
+ blen = buf.length
+ f['left'] = blen
+
+ if blen < f['hlen'] then return f end # incomplete frame
+
+ b1, b2 = buf.unpack('CC')
+ f['opcode'] = b1 & 0x0f
+ f['fin'] = (b1 & 0x80) >> 7
+ has_mask = (b2 & 0x80) >> 7
+
+ f['length'] = b2 & 0x7f
+
+ if f['length'] == 126
+ f['hlen'] = 4
+ if blen < f['hlen'] then return f end # incomplete frame
+ f['length'] = buf.unpack('xxn')[0]
+ elsif f['length'] == 127
+ f['hlen'] = 10
+ if blen < f['hlen'] then return f end # incomplete frame
+ top, bottom = buf.unpack('xxNN')
+ f['length'] = (top << 32) & bottom
+ end
+
+ full_len = f['hlen'] + has_mask * 4 + f['length']
+
+ if blen < full_len then return f end # incomplete frame
+
+ # number of bytes that are part of the next frame(s)
+ f['left'] = blen - full_len
+
+ if has_mask > 0
+ f['payload'] = unmask(buf, f['hlen'], f['length'])
+ else
+ f['payload'] = buf[f['hlen']...full_len]
+ end
+
+ if base64 and [1, 2].include?(f['opcode'])
+ f['payload'] = Base64.decode64(f['payload'])
+ end
+
+ # close frame
+ if f['opcode'] == 0x08
+ if f['length'] >= 2
+ f['close_code'] = f['payload'].unpack('n')
+ end
+ if f['length'] > 3
+ f['close_reason'] = f['payload'][2...f['payload'].length]
+ end
+ end
+
+ return f
+ end
+
+ def encode_hixie(buf)
+ return ["\x00" + Base64.encode64(buf).gsub(/\n/, '') + "\xff", 1, 1]
+ end
+
+ def decode_hixie(buf)
+ last = buf.index("\377")
+ return {'payload' => Base64.decode64(buf[1...last]),
+ 'hlen' => 1,
+ 'length' => last - 1,
+ 'left' => buf.length - (last + 1)}
+ end
+
+ def send_frames(bufs)
+ t = Thread.current
+ if bufs.length > 0
+ encbuf = ""
+ bufs.each do |buf|
+ if t[:version].start_with?("hybi")
+ if t[:base64]
+ encbuf, lenhead, lentail = encode_hybi(
+ buf, opcode=1, base64=true)
+ else
+ encbuf, lenhead, lentail = encode_hybi(
+ buf, opcode=2, base64=false)
+ end
+ else
+ encbuf, lenhead, lentail = encode_hixie(buf)
+ end
+
+ t[:send_parts] << encbuf
+ end
+
+ end
+
+ while t[:send_parts].length > 0
+ buf = t[:send_parts].shift
+ sent = t[:client].send(buf, 0)
+
+ if sent == buf.length
+ traffic "<"
+ else
+ traffic "<."
+ t[:send_parts].unshift(buf[sent...buf.length])
+ end
+ end
+
+ return t[:send_parts].length
+ end
+
+ # Receive and decode Websocket frames
+ # Returns: [bufs_list, closed_string]
+ def recv_frames()
+ t = Thread.current
+ closed = false
+ bufs = []
+
+ buf = t[:client].recv(@@Buffer_size)
+
+ if buf.length == 0
+ return bufs, "Client closed abrubtly"
+ end
+
+ if t[:recv_part]
+ buf = t[:recv_part] + buf
+ t[:recv_part] = nil
+ end
+
+ while buf.length > 0
+ if t[:version].start_with?("hybi")
+ frame = decode_hybi(buf, base64=t[:base64])
+
+ if frame['payload'] == nil
+ traffic "}."
+ if frame['left'] > 0
+ t[:recv_part] = buf[-frame['left']...buf.length]
+ end
+ break
+ else
+ if frame['opcode'] == 0x8
+ closed = "Client closed, reason: %s - %s" % [
+ frame['close_code'], frame['close_reason']]
+ break
+ end
+ end
+ else
+ if buf[0...2] == "\xff\x00":
+ closed = "Client sent orderly close frame"
+ break
+ elsif buf[0...2] == "\x00\xff":
+ buf = buf[2...buf.length]
+ continue # No-op frame
+ elsif buf.count("\xff") == 0
+ # Partial frame
+ traffic "}."
+ t[:recv_part] = buf
+ break
+ end
+
+ frame = decode_hixie(buf)
+ end
+
+ #msg "Receive frame: #{frame.inspect}"
+
+ traffic "}"
+
+ bufs << frame['payload']
+
+ if frame['left'] > 0:
+ buf = buf[-frame['left']...buf.length]
+ else
+ buf = ''
+ end
+ end
+
+ return bufs, closed
+ end
+
+
+ def send_close(code=nil, reason='')
+ t = Thread.current
+ if t[:version].start_with?("hybi")
+ msg = ''
+ if code
+ msg = [reason.length, code].pack("na8")
+ end
+
+ buf, lenh, lent = encode_hybi(msg, opcode=0x08, base64=false)
+ t[:client].send(buf, 0)
+ elsif t[:version] == "hixie-76"
+ buf = "\xff\x00"
+ t[:client].send(buf, 0)
+ end
+ end
+
+ def do_handshake(sock)
+
+ t = Thread.current
+ stype = ""
+
+ if !IO.select([sock], nil, nil, 3)
+ raise EClose, "ignoring socket not ready"
+ end
+
+ handshake = sock.recv(1024, Socket::MSG_PEEK)
+ #msg "Handshake [#{handshake.inspect}]"
+
+ if handshake == ""
+ raise(EClose, "ignoring empty handshake")
+ else
+ stype = "Plain non-SSL (ws://)"
+ scheme = "ws"
+ retsock = sock
+ sock.recv(1024)
+ end
+
+ h = t[:headers] = {}
+ hlines = handshake.split("\r\n")
+ req_split = hlines.shift.match(/^(\w+) (\/[^\s]*) HTTP\/1\.1$/)
+ t[:path] = req_split[2].strip
+ hlines.each do |hline|
+ break if hline == ""
+ hsplit = hline.match(/^([^:]+):\s*(.+)$/)
+ h[hsplit[1].strip.downcase] = hsplit[2]
+ end
+ #puts "Headers: #{h.inspect}"
+
+ unless h.has_key?('upgrade') &&
+ h['upgrade'].downcase == 'websocket'
+ raise EClose, "Non-WebSocket connection"
+ end
+
+ protocols = h.fetch("sec-websocket-protocol", h["websocket-protocol"])
+ ver = h.fetch('sec-websocket-version', nil)
+
+ if ver
+ # HyBi/IETF vesrion of the protocol
+
+ # HyBi 07 reports version 7
+ # HyBi 08 - 12 report version 8
+ # HyBi 13 and up report version 13
+ if ['7', '8', '13'].include?(ver)
+ t[:version] = "hybi-%02d" % [ver.to_i]
+ else
+ raise EClose, "Unsupported protocol version %s" % [ver]
+ end
+
+ # choose binary if client supports it
+ if protocols.include?('binary')
+ t[:base64] = false
+ elsif protocols.include?('base64')
+ t[:base64] = true
+ else
+ raise EClose, "Client must support 'binary' or 'base64' sub-protocol"
+ end
+
+ key = h['sec-websocket-key']
+
+ # Generate the hash value for the accpet header
+ accept = Base64.encode64(
+ Digest::SHA1.digest(key + @@GUID)).gsub(/\n/, '')
+
+ response = @@Server_handshake_hybi % [accept]
+
+ if t[:base64]
+ response += "Sec-WebSocket-Protocol: base64\r\n"
+ else
+ response += "Sec-WebSocket-Protocol: binary\r\n"
+ end
+ response += "\r\n"
+
+ else
+ # Hixie vesrion of the protocol (75 or 76)
+ body = handshake.match(/\r\n\r\n(........)/)
+ if body
+ h['key3'] = body[1]
+ trailer = gen_md5(h)
+ pre = "Sec-"
+ t[:version] = "hixie-76"
+ else
+ trailer = ""
+ pre = ""
+ t[:version] = "hixie-75"
+ end
+
+ # base64 required for Hixie since payload is only UTF-8
+ t[:base64] = true
+
+ response = @@Server_handshake_hixie % [pre, h['origin'], pre,
+ "ws", h['host'], t[:path]]
+
+ if protocols && protocols.include?('base64')
+ response += "%sWebSocket-Protocol: base64\r\n" % [pre]
+ else
+ msg "Warning: client does not report 'base64' protocol support"
+ end
+
+ response += "\r\n" + trailer
+ end
+
+ msg "%s WebSocket connection" % [stype]
+ msg "Version %s, base64: '%s'" % [t[:version], t[:base64]]
+ if t[:path] then msg "Path: '%s'" % [t[:path]] end
+
+ #puts "sending reponse #{response.inspect}"
+ retsock.send(response, 0)
+
+ # Return the WebSocket socket which may be SSL wrapped
+ return retsock
+ end
+
+end
+
+# vim: sw=2