Skip to content

Commit

Permalink
Add a route to kill tunnel if open with same JWT token
Browse files Browse the repository at this point in the history
  • Loading branch information
Olivier MICHAUD committed May 17, 2021
1 parent 1371d03 commit 1122640
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 12 deletions.
7 changes: 6 additions & 1 deletion lib/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}]`);

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

isSecurityTokenEqual(securityToken) {
return this.securityToken !== null && this.securityToken === securityToken;
}

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

export default Client;
export default Client;
3 changes: 2 additions & 1 deletion lib/ClientManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -49,6 +49,7 @@ class ClientManager {
const client = new Client({
id,
agent,
securityToken
});

// add to clients map immediately
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
42 changes: 40 additions & 2 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
49 changes: 49 additions & 0 deletions server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
});

0 comments on commit 1122640

Please sign in to comment.