Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'v2' into integration

  • Loading branch information...
commit ad57044dc3ec66e3196bb3be4d6f2f26b16d73ce 2 parents 1ff9d85 + 6e67964
@progrium authored
View
7 .gitignore
@@ -1,3 +1,10 @@
+bin
+*.pyc
+**/*.pyc
+build
+dist
+*.egg-info
+serviced.log
.idea
.idea/*
.idea/**/*
View
1  Procfile
@@ -0,0 +1 @@
+app: python -c "__requires__ = 'ginkgo'; import sys; from pkg_resources import load_entry_point; sys.exit(load_entry_point('ginkgo', 'console_scripts', 'ginkgo')())" ./config/heroku.conf.py
View
45 README
@@ -0,0 +1,45 @@
+How to use v2:
+
+First:
+
+python setup.py develop
+
+(Alternatively you can do install instead of develop)
+
+Now run some web app locally on, say, port 8000. If you have nothing,
+run this in some directory:
+
+python -m SimpleHTTPServer 8000
+
+Localtunnel does some stuff with the hostname, so you want to set up two
+hostnames. One for localtunnel registration, one for your localtunnel.
+Normally it expects a wildcard, but we'll just hardcode a hostname for
+this example tunnel.
+
+example.localtunnel.local -> 127.0.0.1
+localtunnel.local -> 127.0.0.1
+
+You can do this in /etc/hosts or use that fancy ghost utility.
+
+Now you can start the server. It's based on a configuration file in the
+config directory. You can make your own, but this one is configured to
+run the server on port 9999 and expects the hostname localtunnel.local
+
+ginkgo config/default.conf.py
+
+Like your web app or SimpleHTTPServer, you'll want to leave this
+running. The client is installed as a command called "lt". You use this
+to make the tunnel. We have to specify the broker address, and the name
+of the tunnel:
+
+lt --broker 127.0.0.1:9999 --name example 8000
+
+Leave this running. Now you should be able to browse to:
+
+http://example.localtunnel.local:9999
+
+And you should see the same thing as you would at:
+
+http://localhost:8000
+
+THE END
View
12 deploy/builder
@@ -0,0 +1,12 @@
+#!/bin/bash
+cp -a . ~
+cd
+(cat <<-EOF
+ virtualenv --python=python2.7 env
+ . env/bin/activate
+ pip install -r requirements.txt
+ python setup.py develop
+ python -c "__requires__ = 'ginkgo'; import sys; from pkg_resources import load_entry_point; sys.exit(load_entry_point('ginkgo','console_scripts', 'ginkgo')())" ./config/dotcloud.conf.py
+EOF
+) > run
+chmod a+x run
View
6 deploy/dotcloud.conf.py
@@ -0,0 +1,6 @@
+import os
+
+port = int(os.environ.get("PORT_WWW", 5000))
+hostname = 'v2.localtunnel.com'
+
+service = 'localtunnel.server.TunnelBroker'
View
6 deploy/heroku.conf.py
@@ -0,0 +1,6 @@
+import os
+
+port = int(os.environ.get("PORT", 5000))
+hostname = 'localtunnel.heroku'
+
+service = 'localtunnel.server.TunnelBroker'
View
3  dotcloud.yml
@@ -0,0 +1,3 @@
+www:
+ type: custom
+ buildscript: deploy/builder
View
5 localtunnel/__init__.py
@@ -0,0 +1,5 @@
+def encode_data_packet(conn_id, data):
+ return ''.join([chr(conn_id), data])
+
+def decode_data_packet(data):
+ return data[0], data[1:]
View
103 localtunnel/client.py
@@ -0,0 +1,103 @@
+import json
+import uuid
+
+import gevent
+from gevent.socket import create_connection
+from gevent.coros import Semaphore
+
+from ginkgo import Service
+
+from ws4py.client.geventclient import WebSocketClient
+
+from localtunnel import encode_data_packet
+from localtunnel import decode_data_packet
+
+#WebSocketClient.upgrade_header = 'X-Upgrade'
+
+def main():
+ import argparse
+ parser = argparse.ArgumentParser(description='Open a public HTTP tunnel to a local server')
+ parser.add_argument('port', metavar='port', type=int,
+ help='local port of server to tunnel to')
+ parser.add_argument('--name', dest='name', metavar='name',
+ default=str(uuid.uuid4()).split('-')[-1],
+ help='name of the tunnel (default: randomly generate)')
+ parser.add_argument('--broker', dest='broker', metavar='address',
+ default='localtunnel.com',
+ help='tunnel broker hostname (default: localtunnel.com)')
+ args = parser.parse_args()
+
+ client = TunnelClient(args.port, args.name, args.broker)
+ client.serve_forever()
+
+class TunnelClient(Service):
+
+ def __init__(self, local_port, name, broker_address):
+ self.local_port = local_port
+ self.ws = WebSocketClient('http://%s/t/%s' % (broker_address, name))
+ self.connections = {}
+ self._send_lock = Semaphore()
+
+ def do_start(self):
+ self.ws.connect()
+ self.spawn(self.listen)
+ #gevent.spawn(self.visual_heartbeat)
+
+ def visual_heartbeat(self):
+ while True:
+ print "."
+ gevent.sleep(1)
+
+ def listen(self):
+ while True:
+ msg = self.ws.receive(msg_obj=True)
+ if msg is None:
+ print "Trying to stop"
+ self.stop()
+ if msg.is_text:
+ parsed = json.loads(str(msg))
+ print str(msg)
+ conn_id, event = parsed[0:2]
+ if event == 'open':
+ self.local_open(conn_id)
+ elif event == 'closed':
+ self.local_close(conn_id)
+ elif msg.is_binary:
+ conn_id, data = decode_data_packet(msg.data)
+ self.local_send(conn_id, data)
+
+ def local_open(self, conn_id):
+ socket = create_connection(('0.0.0.0', self.local_port))
+ self.connections[conn_id] = socket
+ self.spawn(self.local_recv, conn_id)
+
+ def local_close(self, conn_id):
+ socket = self.connections.pop(conn_id)
+ try:
+ socket.shutdown(0)
+ socket.close()
+ except:
+ pass
+
+ def local_send(self, conn_id, data):
+ self.connections[conn_id].send(data)
+
+ def local_recv(self, conn_id):
+ while True:
+ data = self.connections[conn_id].recv(1024)
+ if not data:
+ break
+ self.tunnel_send(conn_id, data)
+ self.tunnel_send(conn_id, open=False)
+
+ def tunnel_send(self, conn_id, data=None, open=None):
+ if open is False:
+ msg = [conn_id, 'closed']
+ with self._send_lock:
+ self.ws.send(json.dumps(msg))
+ elif data:
+ msg = encode_data_packet(conn_id, data)
+ with self._send_lock:
+ self.ws.send(msg, binary=True)
+ else:
+ return
View
244 localtunnel/server.py
@@ -0,0 +1,244 @@
+import json
+import re
+import base64
+from socket import MSG_PEEK
+
+import gevent.pywsgi
+from gevent.queue import Queue
+from gevent.pool import Group
+
+from ginkgo import Setting
+from ginkgo import Service
+from ginkgo.async.gevent import ServerWrapper
+
+from ws4py.server.geventserver import UpgradableWSGIHandler
+from ws4py.server.wsgi.middleware import WebSocketUpgradeMiddleware
+
+from localtunnel import encode_data_packet
+from localtunnel import decode_data_packet
+
+#UpgradableWSGIHandler.upgrade_header = 'X-Upgrade'
+
+class CodependentGroup(Group):
+ """Greenlet group that will kill all greenlets if a single one dies
+ """
+ def discard(self, greenlet):
+ super(CodependentGroup, self).discard(greenlet)
+ if not hasattr(self, '_killing'):
+ self._killing = True
+ gevent.spawn(self.kill)
+
+class TunnelBroker(Service):
+ """Top-level service that manages tunnels and runs the frontend"""
+
+ port = Setting('port', default=8000)
+ address = Setting('address', default='0.0.0.0')
+
+ def __init__(self):
+ self.frontend = BrokerFrontend(self)
+ self.add_service(ServerWrapper(self.frontend))
+
+ self.tunnels = {}
+
+ #def do_start(self):
+ # gevent.spawn(self.visual_heartbeat)
+
+ def visual_heartbeat(self):
+ while True:
+ print "."
+ gevent.sleep(1)
+
+ def open_tunnel(self, name):
+ tunnel = Tunnel()
+ self.tunnels[name] = tunnel
+ return tunnel
+
+ def close_tunnel(self, name):
+ tunnel = self.tunnels.pop(name)
+ tunnel.close()
+
+ def lookup_tunnel(self, name):
+ return self.tunnels.get(name)
+
+
+class BrokerFrontend(gevent.pywsgi.WSGIServer):
+ """Server that will manage a tunnel or proxy traffic through a tunnel"""
+
+ hostname = Setting('hostname', default="vcap.me") # *.vcap.me -> 127.0.0.1
+
+ def __init__(self, broker):
+ gevent.pywsgi.WSGIServer.__init__(self, (broker.address, broker.port))
+ self.broker = broker
+
+
+ def handle(self, socket, address):
+ hostname = ''
+ hostheader = re.compile('host: ([^\(\);:,<>]+)', re.I)
+ # Peek up to 512 bytes into data for the Host header
+ for n in [128, 256, 512]:
+ bytes = socket.recv(n, MSG_PEEK)
+ if not bytes:
+ break
+ for line in bytes.split('\r\n'):
+ match = hostheader.match(line)
+ if match:
+ hostname = match.group(1)
+ if hostname:
+ break
+ hostname = hostname.split(':')[0]
+ if hostname.endswith('.%s' % self.hostname):
+ handler = ProxyHandler(socket, hostname, self.broker)
+ handler.handle()
+ else:
+ handler = TunnelHandler(socket, address, self.broker)
+ handler.handle()
+
+class ProxyHandler(object):
+ """TCP-ish proxy handler"""
+
+ def __init__(self, socket, hostname, broker):
+ self.socket = socket
+ self.hostname = hostname
+ self.broker = broker
+
+ def handle(self):
+ tunnel = self.broker.lookup_tunnel(self.hostname.split('.')[0])
+ if tunnel:
+ conn = tunnel.create_connection()
+ group = CodependentGroup([
+ gevent.spawn(self._proxy_in, self.socket, conn),
+ gevent.spawn(self._proxy_out, conn, self.socket),
+ ])
+ gevent.joinall(group.greenlets)
+ try:
+ self.socket.shutdown(0)
+ self.socket.close()
+ except:
+ pass
+
+ def _proxy_in(self, socket, conn):
+ while True:
+ data = socket.recv(2048)
+ if not data:
+ return
+ conn.send(data)
+
+ def _proxy_out(self, conn, socket):
+ while True:
+ data = conn.recv()
+ if data is None:
+ return
+ socket.sendall(data)
+
+
+class TunnelHandler(UpgradableWSGIHandler):
+ """HTTP handler for opening/managing/running a tunnel (via websocket)"""
+
+ def __init__(self, socket, address, broker):
+ UpgradableWSGIHandler.__init__(self, socket, address, broker.frontend)
+ self.server.application = WebSocketUpgradeMiddleware(
+ self.handle_websocket) #, self.handle_http)
+ self.broker = broker
+
+ def handle_http(self, environ, start_response):
+ start_response("200 ok", [])
+ return ['<pre>%s' % environ]
+
+ def handle_websocket(self, websocket, environ):
+ name = environ.get('PATH_INFO', '').split('/')[-1]
+ tunnel = self.broker.open_tunnel(name)
+ group = CodependentGroup([
+ gevent.spawn(self._tunnel_in, tunnel, websocket),
+ gevent.spawn(self._tunnel_out, websocket, tunnel),
+ ])
+ gevent.joinall(group.greenlets)
+ self.broker.close_tunnel(name)
+ websocket.close()
+
+ def _tunnel_in(self, tunnel, websocket):
+ for type, msg in tunnel:
+ binary = bool(type == 'binary')
+ websocket.send(msg, binary=binary)
+
+ def _tunnel_out(self, websocket, tunnel):
+ while True:
+ msg = websocket.receive(msg_obj=True)
+ if msg is None:
+ return
+ if msg.is_text:
+ tunnel.dispatch(message=str(msg))
+ elif msg.is_binary:
+ tunnel.dispatch(data=msg.data)
+
+class Tunnel(object):
+ """Server representation of a tunnel its mux'd connections"""
+ def __init__(self):
+ self.connections = {}
+ self.tunnelq = Queue()
+
+ def create_connection(self):
+ id = 0
+ while id in self.connections.keys():
+ id += 1
+ id %= 2**31
+ conn = ConnectionProxy(id, self)
+ self.connections[id] = conn
+ return conn
+
+ def close(self):
+ for conn_id in self.connections.keys():
+ conn = self.connections.pop(conn_id)
+ conn.close()
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ return self.tunnelq.get()
+
+ def dispatch(self, message=None, data=None):
+ """ From the tunnel (server) to the proxy (client) """
+ if message:
+ try:
+ parsed = json.loads(message)
+ except ValueError:
+ raise
+ conn_id, event = parsed[0:2]
+ if conn_id not in self.connections:
+ return
+ if event == 'closed':
+ conn = self.connections.pop(conn_id)
+ conn.close()
+ elif data:
+ conn_id, data = decode_data_packet(data)
+ self.connections[conn_id].recvq.put(data)
+
+class ConnectionProxy(object):
+ """Socket-like representation of connection on the other end of the tunnel
+ """
+
+ def __init__(self, id, tunnel):
+ self.tunnel = tunnel
+ self.id = id
+ self.recvq = Queue()
+ self.send(open=True)
+
+ def recv(self):
+ return self.recvq.get()
+
+ def send(self, data=None, open=None):
+ """ From the proxy (client) to the tunnel (server) """
+ if open is True:
+ msg = [self.id, 'open']
+ self.tunnel.tunnelq.put(('text', json.dumps(msg)))
+ elif open is False:
+ msg = [self.id, 'closed']
+ self.tunnel.tunnelq.put(('text', json.dumps(msg)))
+ else:
+ data = encode_data_packet(self.id, data)
+ self.tunnel.tunnelq.put(('binary', data))
+
+ def close(self):
+ self.recvq.put(None)
+ self.send(open=False)
+
View
6 requirements.txt
@@ -0,0 +1,6 @@
+git+git://github.com/progrium/ginkgo.git#egg=ginkgo
+git+git://github.com/progrium/WebSocket-for-Python.git#egg=ws4py-dev
+gevent==0.13.6
+greenlet==0.3.1
+nose==1.1.2
+wsgiref==0.1.2
View
17 setup.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python
+import os
+from setuptools import setup, find_packages
+
+setup(
+ name='localtunnel',
+ version='0.4.0',
+ author='Jeff Lindsay',
+ author_email='jeff.lindsay@twilio.com',
+ description='',
+ packages=find_packages(),
+ install_requires=['ginkgo', 'ws4py'],
+ data_files=[],
+ entry_points={
+ 'console_scripts': [
+ 'lt = localtunnel.client:main',]},
+)
Please sign in to comment.
Something went wrong with that request. Please try again.