diff --git a/README.md b/README.md
index 5c705c8..4dff117 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,7 @@ implementations:
websockify |
other/websockify |
other/websockify.js |
+ other/websockify.rb |
other/kumina |
VNCAuthProxy 1 |
@@ -83,6 +84,7 @@ implementations:
| python |
C |
Node (node.js) |
+ Ruby |
C |
python (twisted) |
@@ -91,6 +93,7 @@ implementations:
| yes |
yes |
no |
+ no |
yes |
| Daemon |
@@ -98,6 +101,7 @@ implementations:
yes |
no |
no |
+ no |
yes |
| SSL wss |
@@ -105,12 +109,14 @@ implementations:
yes |
no |
no |
+ no |
yes |
| Flash Policy Server |
yes |
yes |
no |
+ no |
yes |
no |
@@ -120,6 +126,7 @@ implementations:
| no |
no |
no |
+ no |
| Web Server |
yes |
@@ -127,6 +134,7 @@ implementations:
no |
no |
no |
+ no |
| Program Wrap |
yes |
@@ -134,11 +142,13 @@ implementations:
no |
no |
no |
+ no |
| Multiple Targets |
no |
no |
no |
+ no |
yes |
no |
@@ -148,6 +158,7 @@ implementations:
| yes |
no |
no |
+ no |
| Hixie 76 |
yes |
@@ -155,12 +166,14 @@ implementations:
yes |
yes |
yes |
+ yes |
| IETF/HyBi 07-10 |
yes |
no |
no |
no |
+ no |
yes |
diff --git a/other/websocket.rb b/other/websocket.rb
new file mode 100644
index 0000000..336dfe6
--- /dev/null
+++ b/other/websocket.rb
@@ -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
diff --git a/other/websockify.rb b/other/websockify.rb
new file mode 100755
index 0000000..f4456b6
--- /dev/null
+++ b/other/websockify.rb
@@ -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
diff --git a/tests/echo.rb b/tests/echo.rb
new file mode 100755
index 0000000..25f6e72
--- /dev/null
+++ b/tests/echo.rb
@@ -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