mirror of
https://github.com/bitinflow/localtunnel.git
synced 2026-03-14 05:55:53 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9b0274ff4 | ||
|
|
83ecb29eff | ||
|
|
21df257d16 | ||
|
|
18ada0854a | ||
|
|
34afd6537d | ||
|
|
2c38aefb9d | ||
|
|
aa488f6e76 | ||
|
|
f6618953f9 | ||
|
|
092d050fa0 | ||
|
|
0334ace20b | ||
|
|
13afcff1ae | ||
|
|
ed5aa3f16b | ||
|
|
2fcac1336c | ||
|
|
0568ae0bef | ||
|
|
585a8afad7 | ||
|
|
fbe841a1c5 | ||
|
|
929473913f | ||
|
|
5340659954 | ||
|
|
5c6558ed91 | ||
|
|
79ca069c38 | ||
|
|
741db27084 | ||
|
|
b605e9b823 | ||
|
|
b5830c3840 | ||
|
|
06b85ad0aa | ||
|
|
943a7dc35b | ||
|
|
c46a94b7a0 | ||
|
|
2f692b8e29 | ||
|
|
51d91ce0e8 | ||
|
|
ab28444802 |
4
.travis.yml
Normal file
4
.travis.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 0.8
|
||||
- 0.9
|
||||
44
README.md
44
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
localtunnel exposes your localhost to the world for easy testing and sharing! No need to mess with DNS or deploy just to have others test out your changes.
|
||||
|
||||
Great for working with browser testing tools like browserling or external api callback services like twilio which require a public url for callbacks!
|
||||
Great for working with browser testing tools like browserling or external api callback services like twilio which require a public url for callbacks.
|
||||
|
||||
## installation ##
|
||||
|
||||
@@ -10,9 +10,11 @@ Great for working with browser testing tools like browserling or external api ca
|
||||
npm install -g localtunnel
|
||||
```
|
||||
|
||||
This will install the localtunnel module globally and add the 'lt' client cli tool to your PATH.
|
||||
|
||||
## use ##
|
||||
|
||||
Super East! Assuming your local server is running on port 8000, just use the ```lt``` command to start the tunnel.
|
||||
Super Easy! Assuming your local server is running on port 8000, just use the ```lt``` command to start the tunnel.
|
||||
|
||||
```
|
||||
lt --port 8000
|
||||
@@ -21,3 +23,41 @@ lt --port 8000
|
||||
Thats it! It will connect to the tunnel server, setup the tunnel, and tell you what url to use for your testing. This url will remain active for the duration of your session; so feel free to share it with others for happy fun time!
|
||||
|
||||
You can restart your local server all you want, ```lt``` is smart enough to detect this and reconnect once it is back.
|
||||
|
||||
## API ##
|
||||
|
||||
The localtunnel client is also usable through an API (for test integration, automation, etc)
|
||||
|
||||
```javascript
|
||||
var localtunnel = require('localtunnel');
|
||||
|
||||
var client = localtunnel.connect({
|
||||
// the localtunnel server
|
||||
host: 'http://localtunnel.me',
|
||||
// your local application port
|
||||
port: 12345
|
||||
});
|
||||
|
||||
// when your are assigned a url
|
||||
client.on('url', function(url) {
|
||||
// you can now make http requests to the url
|
||||
// they will be proxied to your local server on port [12345]
|
||||
});
|
||||
|
||||
client.on('error', function(err) {
|
||||
// uh oh!
|
||||
});
|
||||
```
|
||||
|
||||
## other clients ##
|
||||
|
||||
Clients in other languages
|
||||
|
||||
*go* [gotunnelme](https://github.com/NoahShen/gotunnelme)
|
||||
|
||||
## server ##
|
||||
|
||||
See shtylman/localtunnel-server for details on the server that powers localtunnel.
|
||||
|
||||
## License ##
|
||||
MIT
|
||||
|
||||
39
bin/client
Executable file
39
bin/client
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env node
|
||||
var lt_client = require('../client');
|
||||
|
||||
var argv = require('optimist')
|
||||
.usage('Usage: $0 --port [num]')
|
||||
.demand(['port'])
|
||||
.options('host', {
|
||||
default: 'http://localtunnel.me',
|
||||
describe: 'upstream server providing forwarding'
|
||||
})
|
||||
.options('subdomain', {
|
||||
describe: 'request this subdomain'
|
||||
})
|
||||
.options('local-host', {
|
||||
describe: 'tunnel traffic to this host instead of localhost'
|
||||
})
|
||||
.default('local-host', 'localhost')
|
||||
.describe('port', 'internal http server port')
|
||||
.argv;
|
||||
|
||||
var opt = {
|
||||
host: argv.host,
|
||||
port: argv.port,
|
||||
local_host: argv['local-host'],
|
||||
subdomain: argv.subdomain,
|
||||
}
|
||||
|
||||
var client = lt_client.connect(opt);
|
||||
|
||||
// only emitted when the url changes
|
||||
client.on('url', function(url) {
|
||||
console.log('your url is: %s', url);
|
||||
});
|
||||
|
||||
client.on('error', function(err) {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
// vim: ft=javascript
|
||||
217
client.js
217
client.js
@@ -1,95 +1,160 @@
|
||||
// builtin
|
||||
var net = require('net');
|
||||
var url = require('url');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
var after = require('after');
|
||||
var request = require('request');
|
||||
|
||||
var argv = require('optimist')
|
||||
.usage('Usage: $0 --port [num]')
|
||||
.demand(['port'])
|
||||
.options('host', {
|
||||
default: 'http://localtunnel.me',
|
||||
describe: 'upstream server providing forwarding'
|
||||
})
|
||||
.describe('port', 'internal http server port')
|
||||
.argv;
|
||||
|
||||
// local port
|
||||
var local_port = argv.port;
|
||||
|
||||
// optionally override the upstream server
|
||||
var upstream = url.parse(argv.host);
|
||||
|
||||
// query options
|
||||
var opt = {
|
||||
host: upstream.hostname,
|
||||
port: upstream.port || 80,
|
||||
path: '/',
|
||||
json: true
|
||||
};
|
||||
|
||||
var base_uri = 'http://' + opt.host + ':' + opt.port + opt.path;
|
||||
|
||||
var internal;
|
||||
var upstream;
|
||||
var prev_id;
|
||||
|
||||
(function connect_proxy() {
|
||||
opt.uri = base_uri + ((prev_id) ? prev_id : '?new');
|
||||
|
||||
request(opt, function(err, res, body) {
|
||||
// request upstream url and connection info
|
||||
var request_url = function(params, cb) {
|
||||
request(params, function(err, res, body) {
|
||||
if (err) {
|
||||
console.error('upstream not available: %s', err.message);
|
||||
return process.exit(-1);
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
// our assigned hostname and tcp port
|
||||
var port = body.port;
|
||||
var host = opt.host;
|
||||
cb(null, body);
|
||||
});
|
||||
};
|
||||
|
||||
// store the id so we can try to get the same one
|
||||
prev_id = body.id;
|
||||
var connect = function(opt) {
|
||||
var ev = new EventEmitter();
|
||||
|
||||
console.log('your url is: %s', body.url);
|
||||
// local host
|
||||
var local_host = opt.local_host;
|
||||
|
||||
// connect to remote tcp server
|
||||
upstream = net.createConnection(port, host);
|
||||
// local port
|
||||
var local_port = opt.port;
|
||||
|
||||
// reconnect internal
|
||||
connect_internal();
|
||||
var base_uri = opt.host + '/';
|
||||
|
||||
upstream.on('end', function() {
|
||||
console.log('> upstream connection terminated');
|
||||
// optionally override the upstream server
|
||||
var upstream = url.parse(opt.host);
|
||||
|
||||
// sever connection to internal server
|
||||
// on reconnect we will re-establish
|
||||
internal.end();
|
||||
// no subdomain at first, maybe use requested domain
|
||||
var assigned_domain = opt.subdomain;
|
||||
|
||||
setTimeout(function() {
|
||||
connect_proxy();
|
||||
}, 1000);
|
||||
// connect to upstream given connection parameters
|
||||
var tunnel = function (remote_host, remote_port, dead) {
|
||||
|
||||
var remote_opt = {
|
||||
host: remote_host,
|
||||
port: remote_port
|
||||
};
|
||||
|
||||
var local_opt = {
|
||||
host: local_host,
|
||||
port: local_port
|
||||
};
|
||||
|
||||
var remote_attempts = 0;
|
||||
|
||||
(function conn(conn_had_error) {
|
||||
if (conn_had_error) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we need a new tunnel
|
||||
if (++remote_attempts >= 3) {
|
||||
return dead();
|
||||
}
|
||||
|
||||
// connection to localtunnel server
|
||||
var remote = net.connect(remote_opt);
|
||||
|
||||
remote.once('error', function(err) {
|
||||
if (err.code !== 'ECONNREFUSED') {
|
||||
remote.emit('error', err);
|
||||
}
|
||||
|
||||
// retrying connection to local server
|
||||
setTimeout(conn, 1000);
|
||||
});
|
||||
|
||||
function recon_local() {
|
||||
remote.pause();
|
||||
remote_attempts = 0;
|
||||
|
||||
// connection to local http server
|
||||
var local = net.connect(local_opt);
|
||||
|
||||
local.once('error', function(err) {
|
||||
if (err.code !== 'ECONNREFUSED') {
|
||||
local.emit('error', err);
|
||||
}
|
||||
|
||||
// retrying connection to local server
|
||||
setTimeout(recon_local, 1000);
|
||||
});
|
||||
|
||||
local.once('connect', function() {
|
||||
remote.resume();
|
||||
remote.pipe(local).pipe(remote, {end: false});
|
||||
});
|
||||
|
||||
local.once('close', function(had_error) {
|
||||
if (had_error) {
|
||||
return;
|
||||
}
|
||||
recon_local();
|
||||
});
|
||||
}
|
||||
|
||||
remote.once('close', conn);
|
||||
remote.once('connect', recon_local);
|
||||
})();
|
||||
};
|
||||
|
||||
var params = {
|
||||
path: '/',
|
||||
json: true
|
||||
};
|
||||
|
||||
// where to quest
|
||||
params.uri = base_uri + ((assigned_domain) ? assigned_domain : '?new');
|
||||
|
||||
function init_tunnel() {
|
||||
// get an id from lt server and setup forwarding tcp connections
|
||||
request_url(params, function(err, body) {
|
||||
if (err) {
|
||||
ev.emit('error', new Error('tunnel server not available: ' + err.message + ', retry 1s'));
|
||||
|
||||
// retry interval for id request
|
||||
return setTimeout(function() {
|
||||
init_tunnel();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// our assigned hostname and tcp port
|
||||
var port = body.port;
|
||||
var host = upstream.hostname;
|
||||
|
||||
// store the id so we can try to get the same one
|
||||
assigned_domain = body.id;
|
||||
|
||||
var max_conn = body.max_conn_count || 1;
|
||||
|
||||
// after all our tunnels die, we ask for new ones
|
||||
// this might happen if the upstream server dies
|
||||
var dead = after(max_conn, function() {
|
||||
init_tunnel();
|
||||
});
|
||||
|
||||
for (var count = 0 ; count < max_conn ; ++count) {
|
||||
tunnel(host, port, dead);
|
||||
}
|
||||
|
||||
ev.emit('url', body.url);
|
||||
});
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
function connect_internal() {
|
||||
init_tunnel();
|
||||
|
||||
internal = net.createConnection(local_port);
|
||||
internal.on('error', function(err) {
|
||||
console.log('error connecting to local server. retrying in 1s');
|
||||
return ev;
|
||||
};
|
||||
|
||||
setTimeout(function() {
|
||||
connect_internal();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
internal.on('end', function() {
|
||||
console.log('disconnected from local server. retrying in 1s');
|
||||
setTimeout(function() {
|
||||
connect_internal();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
upstream.pipe(internal);
|
||||
internal.pipe(upstream);
|
||||
}
|
||||
module.exports.connect = connect;
|
||||
|
||||
// for backwards compatibility
|
||||
// old localtunnel modules had server and client code in same module
|
||||
// so to keep .client working we expose it here
|
||||
module.exports.client = module.exports;
|
||||
|
||||
15
package.json
15
package.json
@@ -2,22 +2,19 @@
|
||||
"author": "Roman Shtylman <shtylman@gmail.com>",
|
||||
"name": "localtunnel",
|
||||
"description": "expose localhost to the world",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/shtylman/localtunnel.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"request": "2.11.4",
|
||||
"book": "1.2.0",
|
||||
"optimist": "0.3.4"
|
||||
"optimist": "0.3.4",
|
||||
"after": "0.8.1"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"optionalDependencies": {},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"bin": {
|
||||
"lt": "./bin/lt"
|
||||
}
|
||||
"lt": "./bin/client"
|
||||
},
|
||||
"main": "./client.js"
|
||||
}
|
||||
|
||||
343
server.js
343
server.js
@@ -1,343 +0,0 @@
|
||||
|
||||
// builtin
|
||||
var http = require('http');
|
||||
var net = require('net');
|
||||
var url = require('url');
|
||||
var FreeList = require('freelist').FreeList;
|
||||
|
||||
var argv = require('optimist')
|
||||
.usage('Usage: $0 --port [num]')
|
||||
.options('port', {
|
||||
default: '80',
|
||||
describe: 'listen on this port for outside requests'
|
||||
})
|
||||
.argv;
|
||||
|
||||
if (argv.help) {
|
||||
require('optimist').showHelp();
|
||||
process.exit();
|
||||
}
|
||||
|
||||
// here be dragons
|
||||
var HTTPParser = process.binding('http_parser').HTTPParser;
|
||||
var ServerResponse = http.ServerResponse;
|
||||
var IncomingMessage = http.IncomingMessage;
|
||||
|
||||
var log = require('book');
|
||||
|
||||
var chars = 'abcdefghiklmnopqrstuvwxyz';
|
||||
function rand_id() {
|
||||
var randomstring = '';
|
||||
for (var i=0; i<4; ++i) {
|
||||
var rnum = Math.floor(Math.random() * chars.length);
|
||||
randomstring += chars[rnum];
|
||||
}
|
||||
|
||||
return randomstring;
|
||||
}
|
||||
|
||||
var server = http.createServer();
|
||||
|
||||
// id -> client http server
|
||||
var clients = {};
|
||||
|
||||
// id -> list of sockets waiting for a valid response
|
||||
var wait_list = {};
|
||||
|
||||
var parsers = http.parsers;
|
||||
|
||||
// data going back to a client (the last client that made a request)
|
||||
function socketOnData(d, start, end) {
|
||||
|
||||
var socket = this;
|
||||
var req = this._httpMessage;
|
||||
|
||||
var current = clients[socket.subdomain].current;
|
||||
|
||||
if (!current) {
|
||||
log.error('no current for http response from backend');
|
||||
return;
|
||||
}
|
||||
|
||||
// send the goodies
|
||||
current.write(d.slice(start, end));
|
||||
|
||||
// invoke parsing so we know when all the goodies have been sent
|
||||
var parser = current.out_parser;
|
||||
parser.socket = socket;
|
||||
|
||||
var ret = parser.execute(d, start, end - start);
|
||||
if (ret instanceof Error) {
|
||||
debug('parse error');
|
||||
freeParser(parser, req);
|
||||
socket.destroy(ret);
|
||||
}
|
||||
}
|
||||
|
||||
function freeParser(parser, req) {
|
||||
if (parser) {
|
||||
parser._headers = [];
|
||||
parser.onIncoming = null;
|
||||
if (parser.socket) {
|
||||
parser.socket.onend = null;
|
||||
parser.socket.ondata = null;
|
||||
parser.socket.parser = null;
|
||||
}
|
||||
parser.socket = null;
|
||||
parser.incoming = null;
|
||||
parsers.free(parser);
|
||||
parser = null;
|
||||
}
|
||||
if (req) {
|
||||
req.parser = null;
|
||||
}
|
||||
}
|
||||
|
||||
// single http connection
|
||||
// gets a single http response back
|
||||
server.on('connection', function(socket) {
|
||||
|
||||
var self = this;
|
||||
|
||||
var for_client = false;
|
||||
var client_id;
|
||||
|
||||
var request;
|
||||
|
||||
var parser = parsers.alloc();
|
||||
parser.socket = socket;
|
||||
parser.reinitialize(HTTPParser.REQUEST);
|
||||
|
||||
// a full request is complete
|
||||
// we wait for the response from the server
|
||||
parser.onIncoming = function(req, shouldKeepAlive) {
|
||||
|
||||
log.trace('request', req.url);
|
||||
request = req;
|
||||
|
||||
for_client = false;
|
||||
|
||||
var hostname = req.headers.host;
|
||||
|
||||
if (!hostname) {
|
||||
log.trace('no hostname: %j', req.headers);
|
||||
// normal processing if not proxy
|
||||
var res = new ServerResponse(req);
|
||||
|
||||
// TODO(shtylman) skip favicon for now, it caused problems
|
||||
if (req.url === '/favicon.ico') {
|
||||
return;
|
||||
}
|
||||
|
||||
res.assignSocket(parser.socket);
|
||||
self.emit('request', req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
var match = hostname.match(/^([a-z]{4})[.].*/);
|
||||
|
||||
if (!match) {
|
||||
// normal processing if not proxy
|
||||
var res = new ServerResponse(req);
|
||||
|
||||
// TODO(shtylman) skip favicon for now, it caused problems
|
||||
if (req.url === '/favicon.ico') {
|
||||
return;
|
||||
}
|
||||
|
||||
res.assignSocket(parser.socket);
|
||||
self.emit('request', req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
client_id = match[1];
|
||||
for_client = true;
|
||||
|
||||
var out_parser = parsers.alloc();
|
||||
out_parser.reinitialize(HTTPParser.RESPONSE);
|
||||
socket.out_parser = out_parser;
|
||||
|
||||
// we have a response
|
||||
out_parser.onIncoming = function(res) {
|
||||
res.on('end', function() {
|
||||
log.trace('done with response for: %s', req.url);
|
||||
|
||||
// done with the parser
|
||||
parsers.free(out_parser);
|
||||
|
||||
var next = wait_list[client_id].shift();
|
||||
|
||||
clients[client_id].current = next;
|
||||
|
||||
if (!next) {
|
||||
return;
|
||||
}
|
||||
|
||||
// write original bytes that we held cause client was busy
|
||||
clients[client_id].write(next.queue);
|
||||
next.resume();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// process new data on the client socket
|
||||
// we may need to forward this it the backend
|
||||
socket.ondata = function(d, start, end) {
|
||||
var ret = parser.execute(d, start, end - start);
|
||||
|
||||
// invalid request from the user
|
||||
if (ret instanceof Error) {
|
||||
debug('parse error');
|
||||
socket.destroy(ret);
|
||||
return;
|
||||
}
|
||||
|
||||
// only write data if previous request to this client is done?
|
||||
log.trace('%s %s', parser.incoming && parser.incoming.upgrade, for_client);
|
||||
|
||||
// what if the subdomains are treated differently
|
||||
// as individual channels to the backend if available?
|
||||
// how can I do that?
|
||||
|
||||
if (parser.incoming && parser.incoming.upgrade) {
|
||||
// websocket shit
|
||||
}
|
||||
|
||||
// wtf do you do with upgraded connections?
|
||||
|
||||
// forward the data to the backend
|
||||
if (for_client) {
|
||||
var client = clients[client_id];
|
||||
|
||||
// requesting a subdomain that doesn't exist
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the client is already processing something
|
||||
// then new connections need to go into pause mode
|
||||
// and when they are revived, then they can send data along
|
||||
if (client.current && client.current !== socket) {
|
||||
log.trace('pausing', request.url);
|
||||
// prevent new data from gathering for this connection
|
||||
// we are waiting for a response to a previous request
|
||||
socket.pause();
|
||||
|
||||
var copy = Buffer(end - start);
|
||||
d.copy(copy, 0, start, end);
|
||||
socket.queue = copy;
|
||||
|
||||
wait_list[client_id].push(socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// this socket needs to receive responses
|
||||
client.current = socket;
|
||||
|
||||
// send through tcp tunnel
|
||||
client.write(d.slice(start, end));
|
||||
}
|
||||
};
|
||||
|
||||
socket.onend = function() {
|
||||
var ret = parser.finish();
|
||||
|
||||
if (ret instanceof Error) {
|
||||
log.trace('parse error');
|
||||
socket.destroy(ret);
|
||||
return;
|
||||
}
|
||||
|
||||
socket.end();
|
||||
};
|
||||
|
||||
socket.on('close', function() {
|
||||
parsers.free(parser);
|
||||
});
|
||||
});
|
||||
|
||||
server.on('request', function(req, res) {
|
||||
|
||||
// ignore favicon
|
||||
if (req.url === '/favicon.ico') {
|
||||
res.writeHead(404);
|
||||
return res.end();
|
||||
}
|
||||
|
||||
var parsed = url.parse(req.url, true);
|
||||
|
||||
// redirect main page to github reference
|
||||
if (req.url === '/' && !parsed.query.new) {
|
||||
res.writeHead(301, { Location: 'http://shtylman.github.com/localtunnel/' });
|
||||
res.end();
|
||||
}
|
||||
|
||||
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 (wait_list[id]) {
|
||||
// new id
|
||||
id = rand_id();
|
||||
}
|
||||
|
||||
// generate new shit for client
|
||||
if (wait_list[id]) {
|
||||
wait_list[id].forEach(function(waiting) {
|
||||
waiting.end();
|
||||
});
|
||||
}
|
||||
|
||||
var client_server = net.createServer();
|
||||
client_server.listen(function() {
|
||||
var port = client_server.address().port;
|
||||
log.info('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({ url: url, id: id, port: port }));
|
||||
});
|
||||
|
||||
// user has 5 seconds to connect before their slot is given up
|
||||
var conn_timeout = setTimeout(function() {
|
||||
client_server.close();
|
||||
}, 5000);
|
||||
|
||||
client_server.on('connection', function(socket) {
|
||||
|
||||
// who the info should route back to
|
||||
socket.subdomain = id;
|
||||
|
||||
// multiplexes socket data out to clients
|
||||
socket.ondata = socketOnData;
|
||||
|
||||
clearTimeout(conn_timeout);
|
||||
|
||||
log.trace('new connection for id: %s', id);
|
||||
clients[id] = socket;
|
||||
wait_list[id] = [];
|
||||
|
||||
socket.on('end', function() {
|
||||
delete clients[id];
|
||||
});
|
||||
});
|
||||
|
||||
client_server.on('err', function(err) {
|
||||
log.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(argv.port, function() {
|
||||
log.info('server listening on port: %d', server.address().port);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user