16f7ebea4b
name in the SocketServer/HTTPServer request handler hierarchy. Prepare for merge pull request #72. This work has been picked out of 7b3dd8a6f5ef26dbfd6c34a91600ea1613aefaa2 .
494 lines
12 KiB
Ruby
494 lines
12 KiB
Ruby
|
|
# 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
|