4
A WebSocket to TCP socket proxy with support for "wss://" encryption.
5
Copyright 2011 Joel Martin
6
Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3)
8
You can make a cert/key with openssl using:
9
openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem
10
as taken from http://docs.python.org/dev/library/ssl.html#certificates
14
import signal, socket, optparse, time, os, sys, subprocess
15
from select import select
18
from urllib.parse import parse_qs, urlparse
20
from cgi import parse_qs
21
from urlparse import urlparse
23
class WebSocketProxy(websocket.WebSocketServer):
25
Proxy traffic to and from a WebSockets client to a normal TCP
26
socket server target. All traffic to/from the client is base64
27
encoded/decoded to allow binary data to be sent/received to/from
36
}. - Client receive partial
40
>. - Target send partial
42
<. - Client send partial
45
def __init__(self, *args, **kwargs):
46
# Save off proxy specific options
47
self.target_host = kwargs.pop('target_host', None)
48
self.target_port = kwargs.pop('target_port', None)
49
self.wrap_cmd = kwargs.pop('wrap_cmd', None)
50
self.wrap_mode = kwargs.pop('wrap_mode', None)
51
self.unix_target = kwargs.pop('unix_target', None)
52
self.ssl_target = kwargs.pop('ssl_target', None)
53
self.target_cfg = kwargs.pop('target_cfg', None)
54
# Last 3 timestamps command was run
55
self.wrap_times = [0, 0, 0]
58
rebinder_path = ['./', os.path.dirname(sys.argv[0])]
61
for rdir in rebinder_path:
62
rpath = os.path.join(rdir, "rebind.so")
63
if os.path.exists(rpath):
68
raise Exception("rebind.so not found, perhaps you need to run make")
69
self.rebinder = os.path.abspath(self.rebinder)
71
self.target_host = "127.0.0.1" # Loopback
72
# Find a free high port
73
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
75
self.target_port = sock.getsockname()[1]
79
"LD_PRELOAD": self.rebinder,
80
"REBIND_OLD_PORT": str(kwargs['listen_port']),
81
"REBIND_NEW_PORT": str(self.target_port)})
84
self.target_cfg = os.path.abspath(self.target_cfg)
86
websocket.WebSocketServer.__init__(self, *args, **kwargs)
88
def run_wrap_cmd(self):
89
print("Starting '%s'" % " ".join(self.wrap_cmd))
90
self.wrap_times.append(time.time())
91
self.wrap_times.pop(0)
92
self.cmd = subprocess.Popen(
93
self.wrap_cmd, env=os.environ, preexec_fn=_subprocess_setup)
94
self.spawn_message = True
98
Called after Websockets server startup (i.e. after daemonize)
100
# Need to call wrapped command after daemonization so we can
101
# know when the wrapped command exits
103
dst_string = "'%s' (port %s)" % (" ".join(self.wrap_cmd), self.target_port)
104
elif self.unix_target:
105
dst_string = self.unix_target
107
dst_string = "%s:%s" % (self.target_host, self.target_port)
110
msg = " - proxying from %s:%s to targets in %s" % (
111
self.listen_host, self.listen_port, self.target_cfg)
113
msg = " - proxying from %s:%s to %s" % (
114
self.listen_host, self.listen_port, dst_string)
117
msg += " (using SSL)"
125
# If we are wrapping a command, check it's status
127
if self.wrap_cmd and self.cmd:
128
ret = self.cmd.poll()
130
self.vmsg("Wrapped command exited (or daemon). Returned %s" % ret)
133
if self.wrap_cmd and self.cmd == None:
134
# Response to wrapped command being gone
135
if self.wrap_mode == "ignore":
137
elif self.wrap_mode == "exit":
139
elif self.wrap_mode == "respawn":
141
avg = sum(self.wrap_times)/len(self.wrap_times)
143
# 3 times in the last 10 seconds
144
if self.spawn_message:
145
print("Command respawning too fast")
146
self.spawn_message = False
151
# Routines above this point are run in the master listener
156
# Routines below this point are connection handler routines and
157
# will be run in a separate forked process for each connection.
160
def new_client(self):
162
Called after a new WebSocket connection has been established.
164
# Checks if we receive a token, and look
165
# for a valid target for it then
167
(self.target_host, self.target_port) = self.get_target(self.target_cfg, self.path)
169
# Connect to the target
171
msg = "connecting to command: '%s' (port %s)" % (" ".join(self.wrap_cmd), self.target_port)
172
elif self.unix_target:
173
msg = "connecting to unix socket: %s" % self.unix_target
175
msg = "connecting to: %s:%s" % (
176
self.target_host, self.target_port)
179
msg += " (using SSL)"
182
tsock = self.socket(self.target_host, self.target_port,
183
connect=True, use_ssl=self.ssl_target, unix_socket=self.unix_target)
185
if self.verbose and not self.daemon:
186
print(self.traffic_legend)
193
tsock.shutdown(socket.SHUT_RDWR)
195
self.vmsg("%s:%s: Closed target" %(
196
self.target_host, self.target_port))
199
def get_target(self, target_cfg, path):
201
Parses the path, extracts a token, and looks for a valid
202
target for that token in the configuration file(s). Sets
203
target_host and target_port if successful
205
# The files in targets contain the lines
206
# in the form of token: host:port
208
# Extract the token parameter from url
209
args = parse_qs(urlparse(path)[4]) # 4 is the query from url
211
if not args.has_key('token') or not len(args['token']):
212
raise self.EClose("Token not present")
214
token = args['token'][0].rstrip('\n')
216
# target_cfg can be a single config file or directory of
218
if os.path.isdir(target_cfg):
219
cfg_files = [os.path.join(target_cfg, f)
220
for f in os.listdir(target_cfg)]
222
cfg_files = [target_cfg]
226
for line in [l.strip() for l in file(f).readlines()]:
227
if line and not line.startswith('#'):
228
ttoken, target = line.split(': ')
229
targets[ttoken] = target.strip()
231
self.vmsg("Target config: %s" % repr(targets))
233
if targets.has_key(token):
234
return targets[token].split(':')
236
raise self.EClose("Token '%s' not found" % token)
238
def do_proxy(self, target):
240
Proxy client WebSocket to normal target socket.
245
rlist = [self.client, target]
250
if tqueue: wlist.append(target)
251
if cqueue or c_pend: wlist.append(self.client)
252
ins, outs, excepts = select(rlist, wlist, [], 1)
253
if excepts: raise Exception("Socket exception")
255
if self.client in outs:
256
# Send queued target data to the client
257
c_pend = self.send_frames(cqueue)
261
if self.client in ins:
262
# Receive client data, decode it, and queue for target
263
bufs, closed = self.recv_frames()
267
# TODO: What about blocking on client socket?
268
self.vmsg("%s:%s: Client closed connection" %(
269
self.target_host, self.target_port))
270
raise self.CClose(closed['code'], closed['reason'])
274
# Send queued client data to the target
276
sent = target.send(dat)
280
# requeue the remaining data
281
tqueue.insert(0, dat[sent:])
286
# Receive target data, encode it and queue for client
287
buf = target.recv(self.buffer_size)
289
self.vmsg("%s:%s: Target closed connection" %(
290
self.target_host, self.target_port))
291
raise self.CClose(1000, "Target closed")
298
def _subprocess_setup():
299
# Python installs a SIGPIPE handler by default. This is usually not what
300
# non-Python successfulbprocesses expect.
301
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
304
def websockify_init():
305
usage = "\n %prog [options]"
306
usage += " [source_addr:]source_port [target_addr:target_port]"
307
usage += "\n %prog [options]"
308
usage += " [source_addr:]source_port -- WRAP_COMMAND_LINE"
309
parser = optparse.OptionParser(usage=usage)
310
parser.add_option("--verbose", "-v", action="store_true",
311
help="verbose messages and per frame traffic")
312
parser.add_option("--record",
313
help="record sessions to FILE.[session_number]", metavar="FILE")
314
parser.add_option("--daemon", "-D",
315
dest="daemon", action="store_true",
316
help="become a daemon (background process)")
317
parser.add_option("--run-once", action="store_true",
318
help="handle a single WebSocket connection and exit")
319
parser.add_option("--timeout", type=int, default=0,
320
help="after TIMEOUT seconds exit when not connected")
321
parser.add_option("--idle-timeout", type=int, default=0,
322
help="server exits after TIMEOUT seconds if there are no "
323
"active connections")
324
parser.add_option("--cert", default="self.pem",
325
help="SSL certificate file")
326
parser.add_option("--key", default=None,
327
help="SSL key file (if separate from cert)")
328
parser.add_option("--ssl-only", action="store_true",
329
help="disallow non-encrypted client connections")
330
parser.add_option("--ssl-target", action="store_true",
331
help="connect to SSL target as SSL client")
332
parser.add_option("--unix-target",
333
help="connect to unix socket target", metavar="FILE")
334
parser.add_option("--web", default=None, metavar="DIR",
335
help="run webserver on same port. Serve files from DIR.")
336
parser.add_option("--wrap-mode", default="exit", metavar="MODE",
337
choices=["exit", "ignore", "respawn"],
338
help="action to take when the wrapped program exits "
339
"or daemonizes: exit (default), ignore, respawn")
340
parser.add_option("--prefer-ipv6", "-6",
341
action="store_true", dest="source_is_ipv6",
342
help="prefer IPv6 when resolving source_addr")
343
parser.add_option("--target-config", metavar="FILE",
345
help="Configuration file containing valid targets "
346
"in the form 'token: host:port' or, alternatively, a "
347
"directory containing configuration files of this form")
348
(opts, args) = parser.parse_args()
351
if len(args) < 2 and not (opts.target_cfg or opts.unix_target):
352
parser.error("Too few arguments")
353
if sys.argv.count('--'):
354
opts.wrap_cmd = args[1:]
358
parser.error("Too many arguments")
360
if not websocket.ssl and opts.ssl_target:
361
parser.error("SSL target requested and Python SSL module not loaded.");
363
if opts.ssl_only and not os.path.exists(opts.cert):
364
parser.error("SSL only and %s not found" % opts.cert)
366
# Parse host:port and convert ports to numbers
367
if args[0].count(':') > 0:
368
opts.listen_host, opts.listen_port = args[0].rsplit(':', 1)
369
opts.listen_host = opts.listen_host.strip('[]')
371
opts.listen_host, opts.listen_port = '', args[0]
373
try: opts.listen_port = int(opts.listen_port)
374
except: parser.error("Error parsing listen port")
376
if opts.wrap_cmd or opts.unix_target or opts.target_cfg:
377
opts.target_host = None
378
opts.target_port = None
380
if args[1].count(':') > 0:
381
opts.target_host, opts.target_port = args[1].rsplit(':', 1)
382
opts.target_host = opts.target_host.strip('[]')
384
parser.error("Error parsing target")
385
try: opts.target_port = int(opts.target_port)
386
except: parser.error("Error parsing target port")
388
# Create and start the WebSockets proxy
389
server = WebSocketProxy(**opts.__dict__)
390
server.start_server()
392
if __name__ == '__main__':