wsproxy.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. #!/usr/bin/python
  2. '''
  3. A WebSocket to TCP socket proxy with support for "wss://" encryption.
  4. Copyright 2010 Joel Martin
  5. Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3)
  6. You can make a cert/key with openssl using:
  7. openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem
  8. as taken from http://docs.python.org/dev/library/ssl.html#certificates
  9. '''
  10. import socket, optparse, time, os, sys, subprocess
  11. from select import select
  12. from websocket import WebSocketServer
  13. class WebSocketProxy(WebSocketServer):
  14. """
  15. Proxy traffic to and from a WebSockets client to a normal TCP
  16. socket server target. All traffic to/from the client is base64
  17. encoded/decoded to allow binary data to be sent/received to/from
  18. the target.
  19. """
  20. buffer_size = 65536
  21. traffic_legend = """
  22. Traffic Legend:
  23. } - Client receive
  24. }. - Client receive partial
  25. { - Target receive
  26. > - Target send
  27. >. - Target send partial
  28. < - Client send
  29. <. - Client send partial
  30. """
  31. def __init__(self, *args, **kwargs):
  32. # Save off proxy specific options
  33. self.target_host = kwargs.pop('target_host')
  34. self.target_port = kwargs.pop('target_port')
  35. self.wrap_cmd = kwargs.pop('wrap_cmd')
  36. self.wrap_mode = kwargs.pop('wrap_mode')
  37. # Last 3 timestamps command was run
  38. self.wrap_times = [0, 0, 0]
  39. if self.wrap_cmd:
  40. rebinder_path = ['./', os.path.dirname(sys.argv[0])]
  41. self.rebinder = None
  42. for rdir in rebinder_path:
  43. rpath = os.path.join(rdir, "rebind.so")
  44. if os.path.exists(rpath):
  45. self.rebinder = rpath
  46. break
  47. if not self.rebinder:
  48. raise Exception("rebind.so not found, perhaps you need to run make")
  49. self.target_host = "127.0.0.1" # Loopback
  50. # Find a free high port
  51. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  52. sock.bind(('', 0))
  53. self.target_port = sock.getsockname()[1]
  54. sock.close()
  55. os.environ.update({
  56. "LD_PRELOAD": self.rebinder,
  57. "REBIND_OLD_PORT": str(kwargs['listen_port']),
  58. "REBIND_NEW_PORT": str(self.target_port)})
  59. WebSocketServer.__init__(self, *args, **kwargs)
  60. def run_wrap_cmd(self):
  61. print "Starting '%s'" % " ".join(self.wrap_cmd)
  62. self.wrap_times.append(time.time())
  63. self.wrap_times.pop(0)
  64. self.cmd = subprocess.Popen(
  65. self.wrap_cmd, env=os.environ)
  66. self.spawn_message = True
  67. def started(self):
  68. """
  69. Called after Websockets server startup (i.e. after daemonize)
  70. """
  71. # Need to call wrapped command after daemonization so we can
  72. # know when the wrapped command exits
  73. if self.wrap_cmd:
  74. print " - proxying from %s:%s to '%s' (port %s)\n" % (
  75. self.listen_host, self.listen_port,
  76. " ".join(self.wrap_cmd), self.target_port)
  77. self.run_wrap_cmd()
  78. else:
  79. print " - proxying from %s:%s to %s:%s\n" % (
  80. self.listen_host, self.listen_port,
  81. self.target_host, self.target_port)
  82. def poll(self):
  83. # If we are wrapping a command, check it's status
  84. if self.wrap_cmd and self.cmd:
  85. ret = self.cmd.poll()
  86. if ret != None:
  87. self.vmsg("Wrapped command exited (or daemon). Returned %s" % ret)
  88. self.cmd = None
  89. if self.wrap_cmd and self.cmd == None:
  90. # Response to wrapped command being gone
  91. if self.wrap_mode == "ignore":
  92. pass
  93. elif self.wrap_mode == "exit":
  94. sys.exit(ret)
  95. elif self.wrap_mode == "respawn":
  96. now = time.time()
  97. avg = sum(self.wrap_times)/len(self.wrap_times)
  98. if (now - avg) < 10:
  99. # 3 times in the last 10 seconds
  100. if self.spawn_message:
  101. print "Command respawning too fast"
  102. self.spawn_message = False
  103. else:
  104. self.run_wrap_cmd()
  105. #
  106. # Routines above this point are run in the master listener
  107. # process.
  108. #
  109. #
  110. # Routines below this point are connection handler routines and
  111. # will be run in a separate forked process for each connection.
  112. #
  113. def new_client(self, client):
  114. """
  115. Called after a new WebSocket connection has been established.
  116. """
  117. self.rec = None
  118. if self.record:
  119. # Record raw frame data as a JavaScript compatible file
  120. fname = "%s.%s" % (self.record,
  121. self.handler_id)
  122. self.msg("opening record file: %s" % fname)
  123. self.rec = open(fname, 'w+')
  124. self.rec.write("var VNC_frame_data = [\n")
  125. # Connect to the target
  126. self.msg("connecting to: %s:%s" % (
  127. self.target_host, self.target_port))
  128. tsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  129. tsock.connect((self.target_host, self.target_port))
  130. if self.verbose and not self.daemon:
  131. print self.traffic_legend
  132. # Stat proxying
  133. try:
  134. self.do_proxy(client, tsock)
  135. except:
  136. if tsock: tsock.close()
  137. if self.rec:
  138. self.rec.write("'EOF']\n")
  139. self.rec.close()
  140. raise
  141. def do_proxy(self, client, target):
  142. """
  143. Proxy client WebSocket to normal target socket.
  144. """
  145. cqueue = []
  146. cpartial = ""
  147. tqueue = []
  148. rlist = [client, target]
  149. tstart = int(time.time()*1000)
  150. while True:
  151. wlist = []
  152. tdelta = int(time.time()*1000) - tstart
  153. if tqueue: wlist.append(target)
  154. if cqueue: wlist.append(client)
  155. ins, outs, excepts = select(rlist, wlist, [], 1)
  156. if excepts: raise Exception("Socket exception")
  157. if target in outs:
  158. # Send queued client data to the target
  159. dat = tqueue.pop(0)
  160. sent = target.send(dat)
  161. if sent == len(dat):
  162. self.traffic(">")
  163. else:
  164. # requeue the remaining data
  165. tqueue.insert(0, dat[sent:])
  166. self.traffic(".>")
  167. if client in outs:
  168. # Send queued target data to the client
  169. dat = cqueue.pop(0)
  170. sent = client.send(dat)
  171. if sent == len(dat):
  172. self.traffic("<")
  173. if self.rec:
  174. self.rec.write("%s,\n" %
  175. repr("{%s{" % tdelta + dat[1:-1]))
  176. else:
  177. cqueue.insert(0, dat[sent:])
  178. self.traffic("<.")
  179. if target in ins:
  180. # Receive target data, encode it and queue for client
  181. buf = target.recv(self.buffer_size)
  182. if len(buf) == 0: raise self.EClose("Target closed")
  183. cqueue.append(self.encode(buf))
  184. self.traffic("{")
  185. if client in ins:
  186. # Receive client data, decode it, and queue for target
  187. buf = client.recv(self.buffer_size)
  188. if len(buf) == 0: raise self.EClose("Client closed")
  189. if buf == '\xff\x00':
  190. raise self.EClose("Client sent orderly close frame")
  191. elif buf[-1] == '\xff':
  192. if buf.count('\xff') > 1:
  193. self.traffic(str(buf.count('\xff')))
  194. self.traffic("}")
  195. if self.rec:
  196. self.rec.write("%s,\n" %
  197. (repr("}%s}" % tdelta + buf[1:-1])))
  198. if cpartial:
  199. # Prepend saved partial and decode frame(s)
  200. tqueue.extend(self.decode(cpartial + buf))
  201. cpartial = ""
  202. else:
  203. # decode frame(s)
  204. tqueue.extend(self.decode(buf))
  205. else:
  206. # Save off partial WebSockets frame
  207. self.traffic(".}")
  208. cpartial = cpartial + buf
  209. if __name__ == '__main__':
  210. usage = "\n %prog [options]"
  211. usage += " [source_addr:]source_port target_addr:target_port"
  212. usage += "\n %prog [options]"
  213. usage += " [source_addr:]source_port -- WRAP_COMMAND_LINE"
  214. parser = optparse.OptionParser(usage=usage)
  215. parser.add_option("--verbose", "-v", action="store_true",
  216. help="verbose messages and per frame traffic")
  217. parser.add_option("--record",
  218. help="record sessions to FILE.[session_number]", metavar="FILE")
  219. parser.add_option("--daemon", "-D",
  220. dest="daemon", action="store_true",
  221. help="become a daemon (background process)")
  222. parser.add_option("--cert", default="self.pem",
  223. help="SSL certificate file")
  224. parser.add_option("--key", default=None,
  225. help="SSL key file (if separate from cert)")
  226. parser.add_option("--ssl-only", action="store_true",
  227. help="disallow non-encrypted connections")
  228. parser.add_option("--web", default=None, metavar="DIR",
  229. help="run webserver on same port. Serve files from DIR.")
  230. parser.add_option("--wrap-mode", default="exit", metavar="MODE",
  231. choices=["exit", "ignore", "respawn"],
  232. help="action to take when the wrapped program exits "
  233. "or daemonizes: exit (default), ignore, respawn")
  234. (opts, args) = parser.parse_args()
  235. # Sanity checks
  236. if len(args) < 2:
  237. parser.error("Too few arguments")
  238. if sys.argv.count('--'):
  239. opts.wrap_cmd = args[1:]
  240. else:
  241. opts.wrap_cmd = None
  242. if len(args) > 2:
  243. parser.error("Too many arguments")
  244. if opts.ssl_only and not os.path.exists(opts.cert):
  245. parser.error("SSL only and %s not found" % opts.cert)
  246. # Parse host:port and convert ports to numbers
  247. if args[0].count(':') > 0:
  248. opts.listen_host, opts.listen_port = args[0].split(':')
  249. else:
  250. opts.listen_host, opts.listen_port = '', args[0]
  251. try: opts.listen_port = int(opts.listen_port)
  252. except: parser.error("Error parsing listen port")
  253. if opts.wrap_cmd:
  254. opts.target_host = None
  255. opts.target_port = None
  256. else:
  257. if args[1].count(':') > 0:
  258. opts.target_host, opts.target_port = args[1].split(':')
  259. else:
  260. parser.error("Error parsing target")
  261. try: opts.target_port = int(opts.target_port)
  262. except: parser.error("Error parsing target port")
  263. # Create and start the WebSockets proxy
  264. server = WebSocketProxy(**opts.__dict__)
  265. server.start_server()