cleaning up, new readme
This commit is contained in:
parent
8b08c02215
commit
9faa31dd23
43
README
43
README
|
@ -1,27 +1,30 @@
|
|||
LocalTunnel
|
||||
===========
|
||||
localtunnel -- instant reverse tunnel for local web servers
|
||||
|
||||
This is the vision:
|
||||
Install: sudo gem install localtunnel
|
||||
|
||||
$ localtunnel 8080
|
||||
http://d8w72a.localtunnel.com is now forwarding to your local port 8080...
|
||||
Usage: localtunnel [options] <localport>
|
||||
-k, --key FILE upload a public key for authentication
|
||||
|
||||
Wouldn't that be magical? Compare to http://novas007.livejournal.com/42971.html
|
||||
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.
|
||||
|
||||
Currently the focus is the forwarding plumbing. Works, but not so great with many concurrent requests.
|
||||
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.
|
||||
|
||||
It should probably be modeled more after this:
|
||||
http://twistedmatrix.com/trac/browser/tags/releases/twisted-8.1.0/twisted/conch/ssh/forwarding.py
|
||||
You typically run it like this:
|
||||
|
||||
To try it out:
|
||||
$ localtunnel 8080
|
||||
|
||||
However, if you haven't run the command before, you'll need to upload a public
|
||||
key to authenticate. You do this like so:
|
||||
|
||||
1) Start the server.
|
||||
python server.py
|
||||
|
||||
2) Point the client at a local port serving HTTP.
|
||||
python client.py 8080
|
||||
|
||||
3) Use curl to request a page from your HTTP through the server.
|
||||
curl http://localhost:8999/
|
||||
|
||||
Browsers work, but it will choke trying to serve up all your assets.
|
||||
$ localtunnel -k ~/.ssh/id_rsa.pub 8080
|
||||
|
||||
After that, you shouldn't have to use -k again.
|
||||
|
||||
The tunnel remains open for as long as the command is running. The tunnel is
|
||||
closed if the command exists.
|
103
bin/localtunnel
103
bin/localtunnel
|
@ -1,4 +1,27 @@
|
|||
#!/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 'net/ssh'
|
||||
require 'net/ssh/gateway'
|
||||
|
@ -7,24 +30,6 @@ require 'uri'
|
|||
require 'optparse'
|
||||
require 'json'
|
||||
|
||||
require 'lib/gateway'
|
||||
|
||||
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
|
||||
puts options
|
||||
exit
|
||||
end
|
||||
|
||||
def register_tunnel(key=nil)
|
||||
url = URI.parse("http://open.localtunnel.com/")
|
||||
if key
|
||||
|
@ -49,14 +54,66 @@ def start_tunnel(port, tunnel)
|
|||
exit
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
start_tunnel(local_port, register_tunnel(key))
|
||||
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'} #{local_port}"
|
||||
puts " localtunnel -k #{possible_key ? possible_key : '~/path/to/key'} #{port}"
|
||||
exit
|
||||
end
|
||||
|
||||
# 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
|
||||
|
||||
### Main
|
||||
|
||||
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
|
||||
puts options
|
||||
exit
|
||||
end
|
||||
|
||||
start_tunnel(local_port, register_tunnel(key))
|
||||
|
|
69
client.py
69
client.py
|
@ -1,69 +0,0 @@
|
|||
from twisted.internet import protocol, reactor, defer, task
|
||||
from twisted.internet.protocol import Protocol, ClientFactory
|
||||
from twisted.protocols import basic
|
||||
from twisted.python import log
|
||||
from twisted.web import http
|
||||
import sys
|
||||
|
||||
class OutgoingChannel(protocol.Protocol):
|
||||
def __init__(self, reqId, factory):
|
||||
self.reqId = reqId
|
||||
self.factory = factory
|
||||
self.tunnel = factory.tunnel
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.tunnel.transport.write(data)
|
||||
|
||||
def connectionMade(self):
|
||||
self.tunnel.requests[self.reqId] = self
|
||||
for l in self.tunnel.buffers[self.reqId]:
|
||||
self.transport.write(l + "\r\n")
|
||||
|
||||
class OutgoingFactory(ClientFactory):
|
||||
def __init__(self, reqId, tunnel):
|
||||
self.reqId = reqId
|
||||
self.tunnel = tunnel
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
self.p = OutgoingChannel(self.reqId, self)
|
||||
return self.p
|
||||
|
||||
class TunnelClientProtocol(basic.LineReceiver):
|
||||
requests = {}
|
||||
buffers = {}
|
||||
|
||||
def lineReceived(self, line):
|
||||
reqId, line = line.split('|',1)
|
||||
if not reqId in self.requests:
|
||||
if line == '^^CONNECT--':
|
||||
self.buffers[reqId] = []
|
||||
reactor.connectTCP("localhost", self.factory.port, OutgoingFactory(reqId, self))
|
||||
else:
|
||||
self.buffers[reqId].append(line)
|
||||
else:
|
||||
if line == '^^CLOSE--':
|
||||
if reqId in self.requests and self.requests[reqId]:
|
||||
self.requests[reqId].transport.loseConnection()
|
||||
self.requests[reqId] = None
|
||||
else:
|
||||
self.requests[reqId].transport.write(line + "\r\n")
|
||||
|
||||
def rawDataReceived(self, data):
|
||||
print "HUH?", data
|
||||
|
||||
def connectionMade(self):
|
||||
print "Listening on port %s. See server for host information." % self.factory.port
|
||||
|
||||
class TunnelClientFactory(ClientFactory):
|
||||
protocol = TunnelClientProtocol
|
||||
|
||||
def __init__(self, port):
|
||||
self.port = port
|
||||
|
||||
#log.startLogging(sys.stdout)
|
||||
try:
|
||||
reactor.connectTCP("localhost", 8777, TunnelClientFactory(int(sys.argv[1])))
|
||||
reactor.run()
|
||||
except IndexError:
|
||||
print "Usage: %s <port>" % sys.argv[0]
|
||||
print " You need to specify a port to forward to."
|
|
@ -1,35 +0,0 @@
|
|||
# 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
|
|
@ -11,6 +11,7 @@ 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 '
|
||||
|
||||
def port_available(port):
|
||||
try:
|
||||
|
@ -53,7 +54,7 @@ class LocalTunnelReverseProxy(proxy.ReverseProxyResource):
|
|||
del self.tunnels[name]
|
||||
|
||||
def install_key(self, key):
|
||||
key = key.strip()+"\n"
|
||||
key = ''.join([SSH_OPTIONS, key.strip(), "\n"])
|
||||
fr = open(AUTHORIZED_KEYS, 'r')
|
||||
if not key in fr.readlines():
|
||||
fa = open(AUTHORIZED_KEYS, 'a')
|
||||
|
|
Loading…
Reference in New Issue