Compare commits

...

47 Commits
master ... v2

Author SHA1 Message Date
Jeff Lindsay f8932371a7 preparing to merge into master 2012-04-26 22:19:23 -07:00
Jeff Lindsay 279bc10eb5 pulling from heroku fixes' 2012-04-26 17:05:23 -07:00
Jeff Lindsay c4d6edc3f7 thought it was a list. dict instead. 2012-04-26 16:57:30 -07:00
Jeff Lindsay 7c478ffa2e iterating on copy of list since pop/removing confuses iteration 2012-04-26 16:55:08 -07:00
Jeff Lindsay 0d7b19b6c8 server was expecting X-Header too ... hmmm 2012-04-26 16:46:44 -07:00
Jeff Lindsay 4f25ef41d7 turning off fake upgrade header 2012-04-26 16:34:32 -07:00
Jeff Lindsay dc80805eb3 turn off http fallback 2012-04-26 16:29:27 -07:00
Jeff Lindsay a5e18f481a reving ws4py failed, updating procfile to not use web so it does not idle 2012-04-26 16:24:53 -07:00
Jeff Lindsay 19d11a4f57 reving ws4py 2012-04-26 16:16:06 -07:00
Jeff Lindsay ee6d24ae1a removing http fallback to expose handshake error 2012-04-26 15:29:05 -07:00
Jeff Lindsay 60acbcce09 need to use non-dotcloud domain 2012-04-26 15:02:52 -07:00
Kenneth Reitz c13c548b60 localtunnel 2012-04-26 18:02:10 -04:00
Kenneth Reitz aaca820807 hmm 2012-04-26 17:42:40 -04:00
Kenneth Reitz fe73b3cdae try that 2012-04-26 17:40:05 -04:00
Jeff Lindsay ec89d207bd get port env var wrong 2012-04-26 14:21:04 -07:00
Jeff Lindsay ea4ca38151 this might have been getting in the way 2012-04-26 14:00:12 -07:00
Jeff Lindsay 8e8a5529e9 okay, no. we use 2.7 features. but we can enable 2.7 with virtualenv 2012-04-26 13:54:15 -07:00
Jeff Lindsay 47d3aa8d49 dotcloud uses python 2.6 by default, so manually install argparse (hopefully the only 2.7 dependency) 2012-04-26 13:52:24 -07:00
Jeff Lindsay 02cb9c8283 run using virtualenv 2012-04-26 13:47:58 -07:00
Jeff Lindsay 87bcb12aa5 is this what dotcloud is complaining about? we don't know 2012-04-26 12:36:23 -07:00
Jeff Lindsay b22e19aedb something added newlines 2012-04-26 12:08:10 -07:00
Jeff Lindsay 6e969c5aeb used the wrong hostname 2012-04-26 12:01:40 -07:00
Jeff Lindsay 73c86b7182 going back to default www_port 2012-04-26 11:56:03 -07:00
Jeff Lindsay a2f1e99799 making builder executable and adding port configuration 2012-04-26 11:53:42 -07:00
Jeff Lindsay dce496c533 prepping for dotcloud custom service deploy 2012-04-26 11:50:58 -07:00
Jeff Lindsay 9f18ab1aa3 adding a simple readme with instructions on how to get it running locally 2012-04-25 21:50:59 -07:00
Jeff Lindsay 598106ebd8 failed attempt at dotcloud 2012-04-25 21:38:16 -07:00
Jeff Lindsay 288fc652d4 updating to work with Ginkgo 2012-04-25 21:19:43 -07:00
Jeff Lindsay 93e6d5ed72 dropping prints. also tracking bytes sent. using sendall instead 2011-10-10 01:32:30 -07:00
Jeff Lindsay 150aa4d0e8 keep sending until its all sent 2011-10-10 00:53:51 -07:00
Jeff Lindsay 6713ea830d print bytes sent, send lock on client, leave bytearray on decode 2011-10-10 00:39:03 -07:00
Jeff Lindsay aed5fbf685 call it dev to force update on heroku 2011-10-09 16:34:52 -07:00
Jeff Lindsay e29a33ecea split client into separate module, made a cli client (lt for now). also fixed a bug where proxy was using full hostname to lookup tunnel. proxy will also now close socket if tunnel is not found 2011-10-09 16:04:06 -07:00
Jeff Lindsay e7a8ef4125 making a few things clearer or simpler 2011-10-09 12:47:05 -07:00
Jeff Lindsay cae8c8d070 forgot to add back http fallback 2011-10-06 02:01:48 -07:00
Jeff Lindsay 8f69134c90 echo request info 2011-10-06 01:59:20 -07:00
Jeff Lindsay 5cb00620db prodding to get heroku working 2011-09-25 02:14:06 -05:00
Jeff Lindsay 86b517b4f6 bind to 0.0.0.0 2011-09-25 01:57:21 -05:00
Jeff Lindsay aa40a0cad0 make sure its an integer port 2011-09-25 01:56:19 -05:00
Jeff Lindsay dae78d2b17 turning off visual heartbeat and using port from environment for heroku 2011-09-25 01:55:30 -05:00
Jeff Lindsay ebf726c7f8 more heroku prep 2011-09-25 01:50:52 -05:00
Jeff Lindsay a8ff1a605f specific requirements 2011-09-25 01:44:06 -05:00
Jeff Lindsay 18b4149207 preparing for heroku deployment 2011-09-25 01:33:11 -05:00
Jeff Lindsay b7a4372628 data packets are now binary websocket messages 2011-09-25 01:32:34 -05:00
Jeff Lindsay ed80ca8683 minor tweaks 2011-09-24 18:53:22 -05:00
Jeff Lindsay aadcbad58d initial commit of localtunnel rewrite. works, but encoding scheme adds 2x latency 2011-09-24 18:29:18 -05:00
Jeff Lindsay 15d409698b making way for the new 2011-09-21 09:56:47 -07:00
20 changed files with 455 additions and 339 deletions

7
.gitignore vendored
View File

@ -1,3 +1,10 @@
bin
*.pyc
**/*.pyc
build
dist
*.egg-info
serviced.log
.idea
.idea/*
.idea/**/*

View File

@ -1,6 +0,0 @@
Rakefile
lib/localtunnel.rb
lib/localtunnel/tunnel.rb
lib/localtunnel/net_ssh_gateway_patch.rb
bin/localtunnel
Manifest

1
Procfile Normal file
View File

@ -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

45
README Normal file
View File

@ -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 File

@ -1,60 +0,0 @@
= localtunnel -- instant public tunnel to your local web server
== Install
To get the dependencies if you don't have them, type:
sudo apt-get install ruby ruby1.8-dev rubygems1.8 libopenssl-ruby
Now you can install localtunnel with RubyGems:
sudo gem install localtunnel
or to get the source:
git clone http://github.com/progrium/localtunnel.git
== Usage
localtunnel [options] <localport>
-k, --key FILE upload a public key for authentication
Localtunnel is a client to a free and open source reverse tunneling
service made specifically for web traffic. It's intended to be used to
temporarily expose local web servers to the greater Internet for
debugging, unit tests, demos, etc.
This is how you make your local port 8080 public:
$ localtunnel 8080
Port 8080 is now publicly accessible from http://8bv2.localtunnel.com ...
Using localtunnel is comparable to using SSH reverse/remote port
forwarding on a remote host that has GatewayPorts enabled, but without
all the configuration or the need of a host. The localtunnel command
works with a server component that is running on localtunnel.com,
which is provided as a free service.
If have never run localtunnel before, you'll need to upload a public
key to authenticate. You do this once:
$ localtunnel -k ~/.ssh/id_rsa.pub 8080
After that, you shouldn't have to use -k again.
Localtunnel can be started before or after the local web server. It
tunnels through to the url given in that status message "publicly
accessible from..." for as long as the command is running. The tunnel
is closed if the command exits.
== Contributors
andyl (andy@r210.com)
Charles Merriam (charles.merriam@gmail.com)
Hunter Gillane (hunter.gillane@gmail.com)
Michael Sofaer (msofaer@pivotallabs.com)
Jeff Lindsay (progrium@gmail.com)
== License
MIT

View File

@ -1,17 +0,0 @@
require 'rubygems'
require 'rake'
require 'echoe'
Echoe.new('localtunnel', '0.3') do |p|
p.description = "instant public tunnel to your local web server"
p.url = "http://github.com/progrium/localtunnel"
p.author = "Jeff Lindsay"
p.email = "jeff.lindsay@twilio.com"
p.has_rdoc = false
p.rdoc_pattern = //
p.rdoc_options = []
p.ignore_pattern = ["tmp/*", "script/*"]
p.executable_pattern = ["bin/*"]
p.runtime_dependencies = ["json >=1.2.4", "net-ssh >=2.0.22", "net-ssh-gateway >=1.0.1"]
p.development_dependencies = []
end

View File

@ -1,47 +0,0 @@
#!/usr/bin/env ruby
# Copyright (c) 2010 Jeff Lindsay
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
require 'rubygems'
require 'optparse'
require 'localtunnel'
key = nil
options = OptionParser.new do |o|
o.banner = "Usage: localtunnel [options] <localport>"
o.on("-k", "--key FILE", "upload a public key for authentication") do |k|
key = File.exist?(k.to_s) ? File.open(k).read : nil
end
o.on('-h', "--help", "show this help") { puts o; exit }
end
args = options.parse!
local_port = args[0]
unless local_port.to_i.between?(1, 65535)
puts options
exit
end
t = LocalTunnel::Tunnel.new(local_port, key)
t.register_tunnel
t.start_tunnel

12
deploy/builder Executable file
View File

@ -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

6
deploy/dotcloud.conf.py Normal file
View File

@ -0,0 +1,6 @@
import os
port = int(os.environ.get("PORT_WWW", 5000))
hostname = 'v2.localtunnel.com'
service = 'localtunnel.server.TunnelBroker'

6
deploy/heroku.conf.py Normal file
View File

@ -0,0 +1,6 @@
import os
port = int(os.environ.get("PORT", 5000))
hostname = 'localtunnel.heroku'
service = 'localtunnel.server.TunnelBroker'

3
dotcloud.yml Normal file
View File

@ -0,0 +1,3 @@
www:
type: custom
buildscript: deploy/builder

View File

@ -1,2 +0,0 @@
$: << File.expand_path(File.dirname(__FILE__))
require 'localtunnel/tunnel'

View File

@ -1,39 +0,0 @@
require 'rubygems'
require 'net/ssh'
require 'net/ssh/gateway'
# http://groups.google.com/group/capistrano/browse_thread/thread/455c0c8a6faa9cc8?pli=1
class Net::SSH::Gateway
# Opens a SSH tunnel from a port on a remote host to a given host and port
# on the local side
# (equivalent to openssh -R parameter)
def open_remote(port, host, remote_port, remote_host = "127.0.0.1")
ensure_open!
@session_mutex.synchronize do
@session.forward.remote(port, host, remote_port, remote_host)
end
if block_given?
begin
yield [remote_port, remote_host]
ensure
close_remote(remote_port, remote_host)
end
else
return [remote_port, remote_host]
end
rescue Errno::EADDRINUSE
retry
end
# Cancels port-forwarding over an open port that was previously opened via
# #open_remote.
def close_remote(port, host = "127.0.0.1")
ensure_open!
@session_mutex.synchronize do
@session.forward.cancel_remote(port, host)
end
end
end

View File

@ -1,62 +0,0 @@
require 'rubygems'
require 'net/ssh'
require 'net/ssh/gateway'
require 'net/http'
require 'uri'
require 'json'
require 'localtunnel/net_ssh_gateway_patch'
module LocalTunnel; end
class LocalTunnel::Tunnel
attr_accessor :port, :key, :host
def initialize(port, key)
@port = port
@key = key
@host = ""
end
def register_tunnel(key=@key)
url = URI.parse("http://open.localtunnel.com/")
if key
resp = JSON.parse(Net::HTTP.post_form(url, {"key" => key}).body)
else
resp = JSON.parse(Net::HTTP.get(url))
end
if resp.has_key? 'error'
puts " [Error] #{resp['error']}"
exit
end
@host = resp['host'].split(':').first
@tunnel = resp
return resp
rescue
puts " [Error] Unable to register tunnel. Perhaps service is down?"
exit
end
def start_tunnel
port = @port
tunnel = @tunnel
gateway = Net::SSH::Gateway.new(@host, tunnel['user'])
gateway.open_remote(port.to_i, '127.0.0.1', tunnel['through_port'].to_i) do |rp,rh|
puts " " << tunnel['banner'] if tunnel.has_key? 'banner'
puts " Port #{port} is now publicly accessible from http://#{tunnel['host']} ..."
begin
sleep 1 while true
rescue Interrupt
gateway.close_remote(rp, rh)
exit
end
end
rescue Net::SSH::AuthenticationFailed
possible_key = Dir[File.expand_path('~/.ssh/*.pub')].first
puts " Failed to authenticate. If this is your first tunnel, you need to"
puts " upload a public key using the -k option. Try this:\n\n"
puts " localtunnel -k #{possible_key ? possible_key : '~/path/to/key'} #{port}"
exit
end
end

5
localtunnel/__init__.py Normal file
View File

@ -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:]

103
localtunnel/client.py Normal file
View File

@ -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

244
localtunnel/server.py Normal file
View File

@ -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)

6
requirements.txt Normal file
View File

@ -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

106
server.py
View File

@ -1,106 +0,0 @@
try:
from twisted.internet import pollreactor
pollreactor.install()
except: pass
from twisted.internet import protocol, reactor, defer, task
from twisted.web import http, proxy, resource, server
from twisted.python import log
import sys, time
import urlparse
import socket
import simplejson
import re
SSH_USER = 'localtunnel'
AUTHORIZED_KEYS = '/home/localtunnel/.ssh/authorized_keys'
PORT_RANGE = [32000, 64000]
BANNER = "This localtunnel service is brought to you by Twilio."
SSH_OPTIONS = 'command="/bin/echo Shell access denied",no-agent-forwarding,no-pty,no-user-rc,no-X11-forwarding '
KEY_REGEX = re.compile(r'^ssh-(\w{3}) [^\n]+$')
def port_available(port):
try:
socket.create_connection(['127.0.0.1', port]).close()
return False
except socket.error:
return True
def baseN(num,b=32,numerals="23456789abcdefghijkmnpqrstuvwxyz"):
return ((num == 0) and "0" ) or (baseN(num // b, b).lstrip("0") + numerals[num % b])
class LocalTunnelReverseProxy(proxy.ReverseProxyResource):
isLeaf = True
def __init__(self, user, host='127.0.0.1'):
self.user = user
self.tunnels = {}
proxy.ReverseProxyResource.__init__(self, host, None, None)
def find_tunnel_name(self):
name = baseN(abs(hash(time.time())))[0:4]
if (name in self.tunnels and not port_available(self.tunnels[name])) or name == 'open':
time.sleep(0.1)
return self.find_tunnel_name()
return name
def find_tunnel_port(self):
port = PORT_RANGE[0]
start_time = time.time()
while not port_available(port):
if time.time()-start_time > 3:
raise Exception("No port available")
port += 1
if port >= PORT_RANGE[1]: port = PORT_RANGE[0]
return port
def garbage_collect(self):
for name in self.tunnels:
if port_available(self.tunnels[name]):
del self.tunnels[name]
def install_key(self, key):
if not KEY_REGEX.match(key.strip()):
return False
key = ''.join([SSH_OPTIONS, key.strip(), "\n"])
fr = open(AUTHORIZED_KEYS, 'r')
if not key in fr.readlines():
fa = open(AUTHORIZED_KEYS, 'a')
fa.write(key)
fa.close()
fr.close()
return True
def register_tunnel(self, superhost, key=None):
if key and not self.install_key(key): return simplejson.dumps(dict(error="Invalid key."))
name = self.find_tunnel_name()
port = self.find_tunnel_port()
self.tunnels[name] = port
return simplejson.dumps(
dict(through_port=port, user=self.user, host='%s.%s' % (name, superhost), banner=BANNER))
def render(self, request):
host = request.getHeader('host')
name, superhost = host.split('.', 1)
if host.startswith('open.'):
request.setHeader('Content-Type', 'application/json')
return self.register_tunnel(superhost, request.args.get('key', [None])[0])
else:
if not name in self.tunnels: return "Not found"
request.content.seek(0, 0)
clientFactory = self.proxyClientFactoryClass(
request.method, request.uri, request.clientproto,
request.getAllHeaders(), request.content.read(), request)
self.reactor.connectTCP(self.host, self.tunnels[name], clientFactory)
return server.NOT_DONE_YET
#if 'location' in request.responseHeaders and host in request.responseHeaders['location']:
# # Strip out the port they think they need
# p = re.compile(r'%s\:\d+' % host)
# location = p.sub(host, request.responseHeaders['location'])
# request.responseHeaders['location'] = location
log.startLogging(sys.stdout)
reactor.listenTCP(80, server.Site(LocalTunnelReverseProxy(SSH_USER)))
reactor.run()

17
setup.py Normal file
View File

@ -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',]},
)