| |
| # 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 'openssl' |
| require 'stringio' |
| require 'digest/md5' |
| require 'digest/sha1' |
| require 'base64' |
| |
| unless OpenSSL::SSL::SSLSocket.instance_methods.index("read_nonblock") |
| module OpenSSL |
| module SSL |
| class SSLSocket |
| alias :read_nonblock :readpartial |
| end |
| end |
| end |
| end |
| |
| 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) |
| msg opts.inspect |
| if opts['server_cert'] |
| msg "creating ssl context" |
| @sslContext = OpenSSL::SSL::SSLContext.new |
| @sslContext.cert = OpenSSL::X509::Certificate.new(File.open(opts['server_cert'])) |
| @sslContext.key = OpenSSL::PKey::RSA.new(File.open(opts['server_key'])) |
| @sslContext.ca_file = opts['server_cert'] |
| @sslContext.verify_mode = OpenSSL::SSL::VERIFY_NONE |
| @sslContext.verify_depth = 0 |
| end |
| |
| @@client_id = 0 # Track client number total on class |
| |
| @verbose = opts['verbose'] |
| @opts = opts |
| end |
| |
| def serve(io) |
| @@client_id += 1 |
| msg self.inspect |
| if @sslContext |
| msg "Enabling SSL context" |
| ssl = OpenSSL::SSL::SSLSocket.new(io, @sslContext) |
| #ssl.sync_close = true |
| #ssl.sync = true |
| msg "SSL accepting" |
| ssl.accept |
| io = ssl # replace the unencrypted handle with the encrypted one |
| end |
| |
| msg "initializing thread" |
| |
| # 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[:my_client_id].inspect}" |
| |
| begin |
| t[:client] = do_handshake(io) |
| new_websocket_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(m) |
| printf("% 3d: %s\n", Thread.current[:my_client_id] || 0, m) |
| end |
| |
| def vmsg(m) |
| if @verbose then msg(m) 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].each_byte.map{|b|b} |
| data = buf[pstart...pstart+length] |
| #data = data.bytes.zip(mask.bytes.cycle(length)).map { |d,m| d^m } |
| i=-1 |
| data = data.each_byte.map{|b| i+=1; (b ^ mask[i % 4]).chr}.join("") |
| 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].write(buf) |
| |
| 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].read_nonblock(@@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].write(buf) |
| elsif t[:version] == "hixie-76" |
| buf = "\xff\x00" |
| t[:client].write(buf) |
| 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 = "" |
| msg "About to read from sock [#{sock.inspect}]" |
| handshake = sock.read_nonblock(1024) |
| msg "Handshake [#{handshake.inspect}]" |
| |
| if handshake == nil or handshake == "" |
| raise(EClose, "ignoring empty handshake") |
| else |
| stype = "Plain non-SSL (ws://)" |
| scheme = "ws" |
| if sock.class == OpenSSL::SSL::SSLSocket |
| stype = "SSL (wss://)" |
| scheme = "wss" |
| end |
| retsock = sock |
| 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.write(response) |
| |
| # Return the WebSocket socket which may be SSL wrapped |
| return retsock |
| end |
| |
| end |
| |
| # vim: sw=2 |