From 48788658e76468c8cbdd636552b87a9401ae9138 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 30 Dec 2020 16:55:08 +0700 Subject: [PATCH] Add app management API endpoints (#63) --- app.js | 2 ++ logic/apps.js | 57 +++++++++++++++++++++++++++++++++++++++++++++++ logic/disk.js | 48 +++++++++++++++++++++++++++++++-------- logic/system.js | 2 +- routes/v1/apps.js | 34 ++++++++++++++++++++++++++++ utils/const.js | 3 +++ 6 files changed, 136 insertions(+), 10 deletions(-) create mode 100644 logic/apps.js create mode 100644 routes/v1/apps.js diff --git a/app.js b/app.js index 1c29617..ac6a2d4 100644 --- a/app.js +++ b/app.js @@ -22,6 +22,7 @@ const ping = require('routes/ping.js'); const account = require('routes/v1/account.js'); const system = require('routes/v1/system.js'); const external = require('routes/v1/external.js'); +const apps = require('routes/v1/apps.js'); const app = express(); @@ -42,6 +43,7 @@ app.use('/ping', ping); app.use('/v1/account', account); app.use('/v1/system', system); app.use('/v1/external', external); +app.use('/v1/apps', apps); app.use(errorHandleMiddleware); app.use((req, res) => { diff --git a/logic/apps.js b/logic/apps.js new file mode 100644 index 0000000..146f04a --- /dev/null +++ b/logic/apps.js @@ -0,0 +1,57 @@ +const diskLogic = require('logic/disk.js'); +const NodeError = require('models/errors.js').NodeError; + +async function get(query) { + let apps = await diskLogic.readAppRegistry(); + + // Do all hidden service lookups concurrently + await Promise.all(apps.map(async app => { + try { + app.hiddenService = await diskLogic.readHiddenService(`app-${app.id}`); + } catch(e) { + app.hiddenService = ''; + } + })); + + if (query.installed === true) { + const {installedApps} = await diskLogic.readUserFile(); + apps = apps.filter(app => installedApps.includes(app.id)); + } + + return apps; +} + +async function isValidAppId(id) { + // TODO: validate id + return true; +} + +async function install(id) { + if(! await isValidAppId(id)) { + throw new NodeError('Invalid app id'); + } + + try { + await diskLogic.writeSignalFile(`app-install-${id}`); + } catch (error) { + throw new NodeError('Could not write the signal file'); + } +}; + +async function uninstall(id) { + if(! await isValidAppId(id)) { + throw new NodeError('Invalid app id'); + } + + try { + await diskLogic.writeSignalFile(`app-uninstall-${id}`); + } catch (error) { + throw new NodeError('Could not write the signal file'); + } +}; + +module.exports = { + get, + install, + uninstall, +}; diff --git a/logic/disk.js b/logic/disk.js index 1f52f8f..ad5cfaf 100644 --- a/logic/disk.js +++ b/logic/disk.js @@ -1,3 +1,5 @@ +const path = require('path'); + const constants = require('utils/const.js'); const diskService = require('services/disk.js'); @@ -48,8 +50,15 @@ async function writeAppVersionFile(application, json) { return diskService.writeJsonFile(constants.WORKING_DIRECTORY + '/' + application, json); } -function readUserFile() { - return diskService.readJsonFile(constants.USER_FILE); +async function readUserFile() { + const defaultProperties = { + name: "", + password: "", + seed: "", + installedApps: [], + }; + const userFile = await diskService.readJsonFile(constants.USER_FILE); + return {...defaultProperties, ...userFile}; } function readSettingsFile() { @@ -81,7 +90,7 @@ function settingsFileExists() { } function hiddenServiceFileExists() { - return readHiddenService() + return diskService.readUtf8File(constants.UMBREL_DASHBOARD_HIDDEN_SERVICE_FILE) .then(() => Promise.resolve(true)) .catch(() => Promise.resolve(false)); } @@ -90,10 +99,6 @@ async function readAppVersionFile(application) { return diskService.readJsonFile(constants.WORKING_DIRECTORY + '/' + application); } -function readHiddenService() { - return diskService.readUtf8File(constants.UMBREL_DASHBOARD_HIDDEN_SERVICE_FILE); -} - function readElectrumHiddenService() { return diskService.readUtf8File(constants.ELECTRUM_HIDDEN_SERVICE_FILE); } @@ -210,6 +215,29 @@ function readSshSignalFile() { return diskService.readFile(constants.SSH_SIGNAL_FILE); } +// TODO: Transition all logic to use this signal function +function writeSignalFile(signalFile) { + if(!/^[0-9a-zA-Z-_]+$/.test(signalFile)) { + throw new Error('Invalid signal file characters'); + } + + const signalFilePath = path.join(constants.SIGNAL_DIR, signalFile); + return diskService.writeFile(signalFilePath, 'true'); +} + +function readAppRegistry() { + const appRegistryFile = path.join(constants.APPS_DIR, 'registry.json'); + return diskService.readJsonFile(appRegistryFile); +} + +function readHiddenService(id) { + if(!/^[0-9a-zA-Z-_]+$/.test(id)) { + throw new Error('Invalid hidden service ID'); + } + const hiddenServiceFile = path.join(constants.TOR_HIDDEN_SERVICE_DIR, id, 'hostname'); + return diskService.readUtf8File(hiddenServiceFile); +} + module.exports = { deleteItemsInDir, deleteUserFile, @@ -228,7 +256,6 @@ module.exports = { settingsFileExists, hiddenServiceFileExists, readAppVersionFile, - readHiddenService, readElectrumHiddenService, readBitcoinP2PHiddenService, readBitcoinRPCHiddenService, @@ -255,5 +282,8 @@ module.exports = { readMigrationStatusFile, migration, enableSsh, - readSshSignalFile + readSshSignalFile, + writeSignalFile, + readAppRegistry, + readHiddenService, }; diff --git a/logic/system.js b/logic/system.js index ce5e86f..a045543 100644 --- a/logic/system.js +++ b/logic/system.js @@ -19,7 +19,7 @@ async function getInfo() { async function getHiddenServiceUrl() { try { - const url = await diskLogic.readHiddenService(); + const url = await diskLogic.readHiddenService('web'); return url; } catch (error) { throw new NodeError('Unable to get hidden service url'); diff --git a/routes/v1/apps.js b/routes/v1/apps.js new file mode 100644 index 0000000..2f81031 --- /dev/null +++ b/routes/v1/apps.js @@ -0,0 +1,34 @@ +const express = require('express'); +const router = express.Router(); + +const appsLogic = require('logic/apps.js'); + +const auth = require('middlewares/auth.js'); + +const constants = require('utils/const.js'); +const safeHandler = require('utils/safeHandler'); + +router.get('/', auth.jwt, safeHandler(async (req, res) => { + const query = { + installed: req.query.installed === '1', + }; + const apps = await appsLogic.get(query); + + return res.status(constants.STATUS_CODES.OK).json(apps); +})); + +router.post('/:id/install', auth.jwt, safeHandler(async (req, res) => { + const {id} = req.params; + const result = await appsLogic.install(id); + + return res.status(constants.STATUS_CODES.OK).json(result); +})); + +router.post('/:id/uninstall', auth.jwt, safeHandler(async (req, res) => { + const {id} = req.params; + const result = await appsLogic.uninstall(id); + + return res.status(constants.STATUS_CODES.OK).json(result); +})); + +module.exports = router; diff --git a/utils/const.js b/utils/const.js index e25b514..335d0d8 100644 --- a/utils/const.js +++ b/utils/const.js @@ -4,6 +4,9 @@ module.exports = { REQUEST_CORRELATION_ID_KEY: 'reqId', DEVICE_HOSTNAME: process.env.DEVICE_HOSTNAME || 'umbrel.local', USER_FILE: process.env.USER_FILE || '/db/user.json', + SIGNAL_DIR: process.env.SIGNAL_DIR || '/signals', + APPS_DIR: process.env.APPS_DIR || '/apps', + TOR_HIDDEN_SERVICE_DIR: process.env.TOR_HIDDEN_SERVICE_DIR || '/var/lib/tor', SHUTDOWN_SIGNAL_FILE: process.env.SHUTDOWN_SIGNAL_FILE || '/signals/shutdown', REBOOT_SIGNAL_FILE: process.env.REBOOT_SIGNAL_FILE || '/signals/reboot', JWT_PUBLIC_KEY_FILE: process.env.JWT_PUBLIC_KEY_FILE || '/db/jwt-public-key/jwt.pem',