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:
parent
55e57c2048
commit
6b9d6c39be
13
README.md
13
README.md
|
@ -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>
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue