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:
Roman Shtylman
2013-06-18 23:00:45 -04:00
parent b153c00cb5
commit d15e568cea
16 changed files with 1085 additions and 299 deletions

View File

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

482
server.js
View File

@@ -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();
}
var match = hostname.match(/^([a-z]{4})[.].*/);
if (match) {
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 + '\'');
}
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;
}
var hostname = req.headers.host;
if (!hostname) {
return res.end();
return false;
}
var match = hostname.match(/^([a-z]{4})[.].*/);
// not a valid client
// not for a specific client
if (!match) {
return res.end();
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) {
// no such subdomain
return res.end();
res.statusCode = 502;
res.end('localtunnel error: no active client for \'' + client_id + '\'');
return true;
}
var socket = client.sockets.shift();
if (!socket) {
// no available sockets to upgrade to
return res.end();
}
++stats.requests;
var stream = req.createRawStream();
socket.ws = ws;
socket.upgraded = true;
stream.once('end', function() {
delete socket.ws;
// when this ends, we just reset the socket to the lt client
// this is easier than trying to figure anything else out
socket.end();
res.on('close', function() {
--stats.requests;
});
stream.pipe(socket);
socket.once('end', ws.end.bind(ws));
var rs = req.createRawStream();
var ws = res.createRawStream();
client.proxy_request(req, res, rs, ws);
return true;
}
var handle_upgrade = function(req, socket, head) {
var hostname = req.headers.host;
if (!hostname) {
return socket.end();
}
var match = hostname.match(/^([a-z]{4})[.].*/);
// not handled by us
if (!match) {
return false;
}
var client_id = match[1];
var client = clients[client_id];
// no such subdomain
if (!client) {
return socket.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 popt = {
id: id,
max_tcp_sockets: opt.max_tcp_sockets
};
var client = Proxy(popt, function(err, info) {
if (err) {
return cb(err);
}
++stats.tunnels;
clients[id] = client;
info.id = id;
cb(err, info);
});
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
View File

@@ -0,0 +1,8 @@
.row {
position: relative;
}
.row .half {
width: 49%;
display: inline-block;
}

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

@@ -0,0 +1 @@
@import 'flip-counter';

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

1
static/img/digits.png Symbolic link
View File

@@ -0,0 +1 @@
../../node_modules/flip-counter/img/digits.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

37
static/js/index.js Normal file
View 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);
});

View File

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

View File

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