diff --git a/lib/Client.js b/lib/Client.js index 369eb079..572a647e 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -13,6 +13,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}]`); @@ -45,6 +46,10 @@ class Client extends EventEmitter { }); } + isSecurityTokenEqual(securityToken) { + return this.securityToken !== null && this.securityToken === securityToken; + } + stats() { return this.agent.stats(); } @@ -129,4 +134,4 @@ class Client extends EventEmitter { } } -export default Client; \ No newline at end of file +export default Client; diff --git a/lib/ClientManager.js b/lib/ClientManager.js index 6d97265c..53a32508 100644 --- a/lib/ClientManager.js +++ b/lib/ClientManager.js @@ -30,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; @@ -49,6 +49,7 @@ class ClientManager { const client = new Client({ id, agent, + securityToken }); // add to clients map immediately diff --git a/lib/ClientManager.test.js b/lib/ClientManager.test.js index d85abe55..852be61a 100644 --- a/lib/ClientManager.test.js +++ b/lib/ClientManager.test.js @@ -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); @@ -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 }, () => { @@ -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 }, () => { @@ -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) diff --git a/server.js b/server.js index 9089132e..b5fb32ef 100644 --- a/server.js +++ b/server.js @@ -61,6 +61,44 @@ export default function(opt) { }; }); + router.get('/api/tunnels/:id/kill', async (ctx, next) => { + const clientId = ctx.params.id; + if (!opt.jwt_shared_secret){ + debug('disconnecting client with id %s, error: jwt_shared_secret is not used', clientId); + ctx.throw(403, { + success: false, + message: 'jwt_shared_secret is not used' + }); + return; + } + + if (!manager.hasClient(clientId)) { + debug('disconnecting client with id %s, error: client is not connected', clientId); + ctx.throw(404, { + success: false, + message: `client with id ${clientId} is not connected` + }); + } + + const securityToken = ctx.request.headers.authorization; + if (!manager.getClient(clientId).isSecurityTokenEqual(securityToken)) { + debug('disconnecting client with id %s, error: securityToken is not equal ', clientId); + ctx.throw(403, { + success: false, + message: `client with id ${clientId} has not the same securityToken than ${securityToken}` + }); + } + + debug('disconnecting client with id %s', clientId); + manager.removeClient(clientId); + + ctx.statusCode = 200; + ctx.body = { + success: true, + message: `client with id ${clientId} is disconected` + }; + }); + app.use(router.routes()); app.use(router.allowedMethods()); @@ -78,7 +116,7 @@ export default function(opt) { if (isNewClientRequest) { const reqId = hri.random(); debug('making new client with id %s', reqId); - const info = await manager.newClient(reqId); + const info = await manager.newClient(reqId, opt.jwt_shared_secret ? ctx.request.headers.authorization : null); const url = schema + '://' + info.id + '.' + ctx.request.host; info.url = url; @@ -116,7 +154,7 @@ export default function(opt) { } debug('making new client with id %s', reqId); - const info = await manager.newClient(reqId); + const info = await manager.newClient(reqId, opt.jwt_shared_secret ? ctx.request.headers.authorization : null); const url = schema + '://' + info.id + '.' + ctx.request.host; info.url = url; diff --git a/server.test.js b/server.test.js index 8b5bc2fb..0d3bb028 100644 --- a/server.test.js +++ b/server.test.js @@ -131,4 +131,53 @@ describe('Server', () => { await new Promise(resolve => server.close(resolve)); }); + + it('should not support the /api/tunnels/:id/kill endpoint if jwt authorization is not enable on server', async () => { + const server = createServer(); + await new Promise(resolve => server.listen(resolve)); + + const res = await request(server).get('/api/tunnels/foobar-test/kill'); + assert.equal(res.statusCode, 403); + assert.equal(res.text, 'jwt_shared_secret is not used'); + + await new Promise(resolve => server.close(resolve)); + }); + + it('should throw error when calling /api/tunnels/:id/kill endpoint if id does not exists', async () => { + const server = createServer({jwt_shared_secret: 'thekey'}); + await new Promise(resolve => server.listen(resolve)); + + { + const jwtoken = jwt.sign({ + name: 'bar' + }, 'thekey'); + await request(server).get('/foobar-test').set('Authorization', `Bearer ${jwtoken}`); + // no such tunnel yet + const res = await request(server).get('/api/tunnels/foobar-test2/kill').set('Authorization', `Bearer ${jwtoken}`); + assert.equal(res.statusCode, 404); + assert.equal(res.text, 'client with id foobar-test2 is not connected'); + } + + await new Promise(resolve => server.close(resolve)); + }); + + it('should disconnect client when calling /api/tunnels/:id/kill endpoint', async () => { + const server = createServer({jwt_shared_secret: 'thekey'}); + await new Promise(resolve => server.listen(resolve)); + + { + const jwtoken = jwt.sign({ + name: 'bar' + }, 'thekey'); + await request(server).get('/foobar-test').set('Authorization', `Bearer ${jwtoken}`); + + const res = await request(server).get('/api/tunnels/foobar-test/kill').set('Authorization', `Bearer ${jwtoken}`); + assert.equal(res.statusCode, 200); + assert.equal(res.text, '{"success":true,"message":"client with id foobar-test is disconected"}'); + const statusResult = await request(server).get('/api/tunnels/foobar-test/status').set('Authorization', `Bearer ${jwtoken}`); + assert.equal(statusResult.text, 'Not Found'); + } + + await new Promise(resolve => server.close(resolve)); + }); });