mirror of
https://github.com/bitinflow/server.git
synced 2026-03-13 13:35:53 +00:00
refactor server
* shows some basic statistics on main page * move tcp proxy setup into separate file * migrate github page theme to be hosted locally
This commit is contained in:
11
package.json
11
package.json
@@ -15,7 +15,16 @@
|
||||
"debug": "0.7.2",
|
||||
"bookrc": "0.0.1",
|
||||
"book-git": "0.0.2",
|
||||
"book-raven": "1.0.0"
|
||||
"book-raven": "1.0.0",
|
||||
"engine.io": "0.6.2",
|
||||
"engine.io-client": "shtylman/engine.io-client#v0.5.0-dz0",
|
||||
"flip-counter": "0.5.3",
|
||||
"browserkthx": "0.0.2",
|
||||
"hbs": "2.3.0",
|
||||
"taters": "0.0.5",
|
||||
"express": "3.2.6",
|
||||
"makeup": "0.0.1",
|
||||
"enchilada": "0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "1.6.0",
|
||||
|
||||
235
proxy.js
Normal file
235
proxy.js
Normal file
@@ -0,0 +1,235 @@
|
||||
var http = require('http');
|
||||
var net = require('net');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
var log = require('bookrc');
|
||||
var debug = require('debug')('localtunnel-server');
|
||||
|
||||
// here be dragons, understanding of node http internals will be required
|
||||
var HTTPParser = process.binding('http_parser').HTTPParser;
|
||||
|
||||
// available parsers for requests
|
||||
// this is borrowed from how node does things by preallocating parsers
|
||||
var parsers = http.parsers;
|
||||
|
||||
var Proxy = function(opt, cb) {
|
||||
if (!(this instanceof Proxy)) {
|
||||
return new Proxy(opt, cb);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
self.sockets = [];
|
||||
self.waiting = [];
|
||||
|
||||
var id = opt.id;
|
||||
|
||||
// default max is 5
|
||||
var max_tcp_sockets = opt.max_tcp_sockets || 5;
|
||||
|
||||
// new tcp server to service requests for this client
|
||||
var client_server = net.createServer();
|
||||
client_server.listen(function() {
|
||||
var port = client_server.address().port;
|
||||
debug('tcp server listening on port: %d', port);
|
||||
|
||||
cb(null, {
|
||||
// port for lt client tcp connections
|
||||
port: port,
|
||||
// maximum number of tcp connections allowed by lt client
|
||||
max_conn_count: max_tcp_sockets
|
||||
});
|
||||
});
|
||||
|
||||
// track initial user connection setup
|
||||
var conn_timeout;
|
||||
|
||||
// user has 5 seconds to connect before their slot is given up
|
||||
function maybe_tcp_close() {
|
||||
clearTimeout(conn_timeout);
|
||||
conn_timeout = setTimeout(client_server.close.bind(client_server), 5000);
|
||||
}
|
||||
|
||||
maybe_tcp_close();
|
||||
|
||||
// no longer accepting connections for this id
|
||||
client_server.on('close', function() {
|
||||
debug('closed tcp socket for client(%s)', id);
|
||||
|
||||
clearTimeout(conn_timeout);
|
||||
|
||||
// clear waiting by ending responses, (requests?)
|
||||
self.waiting.forEach(function(waiting) {
|
||||
waiting[1].end();
|
||||
waiting[3].end(); // write stream
|
||||
});
|
||||
|
||||
self.emit('end');
|
||||
});
|
||||
|
||||
// new tcp connection from lt client
|
||||
client_server.on('connection', function(socket) {
|
||||
|
||||
// no more socket connections allowed
|
||||
if (self.sockets.length >= max_tcp_sockets) {
|
||||
return socket.end();
|
||||
}
|
||||
|
||||
debug('new connection on port: %s', id);
|
||||
|
||||
// a single connection is enough to keep client id slot open
|
||||
clearTimeout(conn_timeout);
|
||||
|
||||
// allocate a response parser for the socket
|
||||
// it only needs one since it will reuse it
|
||||
socket.parser = parsers.alloc();
|
||||
|
||||
socket._orig_ondata = socket.ondata;
|
||||
socket.ondata = upstream_response;
|
||||
|
||||
socket.once('close', function(had_error) {
|
||||
debug('client %s closed socket (error: %s)', id, had_error);
|
||||
|
||||
// what if socket was servicing a request at this time?
|
||||
// then it will be put back in available after right?
|
||||
|
||||
// remove this socket
|
||||
var idx = self.sockets.indexOf(socket);
|
||||
if (idx >= 0) {
|
||||
self.sockets.splice(idx, 1);
|
||||
}
|
||||
|
||||
// need to track total sockets, not just active available
|
||||
|
||||
debug('remaining client sockets: %s', self.sockets.length);
|
||||
|
||||
// no more sockets for this ident
|
||||
if (self.sockets.length === 0) {
|
||||
debug('all client(%s) sockets disconnected', id);
|
||||
maybe_tcp_close();
|
||||
}
|
||||
});
|
||||
|
||||
// close will be emitted after this
|
||||
socket.on('error', function(err) {
|
||||
log.error(err);
|
||||
socket.end();
|
||||
});
|
||||
|
||||
self.sockets.push(socket);
|
||||
|
||||
var next = self.waiting.shift();
|
||||
if (next) {
|
||||
debug('handling queued request');
|
||||
self.proxy_request(next[0], next[1], next[2], next[3]);
|
||||
}
|
||||
});
|
||||
|
||||
client_server.on('error', function(err) {
|
||||
log.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
Proxy.prototype.__proto__ = EventEmitter.prototype;
|
||||
|
||||
Proxy.prototype.proxy_request = function(req, res, rs, ws) {
|
||||
var self = this;
|
||||
|
||||
// socket is a tcp connection back to the user hosting the site
|
||||
var sock = self.sockets.shift();
|
||||
|
||||
// queue request
|
||||
if (!sock) {
|
||||
debug('no more clients, queued: %s', req.url);
|
||||
rs.pause();
|
||||
self.waiting.push([req, res, rs, ws]);
|
||||
return;
|
||||
}
|
||||
|
||||
debug('handle req: %s', req.url);
|
||||
|
||||
// pipe incoming request into tcp socket
|
||||
// incoming request will close the socket when done
|
||||
// lt client should establish a new socket once request is finished
|
||||
// we do this instead of keeping socket open to make things easier
|
||||
rs.pipe(sock);
|
||||
|
||||
sock.ws = ws;
|
||||
sock.req = req;
|
||||
|
||||
// since tcp connection to upstream are kept open
|
||||
// invoke parsing so we know when the response is complete
|
||||
var parser = sock.parser;
|
||||
parser.reinitialize(HTTPParser.RESPONSE);
|
||||
parser.socket = sock;
|
||||
|
||||
// we have completed a response
|
||||
// the tcp socket is free again
|
||||
parser.onIncoming = function (res) {
|
||||
parser.onMessageComplete = function() {
|
||||
debug('ended response: %s', req.url);
|
||||
|
||||
// any request we had going on is now done
|
||||
ws.end();
|
||||
sock.end();
|
||||
|
||||
// no more forwarding
|
||||
delete sock.ws;
|
||||
delete sock.req;
|
||||
delete parser.onIncoming;
|
||||
};
|
||||
};
|
||||
|
||||
rs.resume();
|
||||
};
|
||||
|
||||
Proxy.prototype.proxy_upgrade = function(req, socket, head) {
|
||||
|
||||
var sock = self.sockets.shift();
|
||||
if (!sock) {
|
||||
// no available sockets to upgrade to
|
||||
// TODO queue?
|
||||
return socket.end();
|
||||
}
|
||||
|
||||
var stream = req.createRawStream();
|
||||
|
||||
sock.ws = ws;
|
||||
sock.upgraded = true;
|
||||
|
||||
stream.once('end', function() {
|
||||
delete sock.ws;
|
||||
|
||||
// when this ends, we just reset the socket to the lt client
|
||||
// this is easier than trying to figure anything else out
|
||||
sock.end();
|
||||
});
|
||||
|
||||
stream.pipe(sock);
|
||||
sock.once('end', socket.end.bind(ws));
|
||||
};
|
||||
|
||||
function upstream_response(d, start, end) {
|
||||
var socket = this;
|
||||
|
||||
var ws = socket.ws;
|
||||
if (!ws) {
|
||||
return log.warn('no stream set for req:', socket.req.url);
|
||||
}
|
||||
|
||||
ws.write(d.slice(start, end));
|
||||
|
||||
if (socket.upgraded) {
|
||||
return;
|
||||
}
|
||||
|
||||
var ret = socket.parser.execute(d, start, end - start);
|
||||
if (ret instanceof Error) {
|
||||
log.error(ret);
|
||||
parsers.free(parser);
|
||||
socket.destroy(ret);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Proxy;
|
||||
|
||||
440
server.js
440
server.js
@@ -1,339 +1,229 @@
|
||||
var http = require('http');
|
||||
var net = require('net');
|
||||
var url = require('url');
|
||||
|
||||
var log = require('bookrc');
|
||||
var express = require('express');
|
||||
var taters = require('taters');
|
||||
var enchilada = require('enchilada');
|
||||
var makeup = require('makeup');
|
||||
var engine = require('engine.io');
|
||||
var browserkthx = require('browserkthx');
|
||||
var debug = require('debug')('localtunnel-server');
|
||||
var createRawServer = require('http-raw');
|
||||
|
||||
var Proxy = require('./proxy');
|
||||
var rand_id = require('./lib/rand_id');
|
||||
|
||||
// here be dragons, understanding of node http internals will be required
|
||||
var HTTPParser = process.binding('http_parser').HTTPParser;
|
||||
var kProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
// id -> client http server
|
||||
var clients = {};
|
||||
|
||||
// available parsers for requests
|
||||
// this is borrowed from how node does things by preallocating parsers
|
||||
var parsers = http.parsers;
|
||||
// proxy statistics
|
||||
var stats = {
|
||||
requests: 0,
|
||||
waiting: 0,
|
||||
tunnels: 0,
|
||||
};
|
||||
|
||||
// send this request to the appropriate client
|
||||
// in -> incoming request stream
|
||||
function proxy_request(client, req, res, rs, ws) {
|
||||
|
||||
// socket is a tcp connection back to the user hosting the site
|
||||
var sock = client.sockets.shift();
|
||||
|
||||
// queue request
|
||||
if (!sock) {
|
||||
debug('no more clients, queued: %s', req.url);
|
||||
rs.pause();
|
||||
client.waiting.push([req, res, rs, ws]);
|
||||
return;
|
||||
}
|
||||
|
||||
debug('handle req: %s', req.url);
|
||||
|
||||
// pipe incoming request into tcp socket
|
||||
// incoming request will close the socket when done
|
||||
// lt client should establish a new socket once request is finished
|
||||
// we do this instead of keeping socket open to make things easier
|
||||
rs.pipe(sock);
|
||||
|
||||
sock.ws = ws;
|
||||
sock.req = req;
|
||||
|
||||
// since tcp connection to upstream are kept open
|
||||
// invoke parsing so we know when the response is complete
|
||||
var parser = sock.parser;
|
||||
parser.reinitialize(HTTPParser.RESPONSE);
|
||||
parser.socket = sock;
|
||||
|
||||
// we have completed a response
|
||||
// the tcp socket is free again
|
||||
parser.onIncoming = function (res) {
|
||||
parser.onMessageComplete = function() {
|
||||
debug('ended response: %s', req.url);
|
||||
|
||||
// any request we had going on is now done
|
||||
ws.end();
|
||||
sock.end();
|
||||
|
||||
// no more forwarding
|
||||
delete sock.ws;
|
||||
delete sock.req;
|
||||
delete parser.onIncoming;
|
||||
};
|
||||
};
|
||||
|
||||
rs.resume();
|
||||
}
|
||||
|
||||
function upstream_response(d, start, end) {
|
||||
var socket = this;
|
||||
|
||||
var ws = socket.ws;
|
||||
if (!ws) {
|
||||
return log.warn('no stream set for req:', socket.req.url);
|
||||
}
|
||||
|
||||
ws.write(d.slice(start, end));
|
||||
|
||||
if (socket.upgraded) {
|
||||
return;
|
||||
}
|
||||
|
||||
var ret = socket.parser.execute(d, start, end - start);
|
||||
if (ret instanceof Error) {
|
||||
log.error(ret);
|
||||
parsers.free(parser);
|
||||
socket.destroy(ret);
|
||||
}
|
||||
}
|
||||
|
||||
var handle_req = function (req, res) {
|
||||
|
||||
var max_tcp_sockets = req.socket.server.max_tcp_sockets;
|
||||
// return true if request will be handled, false otherwise
|
||||
function middleware(req, res) {
|
||||
|
||||
// without a hostname, we won't know who the request is for
|
||||
var hostname = req.headers.host;
|
||||
if (!hostname) {
|
||||
log.trace('no hostname: %j', req.headers);
|
||||
return res.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
var match = hostname.match(/^([a-z]{4})[.].*/);
|
||||
if (match) {
|
||||
|
||||
// not for a specific client
|
||||
if (!match) {
|
||||
var match = req.url.match(/\/([a-z]{4})$/);
|
||||
|
||||
var req_id;
|
||||
|
||||
if (req.url === '/?new') {
|
||||
req_id = rand_id();
|
||||
}
|
||||
else if (match && match[1]) {
|
||||
req_id = match[1];
|
||||
}
|
||||
|
||||
// will not handle
|
||||
if (!req_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
new_client(req_id, {}, function(err, info) {
|
||||
if (err) {
|
||||
res.statusCode = 500;
|
||||
return res.end(err.message);
|
||||
}
|
||||
|
||||
var url = 'http://' + req_id + '.' + req.headers.host;
|
||||
info.url = url;
|
||||
res.end(JSON.stringify(info));
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var client_id = match[1];
|
||||
var client = clients[client_id];
|
||||
|
||||
// no such subdomain
|
||||
// we use 502 error to the client to signify we can't service the request
|
||||
if (!client) {
|
||||
debug('no client found for id: ' + client_id);
|
||||
res.statusCode = 502;
|
||||
return res.end('localtunnel error: no active client for \'' + client_id + '\'');
|
||||
res.end('localtunnel error: no active client for \'' + client_id + '\'');
|
||||
return true;
|
||||
}
|
||||
|
||||
++stats.requests;
|
||||
|
||||
res.on('close', function() {
|
||||
--stats.requests;
|
||||
});
|
||||
|
||||
var rs = req.createRawStream();
|
||||
var ws = res.createRawStream();
|
||||
|
||||
return proxy_request(client, req, res, rs, ws);
|
||||
}
|
||||
|
||||
/// NOTE: everything below is for new client setup (not proxied request)
|
||||
|
||||
// ignore favicon requests
|
||||
if (req.url === '/favicon.ico') {
|
||||
res.writeHead(404);
|
||||
return res.end();
|
||||
}
|
||||
|
||||
var parsed = url.parse(req.url, true);
|
||||
|
||||
// redirect main page to github reference for root requests
|
||||
if (req.url === '/' && !parsed.query.new) {
|
||||
res.writeHead(301, { Location: 'http://shtylman.github.com/localtunnel/' });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// at this point, the client is requesting a new tunnel setup
|
||||
// either generate an id or use the one they requested
|
||||
|
||||
var match = req.url.match(/\/([a-z]{4})?/);
|
||||
|
||||
// user can request a particular set of characters
|
||||
// will be given if not already taken
|
||||
// this is useful when the main server is restarted
|
||||
// users can keep testing with their expected ids
|
||||
var requested_id;
|
||||
if (match && match[1]) {
|
||||
requested_id = match[1];
|
||||
}
|
||||
|
||||
var id = requested_id || rand_id();
|
||||
|
||||
// if the id already exists, this client is assigned a random id
|
||||
if (clients[id]) {
|
||||
id = rand_id();
|
||||
}
|
||||
|
||||
// sockets is a list of available sockets for the connection
|
||||
// waiting is a list of requests still needing to be handled
|
||||
var client = clients[id] = {
|
||||
sockets: [],
|
||||
waiting: []
|
||||
};
|
||||
|
||||
// new tcp server to service requests for this client
|
||||
var client_server = net.createServer();
|
||||
client_server.listen(function() {
|
||||
var port = client_server.address().port;
|
||||
debug('tcp server listening on port: %d', port);
|
||||
|
||||
var url = 'http://' + id + '.' + req.headers.host;
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
// full url for internet facing requests
|
||||
url: url,
|
||||
// "subdomain" part
|
||||
id: id,
|
||||
// port for lt client tcp connections
|
||||
port: port,
|
||||
// maximum number of tcp connections allowed by lt client
|
||||
max_conn_count: max_tcp_sockets
|
||||
}));
|
||||
});
|
||||
|
||||
// track initial user connection setup
|
||||
var conn_timeout;
|
||||
|
||||
// user has 5 seconds to connect before their slot is given up
|
||||
function maybe_tcp_close() {
|
||||
clearTimeout(conn_timeout);
|
||||
conn_timeout = setTimeout(client_server.close.bind(client_server), 5000);
|
||||
}
|
||||
|
||||
maybe_tcp_close();
|
||||
|
||||
// no longer accepting connections for this id
|
||||
client_server.on('close', function() {
|
||||
log.trace('closed tcp socket for client(%s)', id);
|
||||
|
||||
clearTimeout(conn_timeout);
|
||||
delete clients[id];
|
||||
|
||||
// clear waiting by ending responses, (requests?)
|
||||
client.waiting.forEach(function(waiting) {
|
||||
waiting[1].end();
|
||||
waiting[3].end(); // write stream
|
||||
});
|
||||
});
|
||||
|
||||
// new tcp connection from lt client
|
||||
client_server.on('connection', function(socket) {
|
||||
|
||||
// no more socket connections allowed
|
||||
if (client.sockets.length >= max_tcp_sockets) {
|
||||
return socket.end();
|
||||
}
|
||||
|
||||
debug('new connection for id: %s', id);
|
||||
|
||||
// a single connection is enough to keep client id slot open
|
||||
clearTimeout(conn_timeout);
|
||||
|
||||
// allocate a response parser for the socket
|
||||
// it only needs one since it will reuse it
|
||||
socket.parser = parsers.alloc();
|
||||
|
||||
socket._orig_ondata = socket.ondata;
|
||||
socket.ondata = upstream_response;
|
||||
|
||||
socket.once('close', function(had_error) {
|
||||
debug('client %s closed socket (error: %s)', id, had_error);
|
||||
|
||||
// what if socket was servicing a request at this time?
|
||||
// then it will be put back in available after right?
|
||||
|
||||
// remove this socket
|
||||
var idx = client.sockets.indexOf(socket);
|
||||
if (idx >= 0) {
|
||||
client.sockets.splice(idx, 1);
|
||||
}
|
||||
|
||||
// need to track total sockets, not just active available
|
||||
|
||||
debug('remaining client sockets: %s', client.sockets.length);
|
||||
|
||||
// no more sockets for this ident
|
||||
if (client.sockets.length === 0) {
|
||||
debug('all client(%s) sockets disconnected', id);
|
||||
maybe_tcp_close();
|
||||
}
|
||||
});
|
||||
|
||||
// close will be emitted after this
|
||||
socket.on('error', function(err) {
|
||||
log.error(err);
|
||||
socket.end();
|
||||
});
|
||||
|
||||
client.sockets.push(socket);
|
||||
|
||||
var next = client.waiting.shift();
|
||||
if (next) {
|
||||
debug('handling queued request');
|
||||
proxy_request(client, next[0], next[1], next[2], next[3]);
|
||||
}
|
||||
});
|
||||
|
||||
client_server.on('error', function(err) {
|
||||
log.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
var handle_upgrade = function(req, ws) {
|
||||
|
||||
if (req.headers.connection !== 'Upgrade') {
|
||||
return;
|
||||
}
|
||||
client.proxy_request(req, res, rs, ws);
|
||||
return true;
|
||||
}
|
||||
|
||||
var handle_upgrade = function(req, socket, head) {
|
||||
var hostname = req.headers.host;
|
||||
if (!hostname) {
|
||||
return res.end();
|
||||
return socket.end();
|
||||
}
|
||||
|
||||
var match = hostname.match(/^([a-z]{4})[.].*/);
|
||||
|
||||
// not a valid client
|
||||
// not handled by us
|
||||
if (!match) {
|
||||
return res.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
var client_id = match[1];
|
||||
var client = clients[client_id];
|
||||
|
||||
if (!client) {
|
||||
// no such subdomain
|
||||
return res.end();
|
||||
if (!client) {
|
||||
return socket.end();
|
||||
}
|
||||
|
||||
var socket = client.sockets.shift();
|
||||
if (!socket) {
|
||||
// no available sockets to upgrade to
|
||||
return res.end();
|
||||
client.handle_upgrade(req, socket, head);
|
||||
return true;
|
||||
};
|
||||
|
||||
function new_client(id, opt, cb) {
|
||||
|
||||
// can't ask for id already is use
|
||||
// TODO check this new id again
|
||||
if (clients[id]) {
|
||||
id = rand_id();
|
||||
}
|
||||
|
||||
var stream = req.createRawStream();
|
||||
var popt = {
|
||||
id: id,
|
||||
max_tcp_sockets: opt.max_tcp_sockets
|
||||
};
|
||||
|
||||
socket.ws = ws;
|
||||
socket.upgraded = true;
|
||||
var client = Proxy(popt, function(err, info) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
stream.once('end', function() {
|
||||
delete socket.ws;
|
||||
++stats.tunnels;
|
||||
clients[id] = client;
|
||||
|
||||
// when this ends, we just reset the socket to the lt client
|
||||
// this is easier than trying to figure anything else out
|
||||
socket.end();
|
||||
info.id = id;
|
||||
|
||||
cb(err, info);
|
||||
});
|
||||
|
||||
stream.pipe(socket);
|
||||
socket.once('end', ws.end.bind(ws));
|
||||
};
|
||||
client.on('end', function() {
|
||||
--stats.tunnels;
|
||||
delete clients[id];
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function(opt) {
|
||||
opt = opt || {};
|
||||
|
||||
var server = createRawServer();
|
||||
|
||||
server.max_tcp_sockets = opt.max_tcp_sockets || 5;
|
||||
server.on('request', handle_req);
|
||||
var app = express();
|
||||
|
||||
app.set('view engine', 'html');
|
||||
app.engine('html', require('hbs').__express);
|
||||
|
||||
app.use(function(req, res, next) {
|
||||
if (middleware(req, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(express.favicon());
|
||||
|
||||
app.use(browserkthx({ ie: '< 9' }));
|
||||
app.use(taters({ cache: kProduction }));
|
||||
|
||||
app.use(enchilada({
|
||||
src: __dirname + '/static/',
|
||||
compress: kProduction,
|
||||
cache: kProduction
|
||||
}));
|
||||
|
||||
app.use('/css/widgets.css', makeup(__dirname + '/static/css/widgets.css'));
|
||||
app.use(express.static(__dirname + '/static'));
|
||||
app.use(app.router);
|
||||
|
||||
app.get('/', function(req, res, next) {
|
||||
return res.render('index');
|
||||
});
|
||||
|
||||
// connected engine.io sockets for stats updates
|
||||
var eio_sockets = [];
|
||||
|
||||
setInterval(function() {
|
||||
eio_sockets.forEach(function(socket) {
|
||||
socket.send(JSON.stringify(stats));
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
var eio_server = new engine.Server();
|
||||
eio_server.on('connection', function (socket) {
|
||||
|
||||
eio_sockets.push(socket);
|
||||
socket.send(JSON.stringify(stats));
|
||||
|
||||
socket.on('close', function() {
|
||||
|
||||
// remove from socket pool so no more updates are sent
|
||||
var idx = eio_sockets.indexOf(socket);
|
||||
if (idx >= 0) {
|
||||
eio_sockets.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/engine.io', function(req, res, next) {
|
||||
eio_server.handleRequest(req, res);
|
||||
});
|
||||
|
||||
server.on('request', app);
|
||||
server.on('upgrade', handle_upgrade);
|
||||
|
||||
server.on('upgrade', function(req, socket, head) {
|
||||
if (handle_upgrade(req, socket, head)) {
|
||||
return;
|
||||
}
|
||||
|
||||
eio_server.handleUpgrade(req, socket, head);
|
||||
});
|
||||
|
||||
return server;
|
||||
};
|
||||
|
||||
|
||||
8
static/css/grid.css
Normal file
8
static/css/grid.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.row {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.row .half {
|
||||
width: 49%;
|
||||
display: inline-block;
|
||||
}
|
||||
70
static/css/pygment_trac.css
Normal file
70
static/css/pygment_trac.css
Normal file
@@ -0,0 +1,70 @@
|
||||
.highlight .hll { background-color: #ffffcc }
|
||||
.highlight { background: #f0f3f3; }
|
||||
.highlight .c { color: #0099FF; font-style: italic } /* Comment */
|
||||
.highlight .err { color: #AA0000; background-color: #FFAAAA } /* Error */
|
||||
.highlight .k { color: #006699; font-weight: bold } /* Keyword */
|
||||
.highlight .o { color: #555555 } /* Operator */
|
||||
.highlight .cm { color: #0099FF; font-style: italic } /* Comment.Multiline */
|
||||
.highlight .cp { color: #009999 } /* Comment.Preproc */
|
||||
.highlight .c1 { color: #0099FF; font-style: italic } /* Comment.Single */
|
||||
.highlight .cs { color: #0099FF; font-weight: bold; font-style: italic } /* Comment.Special */
|
||||
.highlight .gd { background-color: #FFCCCC; border: 1px solid #CC0000 } /* Generic.Deleted */
|
||||
.highlight .ge { font-style: italic } /* Generic.Emph */
|
||||
.highlight .gr { color: #FF0000 } /* Generic.Error */
|
||||
.highlight .gh { color: #003300; font-weight: bold } /* Generic.Heading */
|
||||
.highlight .gi { background-color: #CCFFCC; border: 1px solid #00CC00 } /* Generic.Inserted */
|
||||
.highlight .go { color: #AAAAAA } /* Generic.Output */
|
||||
.highlight .gp { color: #000099; font-weight: bold } /* Generic.Prompt */
|
||||
.highlight .gs { font-weight: bold } /* Generic.Strong */
|
||||
.highlight .gu { color: #003300; font-weight: bold } /* Generic.Subheading */
|
||||
.highlight .gt { color: #99CC66 } /* Generic.Traceback */
|
||||
.highlight .kc { color: #006699; font-weight: bold } /* Keyword.Constant */
|
||||
.highlight .kd { color: #006699; font-weight: bold } /* Keyword.Declaration */
|
||||
.highlight .kn { color: #006699; font-weight: bold } /* Keyword.Namespace */
|
||||
.highlight .kp { color: #006699 } /* Keyword.Pseudo */
|
||||
.highlight .kr { color: #006699; font-weight: bold } /* Keyword.Reserved */
|
||||
.highlight .kt { color: #007788; font-weight: bold } /* Keyword.Type */
|
||||
.highlight .m { color: #FF6600 } /* Literal.Number */
|
||||
.highlight .s { color: #CC3300 } /* Literal.String */
|
||||
.highlight .na { color: #330099 } /* Name.Attribute */
|
||||
.highlight .nb { color: #336666 } /* Name.Builtin */
|
||||
.highlight .nc { color: #00AA88; font-weight: bold } /* Name.Class */
|
||||
.highlight .no { color: #336600 } /* Name.Constant */
|
||||
.highlight .nd { color: #9999FF } /* Name.Decorator */
|
||||
.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */
|
||||
.highlight .ne { color: #CC0000; font-weight: bold } /* Name.Exception */
|
||||
.highlight .nf { color: #CC00FF } /* Name.Function */
|
||||
.highlight .nl { color: #9999FF } /* Name.Label */
|
||||
.highlight .nn { color: #00CCFF; font-weight: bold } /* Name.Namespace */
|
||||
.highlight .nt { color: #330099; font-weight: bold } /* Name.Tag */
|
||||
.highlight .nv { color: #003333 } /* Name.Variable */
|
||||
.highlight .ow { color: #000000; font-weight: bold } /* Operator.Word */
|
||||
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
|
||||
.highlight .mf { color: #FF6600 } /* Literal.Number.Float */
|
||||
.highlight .mh { color: #FF6600 } /* Literal.Number.Hex */
|
||||
.highlight .mi { color: #FF6600 } /* Literal.Number.Integer */
|
||||
.highlight .mo { color: #FF6600 } /* Literal.Number.Oct */
|
||||
.highlight .sb { color: #CC3300 } /* Literal.String.Backtick */
|
||||
.highlight .sc { color: #CC3300 } /* Literal.String.Char */
|
||||
.highlight .sd { color: #CC3300; font-style: italic } /* Literal.String.Doc */
|
||||
.highlight .s2 { color: #CC3300 } /* Literal.String.Double */
|
||||
.highlight .se { color: #CC3300; font-weight: bold } /* Literal.String.Escape */
|
||||
.highlight .sh { color: #CC3300 } /* Literal.String.Heredoc */
|
||||
.highlight .si { color: #AA0000 } /* Literal.String.Interpol */
|
||||
.highlight .sx { color: #CC3300 } /* Literal.String.Other */
|
||||
.highlight .sr { color: #33AAAA } /* Literal.String.Regex */
|
||||
.highlight .s1 { color: #CC3300 } /* Literal.String.Single */
|
||||
.highlight .ss { color: #FFCC33 } /* Literal.String.Symbol */
|
||||
.highlight .bp { color: #336666 } /* Name.Builtin.Pseudo */
|
||||
.highlight .vc { color: #003333 } /* Name.Variable.Class */
|
||||
.highlight .vg { color: #003333 } /* Name.Variable.Global */
|
||||
.highlight .vi { color: #003333 } /* Name.Variable.Instance */
|
||||
.highlight .il { color: #FF6600 } /* Literal.Number.Integer.Long */
|
||||
|
||||
.type-csharp .highlight .k { color: #0000FF }
|
||||
.type-csharp .highlight .kt { color: #0000FF }
|
||||
.type-csharp .highlight .nf { color: #000000; font-weight: normal }
|
||||
.type-csharp .highlight .nc { color: #2B91AF }
|
||||
.type-csharp .highlight .nn { color: #000000 }
|
||||
.type-csharp .highlight .s { color: #A31515 }
|
||||
.type-csharp .highlight .sc { color: #A31515 }
|
||||
437
static/css/style.css
Normal file
437
static/css/style.css
Normal file
@@ -0,0 +1,437 @@
|
||||
/*******************************************************************************
|
||||
Slate Theme for Github Pages
|
||||
by Jason Costello, @jsncostello
|
||||
*******************************************************************************/
|
||||
|
||||
* {
|
||||
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
|
||||
-moz-box-sizing: border-box; /* Firefox, other Gecko */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@import url(pygment_trac.css);
|
||||
|
||||
/*******************************************************************************
|
||||
MeyerWeb Reset
|
||||
*******************************************************************************/
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
blockquote, q {
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
a:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
Theme Styles
|
||||
*******************************************************************************/
|
||||
|
||||
body {
|
||||
box-sizing: border-box;
|
||||
color:#373737;
|
||||
background: #212121;
|
||||
font-size: 16px;
|
||||
font-family: 'Myriad Pro', Calibri, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 10px 0;
|
||||
font-weight: 700;
|
||||
color:#222222;
|
||||
font-family: 'Lucida Grande', 'Calibri', Helvetica, Arial, sans-serif;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding-bottom: 10px;
|
||||
font-size: 32px;
|
||||
background: url('../img/bg_hr.png') repeat-x bottom;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 10px 0 15px 0;
|
||||
}
|
||||
|
||||
footer p {
|
||||
color: #f2f2f2;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #007edf;
|
||||
text-shadow: none;
|
||||
|
||||
transition: color 0.5s ease;
|
||||
transition: text-shadow 0.5s ease;
|
||||
-webkit-transition: color 0.5s ease;
|
||||
-webkit-transition: text-shadow 0.5s ease;
|
||||
-moz-transition: color 0.5s ease;
|
||||
-moz-transition: text-shadow 0.5s ease;
|
||||
-o-transition: color 0.5s ease;
|
||||
-o-transition: text-shadow 0.5s ease;
|
||||
-ms-transition: color 0.5s ease;
|
||||
-ms-transition: text-shadow 0.5s ease;
|
||||
}
|
||||
|
||||
#main_content a:hover {
|
||||
color: #0069ba;
|
||||
text-shadow: #0090ff 0px 0px 2px;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: #43adff;
|
||||
text-shadow: #0090ff 0px 0px 2px;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
max-width: 739px;
|
||||
padding: 5px;
|
||||
margin: 10px 0 10px 0;
|
||||
border: 1px solid #ebebeb;
|
||||
|
||||
box-shadow: 0 0 5px #ebebeb;
|
||||
-webkit-box-shadow: 0 0 5px #ebebeb;
|
||||
-moz-box-shadow: 0 0 5px #ebebeb;
|
||||
-o-box-shadow: 0 0 5px #ebebeb;
|
||||
-ms-box-shadow: 0 0 5px #ebebeb;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
width: 100%;
|
||||
color: #222;
|
||||
background-color: #fff;
|
||||
|
||||
font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace;
|
||||
font-size: 14px;
|
||||
|
||||
border-radius: 2px;
|
||||
-moz-border-radius: 2px;
|
||||
-webkit-border-radius: 2px;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,.1);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 3px;
|
||||
margin: 0 3px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
pre code {
|
||||
display: block;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
padding: 0 0 0 20px;
|
||||
border-left: 3px solid #bbb;
|
||||
}
|
||||
|
||||
ul, ol, dl {
|
||||
margin-bottom: 15px
|
||||
}
|
||||
|
||||
ul li {
|
||||
list-style: inside;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
ol li {
|
||||
list-style: decimal inside;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
dl dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
dl dd {
|
||||
padding-left: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
dl p {
|
||||
padding-left: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 1px;
|
||||
margin-bottom: 5px;
|
||||
border: none;
|
||||
background: url('../img/bg_hr.png') repeat-x center;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 1px solid #373737;
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
font-family: 'Lucida Grande', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
padding: 10px;
|
||||
background: #373737;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px;
|
||||
border: 1px solid #373737;
|
||||
}
|
||||
|
||||
form {
|
||||
background: #f2f2f2;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
Full-Width Styles
|
||||
*******************************************************************************/
|
||||
|
||||
.outer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inner {
|
||||
position: relative;
|
||||
max-width: 640px;
|
||||
padding: 20px 10px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#forkme_banner {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top:0;
|
||||
right: 10px;
|
||||
z-index: 10;
|
||||
padding: 10px 50px 10px 10px;
|
||||
color: #fff;
|
||||
background: url('../img/blacktocat.png') #0090ff no-repeat 95% 50%;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,.5);
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
#header_wrap {
|
||||
background: #212121;
|
||||
background: -moz-linear-gradient(top, #373737, #212121);
|
||||
background: -webkit-linear-gradient(top, #373737, #212121);
|
||||
background: -ms-linear-gradient(top, #373737, #212121);
|
||||
background: -o-linear-gradient(top, #373737, #212121);
|
||||
background: linear-gradient(top, #373737, #212121);
|
||||
}
|
||||
|
||||
#header_wrap .inner {
|
||||
padding: 50px 10px 30px 10px;
|
||||
}
|
||||
|
||||
#project_title {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
text-shadow: #111 0px 0px 10px;
|
||||
}
|
||||
|
||||
#project_tagline {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
background: none;
|
||||
text-shadow: #111 0px 0px 10px;
|
||||
}
|
||||
|
||||
#downloads {
|
||||
position: absolute;
|
||||
width: 210px;
|
||||
z-index: 10;
|
||||
bottom: -40px;
|
||||
right: 0;
|
||||
height: 70px;
|
||||
background: url('../img/icon_download.png') no-repeat 0% 90%;
|
||||
}
|
||||
|
||||
.zip_download_link {
|
||||
display: block;
|
||||
float: right;
|
||||
width: 90px;
|
||||
height:70px;
|
||||
text-indent: -5000px;
|
||||
overflow: hidden;
|
||||
background: url(../img/sprite_download.png) no-repeat bottom left;
|
||||
}
|
||||
|
||||
.tar_download_link {
|
||||
display: block;
|
||||
float: right;
|
||||
width: 90px;
|
||||
height:70px;
|
||||
text-indent: -5000px;
|
||||
overflow: hidden;
|
||||
background: url(../img/sprite_download.png) no-repeat bottom right;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.zip_download_link:hover {
|
||||
background: url(../img/sprite_download.png) no-repeat top left;
|
||||
}
|
||||
|
||||
.tar_download_link:hover {
|
||||
background: url(../img/sprite_download.png) no-repeat top right;
|
||||
}
|
||||
|
||||
#main_content_wrap {
|
||||
background: #f2f2f2;
|
||||
border-top: 1px solid #111;
|
||||
border-bottom: 1px solid #111;
|
||||
}
|
||||
|
||||
#main_content {
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
#footer_wrap {
|
||||
background: #212121;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
Small Device Styles
|
||||
*******************************************************************************/
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
body {
|
||||
font-size:14px;
|
||||
}
|
||||
|
||||
#downloads {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.inner {
|
||||
min-width: 320px;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
#project_title {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
min-width: 320px;
|
||||
max-width: 480px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
}
|
||||
1
static/css/widgets.css
Normal file
1
static/css/widgets.css
Normal file
@@ -0,0 +1 @@
|
||||
@import 'flip-counter';
|
||||
BIN
static/img/bg_hr.png
Normal file
BIN
static/img/bg_hr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 943 B |
BIN
static/img/blacktocat.png
Normal file
BIN
static/img/blacktocat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
1
static/img/digits.png
Symbolic link
1
static/img/digits.png
Symbolic link
@@ -0,0 +1 @@
|
||||
../../node_modules/flip-counter/img/digits.png
|
||||
BIN
static/img/icon_download.png
Normal file
BIN
static/img/icon_download.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
static/img/sprite_download.png
Normal file
BIN
static/img/sprite_download.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
37
static/js/index.js
Normal file
37
static/js/index.js
Normal file
@@ -0,0 +1,37 @@
|
||||
var eio = require('engine.io-client');
|
||||
var flipCounter = require('flip-counter');
|
||||
|
||||
var request_counter = new flipCounter('request-count', {
|
||||
value: 0,
|
||||
pace: 10,
|
||||
fW: 30,
|
||||
tFH: 20,
|
||||
bFH: 40,
|
||||
bOffset: 200,
|
||||
auto: false
|
||||
});
|
||||
|
||||
var user_counter = new flipCounter('user-count', {
|
||||
value: 0,
|
||||
pace: 10,
|
||||
fW: 30,
|
||||
tFH: 20,
|
||||
bFH: 40,
|
||||
bOffset: 200,
|
||||
auto: false
|
||||
});
|
||||
|
||||
var socket = eio('/');
|
||||
socket.on('open', function () {
|
||||
});
|
||||
|
||||
socket.on('message', function (data) {
|
||||
var msg = JSON.parse(data);
|
||||
request_counter.incrementTo(msg.requests);
|
||||
user_counter.incrementTo(msg.tunnels);
|
||||
});
|
||||
|
||||
socket.on('close', function () {
|
||||
request_counter.incrementTo(0);
|
||||
user_counter.incrementTo(0);
|
||||
});
|
||||
@@ -3,7 +3,7 @@ var url = require('url');
|
||||
var assert = require('assert');
|
||||
|
||||
var localtunnel_server = require('../server')();
|
||||
var localtunnel_client = require('localtunnel').client;
|
||||
var localtunnel_client = require('localtunnel');
|
||||
|
||||
test('setup localtunnel server', function(done) {
|
||||
localtunnel_server.listen(3000, function() {
|
||||
|
||||
@@ -6,7 +6,7 @@ var localtunnel_server = require('../server')({
|
||||
max_tcp_sockets: 1
|
||||
});
|
||||
|
||||
var localtunnel_client = require('localtunnel').client;
|
||||
var localtunnel_client = require('localtunnel');
|
||||
|
||||
var server;
|
||||
|
||||
|
||||
98
views/index.html
Normal file
98
views/index.html
Normal file
@@ -0,0 +1,98 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset='utf-8' />
|
||||
<meta http-equiv="X-UA-Compatible" content="chrome=1" />
|
||||
<meta name="description" content="localtunnel : expose yourself to the world" />
|
||||
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="/css/style.css">
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="/css/grid.css">
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="/css/widgets.css">
|
||||
|
||||
<title>localtunnel</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- HEADER -->
|
||||
<div id="header_wrap" class="outer">
|
||||
<header class="inner">
|
||||
<a id="forkme_banner" href="https://github.com/shtylman/localtunnel">View on GitHub</a>
|
||||
|
||||
<h1 id="project_title">localtunnel</h1>
|
||||
<h2 id="project_tagline">expose yourself to the world</h2>
|
||||
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<!-- MAIN CONTENT -->
|
||||
<div id="main_content_wrap" class="outer">
|
||||
|
||||
<section id="main_content" class="inner">
|
||||
|
||||
<div class="row">
|
||||
<div class="half">
|
||||
<h3>Requests</h3>
|
||||
<div id="request-count" class="flip-counter"></div>
|
||||
</div>
|
||||
|
||||
<div class="half">
|
||||
<h3>Tunnels</h3>
|
||||
<div id="user-count" class="flip-counter"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Ever wish you could get feedback from friends or colleagues about a web project without deploying it? Localtunnel helps you do just that! Run the simple proxy and get a web facing url which you can share with anyone.</p>
|
||||
|
||||
<h3>install</h3>
|
||||
|
||||
<div class="highlight"><pre>npm install -g localtunnel
|
||||
</pre></div>
|
||||
|
||||
<p>Lets say I have a local webserver running on port 8000. I can expose it to the world just by running</p>
|
||||
|
||||
<div class="highlight"><pre><span class="nv">$ </span>lt --port 8000
|
||||
your url is: http://gqgh.localtunnel.me
|
||||
</pre></div>
|
||||
|
||||
<p>You can now share <a href="http://gqgh.localtunnel.me">http://gqgh.localtunnel.me</a> with anyone. As long as your local instance of <code>lt</code> is running, this url will remain active. Any requests to that url will be routed to your service on port 8000.</p>
|
||||
|
||||
<h3>uses</h3>
|
||||
|
||||
<p>Beyond sharing with friends, localtunnel makes a great tool for testing with any service which needs to hit internet visible URLs.</p>
|
||||
|
||||
<ul>
|
||||
<li>browserling.com</li>
|
||||
<li>twillio testing</li>
|
||||
<li>sendgrid web api</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<div id="footer_wrap" class="outer">
|
||||
<footer class="inner">
|
||||
<p class="copyright">localtunnel maintained by <a href="https://github.com/shtylman">shtylman</a></p>
|
||||
<p>Theme from <a href="http://pages.github.com">GitHub Pages</a></p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="/js/index.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
var _gaq = _gaq || [];
|
||||
_gaq.push(['_setAccount', 'UA-574889-7']);
|
||||
_gaq.push(['_trackPageview']);
|
||||
|
||||
(function() {
|
||||
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user