Compare commits
47 Commits
Author | SHA1 | Date |
---|---|---|
Jeff Lindsay | f8932371a7 | |
Jeff Lindsay | 279bc10eb5 | |
Jeff Lindsay | c4d6edc3f7 | |
Jeff Lindsay | 7c478ffa2e | |
Jeff Lindsay | 0d7b19b6c8 | |
Jeff Lindsay | 4f25ef41d7 | |
Jeff Lindsay | dc80805eb3 | |
Jeff Lindsay | a5e18f481a | |
Jeff Lindsay | 19d11a4f57 | |
Jeff Lindsay | ee6d24ae1a | |
Jeff Lindsay | 60acbcce09 | |
Kenneth Reitz | c13c548b60 | |
Kenneth Reitz | aaca820807 | |
Kenneth Reitz | fe73b3cdae | |
Jeff Lindsay | ec89d207bd | |
Jeff Lindsay | ea4ca38151 | |
Jeff Lindsay | 8e8a5529e9 | |
Jeff Lindsay | 47d3aa8d49 | |
Jeff Lindsay | 02cb9c8283 | |
Jeff Lindsay | 87bcb12aa5 | |
Jeff Lindsay | b22e19aedb | |
Jeff Lindsay | 6e969c5aeb | |
Jeff Lindsay | 73c86b7182 | |
Jeff Lindsay | a2f1e99799 | |
Jeff Lindsay | dce496c533 | |
Jeff Lindsay | 9f18ab1aa3 | |
Jeff Lindsay | 598106ebd8 | |
Jeff Lindsay | 288fc652d4 | |
Jeff Lindsay | 93e6d5ed72 | |
Jeff Lindsay | 150aa4d0e8 | |
Jeff Lindsay | 6713ea830d | |
Jeff Lindsay | aed5fbf685 | |
Jeff Lindsay | e29a33ecea | |
Jeff Lindsay | e7a8ef4125 | |
Jeff Lindsay | cae8c8d070 | |
Jeff Lindsay | 8f69134c90 | |
Jeff Lindsay | 5cb00620db | |
Jeff Lindsay | 86b517b4f6 | |
Jeff Lindsay | aa40a0cad0 | |
Jeff Lindsay | dae78d2b17 | |
Jeff Lindsay | ebf726c7f8 | |
Jeff Lindsay | a8ff1a605f | |
Jeff Lindsay | 18b4149207 | |
Jeff Lindsay | b7a4372628 | |
Jeff Lindsay | ed80ca8683 | |
Jeff Lindsay | aadcbad58d | |
Jeff Lindsay | 15d409698b |
|
@ -1,3 +1,10 @@
|
|||
bin
|
||||
*.pyc
|
||||
**/*.pyc
|
||||
build
|
||||
dist
|
||||
*.egg-info
|
||||
serviced.log
|
||||
.idea
|
||||
.idea/*
|
||||
.idea/**/*
|
||||
|
|
6
Manifest
6
Manifest
|
@ -1,6 +0,0 @@
|
|||
Rakefile
|
||||
lib/localtunnel.rb
|
||||
lib/localtunnel/tunnel.rb
|
||||
lib/localtunnel/net_ssh_gateway_patch.rb
|
||||
bin/localtunnel
|
||||
Manifest
|
|
@ -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
|
|
@ -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
|
60
README.rdoc
60
README.rdoc
|
@ -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
|
17
Rakefile
17
Rakefile
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
import os
|
||||
|
||||
port = int(os.environ.get("PORT_WWW", 5000))
|
||||
hostname = 'v2.localtunnel.com'
|
||||
|
||||
service = 'localtunnel.server.TunnelBroker'
|
|
@ -0,0 +1,6 @@
|
|||
import os
|
||||
|
||||
port = int(os.environ.get("PORT", 5000))
|
||||
hostname = 'localtunnel.heroku'
|
||||
|
||||
service = 'localtunnel.server.TunnelBroker'
|
|
@ -0,0 +1,3 @@
|
|||
www:
|
||||
type: custom
|
||||
buildscript: deploy/builder
|
|
@ -1,2 +0,0 @@
|
|||
$: << File.expand_path(File.dirname(__FILE__))
|
||||
require 'localtunnel/tunnel'
|
|
@ -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
|
|
@ -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
|
|
@ -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:]
|
|
@ -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
|
|
@ -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)
|
||||
|
|
@ -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
106
server.py
|
@ -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()
|
|
@ -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',]},
|
||||
)
|
Loading…
Reference in New Issue