From d15e568cea623f24954619c6af3f69b42f3027fb Mon Sep 17 00:00:00 2001 From: Roman Shtylman Date: Tue, 18 Jun 2013 23:00:45 -0400 Subject: [PATCH] refactor server * shows some basic statistics on main page * move tcp proxy setup into separate file * migrate github page theme to be hosted locally --- package.json | 11 +- proxy.js | 235 ++++++++++++++++ server.js | 482 +++++++++++++-------------------- static/css/grid.css | 8 + static/css/pygment_trac.css | 70 +++++ static/css/style.css | 437 ++++++++++++++++++++++++++++++ static/css/widgets.css | 1 + static/img/bg_hr.png | Bin 0 -> 943 bytes static/img/blacktocat.png | Bin 0 -> 1428 bytes static/img/digits.png | 1 + static/img/icon_download.png | Bin 0 -> 1162 bytes static/img/sprite_download.png | Bin 0 -> 16799 bytes static/js/index.js | 37 +++ test/basic.js | 2 +- test/queue.js | 2 +- views/index.html | 98 +++++++ 16 files changed, 1085 insertions(+), 299 deletions(-) create mode 100644 proxy.js create mode 100644 static/css/grid.css create mode 100644 static/css/pygment_trac.css create mode 100644 static/css/style.css create mode 100644 static/css/widgets.css create mode 100644 static/img/bg_hr.png create mode 100644 static/img/blacktocat.png create mode 120000 static/img/digits.png create mode 100644 static/img/icon_download.png create mode 100644 static/img/sprite_download.png create mode 100644 static/js/index.js create mode 100644 views/index.html 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 0000000000000000000000000000000000000000..7973bd69888c7e10ccad1111d555ceabb7cd99b6 GIT binary patch literal 943 zcmaJ=O^ee&7!FiK7FWCot{@Ck@nrMW&tx0B-6VAbrk1u~FTzffX&bu9#AIsIdef8t z!QZfdz=K}>3m(LO;6X3qN}Y6@>cJYA%)G<%Jn!ec>9im1@7>wsIBwrMF}iHO!q%;8 zSJ@xEd~(FL18NRvkBsOXMVM>4WQc*~qcQGc17IjxRnj!O_^B1gan0x#EWT48PK->5B2>mI;LIx zC*FSw$Nfc!g)WZCEOJ=mM)}lLsOk|$ltg_(&ax_YCWMlBLPDVT%D_gB7o_$YZ`-OB z#1sV%whRq21>W;qwN$N?OUGtQQe;JvOsQrna;+v+j8dth=*?orHHb6waX>S!yXCgT zo!oR3{E&GzaOAzfZYv@_Sf{LdyJInS>TS60&R9%yCs$y>2x(*gYIJtRrYAja$Ceq} z!N&oc_K1!3-Ft`U>`CM;quEbB4KG%!MovB*9_3!QzFhqHwrbwK|Doo-y>auDJNSP6 T=d)j*_4El@X4^PFK7I8YBT*xD literal 0 HcmV?d00001 diff --git a/static/img/blacktocat.png b/static/img/blacktocat.png new file mode 100644 index 0000000000000000000000000000000000000000..6e264fe57a2e35a2855405ac7d4102c3f6ddcdae GIT binary patch literal 1428 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 literal 0 HcmV?d00001 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.

+ +
    +
  • browserling.com
  • +
  • twillio testing
  • +
  • sendgrid web api
  • +
+
+
+ + + + + + + + + +