Add ruby version of websockify.

Initial version is very basic but works: Hixie-76 only, no embedded
webserver, no SSL, etc.
This commit is contained in:
Joel Martin 2011-10-04 01:20:14 -05:00
parent 55e57c2048
commit 6b9d6c39be
4 changed files with 490 additions and 0 deletions

View File

@ -76,6 +76,7 @@ implementations:
<th>websockify</th>
<th>other/websockify</th>
<th>other/websockify.js</th>
<th>other/websockify.rb</th>
<th>other/kumina</th>
<th>VNCAuthProxy 1</th>
</tr> <tr>
@ -83,6 +84,7 @@ implementations:
<td>python</td>
<td>C</td>
<td>Node (node.js)</td>
<td>Ruby</td>
<td>C</td>
<td>python (twisted)</td>
</tr> <tr>
@ -91,6 +93,7 @@ implementations:
<td>yes</td>
<td>yes</td>
<td>no</td>
<td>no</td>
<td>yes</td>
</tr> <tr>
<th>Daemon</th>
@ -98,6 +101,7 @@ implementations:
<td>yes</td>
<td>no</td>
<td>no</td>
<td>no</td>
<td>yes</td>
</tr> <tr>
<th>SSL wss</th>
@ -105,12 +109,14 @@ implementations:
<td>yes</td>
<td>no</td>
<td>no</td>
<td>no</td>
<td>yes</td>
</tr> <tr>
<th>Flash Policy Server</th>
<td>yes</td>
<td>yes</td>
<td>no</td>
<td>no</td>
<td>yes</td>
<td>no</td>
</tr> <tr>
@ -120,6 +126,7 @@ implementations:
<td>no</td>
<td>no</td>
<td>no</td>
<td>no</td>
</tr> <tr>
<th>Web Server</th>
<td>yes</td>
@ -127,6 +134,7 @@ implementations:
<td>no</td>
<td>no</td>
<td>no</td>
<td>no</td>
</tr> <tr>
<th>Program Wrap</th>
<td>yes</td>
@ -134,11 +142,13 @@ implementations:
<td>no</td>
<td>no</td>
<td>no</td>
<td>no</td>
</tr> <tr>
<th>Multiple Targets</th>
<td>no</td>
<td>no</td>
<td>no</td>
<td>no</td>
<td>yes</td>
<td>no</td>
</tr> <tr>
@ -148,6 +158,7 @@ implementations:
<td>yes</td>
<td>no</td>
<td>no</td>
<td>no</td>
</tr> <tr>
<th>Hixie 76</th>
<td>yes</td>
@ -155,12 +166,14 @@ implementations:
<td>yes</td>
<td>yes</td>
<td>yes</td>
<td>yes</td>
</tr> <tr>
<th>IETF/HyBi 07-10</th>
<td>yes</td>
<td>no</td>
<td>no</td>
<td>no</td>
<td>no</td>
<td>yes</td>
</tr>
</table>

250
other/websocket.rb Normal file
View File

@ -0,0 +1,250 @@
# 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 'base64'
class EClose < Exception
end
class WebSocketServer < GServer
@@buffer_size = 65536
@@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
"
def initialize(port, host, opts, *args)
vmsg "in WebSocketServer.initialize"
super(port, host, *args)
@verbose = opts['verbose']
@opts = opts
# Keep an overall record of the client IDs allocated
# and the lines of chat
@@client_id = 0
end
#
# WebSocketServer logging/output functions
#
def traffic(token)
if @verbose
print token
STDOUT.flush
end
end
def msg(msg)
puts "% 3d: %s" % [@my_client_id, msg]
end
def vmsg(msg)
if @verbose
msg(msg)
end
end
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 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)
if bufs.length > 0
encbuf = ""
bufs.each do |buf|
#puts "Sending frame: #{buf.inspect}"
encbuf, lenhead, lentail = encode_hixie(buf)
@send_parts << encbuf
end
end
while @send_parts.length > 0
buf = @send_parts.shift
sent = @client.send(buf, 0)
if sent == buf.length
traffic "<"
else
traffic "<."
@send_parts.unshift(buf[sent...buf.length])
end
end
return @send_parts.length
end
# Receive and decode Websocket frames
# Returns: [bufs_list, closed_string]
def recv_frames()
closed = false
bufs = []
buf = @client.recv(@@buffer_size)
if buf.length == 0
return bufs, "Client closed abrubtly"
end
if @recv_part
buf = @recv_part + buf
@recv_part = nil
end
while buf.length > 0
if buf[0...2] == "\xff\x00":
closed = "Client sent orderly close frame"
break
elsif buf[0...2] == "\x00\xff":
# Partial frame
traffic "}."
@recv_part = buf
break
end
frame = decode_hixie(buf)
#msg "Receive frame: #{frame.inspect}"
traffic "}"
bufs << frame['payload']
if frame['left'] > 0:
buf = buf[buf.length-frame['left']...buf.length]
else
buf = ''
end
end
return bufs, closed
end
def send_close(code=nil, reason='')
buf = "\xff\x00"
@client.send(buf, 0)
end
def do_handshake(sock)
if !IO.select([sock], nil, nil, 3)
raise EClose, "ignoring socket not ready"
end
handshake = sock.recv(1024, Socket::MSG_PEEK)
#puts "Handshake [#{handshake.inspect}]"
if handshake == ""
raise(EClose, "ignoring empty handshake")
else
scheme = "ws"
retsock = sock
sock.recv(1024)
end
h = @headers = {}
hlines = handshake.split("\r\n")
req_split = hlines.shift.match(/^(\w+) (\/[^\s]*) HTTP\/1\.1$/)
@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}"
if h.has_key?('upgrade') &&
h['upgrade'].downcase == 'websocket'
msg "Got WebSocket connection"
else
raise EClose, "Non-WebSocket connection"
end
body = handshake.match(/\r\n\r\n(........)/)
if body
h['key3'] = body[1]
trailer = gen_md5(h)
pre = "Sec-"
protocols = h["sec-websocket-protocol"]
else
raise EClose, "Only Hixie-76 supported for now"
end
response = sprintf(@@server_handshake_hixie, pre, h['origin'],
pre, "ws", h['host'], @path)
if protocols.include?('base64')
response += sprintf("%sWebSocket-Protocol: base64\r\n", pre)
else
msg "Warning: client does not report 'base64' protocol support"
end
response += "\r\n" + trailer
#puts "Response: [#{response.inspect}]"
retsock.send(response, 0)
return retsock
end
def serve(io)
@@client_id += 1
@my_client_id = @@client_id
@send_parts = []
@recv_part = nil
@base64 = nil
begin
@client = do_handshake(io)
new_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
end
# vim: sw=2

161
other/websockify.rb Executable file
View File

@ -0,0 +1,161 @@
#!/usr/bin/env ruby
# A WebSocket to TCP socket proxy with support for "wss://" encryption.
# Copyright 2011 Joel Martin
# Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3)
require 'socket'
$: << "other"
$: << "../other"
require 'websocket'
require 'optparse'
class WebSocketProxy < WebSocketServer
def initialize(port, host, opts, *args)
vmsg "in WebSocketProxy.initialize"
super(port, host, opts, *args)
@target_host = opts["target_host"]
@target_port = opts["target_port"]
end
# Echo back whatever is received
def new_client()
vmsg "in new_client"
tsock = TCPSocket.open(@target_host, @target_port)
msg "opened target socket"
begin
do_proxy(tsock)
rescue
tsock.shutdown(Socket::SHUT_RDWR)
tsock.close
raise
end
end
def do_proxy(target)
cqueue = []
c_pend = 0
tqueue = []
rlist = [@client, target]
loop do
wlist = []
if tqueue.length > 0
wlist << target
end
if cqueue.length > 0 || c_pend > 0
wlist << @client
end
ins, outs, excepts = IO.select(rlist, wlist, nil, 0.001)
if excepts && excepts.length > 0
raise Exception, "Socket exception"
end
if outs && outs.include?(target)
# Send queued client data to the target
dat = tqueue.shift
sent = target.send(dat, 0)
if sent == dat.length
traffic ">"
else
tqueue.unshift(dat[sent...dat.length])
traffic ".>"
end
end
if ins && ins.include?(target)
# Receive target data and queue for the client
buf = target.recv(@@buffer_size)
if buf.length == 0:
raise EClose, "Target closed"
end
cqueue << buf
traffic "{"
end
if outs && outs.include?(@client)
# Encode and send queued data to the client
c_pend = send_frames(cqueue)
cqueue = []
end
if ins && ins.include?(@client)
# Receive client data, decode it, and send it back
frames, closed = recv_frames
tqueue += frames
#msg "[#{cqueue.inspect}]"
if closed
send_close
raise EClose, closed
end
end
end # loop
end
end
# Parse parameters
opts = {}
parser = OptionParser.new do |o|
o.on('--verbose', '-v') { |b| opts['verbose'] = b }
o.parse!
end
puts "opts: #{opts.inspect}"
puts "ARGV: #{ARGV.inspect}"
if ARGV.length < 2:
puts "Too few arguments"
exit 2
end
# Parse host:port and convert ports to numbers
if ARGV[0].count(":") > 0
opts['listen_host'], _, opts['listen_port'] = ARGV[0].rpartition(':')
else
opts['listen_host'], opts['listen_port'] = GServer::DEFAULT_HOST, ARGV[0]
end
begin
opts['listen_port'] = opts['listen_port'].to_i
rescue
puts "Error parsing listen port"
exit 2
end
if ARGV[1].count(":") > 0
opts['target_host'], _, opts['target_port'] = ARGV[1].rpartition(':')
else
puts "Error parsing target"
exit 2
end
begin
opts['target_port'] = opts['target_port'].to_i
rescue
puts "Error parsing target port"
exit 2
end
puts "Starting server on #{opts['listen_host']}:#{opts['listen_port']}"
server = WebSocketProxy.new(opts['listen_port'], opts['listen_host'], opts)
#server = WebSocketProxy.new(opts['listen_port'])
server.start
loop do
break if server.stopped?
end
puts "Server has been terminated"
# vim: sw=2

66
tests/echo.rb Executable file
View File

@ -0,0 +1,66 @@
#!/usr/bin/env ruby
# A WebSocket server that echos back whatever it receives from the client.
# Copyright 2011 Joel Martin
# Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3)
require 'socket'
$: << "other"
$: << "../other"
require 'websocket'
class WebSocketEcho < WebSocketServer
# Echo back whatever is received
def new_client()
cqueue = []
c_pend = 0
rlist = [@client]
loop do
wlist = []
if cqueue.length > 0 or c_pend
wlist << @client
end
ins, outs, excepts = IO.select(rlist, wlist, nil, 1)
if excepts.length > 0
raise Exception, "Socket exception"
end
if outs.include?(@client)
# Send queued data to the client
c_pend = send_frames(cqueue)
cqueue = []
end
if ins.include?(@client)
# Receive client data, decode it, and send it back
frames, closed = recv_frames
cqueue += frames
#puts "#{@my_client_id}: >#{cqueue.inspect}<"
if closed
raise EClose, closed
end
end
end # loop
end
end
puts "Starting server on port 1234"
server = WebSocketEcho.new(1234)
server.start
loop do
break if server.stopped?
end
puts "Server has been terminated"
# vim: sw=2