Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions bin/server
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ const argv = optimist
default: 10,
describe: 'maximum number of tcp sockets each client is allowed to establish at one time (the tunnels)'
})
.options('range', {
default: null,
describe: 'will bind incoming connections only on ports in range xxx:xxxx'
})
.options('jwt-shared-secret', {
default: null,
describe: 'JWT shared secret used to encode tokens'
})
.argv;

if (argv.help) {
Expand All @@ -42,6 +50,8 @@ const server = CreateServer({
max_tcp_sockets: argv['max-sockets'],
secure: argv.secure,
domain: argv.domain,
range: argv.range,
jwt_shared_secret: argv['jwt-shared-secret']
});

server.listen(argv.port, argv.address, () => {
Expand Down
19 changes: 18 additions & 1 deletion lib/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import http from 'http';
import Debug from 'debug';
import pump from 'pump';
import EventEmitter from 'events';
import jwt from 'jsonwebtoken';

// A client encapsulates req/res handling using an agent
//
Expand All @@ -13,6 +14,7 @@ class Client extends EventEmitter {

const agent = this.agent = options.agent;
const id = this.id = options.id;
this.securityToken = options.securityToken;

this.debug = Debug(`lt:Client[${this.id}]`);

Expand Down Expand Up @@ -45,6 +47,21 @@ class Client extends EventEmitter {
});
}

isSecurityTokenEqual(securityToken) {
const decodeJWT = (token) => {
return jwt.decode(token.replace(/Bearer /, ''), {complete: false});
}
if (this.securityToken === null) {
return false;
}
try{
return decodeJWT(this.securityToken).name === decodeJWT(securityToken).name;
}catch(error){
this.debug('error with jwt %s %s', securityToken, error);
return false;
}
}

stats() {
return this.agent.stats();
}
Expand Down Expand Up @@ -129,4 +146,4 @@ class Client extends EventEmitter {
}
}

export default Client;
export default Client;
7 changes: 6 additions & 1 deletion lib/ClientManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Debug from 'debug';

import Client from './Client';
import TunnelAgent from './TunnelAgent';
import PortManager from "./PortManager";

// Manage sets of clients
//
Expand All @@ -13,6 +14,7 @@ class ClientManager {

// id -> client instance
this.clients = new Map();
this.portManager = new PortManager({range: this.opt.range||null})

// statistics
this.stats = {
Expand All @@ -28,7 +30,7 @@ class ClientManager {
// create a new tunnel with `id`
// if the id is already used, a random id is assigned
// if the tunnel could not be created, throws an error
async newClient(id) {
async newClient(id, securityToken) {
const clients = this.clients;
const stats = this.stats;

Expand All @@ -39,13 +41,15 @@ class ClientManager {

const maxSockets = this.opt.max_tcp_sockets;
const agent = new TunnelAgent({
portManager: this.portManager,
clientId: id,
maxSockets: 10,
});

const client = new Client({
id,
agent,
securityToken
});

// add to clients map immediately
Expand Down Expand Up @@ -79,6 +83,7 @@ class ClientManager {
if (!client) {
return;
}
this.portManager.release(client.agent.port);
--this.stats.tunnels;
delete this.clients[id];
client.close();
Expand Down
16 changes: 8 additions & 8 deletions lib/ClientManager.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,22 @@ describe('ClientManager', () => {

it('should create a new client with random id', async () => {
const manager = new ClientManager();
const client = await manager.newClient();
const client = await manager.newClient(null, null);
assert(manager.hasClient(client.id));
manager.removeClient(client.id);
});

it('should create a new client with id', async () => {
const manager = new ClientManager();
const client = await manager.newClient('foobar');
const client = await manager.newClient('foobar', null);
assert(manager.hasClient('foobar'));
manager.removeClient('foobar');
});

it('should create a new client with random id if previous exists', async () => {
const manager = new ClientManager();
const clientA = await manager.newClient('foobar');
const clientB = await manager.newClient('foobar');
const clientA = await manager.newClient('foobar', null);
const clientB = await manager.newClient('foobar', null);
assert(clientA.id, 'foobar');
assert(manager.hasClient(clientB.id));
assert(clientB.id != clientA.id);
Expand All @@ -36,7 +36,7 @@ describe('ClientManager', () => {

it('should remove client once it goes offline', async () => {
const manager = new ClientManager();
const client = await manager.newClient('foobar');
const client = await manager.newClient('foobar', null);

const socket = await new Promise((resolve) => {
const netClient = net.createConnection({ port: client.port }, () => {
Expand All @@ -57,8 +57,8 @@ describe('ClientManager', () => {

it('should remove correct client once it goes offline', async () => {
const manager = new ClientManager();
const clientFoo = await manager.newClient('foo');
const clientBar = await manager.newClient('bar');
const clientFoo = await manager.newClient('foo', null);
const clientBar = await manager.newClient('bar', null);

const socket = await new Promise((resolve) => {
const netClient = net.createConnection({ port: clientFoo.port }, () => {
Expand All @@ -80,7 +80,7 @@ describe('ClientManager', () => {

it('should remove clients if they do not connect within 5 seconds', async () => {
const manager = new ClientManager();
const clientFoo = await manager.newClient('foo');
const clientFoo = await manager.newClient('foo', null);
assert(manager.hasClient('foo'));

// wait past grace period (1s)
Expand Down
60 changes: 60 additions & 0 deletions lib/PortManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Debug from 'debug';

class PortManager {
constructor(opt) {
this.debug = Debug('lt:PortManager');
this.range = opt.range || null;
this.first = null;
this.last = null;
this.pool = {};
this.initializePool();
}

initializePool() {
if (this.range === null) {
return;
}

if (!/^[0-9]+:[0-9]+$/.test(this.range)) {
throw new Error('Bad range expression: ' + this.range);
}

[this.first, this.last] = this.range.split(':').map((port) => parseInt(port));

if (this.first > this.last) {
throw new Error('Bad range expression min > max: ' + this.range);
}

for (let port = this.first; port <= this.last; port++) {
this.pool['_' + port] = null;
}
this.debug = Debug('lt:PortManager');
this.debug('Pool initialized ' + JSON.stringify(this.pool));
}

release(port) {
if (this.range === null) {
return;
}
this.debug('Release port ' + port);
this.pool['_' + port] = null;
}

getNextAvailable(clientId) {
if (this.range === null) {
return null;
}

for (let port = this.first; port <= this.last; port++) {
if (this.pool['_' + port] === null) {
this.pool['_' + port] = clientId;
this.debug('Port found ' + port);
return port;
}
}
this.debug('No more ports available ');
throw new Error('No more ports available in range ' + this.range);
}
}

export default PortManager;
51 changes: 51 additions & 0 deletions lib/PortManager.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import assert from 'assert';

import PortManager from './PortManager';

describe('PortManager', () => {
it('should construct with no range', () => {
const portManager = new PortManager({});
assert.equal(portManager.range, null);
assert.equal(portManager.first, null);
assert.equal(portManager.last, null);
});

it('should construct with range', () => {
const portManager = new PortManager({range: '10:20'});
assert.equal(portManager.range, '10:20');
assert.equal(portManager.first, 10);
assert.equal(portManager.last, 20);
});

it('should not construct with bad range expression', () => {
assert.throws(()=>{
new PortManager({range: 'a1020'});
}, /Bad range expression: a1020/)
});

it('should not construct with bad range max>min', () => {
assert.throws(()=>{
new PortManager({range: '20:10'});
}, /Bad range expression min > max: 20:10/)
});

it('should work has expected', async () => {
const portManager = new PortManager({range: '10:12'});
assert.equal(10,portManager.getNextAvailable('a'));
assert.equal(11,portManager.getNextAvailable('b'));
assert.equal(12,portManager.getNextAvailable('c'));

assert.throws(()=>{
portManager.getNextAvailable();
}, /No more ports available in range 10:12/)

portManager.release(11);
assert.equal(11,portManager.getNextAvailable('bb'));

portManager.release(10);
portManager.release(12);

assert.equal(10,portManager.getNextAvailable('cc'));
assert.equal(12,portManager.getNextAvailable('dd'));
});
});
15 changes: 11 additions & 4 deletions lib/TunnelAgent.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class TunnelAgent extends Agent {

// sockets we can hand out via createConnection
this.availableSockets = [];
this.port = null;
this.clientId = options.clientId
this.portManager = options.portManager || null;

// when a createConnection cannot return a socket, it goes into a queue
// once a socket is available it is handed out to the next callback
Expand Down Expand Up @@ -63,13 +66,14 @@ class TunnelAgent extends Agent {
});

return new Promise((resolve) => {
server.listen(() => {
const port = server.address().port;
this.debug('tcp server listening on port: %d', port);
const port = this.portManager ? this.portManager.getNextAvailable(this.options.clientId) : null;
server.listen(port,() => {
this.port = server.address().port
this.debug('tcp server listening on port: %d (%s)', this.port, this.clientId);

resolve({
// port for lt client tcp connections
port: port,
port: this.port,
});
});
});
Expand Down Expand Up @@ -115,6 +119,9 @@ class TunnelAgent extends Agent {
socket.once('error', (err) => {
// we do not log these errors, sessions can drop from clients for many reasons
// these are not actionable errors for our server
if(this.portManager){
this.portManager.release(this.port);
}
socket.destroy();
});

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"esm": "3.0.34",
"human-readable-ids": "1.0.3",
"koa": "2.5.1",
"koa-jwt": "4.0.0",
"koa-router": "7.4.0",
"localenv": "0.2.2",
"optimist": "0.6.1",
Expand All @@ -29,6 +30,6 @@
"scripts": {
"test": "mocha --check-leaks --require esm './**/*.test.js'",
"start": "./bin/server",
"dev": "node-dev bin/server --port 3000"
"dev": "node-dev bin/server --require esm --port 3000"
}
}
Loading