mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-01-02 03:56:41 +00:00
Added endpoint to check if VPN is connected
Change-type: minor Signed-off-by: Miguel Casqueira <miguel@balena.io>
This commit is contained in:
parent
402a85cf2b
commit
8295858b32
23
docs/API.md
23
docs/API.md
@ -1152,6 +1152,29 @@ Response:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Device VPN Information
|
||||||
|
|
||||||
|
Added in supervisor version v11.4.0
|
||||||
|
|
||||||
|
Retrieve information about the VPN connection running on the device.
|
||||||
|
|
||||||
|
From an application container:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ curl "$BALENA_SUPERVISOR_ADDRESS/v2/device/vpn?apikey=$BALENA_SUPERVISOR_API_KEY"
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"vpn": {
|
||||||
|
"enabled": true,
|
||||||
|
"connected": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### V2 Utilities
|
### V2 Utilities
|
||||||
|
|
||||||
#### Cleanup volumes with no references
|
#### Cleanup volumes with no references
|
||||||
|
82
package-lock.json
generated
82
package-lock.json
generated
@ -247,6 +247,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/cookiejar": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/dbus": {
|
"@types/dbus": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/dbus/-/dbus-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/dbus/-/dbus-1.0.0.tgz",
|
||||||
@ -523,6 +529,25 @@
|
|||||||
"@types/sinon": "*"
|
"@types/sinon": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/superagent": {
|
||||||
|
"version": "4.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.7.tgz",
|
||||||
|
"integrity": "sha512-JSwNPgRYjIC4pIeOqLwWwfGj6iP1n5NE6kNBEbGx2V8H78xCPwx7QpNp9plaI30+W3cFEzJO7BIIsXE+dbtaGg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/cookiejar": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/supertest": {
|
||||||
|
"version": "2.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.9.tgz",
|
||||||
|
"integrity": "sha512-0BTpWWWAO1+uXaP/oA0KW1eOZv4hc0knhrWowV06Gwwz3kqQxNO98fUFM2e15T+PdPRmOouNFrYvaBgdojPJ3g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/superagent": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/tar-stream": {
|
"@types/tar-stream": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/tar-stream/-/tar-stream-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/tar-stream/-/tar-stream-2.1.0.tgz",
|
||||||
@ -2597,6 +2622,12 @@
|
|||||||
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
|
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"cookiejar": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"copy-concurrently": {
|
"copy-concurrently": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz",
|
||||||
@ -5142,6 +5173,12 @@
|
|||||||
"mime-types": "^2.1.12"
|
"mime-types": "^2.1.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"formidable": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"forwarded": {
|
"forwarded": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
||||||
@ -10390,6 +10427,51 @@
|
|||||||
"integrity": "sha1-JAIuSG878c3KCUKDt2nEctO3KJc=",
|
"integrity": "sha1-JAIuSG878c3KCUKDt2nEctO3KJc=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"superagent": {
|
||||||
|
"version": "3.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz",
|
||||||
|
"integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"component-emitter": "^1.2.0",
|
||||||
|
"cookiejar": "^2.1.0",
|
||||||
|
"debug": "^3.1.0",
|
||||||
|
"extend": "^3.0.0",
|
||||||
|
"form-data": "^2.3.1",
|
||||||
|
"formidable": "^1.2.0",
|
||||||
|
"methods": "^1.1.1",
|
||||||
|
"mime": "^1.4.1",
|
||||||
|
"qs": "^6.5.1",
|
||||||
|
"readable-stream": "^2.3.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"debug": {
|
||||||
|
"version": "3.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||||
|
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"ms": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ms": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supertest": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"methods": "^1.1.2",
|
||||||
|
"superagent": "^3.8.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"supports-color": {
|
"supports-color": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
"build:debug": "npm run typescript:release && npm run packagejson:copy",
|
"build:debug": "npm run typescript:release && npm run packagejson:copy",
|
||||||
"lint": "npm run lint:coffee && npm run lint:typescript",
|
"lint": "npm run lint:coffee && npm run lint:typescript",
|
||||||
"test": "npm run lint && npm run test-nolint",
|
"test": "npm run lint && npm run test-nolint",
|
||||||
"test-nolint": "npm run test:build && mocha",
|
"test-nolint": "npm run test:build && TEST=1 mocha",
|
||||||
"test:build": "npm run typescript:test-build && npm run coffeescript:test && npm run testitems:copy && npm run packagejson:copy",
|
"test:build": "npm run typescript:test-build && npm run coffeescript:test && npm run testitems:copy && npm run packagejson:copy",
|
||||||
"test:fast": "TEST=1 mocha --opts test/fast-mocha.opts",
|
"test:fast": "TEST=1 mocha --opts test/fast-mocha.opts",
|
||||||
"test:debug": "npm run test:build && TEST=1 mocha --inspect-brk",
|
"test:debug": "npm run test:build && TEST=1 mocha --inspect-brk",
|
||||||
@ -61,6 +61,7 @@
|
|||||||
"@types/shell-quote": "^1.6.1",
|
"@types/shell-quote": "^1.6.1",
|
||||||
"@types/sinon": "^7.5.2",
|
"@types/sinon": "^7.5.2",
|
||||||
"@types/sinon-chai": "^3.2.3",
|
"@types/sinon-chai": "^3.2.3",
|
||||||
|
"@types/supertest": "^2.0.9",
|
||||||
"@types/tmp": "^0.1.0",
|
"@types/tmp": "^0.1.0",
|
||||||
"@types/yargs": "^15.0.4",
|
"@types/yargs": "^15.0.4",
|
||||||
"balena-register-device": "^6.1.1",
|
"balena-register-device": "^6.1.1",
|
||||||
@ -114,6 +115,7 @@
|
|||||||
"sinon": "^7.5.0",
|
"sinon": "^7.5.0",
|
||||||
"sinon-chai": "^3.5.0",
|
"sinon-chai": "^3.5.0",
|
||||||
"strict-event-emitter-types": "^2.0.0",
|
"strict-event-emitter-types": "^2.0.0",
|
||||||
|
"supertest": "^4.0.2",
|
||||||
"tar-stream": "^2.1.2",
|
"tar-stream": "^2.1.2",
|
||||||
"terser-webpack-plugin": "^2.3.5",
|
"terser-webpack-plugin": "^2.3.5",
|
||||||
"tmp": "^0.1.0",
|
"tmp": "^0.1.0",
|
||||||
|
@ -4,19 +4,18 @@ import * as _ from 'lodash';
|
|||||||
|
|
||||||
import { ApplicationManager } from '../application-manager';
|
import { ApplicationManager } from '../application-manager';
|
||||||
import { Service } from '../compose/service';
|
import { Service } from '../compose/service';
|
||||||
|
import Volume from '../compose/volume';
|
||||||
|
import { spawnJournalctl } from '../lib/journald';
|
||||||
import {
|
import {
|
||||||
appNotFoundMessage,
|
appNotFoundMessage,
|
||||||
serviceNotFoundMessage,
|
serviceNotFoundMessage,
|
||||||
v2ServiceEndpointInputErrorMessage,
|
v2ServiceEndpointInputErrorMessage,
|
||||||
} from '../lib/messages';
|
} from '../lib/messages';
|
||||||
import { doPurge, doRestart, serviceAction } from './common';
|
|
||||||
|
|
||||||
import Volume from '../compose/volume';
|
|
||||||
import { spawnJournalctl } from '../lib/journald';
|
|
||||||
|
|
||||||
import log from '../lib/supervisor-console';
|
import log from '../lib/supervisor-console';
|
||||||
import supervisorVersion = require('../lib/supervisor-version');
|
import supervisorVersion = require('../lib/supervisor-version');
|
||||||
import { checkInt, checkTruthy } from '../lib/validation';
|
import { checkInt, checkTruthy } from '../lib/validation';
|
||||||
|
import { isVPNActive } from '../network';
|
||||||
|
import { doPurge, doRestart, serviceAction } from './common';
|
||||||
|
|
||||||
export function createV2Api(router: Router, applications: ApplicationManager) {
|
export function createV2Api(router: Router, applications: ApplicationManager) {
|
||||||
const { _lockingIfNecessary, deviceState } = applications;
|
const { _lockingIfNecessary, deviceState } = applications;
|
||||||
@ -459,6 +458,20 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get('/v2/device/vpn', async (_req, res) => {
|
||||||
|
const config = await deviceState.deviceConfig.getCurrent();
|
||||||
|
// Build VPNInfo
|
||||||
|
const info = {
|
||||||
|
enabled: config.SUPERVISOR_VPN_CONTROL === 'true',
|
||||||
|
connected: await isVPNActive(),
|
||||||
|
};
|
||||||
|
// Return payload
|
||||||
|
return res.json({
|
||||||
|
status: 'success',
|
||||||
|
vpn: info,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/v2/cleanup-volumes', async (_req, res) => {
|
router.get('/v2/cleanup-volumes', async (_req, res) => {
|
||||||
const targetState = await applications.getTargetApps();
|
const targetState = await applications.getTargetApps();
|
||||||
const referencedVolumes: string[] = [];
|
const referencedVolumes: string[] = [];
|
||||||
|
@ -1,68 +1,115 @@
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { fs } from 'mz';
|
|
||||||
import { spy } from 'sinon';
|
import { spy } from 'sinon';
|
||||||
|
import * as supertest from 'supertest';
|
||||||
|
|
||||||
import Config from '../src/config';
|
|
||||||
import Database from '../src/db';
|
|
||||||
import EventTracker from '../src/event-tracker';
|
|
||||||
import Log from '../src/lib/supervisor-console';
|
import Log from '../src/lib/supervisor-console';
|
||||||
import SupervisorAPI from '../src/supervisor-api';
|
import SupervisorAPI from '../src/supervisor-api';
|
||||||
|
import sampleResponses = require('./data/device-api-responses.json');
|
||||||
|
import mockedAPI = require('./lib/mocked-device-api');
|
||||||
|
|
||||||
const mockedOptions = {
|
const mockedOptions = {
|
||||||
listenPort: 12345,
|
listenPort: 54321,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
dbPath: './test/data/supervisor-api.sqlite',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const VALID_SECRET = mockedAPI.DEFAULT_SECRET;
|
||||||
const ALLOWED_INTERFACES = ['lo']; // Only need loopback since this is for testing
|
const ALLOWED_INTERFACES = ['lo']; // Only need loopback since this is for testing
|
||||||
|
|
||||||
describe('SupervisorAPI', () => {
|
describe('SupervisorAPI', () => {
|
||||||
describe('State change logging', () => {
|
|
||||||
let api: SupervisorAPI;
|
let api: SupervisorAPI;
|
||||||
let db: Database;
|
const request = supertest(`http://127.0.0.1:${mockedOptions.listenPort}`);
|
||||||
let mockedConfig: Config;
|
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
db = new Database({
|
// Create test API
|
||||||
databasePath: mockedOptions.dbPath,
|
api = await mockedAPI.create();
|
||||||
});
|
// Start test API
|
||||||
await db.init();
|
return api.listen(
|
||||||
mockedConfig = new Config({ db });
|
ALLOWED_INTERFACES,
|
||||||
await mockedConfig.init();
|
mockedOptions.listenPort,
|
||||||
|
mockedOptions.timeout,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
after(async () => {
|
||||||
api = new SupervisorAPI({
|
try {
|
||||||
config: mockedConfig,
|
await api.stop();
|
||||||
eventTracker: new EventTracker(),
|
} catch (e) {
|
||||||
routers: [],
|
if (e.message !== 'Server is not running.') {
|
||||||
healthchecks: [],
|
throw e;
|
||||||
});
|
}
|
||||||
spy(Log, 'info');
|
}
|
||||||
spy(Log, 'error');
|
// Remove any test data generated
|
||||||
});
|
await mockedAPI.cleanUp();
|
||||||
|
});
|
||||||
afterEach(async () => {
|
|
||||||
// @ts-ignore
|
describe('/ping', () => {
|
||||||
Log.info.restore();
|
it('responds with OK (without auth)', async () => {
|
||||||
// @ts-ignore
|
await request
|
||||||
Log.error.restore();
|
.get('/ping')
|
||||||
|
.set('Accept', 'application/json')
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
it('responds with OK (with auth)', async () => {
|
||||||
|
await request
|
||||||
|
.get('/ping')
|
||||||
|
.set('Accept', 'application/json')
|
||||||
|
.set('Authorization', `Bearer ${VALID_SECRET}`)
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.skip('V1 endpoints', () => {
|
||||||
|
// TODO: add tests for V1 endpoints
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('V2 endpoints', () => {
|
||||||
|
describe('GET /v2/device/vpn', () => {
|
||||||
|
it('returns information about VPN connection', async () => {
|
||||||
|
await request
|
||||||
|
.get('/v2/device/vpn')
|
||||||
|
.set('Accept', 'application/json')
|
||||||
|
.set('Authorization', `Bearer ${VALID_SECRET}`)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(sampleResponses.V2.GET['/device/vpn'].statusCode)
|
||||||
|
.then(response => {
|
||||||
|
expect(response.body).to.deep.equal(
|
||||||
|
sampleResponses.V2.GET['/device/vpn'].body,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// TODO: add tests for rest of V2 endpoints
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('State change logging', () => {
|
||||||
|
before(() => {
|
||||||
|
// Spy on functions we will be testing
|
||||||
|
spy(Log, 'info');
|
||||||
|
spy(Log, 'error');
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Start each case with API stopped
|
||||||
try {
|
try {
|
||||||
await api.stop();
|
await api.stop();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message !== 'Server is not running.') {
|
if (e.message !== 'Server is not running.') {
|
||||||
// Ignore since server is already closed
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
after(async () => {
|
after(async () => {
|
||||||
try {
|
// @ts-ignore
|
||||||
await fs.unlink(mockedOptions.dbPath);
|
Log.info.restore();
|
||||||
} catch (e) {
|
// @ts-ignore
|
||||||
/* noop */
|
Log.error.restore();
|
||||||
}
|
// Resume API for other test suites
|
||||||
|
return api.listen(
|
||||||
|
ALLOWED_INTERFACES,
|
||||||
|
mockedOptions.listenPort,
|
||||||
|
mockedOptions.timeout,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('logs successful start', async () => {
|
it('logs successful start', async () => {
|
||||||
|
@ -1,40 +1,25 @@
|
|||||||
import { expect } from 'chai';
|
import * as supertest from 'supertest';
|
||||||
import { fs } from 'mz';
|
|
||||||
import * as requestLib from 'request';
|
|
||||||
|
|
||||||
import Config from '../src/config';
|
|
||||||
import Database from '../src/db';
|
|
||||||
import EventTracker from '../src/event-tracker';
|
|
||||||
import SupervisorAPI from '../src/supervisor-api';
|
import SupervisorAPI from '../src/supervisor-api';
|
||||||
|
import mockedAPI = require('./lib/mocked-device-api');
|
||||||
|
|
||||||
const mockedOptions = {
|
const mockedOptions = {
|
||||||
listenPort: 12345,
|
listenPort: 12345,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
dbPath: './test/data/supervisor-api.sqlite',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const VALID_SECRET = 'secure_api_secret';
|
const VALID_SECRET = mockedAPI.DEFAULT_SECRET;
|
||||||
const INVALID_SECRET = 'bad_api_secret';
|
const INVALID_SECRET = 'bad_api_secret';
|
||||||
const ALLOWED_INTERFACES = ['lo']; // Only need loopback since this is for testing
|
const ALLOWED_INTERFACES = ['lo']; // Only need loopback since this is for testing
|
||||||
|
|
||||||
describe('SupervisorAPI authentication', () => {
|
describe('SupervisorAPI authentication', () => {
|
||||||
let api: SupervisorAPI;
|
let api: SupervisorAPI;
|
||||||
|
const request = supertest(`http://127.0.0.1:${mockedOptions.listenPort}`);
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
const db = new Database({
|
// Create test API
|
||||||
databasePath: mockedOptions.dbPath,
|
api = await mockedAPI.create();
|
||||||
});
|
// Start test API
|
||||||
await db.init();
|
|
||||||
const mockedConfig = new Config({ db });
|
|
||||||
await mockedConfig.init();
|
|
||||||
// Set apiSecret that we can test with
|
|
||||||
await mockedConfig.set({ apiSecret: VALID_SECRET });
|
|
||||||
api = new SupervisorAPI({
|
|
||||||
config: mockedConfig,
|
|
||||||
eventTracker: new EventTracker(),
|
|
||||||
routers: [],
|
|
||||||
healthchecks: [],
|
|
||||||
});
|
|
||||||
return api.listen(
|
return api.listen(
|
||||||
ALLOWED_INTERFACES,
|
ALLOWED_INTERFACES,
|
||||||
mockedOptions.listenPort,
|
mockedOptions.listenPort,
|
||||||
@ -43,36 +28,37 @@ describe('SupervisorAPI authentication', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
after(async () => {
|
after(async () => {
|
||||||
api.stop();
|
|
||||||
try {
|
try {
|
||||||
await fs.unlink(mockedOptions.dbPath);
|
await api.stop();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* noop */
|
if (e.message !== 'Server is not running.') {
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Remove any test data generated
|
||||||
|
await mockedAPI.cleanUp();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('finds no apiKey and rejects', async () => {
|
it('finds no apiKey and rejects', async () => {
|
||||||
const response = await postAsync('/v1/blink');
|
return request.post('/v1/blink').expect(401);
|
||||||
expect(response.statusCode).to.equal(401);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('finds apiKey from query', async () => {
|
it('finds apiKey from query', async () => {
|
||||||
const response = await postAsync(`/v1/blink?apikey=${VALID_SECRET}`);
|
return request.post(`/v1/blink?apikey=${VALID_SECRET}`).expect(200);
|
||||||
expect(response.statusCode).to.equal(200);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('finds apiKey from Authorization header (ApiKey scheme)', async () => {
|
it('finds apiKey from Authorization header (ApiKey scheme)', async () => {
|
||||||
const response = await postAsync(`/v1/blink`, {
|
return request
|
||||||
Authorization: `ApiKey ${VALID_SECRET}`,
|
.post('/v1/blink')
|
||||||
});
|
.set('Authorization', `ApiKey ${VALID_SECRET}`)
|
||||||
expect(response.statusCode).to.equal(200);
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('finds apiKey from Authorization header (Bearer scheme)', async () => {
|
it('finds apiKey from Authorization header (Bearer scheme)', async () => {
|
||||||
const response = await postAsync(`/v1/blink`, {
|
return request
|
||||||
Authorization: `Bearer ${VALID_SECRET}`,
|
.post('/v1/blink')
|
||||||
});
|
.set('Authorization', `Bearer ${VALID_SECRET}`)
|
||||||
expect(response.statusCode).to.equal(200);
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('finds apiKey from Authorization header (case insensitive)', async () => {
|
it('finds apiKey from Authorization header (case insensitive)', async () => {
|
||||||
@ -87,46 +73,28 @@ describe('SupervisorAPI authentication', () => {
|
|||||||
'ApIKeY',
|
'ApIKeY',
|
||||||
];
|
];
|
||||||
for (const scheme of randomCases) {
|
for (const scheme of randomCases) {
|
||||||
const response = await postAsync(`/v1/blink`, {
|
return request
|
||||||
Authorization: `${scheme} ${VALID_SECRET}`,
|
.post('/v1/blink')
|
||||||
});
|
.set('Authorization', `${scheme} ${VALID_SECRET}`)
|
||||||
expect(response.statusCode).to.equal(200);
|
.expect(200);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects invalid apiKey from query', async () => {
|
it('rejects invalid apiKey from query', async () => {
|
||||||
const response = await postAsync(`/v1/blink?apikey=${INVALID_SECRET}`);
|
return request.post(`/v1/blink?apikey=${INVALID_SECRET}`).expect(401);
|
||||||
expect(response.statusCode).to.equal(401);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects invalid apiKey from Authorization header (ApiKey scheme)', async () => {
|
it('rejects invalid apiKey from Authorization header (ApiKey scheme)', async () => {
|
||||||
const response = await postAsync(`/v1/blink`, {
|
return request
|
||||||
Authorization: `ApiKey ${INVALID_SECRET}`,
|
.post('/v1/blink')
|
||||||
});
|
.set('Authorization', `ApiKey ${INVALID_SECRET}`)
|
||||||
expect(response.statusCode).to.equal(401);
|
.expect(401);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects invalid apiKey from Authorization header (Bearer scheme)', async () => {
|
it('rejects invalid apiKey from Authorization header (Bearer scheme)', async () => {
|
||||||
const response = await postAsync(`/v1/blink`, {
|
return request
|
||||||
Authorization: `Bearer ${INVALID_SECRET}`,
|
.post('/v1/blink')
|
||||||
});
|
.set('Authorization', `Bearer ${INVALID_SECRET}`)
|
||||||
expect(response.statusCode).to.equal(401);
|
.expect(401);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function postAsync(path: string, headers = {}): Promise<any> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
requestLib.post(
|
|
||||||
{
|
|
||||||
url: `http://127.0.0.1:${mockedOptions.listenPort}${path}`,
|
|
||||||
headers,
|
|
||||||
},
|
|
||||||
(error: Error, response: requestLib.Response) => {
|
|
||||||
if (error) {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
resolve(response);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
18
test/data/device-api-responses.json
Normal file
18
test/data/device-api-responses.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"V1": {},
|
||||||
|
"V2": {
|
||||||
|
"GET": {
|
||||||
|
"/device/vpn": {
|
||||||
|
"statusCode": 200,
|
||||||
|
"body": {
|
||||||
|
"status": "success",
|
||||||
|
"vpn": {
|
||||||
|
"enabled": true,
|
||||||
|
"connected": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"POST": {}
|
||||||
|
}
|
||||||
|
}
|
97
test/lib/mocked-device-api.ts
Normal file
97
test/lib/mocked-device-api.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { fs } from 'mz';
|
||||||
|
|
||||||
|
import { ApplicationManager } from '../../src/application-manager';
|
||||||
|
import Config from '../../src/config';
|
||||||
|
import Database from '../../src/db';
|
||||||
|
import { createV1Api } from '../../src/device-api/v1';
|
||||||
|
import { createV2Api } from '../../src/device-api/v2';
|
||||||
|
import DeviceState from '../../src/device-state';
|
||||||
|
import EventTracker from '../../src/event-tracker';
|
||||||
|
import SupervisorAPI from '../../src/supervisor-api';
|
||||||
|
|
||||||
|
const DB_PATH = './test/data/supervisor-api.sqlite';
|
||||||
|
const DEFAULT_SECRET = 'secure_api_secret';
|
||||||
|
|
||||||
|
async function create(): Promise<SupervisorAPI> {
|
||||||
|
// Get SupervisorAPI construct options
|
||||||
|
const { db, config, eventTracker, deviceState } = await createAPIOpts();
|
||||||
|
// Create ApplicationManager
|
||||||
|
const appManager = new ApplicationManager({
|
||||||
|
db,
|
||||||
|
config,
|
||||||
|
eventTracker,
|
||||||
|
logger: null,
|
||||||
|
deviceState,
|
||||||
|
apiBinder: null,
|
||||||
|
});
|
||||||
|
// Create SupervisorAPI
|
||||||
|
const api = new SupervisorAPI({
|
||||||
|
config,
|
||||||
|
eventTracker,
|
||||||
|
routers: [buildRoutes(appManager)],
|
||||||
|
healthchecks: [],
|
||||||
|
});
|
||||||
|
// Return SupervisorAPI that is not listening yet
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanUp(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// clean up test data
|
||||||
|
await fs.unlink(DB_PATH);
|
||||||
|
} catch (e) {
|
||||||
|
/* noop */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createAPIOpts(): Promise<SupervisorAPIOpts> {
|
||||||
|
// Create database
|
||||||
|
const db = new Database({
|
||||||
|
databasePath: DB_PATH,
|
||||||
|
});
|
||||||
|
await db.init();
|
||||||
|
// Create config
|
||||||
|
const mockedConfig = new Config({ db });
|
||||||
|
// Set testing secret
|
||||||
|
await mockedConfig.set({
|
||||||
|
apiSecret: DEFAULT_SECRET,
|
||||||
|
});
|
||||||
|
await mockedConfig.init();
|
||||||
|
// Create EventTracker
|
||||||
|
const tracker = new EventTracker();
|
||||||
|
// Create deviceState
|
||||||
|
const deviceState = new DeviceState({
|
||||||
|
db,
|
||||||
|
config: mockedConfig,
|
||||||
|
eventTracker: tracker,
|
||||||
|
logger: null as any,
|
||||||
|
apiBinder: null as any,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
db,
|
||||||
|
config: mockedConfig,
|
||||||
|
eventTracker: tracker,
|
||||||
|
deviceState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRoutes(appManager: ApplicationManager): Router {
|
||||||
|
// Create new Router
|
||||||
|
const router = Router();
|
||||||
|
// Add V1 routes
|
||||||
|
createV1Api(router, appManager);
|
||||||
|
// Add V2 routes
|
||||||
|
createV2Api(router, appManager);
|
||||||
|
// Return modified Router
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SupervisorAPIOpts {
|
||||||
|
db: Database;
|
||||||
|
config: Config;
|
||||||
|
eventTracker: EventTracker;
|
||||||
|
deviceState: DeviceState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export = { create, cleanUp, DEFAULT_SECRET };
|
Loading…
Reference in New Issue
Block a user