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