diff --git a/package.json b/package.json index a68b478..86d10ab 100644 --- a/package.json +++ b/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", diff --git a/proxy.js b/proxy.js new file mode 100644 index 0000000..84f4093 --- /dev/null +++ b/proxy.js @@ -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; + diff --git a/server.js b/server.js index 2644d64..be00e0b 100644 --- a/server.js +++ b/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(); - } - - 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; }; diff --git a/static/css/grid.css b/static/css/grid.css new file mode 100644 index 0000000..3a02af8 --- /dev/null +++ b/static/css/grid.css @@ -0,0 +1,8 @@ +.row { + position: relative; +} + +.row .half { + width: 49%; + display: inline-block; +} diff --git a/static/css/pygment_trac.css b/static/css/pygment_trac.css new file mode 100644 index 0000000..e65cedf --- /dev/null +++ b/static/css/pygment_trac.css @@ -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 } diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..58f37a3 --- /dev/null +++ b/static/css/style.css @@ -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; + } + +} diff --git a/static/css/widgets.css b/static/css/widgets.css new file mode 100644 index 0000000..ba3ed2e --- /dev/null +++ b/static/css/widgets.css @@ -0,0 +1 @@ +@import 'flip-counter'; diff --git a/static/img/bg_hr.png b/static/img/bg_hr.png new file mode 100644 index 0000000..7973bd6 Binary files /dev/null and b/static/img/bg_hr.png differ diff --git a/static/img/blacktocat.png b/static/img/blacktocat.png new file mode 100644 index 0000000..6e264fe Binary files /dev/null and b/static/img/blacktocat.png differ diff --git a/static/img/digits.png b/static/img/digits.png new file mode 120000 index 0000000..ab1b6d6 --- /dev/null +++ b/static/img/digits.png @@ -0,0 +1 @@ +../../node_modules/flip-counter/img/digits.png \ No newline at end of file diff --git a/static/img/icon_download.png b/static/img/icon_download.png new file mode 100644 index 0000000..a2a287f Binary files /dev/null and b/static/img/icon_download.png differ diff --git a/static/img/sprite_download.png b/static/img/sprite_download.png new file mode 100644 index 0000000..f2babd5 Binary files /dev/null and b/static/img/sprite_download.png differ diff --git a/static/js/index.js b/static/js/index.js new file mode 100644 index 0000000..30ddb0d --- /dev/null +++ b/static/js/index.js @@ -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); +}); diff --git a/test/basic.js b/test/basic.js index f4a4073..6c0809d 100644 --- a/test/basic.js +++ b/test/basic.js @@ -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() { diff --git a/test/queue.js b/test/queue.js index 844f542..d7e292c 100644 --- a/test/queue.js +++ b/test/queue.js @@ -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; diff --git a/views/index.html b/views/index.html new file mode 100644 index 0000000..e59e5fa --- /dev/null +++ b/views/index.html @@ -0,0 +1,98 @@ + + + + + + + + + + + + + localtunnel + + + + + +
+
+ View on GitHub + +

localtunnel

+

expose yourself to the world

+ +
+
+ + +
+ +
+ +
+
+

Requests

+
+
+ +
+

Tunnels

+
+
+
+ +

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.

+ +

install

+ +
npm install -g localtunnel
+
+ +

Lets say I have a local webserver running on port 8000. I can expose it to the world just by running

+ +
$ lt --port 8000
+your url is: http://gqgh.localtunnel.me
+
+ +

You can now share http://gqgh.localtunnel.me with anyone. As long as your local instance of lt is running, this url will remain active. Any requests to that url will be routed to your service on port 8000.

+ +

uses

+ +

Beyond sharing with friends, localtunnel makes a great tool for testing with any service which needs to hit internet visible URLs.

+ + +
+
+ + + + + + + + + +