From 6b9d6c39beb1c9dd71da4a75ee6ade41f346c23a Mon Sep 17 00:00:00 2001 From: Joel Martin Date: Tue, 4 Oct 2011 01:20:14 -0500 Subject: [PATCH] Add ruby version of websockify. Initial version is very basic but works: Hixie-76 only, no embedded webserver, no SSL, etc. --- README.md | 13 +++ other/websocket.rb | 250 ++++++++++++++++++++++++++++++++++++++++++++ other/websockify.rb | 161 ++++++++++++++++++++++++++++ tests/echo.rb | 66 ++++++++++++ 4 files changed, 490 insertions(+) create mode 100644 other/websocket.rb create mode 100755 other/websockify.rb create mode 100755 tests/echo.rb 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