spiegel-keyman/developer/server/src/routes.ts
Marc Durdin 273c689629
Merge pull request #6300 from keymanapp/chore/developer/disable-upload-in-server-non-localhost
chore(developer): disable upload for server except localhost
2022-03-02 11:51:41 +11:00

209 lines
6.9 KiB
TypeScript

import express = require('express');
import multer = require('multer');
import ws = require('ws');
import handleIncKeyboardsJs from './handlers/inc/keyboards-js';
import { data, DebugFont, DebugKeyboard, DebugModel, DebugObject, DebugPackage, isValidId } from './data';
import apiGet from './handlers/api/debugobject/get';
import apiRegister, { apiRegisterFile } from './handlers/api/debugobject/register';
import apiKeyboardRegister from './handlers/api/keyboard/register';
import apiFontRegister from './handlers/api/font/register';
import apiUnregister from './handlers/api/debugobject/unregister';
import handleIncPackagesJson from './handlers/inc/packages-json';
import apiPackageRegister from './handlers/api/package/register';
import handleIncKeyboardsCss from './handlers/inc/keyboards-css';
import { Environment } from './version-data';
import { configuration } from './config';
import chalk = require('chalk');
export default function setupRoutes(app: express.Express, upload: multer.Multer, wsServer: ws.Server, environment: Environment ) {
/* Middleware - JSON and logging */
app.use(express.json()); // for parsing application/json
app.use(function (req, _res, next) {
// if(environment.environment == 'local') {
console.log(req.method + ' ' + req.path);
// }
next();
});
function isLocalhost(req: express.Request) {
return (
req.socket.remoteAddress == '127.0.0.1' ||
req.socket.remoteAddress == '::1' ||
req.socket.remoteAddress == '::ffff:127.0.0.1' // ipv4 localhost over ipv6
);
}
function localhostOnly(req: express.Request, res: express.Response, next: express.NextFunction) {
if(!isLocalhost(req)) {
res.sendStatus(401);
} else {
next();
}
}
// Only allow connections from localhost to /api/
app.use('/api/', localhostOnly);
/* All routes */
app.use('/', express.static('dist/site'));
app.post('/upload', localhostOnly, upload.single('file'), (req, res, next) => {
const name = req.file.originalname;
if(!isValidId(name)) {
res.sendStatus(400);
return;
}
let fileType = '';
const rModel = /^([a-zA-Z0-9_\.-]+)\.model\.js$/.exec(name);
const rKeyboard = /^([a-zA-Z0-9_]+)\.js$/.exec(name);
const rPackage = /^([a-zA-Z0-9_]+)\.kmp$/.exec(name);
const rFont = /^(.+\.(ttf|otf))$/.exec(name);
if(rModel) {
apiRegisterFile(DebugModel, data.models, rModel[1], req.file.buffer);
fileType = 'model';
} else if(rKeyboard) {
apiRegisterFile(DebugKeyboard, data.keyboards, rKeyboard[1], req.file.buffer);
fileType = 'keyboard';
} else if(rPackage) {
apiRegisterFile(DebugPackage, data.packages, rPackage[1], req.file.buffer);
fileType = 'package';
} else if(rFont) {
apiRegisterFile(DebugFont, data.fonts, rFont[1], req.file.buffer);
fileType = 'font';
} else {
res.status(400).json({message: 'unrecognised file type'});
return;
}
res.json({message: 'success', type: fileType});
next();
},
saveState,
(req,res,next)=>notifyClients(wsServer,req,res,next)
);
app.get('/inc/keyboards.js', handleIncKeyboardsJs);
app.get('/inc/keyboards.css', handleIncKeyboardsCss);
app.get('/inc/packages.json', handleIncPackagesJson);
app.get('/api-public/version', (req,res,next)=>{
res.json({version: environment.versionWithTag, isApiAvailable: isLocalhost(req)});
next();
});
/* Localhost only routes -- todo /api/internal/ vs /api/... */
app.post('/api/shutdown', (_req,res) => { setTimeout(() => process.exit(0), 100); res.send('ok'); });
appGetData(app, /\/data\/keyboard\/(.+)\.js$/, data.keyboards);
appGetData(app, /\/data\/model\/(.+)\.model\.js$/, data.models);
appGetData(app, /\/data\/package\/(.+)\.kmp$/, data.packages);
appGetData(app, /\/data\/font\/(.+)\.ttf$/, data.fonts);
app.get('/api/font', (req,res,next)=>apiGet(data.fonts,req,res,next));
app.post('/api/font/register',
upload.single('file'),
(req,res,next)=>apiRegister(DebugFont, data.fonts, req, res, next),
apiFontRegister,
saveState,
(req,res,next)=>notifyClients(wsServer,req,res,next)
);
app.post('/api/font/unregister',
(req,res,next)=>apiUnregister(data.fonts,req,res,next),
saveState,
(req,res,next)=>notifyClients(wsServer,req,res,next)
);
app.get('/api/keyboard', (req,res,next)=>apiGet(data.keyboards,req,res,next));
app.post('/api/keyboard/register',
upload.single('file'),
(req,res,next)=>apiRegister(DebugKeyboard, data.keyboards, req, res, next),
apiKeyboardRegister,
saveState,
(req,res,next)=>notifyClients(wsServer,req,res,next)
);
app.post('/api/keyboard/unregister',
(req,res,next)=>apiUnregister(data.keyboards,req,res,next),
saveState,
(req,res,next)=>notifyClients(wsServer,req,res,next)
);
app.get('/api/model', (req,res,next)=>apiGet(data.models,req,res,next));
app.post('/api/model/register',
upload.single('file'),
(req,res,next)=>apiRegister(DebugModel, data.models, req, res, next),
saveState,
(req,res,next)=>notifyClients(wsServer,req,res,next)
);
app.post('/api/model/unregister',
(req,res,next)=>apiUnregister(data.models,req,res,next),
saveState,
(req,res,next)=>notifyClients(wsServer,req,res,next)
);
app.get('/api/package', (req,res,next)=>apiGet(data.packages,req,res,next));
app.post('/api/package/register',
upload.single('file'),
(req,res,next)=>apiRegister(DebugPackage, data.packages, req, res, next),
apiPackageRegister,
saveState,
(req,res,next)=>notifyClients(wsServer,req,res,next)
);
app.post('/api/package/unregister',
(req,res,next)=>apiUnregister(data.packages,req,res,next),
saveState,
(req,res,next)=>notifyClients(wsServer,req,res,next)
);
/* ngrok data */
app.get('/api/status', (_req,res,next) => {
const response = { ngrokEnabled: configuration.useNgrok, ngrokEndpoint: configuration.ngrokEndpoint };
res.send(response);
next();
});
}
/* Utility functions */
function notifyClients(wsServer: ws.Server, res: express.Request, req: express.Response, next: express.NextFunction) {
wsServer.clients.forEach(c => {
c.send('refresh', (err) => {
if(err) console.error(chalk.red('Websocket send error '+err.message));
});
});
next();
}
function saveState (res: express.Request, req: express.Response, next: express.NextFunction) {
data.saveState();
if(next) next();
}
function appGetData(app: express.Express, pathregex: RegExp, root: { [id: string]: DebugObject }) {
app.get(pathregex, (req,res,next)=>{
const r = pathregex.exec(req.path);
if(!r) {
res.status(404).send('invalid path');
return;
}
if(!isValidId(r[1])) {
res.sendStatus(400);
return;
}
const o = root[r[1]];
if(!o) {
res.status(404).send('not found');
} else {
o.lastUse = new Date();
saveState(null, null, null);
res.sendFile(o.filename, (err)=>{if(err) console.error(chalk.red(err))});
}
});
}