diff options
author | Alon Zakai <alonzakai@gmail.com> | 2012-09-30 10:24:53 -0700 |
---|---|---|
committer | Alon Zakai <alonzakai@gmail.com> | 2012-09-30 10:24:53 -0700 |
commit | b614f2bc5d9fc421565824b1ceb9a3384f26f35f (patch) | |
tree | 243c47baa4c6ce4273a5743d79f3c0dbc78789e5 /third_party/websockify/other/websocket.rb | |
parent | c70758e3b49beb016a3d9db7b609c499d55de48b (diff) | |
parent | 7eaa78060c34489c7e56193c725641303d520f31 (diff) |
Merge branch 'incoming'
Diffstat (limited to 'third_party/websockify/other/websocket.rb')
-rw-r--r-- | third_party/websockify/other/websocket.rb | 456 |
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 |