Skip to content

Commit

Permalink
Add auth to frontend and remove development server (Koenkk#5079)
Browse files Browse the repository at this point in the history
* Add frontend authentification

* Delete auth_token from bridge/info
  • Loading branch information
nurikk authored Nov 25, 2020
1 parent 4b1b1a4 commit 68f2432
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 67 deletions.
1 change: 1 addition & 0 deletions lib/extension/bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ class Bridge extends Extension {
const config = objectAssignDeep.noMutate({}, settings.get());
delete config.advanced.network_key;
delete config.mqtt.password;
config.frontend && delete config.frontend.auth_token;
const payload = {
version: this.zigbee2mqttVersion.version,
commit: this.zigbee2mqttVersion.commitHash,
Expand Down
57 changes: 22 additions & 35 deletions lib/extension/frontend.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const http = require('http');
const httpProxy = require('http-proxy');
const serveStatic = require('serve-static');
const finalhandler = require('finalhandler');
const Extension = require('./extension');
Expand All @@ -25,33 +24,22 @@ class Frontend extends Extension {
this.onWebSocketConnection = this.onWebSocketConnection.bind(this);
this.server = http.createServer(this.onRequest);
this.server.on('upgrade', this.onUpgrade);
this.developmentServer = settings.get().frontend.development_server;
this.development = !!this.developmentServer;
this.host = settings.get().frontend.host || '0.0.0.0';
this.port = settings.get().frontend.port || 8080;
this.authToken = settings.get().frontend.auth_token || false;
this.retainedMessages = new Map();

if (this.development) {
this.proxy = httpProxy.createProxyServer({ws: true});
} else {
/* istanbul ignore next */
const options = {setHeaders: (res, path) => {
if (path.endsWith('index.html')) {
res.setHeader('Cache-Control', 'no-store');
}
}};
this.fileServer = serveStatic(frontend.getPath(), options);
}

/* istanbul ignore next */
const options = {setHeaders: (res, path) => {
if (path.endsWith('index.html')) {
res.setHeader('Cache-Control', 'no-store');
}
}};
this.fileServer = serveStatic(frontend.getPath(), options);
this.wss = new WebSocket.Server({noServer: true});
this.wss.on('connection', this.onWebSocketConnection);
}

onZigbeeStarted() {
if (this.development) {
logger.info(`Running frontend in development mode (${this.developmentServer})`);
}

this.server.listen(this.port, this.host);
logger.info(`Started frontend on port ${this.host}:${this.port}`);
}
Expand All @@ -65,25 +53,24 @@ class Frontend extends Extension {
this.server.close(resolve);
});
}

onRequest(request, response) {
if (this.development) {
this.proxy.web(request, response, {target: `http://${this.developmentServer}`});
} else {
this.fileServer(request, response, finalhandler(request, response));
}
this.fileServer(request, response, finalhandler(request, response));
}
authenticate(request, cb) {
const {query} = url.parse(request.url, true);
cb(!this.authToken || this.authToken === query.token);
}

onUpgrade(request, socket, head) {
const pathname = url.parse(request.url).pathname;
if (pathname === '/api') {
const wss = this.wss;
wss.handleUpgrade(request, socket, head, (ws) => wss.emit('connection', ws, request));
} else if (this.development && pathname === '/sockjs-node') {
this.proxy.ws(request, socket, head, {target: `ws://${this.developmentServer}`});
} else {
socket.destroy();
}
this.wss.handleUpgrade(request, socket, head, (ws) => {
this.authenticate(request, (isAuthentificated) => {
if (isAuthentificated) {
this.wss.emit('connection', ws, request);
} else {
ws.close(4401, 'Unauthorized');
}
});
});
}

onWebSocketConnection(ws) {
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
"fast-deep-equal": "^3.1.3",
"finalhandler": "^1.1.2",
"git-last-commit": "^1.0.0",
"http-proxy": "^1.18.1",
"humanize-duration": "^3.23.1",
"js-yaml": "^3.14.0",
"json-stable-stringify-without-jsonify": "=1.0.1",
Expand Down
64 changes: 33 additions & 31 deletions test/frontend.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,17 @@ const mockHTTP = {
events: {},
};

const mockHTTPProxy = {
implementation: {
web: jest.fn(),
ws: jest.fn(),
},
variables: {},
events: {},
const mockWSocket = {
close: jest.fn(),
};

const mockWS = {
implementation: {
clients: [],
on: (event, handler) => {mockWS.events[event] = handler},
handleUpgrade: jest.fn(),
handleUpgrade: jest.fn().mockImplementation((request, socket, head, cb) => {
cb(mockWSocket)
}),
emit: jest.fn(),
},
variables: {},
Expand All @@ -52,13 +49,6 @@ jest.mock('http', () => ({
}),
}));

jest.mock('http-proxy', () => ({
createProxyServer: jest.fn().mockImplementation((initParameter) => {
mockHTTPProxy.variables.initParameter = initParameter;
return mockHTTPProxy.implementation;
}),
}));

jest.mock("serve-static", () =>
jest.fn().mockImplementation((path) => {
mockNodeStatic.variables.path = path
Expand Down Expand Up @@ -182,32 +172,44 @@ describe('Frontend', () => {
mockWS.implementation.handleUpgrade.mock.calls[0][3](99);
expect(mockWS.implementation.emit).toHaveBeenCalledWith('connection', 99, {"url": "http://localhost:8080/api"});

mockWS.implementation.handleUpgrade.mockClear();
mockHTTP.events.upgrade({url: 'http://localhost:8080/unkown'}, mockSocket, 3);
expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledTimes(0);
expect(mockSocket.destroy).toHaveBeenCalledTimes(1);

mockHTTP.variables.onRequest(1, 2);
expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1);
expect(mockNodeStatic.implementation).toHaveBeenCalledWith(1, 2, expect.any(Function));
});

it('Development server', async () => {
settings.set(['frontend'], {development_server: 'localhost:3001'});
it('Static server', async () => {
controller = new Controller();
await controller.start();
expect(mockHTTPProxy.variables.initParameter).toStrictEqual({ws: true});
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8080, "0.0.0.0");

mockHTTP.variables.onRequest(1, 2);
expect(mockHTTPProxy.implementation.web).toHaveBeenCalledTimes(1);
expect(mockHTTPProxy.implementation.web).toHaveBeenCalledWith(1, 2, {"target": "http://localhost:3001"});
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, "127.0.0.1");
});

it('Authentification', async () => {
const authToken = 'sample-secure-token'
settings.set(['frontend'], {auth_token: authToken});
controller = new Controller();
await controller.start();

const mockSocket = {destroy: jest.fn()};
mockHTTPProxy.implementation.ws.mockClear();
mockHTTP.events.upgrade({url: 'http://localhost:8080/sockjs-node'}, mockSocket, 3);
expect(mockHTTPProxy.implementation.ws).toHaveBeenCalledTimes(1);
mockWS.implementation.handleUpgrade.mockClear();
mockHTTP.events.upgrade({url: '/api'}, mockSocket, mockWSocket);
expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledTimes(1);
expect(mockSocket.destroy).toHaveBeenCalledTimes(0);
expect(mockHTTPProxy.implementation.ws).toHaveBeenCalledWith({"url": "http://localhost:8080/sockjs-node"}, mockSocket, 3, {"target": "ws://localhost:3001"});
expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledWith({"url": "/api"}, mockSocket, mockWSocket, expect.any(Function));
expect(mockWSocket.close).toHaveBeenCalledWith(4401, "Unauthorized");

mockWSocket.close.mockClear();
mockWS.implementation.emit.mockClear();

const url = `/api?token=${authToken}`;
mockWS.implementation.handleUpgrade.mockClear();
mockHTTP.events.upgrade({url: url}, mockSocket, 3);
expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledTimes(1);
expect(mockSocket.destroy).toHaveBeenCalledTimes(0);
expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledWith({url}, mockSocket, 3, expect.any(Function));
expect(mockWSocket.close).toHaveBeenCalledTimes(0);
mockWS.implementation.handleUpgrade.mock.calls[0][3](mockWSocket);
expect(mockWS.implementation.emit).toHaveBeenCalledWith('connection', mockWSocket, {url});

});
});

0 comments on commit 68f2432

Please sign in to comment.