diff --git a/src/app.ts b/src/app.ts index 1e775d40d..de34657e0 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,8 +11,8 @@ export interface ServerApp { interfaces: { [key: string]: any } intervals: NodeJS.Timeout[] providers: any[] - server: any - redirectServer?: any + servers: any[] + redirectServers?: any[] deltaCache: DeltaCache getProviderStatus: () => any lastServerEvents: { [key: string]: any } diff --git a/src/config/config.ts b/src/config/config.ts index 0b9d587e4..83c773bec 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -75,6 +75,7 @@ export interface Config { enablePluginLogging?: boolean loggingDirectory?: string sourcePriorities?: any + networkInterfaces?: string[] } defaults: object } diff --git a/src/index.ts b/src/index.ts index 574a7d519..79361649a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -70,7 +70,6 @@ import { pipedProviders } from './pipedproviders' import { EventsActorId, WithWrappedEmitter, wrapEmitter } from './events' import { Zones } from './zones' const debug = createDebug('signalk-server') - const { StreamBundle } = require('./streambundle') interface ServerOptions { @@ -427,12 +426,12 @@ class Server { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { - createServer(app, async (err, server) => { - if (err) { + createServer(app, async (err, servers) => { + if (err || _.isUndefined(servers)) { reject(err) return } - app.server = server + app.servers = servers app.interfaces = {} app.clients = 0 @@ -448,26 +447,31 @@ class Server { const primaryPort = getPrimaryPort(app) debug(`primary port:${primaryPort}`) - server.listen(primaryPort, () => { + + await serverListen( + app.config.settings.networkInterfaces, + servers, + primaryPort + ) + + servers.forEach((server) => { console.log( - 'signalk-server running at 0.0.0.0:' + primaryPort.toString() + '\n' + `signalk-server running at ${JSON.stringify(server.address())}` ) - app.started = true - resolve(self) }) + const secondaryPort = getSecondaryPort(app) debug(`secondary port:${primaryPort}`) if (app.config.settings.ssl && secondaryPort) { - startRedirectToSsl( + app.redirectServers = await startRedirectToSsl( secondaryPort, getExternalPort(app), - (anErr: any, aServer: any) => { - if (!anErr) { - app.redirectServer = aServer - } - } + app.config.settings.networkInterfaces ) } + + app.started = true + resolve(self) }) }) } @@ -528,26 +532,19 @@ class Server { debug('Closing server...') const that = this - this.app.server.close(() => { - debug('Server closed') - if (that.app.redirectServer) { - try { - that.app.redirectServer.close(() => { - debug('Redirect server closed') - delete that.app.redirectServer - that.app.started = false - cb && cb() - resolve(that) - }) - } catch (err) { - reject(err) - } - } else { + Promise.all([ + closeServers(this.app.servers), + closeServers(this.app.redirectServers) + ]) + .then(() => { + debug('Servers closed') that.app.started = false cb && cb() resolve(that) - } - }) + }) + .catch((err) => { + reject(err) + }) } catch (err) { reject(err) } @@ -558,44 +555,117 @@ class Server { module.exports = Server -function createServer(app: any, cb: (err: any, server?: any) => void) { +function closeServers(servers: any[] | undefined) { + if (!servers) { + return null + } else { + return Promise.all( + servers.map((server) => { + return new Promise((resolve, reject) => { + try { + server.close(() => { + resolve(server) + }) + } catch (err) { + reject(err) + } + }) + }) + ) + } +} + +function createServer(app: any, cb: (err: any, servers?: any[]) => void) { + const serverCount = app.config.settings.networkInterfaces + ? app.config.settings.networkInterfaces.length + : 1 + if (app.config.settings.ssl) { getCertificateOptions(app, (err: any, options: any) => { if (err) { cb(err) } else { debug('Starting server to serve both http and https') - cb(null, https.createServer(options, app)) + + const servers = [] + for (let i = 0; i < serverCount; i++) { + servers.push(https.createServer(options, app)) + } + + cb(null, servers) } }) return } - let server + const servers = [] try { debug('Starting server to serve only http') - server = http.createServer(app) + for (let i = 0; i < serverCount; i++) { + servers.push(http.createServer(app)) + } } catch (e) { cb(e) return } - cb(null, server) + cb(null, servers) +} + +function serverListen( + networkInterfaces: string[] | undefined, + servers: any[], + port: number | { fd: number } +) { + let interfaces: any + + interfaces = networkInterfaces + + if (!interfaces) { + interfaces = [undefined] + } + + const promises: any[] = [] + + servers.forEach((server: any, idx: number) => { + promises.push( + new Promise((resolve, reject) => { + try { + server.listen(port, interfaces[idx], () => { + resolve(null) + }) + } catch (err) { + reject(err) + } + }) + ) + }) + return Promise.all(promises) } -function startRedirectToSsl( +async function startRedirectToSsl( port: number, redirectPort: number, - cb: (e: unknown, server: any) => void + networkInterfaces: string[] | undefined ) { const redirectApp = express() redirectApp.use((req: Request, res: Response) => { const host = req.headers.host?.split(':')[0] res.redirect(`https://${host}:${redirectPort}${req.path}`) }) - const server = http.createServer(redirectApp) - server.listen(port, () => { - console.log(`Redirect server running on port ${port.toString()}`) - cb(null, server) + const servers = (networkInterfaces || [undefined]).map(() => { + return http.createServer(redirectApp) + }) + + await serverListen(networkInterfaces, servers, port) + + servers.forEach((server) => { + console.log( + `signalk-server redirect server running at ${JSON.stringify( + server.address() + )}` + ) }) + + return servers } function startMdns(app: ServerApp & WithConfig) { diff --git a/src/interfaces/ws.js b/src/interfaces/ws.js index ed02b3c40..fae07ce57 100644 --- a/src/interfaces/ws.js +++ b/src/interfaces/ws.js @@ -153,157 +153,161 @@ module.exports = function (app) { const assertBufferSize = getAssertBufferSize(app.config) - primuses = allWsOptions.map((primusOptions) => { - const primus = new Primus(app.server, primusOptions) + primuses = allWsOptions + .map((primusOptions) => { + return app.servers.map((server) => { + const primus = new Primus(server, primusOptions) + + if (app.securityStrategy.canAuthorizeWS()) { + primus.authorize( + createPrimusAuthorize(app.securityStrategy.authorizeWS) + ) + } - if (app.securityStrategy.canAuthorizeWS()) { - primus.authorize( - createPrimusAuthorize(app.securityStrategy.authorizeWS) - ) - } + primus.on('connection', function (spark) { + let principalId + if (spark.request.skPrincipal) { + principalId = spark.request.skPrincipal.identifier + } - primus.on('connection', function (spark) { - let principalId - if (spark.request.skPrincipal) { - principalId = spark.request.skPrincipal.identifier - } + debugConnection( + `${spark.id} connected ${JSON.stringify(spark.query)} ${ + spark.request.connection.remoteAddress + }:${principalId}` + ) - debugConnection( - `${spark.id} connected ${JSON.stringify(spark.query)} ${ - spark.request.connection.remoteAddress - }:${principalId}` - ) + spark.sendMetaDeltas = spark.query.sendMeta === 'all' + spark.sentMetaData = {} - spark.sendMetaDeltas = spark.query.sendMeta === 'all' - spark.sentMetaData = {} - - let onChange = (delta) => { - const filtered = app.securityStrategy.filterReadDelta( - spark.request.skPrincipal, - delta - ) - if (filtered) { - sendMetaData(app, spark, filtered) - spark.write(filtered) - assertBufferSize(spark) - } - } + let onChange = (delta) => { + const filtered = app.securityStrategy.filterReadDelta( + spark.request.skPrincipal, + delta + ) + if (filtered) { + sendMetaData(app, spark, filtered) + spark.write(filtered) + assertBufferSize(spark) + } + } - const unsubscribes = [] + const unsubscribes = [] - if (primusOptions.isPlayback) { - spark.on('data', () => { - console.error('Playback does not support ws upstream messages') - spark.end('Playback does not support ws upstream messages') - }) - } else { - spark.on('data', function (msg) { - debug('<' + JSON.stringify(msg)) + if (primusOptions.isPlayback) { + spark.on('data', () => { + console.error('Playback does not support ws upstream messages') + spark.end('Playback does not support ws upstream messages') + }) + } else { + spark.on('data', function (msg) { + debug('<' + JSON.stringify(msg)) - try { - if (msg.token) { - spark.request.token = msg.token - } + try { + if (msg.token) { + spark.request.token = msg.token + } - if (msg.updates) { - processUpdates(app, pathSources, spark, msg) - } + if (msg.updates) { + processUpdates(app, pathSources, spark, msg) + } - if (msg.subscribe) { - processSubscribe( - app, - unsubscribes, - spark, - assertBufferSize, - msg - ) - } + if (msg.subscribe) { + processSubscribe( + app, + unsubscribes, + spark, + assertBufferSize, + msg + ) + } - if (msg.unsubscribe) { - processUnsubscribe(app, unsubscribes, msg, onChange, spark) - } + if (msg.unsubscribe) { + processUnsubscribe(app, unsubscribes, msg, onChange, spark) + } - if (msg.accessRequest) { - processAccessRequest(spark, msg) - } + if (msg.accessRequest) { + processAccessRequest(spark, msg) + } - if (msg.login && app.securityStrategy.supportsLogin()) { - processLoginRequest(spark, msg) - } + if (msg.login && app.securityStrategy.supportsLogin()) { + processLoginRequest(spark, msg) + } - if (msg.put) { - processPutRequest(spark, msg) - } + if (msg.put) { + processPutRequest(spark, msg) + } - if (msg.delete) { - processDeleteRequest(spark, msg) - } + if (msg.delete) { + processDeleteRequest(spark, msg) + } - if (msg.requestId && msg.query) { - processReuestQuery(spark, msg) - } - } catch (e) { - console.error(e) + if (msg.requestId && msg.query) { + processReuestQuery(spark, msg) + } + } catch (e) { + console.error(e) + } + }) } - }) - } - spark.on('end', function () { - debugConnection( - `${spark.id} end ${JSON.stringify(spark.query)} ${ - spark.request.connection.remoteAddress - }:${principalId}` - ) + spark.on('end', function () { + debugConnection( + `${spark.id} end ${JSON.stringify(spark.query)} ${ + spark.request.connection.remoteAddress + }:${principalId}` + ) - unsubscribes.forEach((unsubscribe) => unsubscribe()) + unsubscribes.forEach((unsubscribe) => unsubscribe()) - _.keys(pathSources).forEach((path) => { - _.keys(pathSources[path]).forEach((source) => { - if (pathSources[path][source] === spark) { - debug('removing source for %s', path) - delete pathSources[path][source] - } + _.keys(pathSources).forEach((path) => { + _.keys(pathSources[path]).forEach((source) => { + if (pathSources[path][source] === spark) { + debug('removing source for %s', path) + delete pathSources[path][source] + } + }) + }) }) - }) - }) - if (isSelfSubscription(spark.query)) { - const realOnChange = onChange - onChange = function (msg) { - if (!msg.context || msg.context === app.selfContext) { - realOnChange(msg) + if (isSelfSubscription(spark.query)) { + const realOnChange = onChange + onChange = function (msg) { + if (!msg.context || msg.context === app.selfContext) { + realOnChange(msg) + } + } } - } - } - if (spark.query.subscribe === 'none') { - onChange = () => undefined - } + if (spark.query.subscribe === 'none') { + onChange = () => undefined + } - onChange = wrapWithverifyWS(app.securityStrategy, spark, onChange) + onChange = wrapWithverifyWS(app.securityStrategy, spark, onChange) - spark.onDisconnects = [] + spark.onDisconnects = [] - if (primusOptions.isPlayback) { - if (!spark.query.startTime) { - spark.end( - 'startTime is a required query parameter for playback connections' - ) - } else { - handlePlaybackConnection(app, spark, onChange) - } - } else { - handleRealtimeConnection(app, spark, onChange) - } - }) + if (primusOptions.isPlayback) { + if (!spark.query.startTime) { + spark.end( + 'startTime is a required query parameter for playback connections' + ) + } else { + handlePlaybackConnection(app, spark, onChange) + } + } else { + handleRealtimeConnection(app, spark, onChange) + } + }) - primus.on('disconnection', function (spark) { - spark.onDisconnects.forEach((f) => f()) - debug(spark.id + ' disconnected') - }) + primus.on('disconnection', function (spark) { + spark.onDisconnects.forEach((f) => f()) + debug(spark.id + ' disconnected') + }) - return primus - }) + return primus + }) + }) + .reduce((prev, current) => [...prev, ...current]) } api.stop = function () { diff --git a/src/tokensecurity.js b/src/tokensecurity.js index 5220d941d..753fe5446 100644 --- a/src/tokensecurity.js +++ b/src/tokensecurity.js @@ -184,7 +184,8 @@ module.exports = function (app, config) { const remember = req.body.rememberMe const configuration = getConfiguration() - login(name, password) + strategy + .login(name, password) .then((reply) => { const requestType = req.get('Content-Type') @@ -674,7 +675,7 @@ module.exports = function (app, config) { } } - function getAuthorizationFromHeaders(req) { + strategy.getAuthorizationFromHeaders = (req) => { if (req.headers) { let header = req.headers.authorization if (!header) { @@ -705,7 +706,7 @@ module.exports = function (app, config) { if (req.query && req.query.token) { token = req.query.token } else { - token = getAuthorizationFromHeaders(req) + token = strategy.getAuthorizationFromHeaders(req) } } @@ -869,65 +870,69 @@ module.exports = function (app, config) { function http_authorize(redirect, forLoginStatus) { // debug('http_authorize: ' + redirect) return function (req, res, next) { - let token = req.cookies.JAUTHENTICATION + strategy.httpAuthorize(redirect, forLoginStatus, req, res, next) + } + } - debug(`http_authorize: ${req.path} (forLogin: ${forLoginStatus})`) + strategy.httpAuthorize = (redirect, forLoginStatus, req, res, next) => { + debug(`http_authorize: ${req.path} (forLogin: ${forLoginStatus})`) - if (!getIsEnabled()) { - return next() - } + if (!getIsEnabled()) { + return next() + } - const configuration = getConfiguration() + const configuration = getConfiguration() - if (!token) { - token = getAuthorizationFromHeaders(req) - } + let token = req.cookies.JAUTHENTICATION - if (token) { - jwt.verify(token, configuration.secretKey, function (err, decoded) { - debug('verify') - if (!err) { - const principal = getPrincipal(decoded) - if (principal) { - debug('authorized') - req.skPrincipal = principal - req.skIsAuthenticated = true - req.userLoggedIn = true - next() - return - } else { - debug('unknown user: ' + (decoded.id || decoded.device)) - } - } else { - debug(`bad token: ${err.message} ${req.path}`) - res.clearCookie('JAUTHENTICATION') - } + if (!token) { + token = strategy.getAuthorizationFromHeaders(req) + } - if (configuration.allow_readonly) { - req.skIsAuthenticated = false + if (token) { + jwt.verify(token, configuration.secretKey, function (err, decoded) { + debug('verify') + if (!err) { + const principal = getPrincipal(decoded) + if (principal) { + debug('authorized') + req.skPrincipal = principal + req.skIsAuthenticated = true + req.userLoggedIn = true next() + return } else { - res.status(401).send('bad auth token') + debug('unknown user: ' + (decoded.id || decoded.device)) } - }) - } else { - debug('no token') - - if (configuration.allow_readonly && !forLoginStatus) { - req.skPrincipal = { identifier: 'AUTO', permissions: 'readonly' } - req.skIsAuthenticated = true - return next() } else { + debug(`bad token: ${err.message} ${req.path}`) + res.clearCookie('JAUTHENTICATION') + } + + if (configuration.allow_readonly) { req.skIsAuthenticated = false + next() + } else { + res.status(401).send('bad auth token') + } + }) + } else { + debug('no token') - if (forLoginStatus) { - next() - } else if (redirect) { - debug('redirecting to login') - res.redirect('/@signalk/server-admin-ui/#/login') - } else { - res.status(401).send('Unauthorized') - } + if (configuration.allow_readonly && !forLoginStatus) { + req.skPrincipal = { identifier: 'AUTO', permissions: 'readonly' } + req.skIsAuthenticated = true + return next() + } else { + req.skIsAuthenticated = false + + if (forLoginStatus) { + next() + } else if (redirect) { + debug('redirecting to login') + res.redirect('/@signalk/server-admin-ui/#/login') + } else { + res.status(401).send('Unauthorized') } } } diff --git a/src/venussecurity.js b/src/venussecurity.js new file mode 100644 index 000000000..4f53475c5 --- /dev/null +++ b/src/venussecurity.js @@ -0,0 +1,115 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* + * Copyright 2017 Teppo Kurki + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +import fs from 'fs' +//import { createDebug } from './debug' +//const debug = createDebug('signalk-server:venussecurity') +import dummysecurity from './dummysecurity' +import { saveSecurityConfig } from './security' + +//const passwordFile = '/data/conf/vncpassword.txt' +const passwordFile = './vncpassword.txt' + +module.exports = function (app, config) { + let security + + if (fs.existsSync(passwordFile) && fs.readFileSync(passwordFile).length) { + if (!config.users || config.users.length == 0) { + const user = { + username: 'admin', + type: 'admin', + password: fs.readFileSync(passwordFile).toString().trim(), + venusAdminUser: true + } + + security = require('./tokensecurity')(app, config) + config = security.getConfiguration() + config.users = [user] + app.securityStrategy = security + saveSecurityConfig(app, config, (theError) => { + if (theError) { + console.error(theError) + } + }) + } else { + security = require('./tokensecurity')(app, config) + } + const tslogin = security.login + + security.login = (username, password) => { + if (username === 'admin') { + const user = security + .getConfiguration() + .users.find((aUser) => aUser.username === username) + + if (user.venusAdminUser) { + const password = fs.readFileSync(passwordFile).toString().trim() + + if (password !== user.password) { + user.password = password + saveSecurityConfig(app, config, (theError) => { + if (theError) { + console.error(theError) + } + }) + } + } + } + return tslogin(username, password) + } + + const tsAuthorizeWS = security.authorizeWS + security.authorizeWS = (req) => { + tsAuthorizeWS(req) + if ( + !req.skIsAuthenticated && + req.headers.venus_os_authenticated === 'true' + ) { + req.skIsAuthenticated = true + req.skPrincipal = { + identifier: 'admin', + permissions: 'admin' + } + } + } + + const tsHttpAuthorize = security.httpAuthorize + security.httpAuthorize = (redirect, forLoginStatus, req, res, next) => { + if ( + req.cookies.JAUTHENTICATION || + security.getAuthorizationFromHeaders(req) + ) { + return tsHttpAuthorize(redirect, forLoginStatus, req, res, next) + } + + if (req.headers.venus_os_authenticated === 'true') { + req.skIsAuthenticated = true + req.userLoggedIn = true + req.skPrincipal = { + identifier: 'admin', + permissions: 'admin' + } + return next() + } else { + return tsHttpAuthorize(redirect, forLoginStatus, req, res, next) + } + } + } else { + security = dummysecurity() + } + return security +}