Commit 204675c8 authored by Joel Martin's avatar Joel Martin

Update websockify/websock.js.

This change pulls websockify 6d9deda9c5.

Most note worthy changes:
- Pulls in web-socket-js 7677e7a954 which updates to IETF 6455 (from
  Hixie)
- Binary support detection and use in include/websock.js
- Add ssl and unix target support
- Add multiple target support via config file/dir.
- Idle timeout exit
parent 3435491e
// Copyright: Hiroshi Ichikawa <http://gimite.net/en/> // Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
// License: New BSD License // License: New BSD License
// Reference: http://dev.w3.org/html5/websockets/ // Reference: http://dev.w3.org/html5/websockets/
// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol // Reference: http://tools.ietf.org/html/rfc6455
(function() { (function() {
if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) return; if (window.WEB_SOCKET_FORCE_FLASH) {
// Keeps going.
} else if (window.WebSocket) {
return;
} else if (window.MozWebSocket) {
// Firefox.
window.WebSocket = MozWebSocket;
return;
}
var console = window.console; var logger;
if (!console || !console.log || !console.error) { if (window.WEB_SOCKET_LOGGER) {
console = {log: function(){ }, error: function(){ }}; logger = WEB_SOCKET_LOGGER;
} else if (window.console && window.console.log && window.console.error) {
// In some environment, console is defined but console.log or console.error is missing.
logger = window.console;
} else {
logger = {log: function(){ }, error: function(){ }};
} }
if (!swfobject.hasFlashPlayerVersion("10.0.0")) { // swfobject.hasFlashPlayerVersion("10.0.0") doesn't work with Gnash.
console.error("Flash Player >= 10.0.0 is required."); if (swfobject.getFlashPlayerVersion().major < 10) {
logger.error("Flash Player >= 10.0.0 is required.");
return; return;
} }
if (location.protocol == "file:") { if (location.protocol == "file:") {
console.error( logger.error(
"WARNING: web-socket-js doesn't work in file:///... URL " + "WARNING: web-socket-js doesn't work in file:///... URL " +
"unless you set Flash Security Settings properly. " + "unless you set Flash Security Settings properly. " +
"Open the page via Web server i.e. http://..."); "Open the page via Web server i.e. http://...");
} }
/** /**
* This class represents a faux web socket. * Our own implementation of WebSocket class using Flash.
* @param {string} url * @param {string} url
* @param {string} protocol * @param {array or string} protocols
* @param {string} proxyHost * @param {string} proxyHost
* @param {int} proxyPort * @param {int} proxyPort
* @param {string} headers * @param {string} headers
*/ */
WebSocket = function(url, protocol, proxyHost, proxyPort, headers) { window.WebSocket = function(url, protocols, proxyHost, proxyPort, headers) {
var self = this; var self = this;
self.__id = WebSocket.__nextId++; self.__id = WebSocket.__nextId++;
WebSocket.__instances[self.__id] = self; WebSocket.__instances[self.__id] = self;
self.readyState = WebSocket.CONNECTING; self.readyState = WebSocket.CONNECTING;
self.bufferedAmount = 0; self.bufferedAmount = 0;
self.__events = {}; self.__events = {};
if (!protocols) {
protocols = [];
} else if (typeof protocols == "string") {
protocols = [protocols];
}
// Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc. // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc.
// Otherwise, when onopen fires immediately, onopen is called before it is set. // Otherwise, when onopen fires immediately, onopen is called before it is set.
setTimeout(function() { self.__createTask = setTimeout(function() {
WebSocket.__addTask(function() { WebSocket.__addTask(function() {
self.__createTask = null;
WebSocket.__flash.create( WebSocket.__flash.create(
self.__id, url, protocol, proxyHost || null, proxyPort || 0, headers || null); self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null);
}); });
}, 0); }, 0);
}; };
...@@ -78,6 +98,12 @@ ...@@ -78,6 +98,12 @@
* Close this web socket gracefully. * Close this web socket gracefully.
*/ */
WebSocket.prototype.close = function() { WebSocket.prototype.close = function() {
if (this.__createTask) {
clearTimeout(this.__createTask);
this.__createTask = null;
this.readyState = WebSocket.CLOSED;
return;
}
if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) { if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) {
return; return;
} }
...@@ -131,7 +157,7 @@ ...@@ -131,7 +157,7 @@
events[i](event); events[i](event);
} }
var handler = this["on" + event.type]; var handler = this["on" + event.type];
if (handler) handler(event); if (handler) handler.apply(this, [event]);
}; };
/** /**
...@@ -139,16 +165,22 @@ ...@@ -139,16 +165,22 @@
* @param {Object} flashEvent * @param {Object} flashEvent
*/ */
WebSocket.prototype.__handleEvent = function(flashEvent) { WebSocket.prototype.__handleEvent = function(flashEvent) {
if ("readyState" in flashEvent) { if ("readyState" in flashEvent) {
this.readyState = flashEvent.readyState; this.readyState = flashEvent.readyState;
} }
if ("protocol" in flashEvent) {
this.protocol = flashEvent.protocol;
}
var jsEvent; var jsEvent;
if (flashEvent.type == "open" || flashEvent.type == "error") { if (flashEvent.type == "open" || flashEvent.type == "error") {
jsEvent = this.__createSimpleEvent(flashEvent.type); jsEvent = this.__createSimpleEvent(flashEvent.type);
} else if (flashEvent.type == "close") { } else if (flashEvent.type == "close") {
// TODO implement jsEvent.wasClean
jsEvent = this.__createSimpleEvent("close"); jsEvent = this.__createSimpleEvent("close");
jsEvent.wasClean = flashEvent.wasClean ? true : false;
jsEvent.code = flashEvent.code;
jsEvent.reason = flashEvent.reason;
} else if (flashEvent.type == "message") { } else if (flashEvent.type == "message") {
var data = decodeURIComponent(flashEvent.message); var data = decodeURIComponent(flashEvent.message);
jsEvent = this.__createMessageEvent("message", data); jsEvent = this.__createMessageEvent("message", data);
...@@ -157,6 +189,7 @@ ...@@ -157,6 +189,7 @@
} }
this.dispatchEvent(jsEvent); this.dispatchEvent(jsEvent);
}; };
WebSocket.prototype.__createSimpleEvent = function(type) { WebSocket.prototype.__createSimpleEvent = function(type) {
...@@ -188,6 +221,9 @@ ...@@ -188,6 +221,9 @@
WebSocket.CLOSING = 2; WebSocket.CLOSING = 2;
WebSocket.CLOSED = 3; WebSocket.CLOSED = 3;
// Field to check implementation of WebSocket.
WebSocket.__isFlashImplementation = true;
WebSocket.__initialized = false;
WebSocket.__flash = null; WebSocket.__flash = null;
WebSocket.__instances = {}; WebSocket.__instances = {};
WebSocket.__tasks = []; WebSocket.__tasks = [];
...@@ -207,16 +243,31 @@ ...@@ -207,16 +243,31 @@
* Loads WebSocketMain.swf and creates WebSocketMain object in Flash. * Loads WebSocketMain.swf and creates WebSocketMain object in Flash.
*/ */
WebSocket.__initialize = function() { WebSocket.__initialize = function() {
if (WebSocket.__flash) return;
if (WebSocket.__initialized) return;
WebSocket.__initialized = true;
if (WebSocket.__swfLocation) { if (WebSocket.__swfLocation) {
// For backword compatibility. // For backword compatibility.
window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation; window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation;
} }
if (!window.WEB_SOCKET_SWF_LOCATION) { if (!window.WEB_SOCKET_SWF_LOCATION) {
console.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf"); logger.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf");
return; return;
} }
if (!window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR &&
!WEB_SOCKET_SWF_LOCATION.match(/(^|\/)WebSocketMainInsecure\.swf(\?.*)?$/) &&
WEB_SOCKET_SWF_LOCATION.match(/^\w+:\/\/([^\/]+)/)) {
var swfHost = RegExp.$1;
if (location.host != swfHost) {
logger.error(
"[WebSocket] You must host HTML and WebSocketMain.swf in the same host " +
"('" + location.host + "' != '" + swfHost + "'). " +
"See also 'How to host HTML file and SWF file in different domains' section " +
"in README.md. If you use WebSocketMainInsecure.swf, you can suppress this message " +
"by WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;");
}
}
var container = document.createElement("div"); var container = document.createElement("div");
container.id = "webSocketContainer"; container.id = "webSocketContainer";
// Hides Flash box. We cannot use display: none or visibility: hidden because it prevents // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents
...@@ -250,9 +301,11 @@ ...@@ -250,9 +301,11 @@
null, null,
function(e) { function(e) {
if (!e.success) { if (!e.success) {
console.error("[WebSocket] swfobject.embedSWF failed"); logger.error("[WebSocket] swfobject.embedSWF failed");
} }
}); }
);
}; };
/** /**
...@@ -287,7 +340,7 @@ ...@@ -287,7 +340,7 @@
WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]); WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]);
} }
} catch (e) { } catch (e) {
console.error(e); logger.error(e);
} }
}, 0); }, 0);
return true; return true;
...@@ -295,12 +348,12 @@ ...@@ -295,12 +348,12 @@
// Called by Flash. // Called by Flash.
WebSocket.__log = function(message) { WebSocket.__log = function(message) {
console.log(decodeURIComponent(message)); logger.log(decodeURIComponent(message));
}; };
// Called by Flash. // Called by Flash.
WebSocket.__error = function(message) { WebSocket.__error = function(message) {
console.error(decodeURIComponent(message)); logger.error(decodeURIComponent(message));
}; };
WebSocket.__addTask = function(task) { WebSocket.__addTask = function(task) {
...@@ -327,15 +380,12 @@ ...@@ -327,15 +380,12 @@
}; };
if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) { if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) {
if (window.addEventListener) { // NOTE:
window.addEventListener("load", function(){ // This fires immediately if web_socket.js is dynamically loaded after
WebSocket.__initialize(); // the document is loaded.
}, false); swfobject.addDomLoadEvent(function() {
} else {
window.attachEvent("onload", function(){
WebSocket.__initialize(); WebSocket.__initialize();
}); });
} }
}
})(); })();
...@@ -128,9 +128,7 @@ function rQshiftStr(len) { ...@@ -128,9 +128,7 @@ function rQshiftStr(len) {
if (typeof(len) === 'undefined') { len = rQlen(); } if (typeof(len) === 'undefined') { len = rQlen(); }
var arr = rQ.slice(rQi, rQi + len); var arr = rQ.slice(rQi, rQi + len);
rQi += len; rQi += len;
return arr.map(function (num) { return String.fromCharCode.apply(null, arr);
return String.fromCharCode(num); } ).join('');
} }
function rQshiftBytes(len) { function rQshiftBytes(len) {
if (typeof(len) === 'undefined') { len = rQlen(); } if (typeof(len) === 'undefined') { len = rQlen(); }
...@@ -313,11 +311,19 @@ function init(protocols) { ...@@ -313,11 +311,19 @@ function init(protocols) {
throw("WebSocket binary sub-protocol requested but not supported"); throw("WebSocket binary sub-protocol requested but not supported");
} }
if (typeof(protocols) === "object") { if (typeof(protocols) === "object") {
var new_protocols = [];
for (var i = 0; i < protocols.length; i++) { for (var i = 0; i < protocols.length; i++) {
if (protocols[i] === 'binary') { if (protocols[i] === 'binary') {
throw("WebSocket binary sub-protocol requested but not supported"); Util.Error("Skipping unsupported WebSocket binary sub-protocol");
} else {
new_protocols.push(protocols[i]);
} }
} }
if (new_protocols.length > 0) {
protocols = new_protocols;
} else {
throw("Only WebSocket binary sub-protocol was requested and not supported.");
}
} }
} }
......
...@@ -18,7 +18,6 @@ as taken from http://docs.python.org/dev/library/ssl.html#certificates ...@@ -18,7 +18,6 @@ as taken from http://docs.python.org/dev/library/ssl.html#certificates
import os, sys, time, errno, signal, socket, traceback, select import os, sys, time, errno, signal, socket, traceback, select
import array, struct import array, struct
from cgi import parse_qsl
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
# Imports that vary by python version # Imports that vary by python version
...@@ -36,8 +35,6 @@ try: from io import StringIO ...@@ -36,8 +35,6 @@ try: from io import StringIO
except: from cStringIO import StringIO except: from cStringIO import StringIO
try: from http.server import SimpleHTTPRequestHandler try: from http.server import SimpleHTTPRequestHandler
except: from SimpleHTTPServer import SimpleHTTPRequestHandler except: from SimpleHTTPServer import SimpleHTTPRequestHandler
try: from urllib.parse import urlsplit
except: from urlparse import urlsplit
# python 2.6 differences # python 2.6 differences
try: from hashlib import md5, sha1 try: from hashlib import md5, sha1
...@@ -75,6 +72,7 @@ class WebSocketServer(object): ...@@ -75,6 +72,7 @@ class WebSocketServer(object):
buffer_size = 65536 buffer_size = 65536
server_handshake_hixie = """HTTP/1.1 101 Web Socket Protocol Handshake\r server_handshake_hixie = """HTTP/1.1 101 Web Socket Protocol Handshake\r
Upgrade: WebSocket\r Upgrade: WebSocket\r
Connection: Upgrade\r Connection: Upgrade\r
...@@ -103,16 +101,18 @@ Sec-WebSocket-Accept: %s\r ...@@ -103,16 +101,18 @@ Sec-WebSocket-Accept: %s\r
def __init__(self, listen_host='', listen_port=None, source_is_ipv6=False, def __init__(self, listen_host='', listen_port=None, source_is_ipv6=False,
verbose=False, cert='', key='', ssl_only=None, verbose=False, cert='', key='', ssl_only=None,
daemon=False, record='', web='', daemon=False, record='', web='',
run_once=False, timeout=0): run_once=False, timeout=0, idle_timeout=0):
# settings # settings
self.verbose = verbose self.verbose = verbose
self.listen_host = listen_host self.listen_host = listen_host
self.listen_port = listen_port self.listen_port = listen_port
self.prefer_ipv6 = source_is_ipv6
self.ssl_only = ssl_only self.ssl_only = ssl_only
self.daemon = daemon self.daemon = daemon
self.run_once = run_once self.run_once = run_once
self.timeout = timeout self.timeout = timeout
self.idle_timeout = idle_timeout
self.launch_time = time.time() self.launch_time = time.time()
self.ws_connection = False self.ws_connection = False
...@@ -163,7 +163,7 @@ Sec-WebSocket-Accept: %s\r ...@@ -163,7 +163,7 @@ Sec-WebSocket-Accept: %s\r
# #
@staticmethod @staticmethod
def socket(host, port=None, connect=False, prefer_ipv6=False): def socket(host, port=None, connect=False, prefer_ipv6=False, unix_socket=None, use_ssl=False):
""" Resolve a host (and optional port) to an IPv4 or IPv6 """ Resolve a host (and optional port) to an IPv4 or IPv6
address. Create a socket. Bind to it if listen is set, address. Create a socket. Bind to it if listen is set,
otherwise connect to it. Return the socket. otherwise connect to it. Return the socket.
...@@ -171,24 +171,36 @@ Sec-WebSocket-Accept: %s\r ...@@ -171,24 +171,36 @@ Sec-WebSocket-Accept: %s\r
flags = 0 flags = 0
if host == '': if host == '':
host = None host = None
if connect and not port: if connect and not (port or unix_socket):
raise Exception("Connect mode requires a port") raise Exception("Connect mode requires a port")
if use_ssl and not ssl:
raise Exception("SSL socket requested but Python SSL module not loaded.");
if not connect and use_ssl:
raise Exception("SSL only supported in connect mode (for now)")
if not connect: if not connect:
flags = flags | socket.AI_PASSIVE flags = flags | socket.AI_PASSIVE
if not unix_socket:
addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM, addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM,
socket.IPPROTO_TCP, flags) socket.IPPROTO_TCP, flags)
if not addrs: if not addrs:
raise Exception("Could resolve host '%s'" % host) raise Exception("Could not resolve host '%s'" % host)
addrs.sort(key=lambda x: x[0]) addrs.sort(key=lambda x: x[0])
if prefer_ipv6: if prefer_ipv6:
addrs.reverse() addrs.reverse()
sock = socket.socket(addrs[0][0], addrs[0][1]) sock = socket.socket(addrs[0][0], addrs[0][1])
if connect: if connect:
sock.connect(addrs[0][4]) sock.connect(addrs[0][4])
if use_ssl:
sock = ssl.wrap_socket(sock)
else: else:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(addrs[0][4]) sock.bind(addrs[0][4])
sock.listen(100) sock.listen(100)
else:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(unix_socket)
return sock return sock
@staticmethod @staticmethod
...@@ -552,6 +564,72 @@ Sec-WebSocket-Accept: %s\r ...@@ -552,6 +564,72 @@ Sec-WebSocket-Accept: %s\r
# No orderly close for 75 # No orderly close for 75
def do_websocket_handshake(self, headers, path):
h = self.headers = headers
self.path = path
prot = 'WebSocket-Protocol'
protocols = h.get('Sec-'+prot, h.get(prot, '')).split(',')
ver = h.get('Sec-WebSocket-Version')
if ver:
# HyBi/IETF version of the protocol
# HyBi-07 report version 7
# HyBi-08 - HyBi-12 report version 8
# HyBi-13 reports version 13
if ver in ['7', '8', '13']:
self.version = "hybi-%02d" % int(ver)
else:
raise self.EClose('Unsupported protocol version %s' % ver)
key = h['Sec-WebSocket-Key']
# Choose binary if client supports it
if 'binary' in protocols:
self.base64 = False
elif 'base64' in protocols:
self.base64 = True
else:
raise self.EClose("Client must support 'binary' or 'base64' protocol")
# Generate the hash value for the accept header
accept = b64encode(sha1(s2b(key + self.GUID)).digest())
response = self.server_handshake_hybi % b2s(accept)
if self.base64:
response += "Sec-WebSocket-Protocol: base64\r\n"
else:
response += "Sec-WebSocket-Protocol: binary\r\n"
response += "\r\n"
else:
# Hixie version of the protocol (75 or 76)
if h.get('key3'):
trailer = self.gen_md5(h)
pre = "Sec-"
self.version = "hixie-76"
else:
trailer = ""
pre = ""
self.version = "hixie-75"
# We only support base64 in Hixie era
self.base64 = True
response = self.server_handshake_hixie % (pre,
h['Origin'], pre, self.scheme, h['Host'], path)
if 'base64' in protocols:
response += "%sWebSocket-Protocol: base64\r\n" % pre
else:
self.msg("Warning: client does not report 'base64' protocol support")
response += "\r\n" + trailer
return response
def do_handshake(self, sock, address): def do_handshake(self, sock, address):
""" """
do_handshake does the following: do_handshake does the following:
...@@ -569,10 +647,10 @@ Sec-WebSocket-Accept: %s\r ...@@ -569,10 +647,10 @@ Sec-WebSocket-Accept: %s\r
- Send a WebSockets handshake server response. - Send a WebSockets handshake server response.
- Return the socket for this WebSocket client. - Return the socket for this WebSocket client.
""" """
stype = "" stype = ""
ready = select.select([sock], [], [], 3)[0] ready = select.select([sock], [], [], 3)[0]
if not ready: if not ready:
raise self.EClose("ignoring socket not ready") raise self.EClose("ignoring socket not ready")
# Peek, but do not read the data so that we have a opportunity # Peek, but do not read the data so that we have a opportunity
...@@ -613,7 +691,7 @@ Sec-WebSocket-Accept: %s\r ...@@ -613,7 +691,7 @@ Sec-WebSocket-Accept: %s\r
else: else:
raise raise
scheme = "wss" self.scheme = "wss"
stype = "SSL/TLS (wss://)" stype = "SSL/TLS (wss://)"
elif self.ssl_only: elif self.ssl_only:
...@@ -621,7 +699,7 @@ Sec-WebSocket-Accept: %s\r ...@@ -621,7 +699,7 @@ Sec-WebSocket-Accept: %s\r
else: else:
retsock = sock retsock = sock
scheme = "ws" self.scheme = "ws"
stype = "Plain non-SSL (ws://)" stype = "Plain non-SSL (ws://)"
wsh = WSRequestHandler(retsock, address, not self.web) wsh = WSRequestHandler(retsock, address, not self.web)
...@@ -637,67 +715,7 @@ Sec-WebSocket-Accept: %s\r ...@@ -637,67 +715,7 @@ Sec-WebSocket-Accept: %s\r
else: else:
raise self.EClose("") raise self.EClose("")
h = self.headers = wsh.headers response = self.do_websocket_handshake(wsh.headers, wsh.path)
path = self.path = wsh.path
prot = 'WebSocket-Protocol'
protocols = h.get('Sec-'+prot, h.get(prot, '')).split(',')
ver = h.get('Sec-WebSocket-Version')
if ver:
# HyBi/IETF version of the protocol
# HyBi-07 report version 7
# HyBi-08 - HyBi-12 report version 8
# HyBi-13 reports version 13
if ver in ['7', '8', '13']:
self.version = "hybi-%02d" % int(ver)
else:
raise self.EClose('Unsupported protocol version %s' % ver)
key = h['Sec-WebSocket-Key']
# Choose binary if client supports it
if 'binary' in protocols:
self.base64 = False
elif 'base64' in protocols:
self.base64 = True
else:
raise self.EClose("Client must support 'binary' or 'base64' protocol")
# Generate the hash value for the accept header
accept = b64encode(sha1(s2b(key + self.GUID)).digest())
response = self.server_handshake_hybi % b2s(accept)
if self.base64:
response += "Sec-WebSocket-Protocol: base64\r\n"
else:
response += "Sec-WebSocket-Protocol: binary\r\n"
response += "\r\n"
else:
# Hixie version of the protocol (75 or 76)
if h.get('key3'):
trailer = self.gen_md5(h)
pre = "Sec-"
self.version = "hixie-76"
else:
trailer = ""
pre = ""
self.version = "hixie-75"
# We only support base64 in Hixie era
self.base64 = True
response = self.server_handshake_hixie % (pre,
h['Origin'], pre, scheme, h['Host'], path)
if 'base64' in protocols:
response += "%sWebSocket-Protocol: base64\r\n" % pre
else:
self.msg("Warning: client does not report 'base64' protocol support")
response += "\r\n" + trailer
self.msg("%s: %s WebSocket connection" % (address[0], stype)) self.msg("%s: %s WebSocket connection" % (address[0], stype))
self.msg("%s: Version %s, base64: '%s'" % (address[0], self.msg("%s: Version %s, base64: '%s'" % (address[0],
...@@ -801,7 +819,7 @@ Sec-WebSocket-Accept: %s\r ...@@ -801,7 +819,7 @@ Sec-WebSocket-Accept: %s\r
is a WebSockets client then call new_client() method (which must is a WebSockets client then call new_client() method (which must
be overridden) for each new client connection. be overridden) for each new client connection.
""" """
lsock = self.socket(self.listen_host, self.listen_port) lsock = self.socket(self.listen_host, self.listen_port, False, self.prefer_ipv6)
if self.daemon: if self.daemon:
self.daemonize(keepfd=lsock.fileno(), chdir=self.web) self.daemonize(keepfd=lsock.fileno(), chdir=self.web)
...@@ -814,12 +832,17 @@ Sec-WebSocket-Accept: %s\r ...@@ -814,12 +832,17 @@ Sec-WebSocket-Accept: %s\r
# os.fork() (python 2.4) child reaper # os.fork() (python 2.4) child reaper
signal.signal(signal.SIGCHLD, self.fallback_SIGCHLD) signal.signal(signal.SIGCHLD, self.fallback_SIGCHLD)
last_active_time = self.launch_time
while True: while True:
try: try:
try: try:
self.client = None self.client = None
startsock = None startsock = None
pid = err = 0 pid = err = 0
child_count = 0
if multiprocessing and self.idle_timeout:
child_count = len(multiprocessing.active_children())
time_elapsed = time.time() - self.launch_time time_elapsed = time.time() - self.launch_time
if self.timeout and time_elapsed > self.timeout: if self.timeout and time_elapsed > self.timeout:
...@@ -827,6 +850,19 @@ Sec-WebSocket-Accept: %s\r ...@@ -827,6 +850,19 @@ Sec-WebSocket-Accept: %s\r
% self.timeout) % self.timeout)
break break
if self.idle_timeout:
idle_time = 0
if child_count == 0:
idle_time = time.time() - last_active_time
else:
idle_time = 0
last_active_time = time.time()
if idle_time > self.idle_timeout and child_count == 0:
self.msg('listener exit due to --idle-timeout %s'
% self.idle_timeout)
break
try: try:
self.poll() self.poll()
...@@ -927,4 +963,3 @@ class WSRequestHandler(SimpleHTTPRequestHandler): ...@@ -927,4 +963,3 @@ class WSRequestHandler(SimpleHTTPRequestHandler):
def log_message(self, f, *args): def log_message(self, f, *args):
# Save instead of printing # Save instead of printing
self.last_message = f % args self.last_message = f % args
...@@ -13,9 +13,11 @@ as taken from http://docs.python.org/dev/library/ssl.html#certificates ...@@ -13,9 +13,11 @@ as taken from http://docs.python.org/dev/library/ssl.html#certificates
import socket, optparse, time, os, sys, subprocess import socket, optparse, time, os, sys, subprocess
from select import select from select import select
from websocket import WebSocketServer import websocket
try: from urllib.parse import parse_qs, urlparse
except: from urlparse import parse_qs, urlparse
class WebSocketProxy(WebSocketServer): class WebSocketProxy(websocket.WebSocketServer):
""" """
Proxy traffic to and from a WebSockets client to a normal TCP Proxy traffic to and from a WebSockets client to a normal TCP
socket server target. All traffic to/from the client is base64 socket server target. All traffic to/from the client is base64
...@@ -43,6 +45,9 @@ Traffic Legend: ...@@ -43,6 +45,9 @@ Traffic Legend:
self.target_port = kwargs.pop('target_port') self.target_port = kwargs.pop('target_port')
self.wrap_cmd = kwargs.pop('wrap_cmd') self.wrap_cmd = kwargs.pop('wrap_cmd')
self.wrap_mode = kwargs.pop('wrap_mode') self.wrap_mode = kwargs.pop('wrap_mode')
self.unix_target = kwargs.pop('unix_target')
self.ssl_target = kwargs.pop('ssl_target')
self.target_cfg = kwargs.pop('target_cfg')
# Last 3 timestamps command was run # Last 3 timestamps command was run
self.wrap_times = [0, 0, 0] self.wrap_times = [0, 0, 0]
...@@ -58,6 +63,7 @@ Traffic Legend: ...@@ -58,6 +63,7 @@ Traffic Legend:
if not self.rebinder: if not self.rebinder:
raise Exception("rebind.so not found, perhaps you need to run make") raise Exception("rebind.so not found, perhaps you need to run make")
self.rebinder = os.path.abspath(self.rebinder)
self.target_host = "127.0.0.1" # Loopback self.target_host = "127.0.0.1" # Loopback
# Find a free high port # Find a free high port
...@@ -71,7 +77,10 @@ Traffic Legend: ...@@ -71,7 +77,10 @@ Traffic Legend:
"REBIND_OLD_PORT": str(kwargs['listen_port']), "REBIND_OLD_PORT": str(kwargs['listen_port']),
"REBIND_NEW_PORT": str(self.target_port)}) "REBIND_NEW_PORT": str(self.target_port)})
WebSocketServer.__init__(self, *args, **kwargs) if self.target_cfg:
self.target_cfg = os.path.abspath(self.target_cfg)
websocket.WebSocketServer.__init__(self, *args, **kwargs)
def run_wrap_cmd(self): def run_wrap_cmd(self):
print("Starting '%s'" % " ".join(self.wrap_cmd)) print("Starting '%s'" % " ".join(self.wrap_cmd))
...@@ -88,14 +97,26 @@ Traffic Legend: ...@@ -88,14 +97,26 @@ Traffic Legend:
# Need to call wrapped command after daemonization so we can # Need to call wrapped command after daemonization so we can
# know when the wrapped command exits # know when the wrapped command exits
if self.wrap_cmd: if self.wrap_cmd:
print(" - proxying from %s:%s to '%s' (port %s)\n" % ( dst_string = "'%s' (port %s)" % (" ".join(self.wrap_cmd), self.target_port)
self.listen_host, self.listen_port, elif self.unix_target:
" ".join(self.wrap_cmd), self.target_port)) dst_string = self.unix_target
self.run_wrap_cmd()
else: else:
print(" - proxying from %s:%s to %s:%s\n" % ( dst_string = "%s:%s" % (self.target_host, self.target_port)
self.listen_host, self.listen_port,
self.target_host, self.target_port)) if self.target_cfg:
msg = " - proxying from %s:%s to targets in %s" % (
self.listen_host, self.listen_port, self.target_cfg)
else:
msg = " - proxying from %s:%s to %s" % (
self.listen_host, self.listen_port, dst_string)
if self.ssl_target:
msg += " (using SSL)"
print(msg + "\n")
if self.wrap_cmd:
self.run_wrap_cmd()
def poll(self): def poll(self):
# If we are wrapping a command, check it's status # If we are wrapping a command, check it's status
...@@ -137,12 +158,26 @@ Traffic Legend: ...@@ -137,12 +158,26 @@ Traffic Legend:
""" """
Called after a new WebSocket connection has been established. Called after a new WebSocket connection has been established.
""" """
# Checks if we receive a token, and look
# for a valid target for it then
if self.target_cfg:
(self.target_host, self.target_port) = self.get_target(self.target_cfg, self.path)
# Connect to the target # Connect to the target
self.msg("connecting to: %s:%s" % ( if self.wrap_cmd:
self.target_host, self.target_port)) msg = "connecting to command: %s" % (" ".join(self.wrap_cmd), self.target_port)
elif self.unix_target:
msg = "connecting to unix socket: %s" % self.unix_target
else:
msg = "connecting to: %s:%s" % (
self.target_host, self.target_port)
if self.ssl_target:
msg += " (using SSL)"
self.msg(msg)
tsock = self.socket(self.target_host, self.target_port, tsock = self.socket(self.target_host, self.target_port,
connect=True) connect=True, use_ssl=self.ssl_target, unix_socket=self.unix_target)
if self.verbose and not self.daemon: if self.verbose and not self.daemon:
print(self.traffic_legend) print(self.traffic_legend)
...@@ -154,10 +189,49 @@ Traffic Legend: ...@@ -154,10 +189,49 @@ Traffic Legend:
if tsock: if tsock:
tsock.shutdown(socket.SHUT_RDWR) tsock.shutdown(socket.SHUT_RDWR)
tsock.close() tsock.close()
self.vmsg("%s:%s: Target closed" %( self.vmsg("%s:%s: Closed target" %(
self.target_host, self.target_port)) self.target_host, self.target_port))
raise raise
def get_target(self, target_cfg, path):
"""
Parses the path, extracts a token, and looks for a valid
target for that token in the configuration file(s). Sets
target_host and target_port if successful
"""
# The files in targets contain the lines
# in the form of token: host:port
# Extract the token parameter from url
args = parse_qs(urlparse(path)[4]) # 4 is the query from url
if not len(args['token']):
raise self.EClose("Token not present")
token = args['token'][0].rstrip('\n')
# target_cfg can be a single config file or directory of
# config files
if os.path.isdir(target_cfg):
cfg_files = [os.path.join(target_cfg, f)
for f in os.listdir(target_cfg)]
else:
cfg_files = [target_cfg]
targets = {}
for f in cfg_files:
for line in [l.strip() for l in file(f).readlines()]:
if line and not line.startswith('#'):
ttoken, target = line.split(': ')
targets[ttoken] = target.strip()
self.vmsg("Target config: %s" % repr(targets))
if targets.has_key(token):
return targets[token].split(':')
else:
raise self.EClose("Token '%s' not found" % token)
def do_proxy(self, target): def do_proxy(self, target):
""" """
Proxy client WebSocket to normal target socket. Proxy client WebSocket to normal target socket.
...@@ -191,6 +265,8 @@ Traffic Legend: ...@@ -191,6 +265,8 @@ Traffic Legend:
# Receive target data, encode it and queue for client # Receive target data, encode it and queue for client
buf = target.recv(self.buffer_size) buf = target.recv(self.buffer_size)
if len(buf) == 0: if len(buf) == 0:
self.vmsg("%s:%s: Target closed connection" %(
self.target_host, self.target_port))
raise self.CClose(1000, "Target closed") raise self.CClose(1000, "Target closed")
cqueue.append(buf) cqueue.append(buf)
...@@ -211,11 +287,13 @@ Traffic Legend: ...@@ -211,11 +287,13 @@ Traffic Legend:
if closed: if closed:
# TODO: What about blocking on client socket? # TODO: What about blocking on client socket?
self.vmsg("%s:%s: Client closed connection" %(
self.target_host, self.target_port))
raise self.CClose(closed['code'], closed['reason']) raise self.CClose(closed['code'], closed['reason'])
def websockify_init(): def websockify_init():
usage = "\n %prog [options]" usage = "\n %prog [options]"
usage += " [source_addr:]source_port target_addr:target_port" usage += " [source_addr:]source_port [target_addr:target_port]"
usage += "\n %prog [options]" usage += "\n %prog [options]"
usage += " [source_addr:]source_port -- WRAP_COMMAND_LINE" usage += " [source_addr:]source_port -- WRAP_COMMAND_LINE"
parser = optparse.OptionParser(usage=usage) parser = optparse.OptionParser(usage=usage)
...@@ -230,22 +308,37 @@ def websockify_init(): ...@@ -230,22 +308,37 @@ def websockify_init():
help="handle a single WebSocket connection and exit") help="handle a single WebSocket connection and exit")
parser.add_option("--timeout", type=int, default=0, parser.add_option("--timeout", type=int, default=0,
help="after TIMEOUT seconds exit when not connected") help="after TIMEOUT seconds exit when not connected")
parser.add_option("--idle-timeout", type=int, default=0,
help="server exits after TIMEOUT seconds if there are no "
"active connections")
parser.add_option("--cert", default="self.pem", parser.add_option("--cert", default="self.pem",
help="SSL certificate file") help="SSL certificate file")
parser.add_option("--key", default=None, parser.add_option("--key", default=None,
help="SSL key file (if separate from cert)") help="SSL key file (if separate from cert)")
parser.add_option("--ssl-only", action="store_true", parser.add_option("--ssl-only", action="store_true",
help="disallow non-encrypted connections") help="disallow non-encrypted client connections")
parser.add_option("--ssl-target", action="store_true",
help="connect to SSL target as SSL client")
parser.add_option("--unix-target",
help="connect to unix socket target", metavar="FILE")
parser.add_option("--web", default=None, metavar="DIR", parser.add_option("--web", default=None, metavar="DIR",
help="run webserver on same port. Serve files from DIR.") help="run webserver on same port. Serve files from DIR.")
parser.add_option("--wrap-mode", default="exit", metavar="MODE", parser.add_option("--wrap-mode", default="exit", metavar="MODE",
choices=["exit", "ignore", "respawn"], choices=["exit", "ignore", "respawn"],
help="action to take when the wrapped program exits " help="action to take when the wrapped program exits "
"or daemonizes: exit (default), ignore, respawn") "or daemonizes: exit (default), ignore, respawn")
parser.add_option("--prefer-ipv6", "-6",
action="store_true", dest="source_is_ipv6",
help="prefer IPv6 when resolving source_addr")
parser.add_option("--target-config", metavar="FILE",
dest="target_cfg",
help="Configuration file containing valid targets "
"in the form 'token: host:port' or, alternatively, a "
"directory containing configuration files of this form")
(opts, args) = parser.parse_args() (opts, args) = parser.parse_args()
# Sanity checks # Sanity checks
if len(args) < 2: if len(args) < 2 and not opts.target_cfg:
parser.error("Too few arguments") parser.error("Too few arguments")
if sys.argv.count('--'): if sys.argv.count('--'):
opts.wrap_cmd = args[1:] opts.wrap_cmd = args[1:]
...@@ -254,24 +347,29 @@ def websockify_init(): ...@@ -254,24 +347,29 @@ def websockify_init():
if len(args) > 2: if len(args) > 2:
parser.error("Too many arguments") parser.error("Too many arguments")
if not websocket.ssl and opts.ssl_target:
parser.error("SSL target requested and Python SSL module not loaded.");
if opts.ssl_only and not os.path.exists(opts.cert): if opts.ssl_only and not os.path.exists(opts.cert):
parser.error("SSL only and %s not found" % opts.cert) parser.error("SSL only and %s not found" % opts.cert)
# Parse host:port and convert ports to numbers # Parse host:port and convert ports to numbers
if args[0].count(':') > 0: if args[0].count(':') > 0:
opts.listen_host, opts.listen_port = args[0].rsplit(':', 1) opts.listen_host, opts.listen_port = args[0].rsplit(':', 1)
opts.listen_host = opts.listen_host.strip('[]')
else: else:
opts.listen_host, opts.listen_port = '', args[0] opts.listen_host, opts.listen_port = '', args[0]
try: opts.listen_port = int(opts.listen_port) try: opts.listen_port = int(opts.listen_port)
except: parser.error("Error parsing listen port") except: parser.error("Error parsing listen port")
if opts.wrap_cmd: if opts.wrap_cmd or opts.unix_target or opts.target_cfg:
opts.target_host = None opts.target_host = None
opts.target_port = None opts.target_port = None
else: else:
if args[1].count(':') > 0: if args[1].count(':') > 0:
opts.target_host, opts.target_port = args[1].rsplit(':', 1) opts.target_host, opts.target_port = args[1].rsplit(':', 1)
opts.target_host = opts.target_host.strip('[]')
else: else:
parser.error("Error parsing target") parser.error("Error parsing target")
try: opts.target_port = int(opts.target_port) try: opts.target_port = int(opts.target_port)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment