mirror of
https://github.com/HeyPuter/puter.git
synced 2026-02-27 20:38:44 -06:00
dev: move web service utilities
This commit is contained in:
@@ -1,198 +1,2 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const multest = require('@heyputer/multest');
|
||||
const api_error_handler = require('../api/api_error_handler.js');
|
||||
|
||||
const fsBeforeMW = require('../middleware/fs');
|
||||
const APIError = require('./APIError.js');
|
||||
const { Context } = require('../util/context.js');
|
||||
|
||||
/**
|
||||
* eggspress() is a factory function for creating express routers.
|
||||
*
|
||||
* @param {*} route the route to the router
|
||||
* @param {*} settings the settings for the router. The following
|
||||
* properties are supported:
|
||||
* - auth: whether or not to use the auth middleware
|
||||
* - fs: whether or not to use the fs middleware
|
||||
* - json: whether or not to use the json middleware
|
||||
* - customArgs: custom arguments to pass to the router
|
||||
* - allowedMethods: the allowed HTTP methods
|
||||
* @param {*} handler the handler for the router
|
||||
* @returns {express.Router} the router
|
||||
*/
|
||||
module.exports = function eggspress (route, settings, handler) {
|
||||
const router = express.Router();
|
||||
const mw = [];
|
||||
const afterMW = [];
|
||||
|
||||
// These flags enable specific middleware.
|
||||
if ( settings.abuse ) mw.push(require('../middleware/abuse')(settings.abuse));
|
||||
if ( settings.auth ) mw.push(require('../middleware/auth'));
|
||||
if ( settings.auth2 ) mw.push(require('../middleware/auth2'));
|
||||
if ( settings.fs ) {
|
||||
mw.push(fsBeforeMW);
|
||||
}
|
||||
if ( settings.verified ) mw.push(require('../middleware/verified'));
|
||||
if ( settings.json ) mw.push(express.json());
|
||||
|
||||
// The `files` setting is an array of strings. Each string is the name
|
||||
// of a multipart field that contains files. `multer` is used to parse
|
||||
// the multipart request and store the files in `req.files`.
|
||||
if ( settings.files ) {
|
||||
for ( const key of settings.files ) {
|
||||
mw.push(multer().array(key));
|
||||
}
|
||||
}
|
||||
|
||||
if ( settings.multest ) {
|
||||
mw.push(multest());
|
||||
}
|
||||
|
||||
// The `multipart_jsons` setting is an array of strings. Each string
|
||||
// is the name of a multipart field that contains JSON. This middleware
|
||||
// parses the JSON in each field and stores the result in `req.body`.
|
||||
if ( settings.multipart_jsons ) {
|
||||
for ( const key of settings.multipart_jsons ) {
|
||||
mw.push((req, res, next) => {
|
||||
try {
|
||||
if ( ! Array.isArray(req.body[key]) ) {
|
||||
req.body[key] = [JSON.parse(req.body[key])];
|
||||
} else {
|
||||
req.body[key] = req.body[key].map(JSON.parse);
|
||||
}
|
||||
} catch (e) {
|
||||
return res.status(400).send({
|
||||
error: {
|
||||
message: `Invalid JSON in multipart field ${key}`
|
||||
}
|
||||
});
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// The `alias` setting is an object. Each key is the name of a
|
||||
// parameter. Each value is the name of a parameter that should
|
||||
// be aliased to the key.
|
||||
if ( settings.alias ) {
|
||||
for ( const alias in settings.alias ) {
|
||||
const target = settings.alias[alias];
|
||||
mw.push((req, res, next) => {
|
||||
const values = req.method === 'GET' ? req.query : req.body;
|
||||
if ( values[alias] ) {
|
||||
values[target] = values[alias];
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// The `parameters` setting is an object. Each key is the name of a
|
||||
// parameter. Each value is a `Param` object. The `Param` object
|
||||
// specifies how to validate the parameter.
|
||||
if ( settings.parameters ) {
|
||||
for ( const key in settings.parameters ) {
|
||||
const param = settings.parameters[key];
|
||||
mw.push(async (req, res, next) => {
|
||||
if ( ! req.values ) req.values = {};
|
||||
|
||||
const values = req.method === 'GET' ? req.query : req.body;
|
||||
const getParam = (key) => values[key];
|
||||
try {
|
||||
const result = await param.consolidate({ req, getParam });
|
||||
req.values[key] = result;
|
||||
} catch (e) {
|
||||
api_error_handler(e, req, res, next);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// what if I wanted to pass arguments to, for example, `json`?
|
||||
if ( settings.customArgs ) mw.push(settings.customArgs);
|
||||
|
||||
if ( settings.alarm_timeout ) {
|
||||
mw.push((req, res, next) => {
|
||||
setTimeout(() => {
|
||||
if ( ! res.headersSent ) {
|
||||
const log = req.services.get('log-service').create('eggspress:timeout');
|
||||
const errors = req.services.get('error-service').create(log);
|
||||
let id = Array.isArray(route) ? route[0] : route;
|
||||
id = id.replace(/\//g, '_');
|
||||
errors.report(id, {
|
||||
source: new Error('Response timed out.'),
|
||||
message: 'Response timed out.',
|
||||
trace: true,
|
||||
alarm: true,
|
||||
});
|
||||
}
|
||||
}, settings.alarm_timeout);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
if ( settings.response_timeout ) {
|
||||
mw.push((req, res, next) => {
|
||||
setTimeout(() => {
|
||||
if ( ! res.headersSent ) {
|
||||
api_error_handler(APIError.create('response_timeout'), req, res, next);
|
||||
}
|
||||
}, settings.response_timeout);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
if ( settings.mw ) mw.push(...settings.mw);
|
||||
|
||||
const errorHandledHandler = async function (req, res, next) {
|
||||
if ( settings.subdomain ) {
|
||||
if ( require('../helpers').subdomain(req) !== settings.subdomain ) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
try {
|
||||
const expected_ctx = res.locals.ctx;
|
||||
const received_ctx = Context.get(undefined, { allow_fallback: true });
|
||||
|
||||
if ( expected_ctx != received_ctx ) {
|
||||
await expected_ctx.arun(async () => {
|
||||
await handler(req, res, next);
|
||||
});
|
||||
} else await handler(req, res, next);
|
||||
} catch (e) {
|
||||
api_error_handler(e, req, res, next);
|
||||
}
|
||||
};
|
||||
|
||||
if ( settings.allowedMethods.includes('GET') ) {
|
||||
router.get(route, ...mw, errorHandledHandler, ...afterMW);
|
||||
}
|
||||
|
||||
if ( settings.allowedMethods.includes('POST') ) {
|
||||
router.post(route, ...mw, errorHandledHandler, ...afterMW);
|
||||
}
|
||||
|
||||
return router;
|
||||
}
|
||||
// This file is a legacy alias
|
||||
module.exports = require('../modules/web/lib/eggspress.js');
|
||||
|
||||
@@ -104,7 +104,7 @@ class WebServerService extends BaseService {
|
||||
// error handling middleware goes last, as per the
|
||||
// expressjs documentation:
|
||||
// https://expressjs.com/en/guide/error-handling.html
|
||||
this.app.use(require('../../api/api_error_handler.js'));
|
||||
this.app.use(require('./lib/api_error_handler.js'));
|
||||
|
||||
const { jwt_auth } = require('../../helpers.js');
|
||||
|
||||
|
||||
4
src/backend/src/modules/web/lib/__lib__.js
Normal file
4
src/backend/src/modules/web/lib/__lib__.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
eggspress: require("./eggspress"),
|
||||
api_error_handler: require("./api_error_handler"),
|
||||
};
|
||||
78
src/backend/src/modules/web/lib/api_error_handler.js
Normal file
78
src/backend/src/modules/web/lib/api_error_handler.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const APIError = require('../../../api/APIError.js');
|
||||
|
||||
/**
|
||||
* api_error_handler() is an express error handler for API errors.
|
||||
* It adheres to the express error handler signature and should be
|
||||
* used as the last middleware in an express app.
|
||||
*
|
||||
* Since Express 5 is not yet released, this function is used by
|
||||
* eggspress() to handle errors instead of as a middleware.
|
||||
*
|
||||
* @todo remove this function and use express error handling
|
||||
* when Express 5 is released
|
||||
*
|
||||
* @param {*} err
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @param {*} next
|
||||
* @returns
|
||||
*/
|
||||
module.exports = function api_error_handler (err, req, res, next) {
|
||||
if (res.headersSent) {
|
||||
console.error('error after headers were sent:', err);
|
||||
return next(err)
|
||||
}
|
||||
|
||||
// API errors might have a response to help the
|
||||
// developer resolve the issue.
|
||||
if ( err instanceof APIError ) {
|
||||
return err.write(res);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof err === 'object' &&
|
||||
! (err instanceof Error) &&
|
||||
err.hasOwnProperty('message')
|
||||
) {
|
||||
const apiError = APIError.create(400, err);
|
||||
return apiError.write(res);
|
||||
}
|
||||
|
||||
console.error('internal server error:', err);
|
||||
|
||||
const services = globalThis.services;
|
||||
if ( services && services.has('alarm') ) {
|
||||
const alarm = services.get('alarm');
|
||||
alarm.create('api_error_handler', err.message, {
|
||||
error: err,
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
headers: req.headers,
|
||||
});
|
||||
}
|
||||
|
||||
req.__error_handled = true;
|
||||
|
||||
// Other errors should provide as little information
|
||||
// to the client as possible for security reasons.
|
||||
return res.send(500, 'Internal Server Error');
|
||||
};
|
||||
199
src/backend/src/modules/web/lib/eggspress.js
Normal file
199
src/backend/src/modules/web/lib/eggspress.js
Normal file
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const multest = require('@heyputer/multest');
|
||||
const api_error_handler = require('./api_error_handler.js');
|
||||
|
||||
const fsBeforeMW = require('../../../middleware/fs.js');
|
||||
const APIError = require('../../../api/APIError.js');
|
||||
const { Context } = require('../../../util/context.js');
|
||||
const { subdomain } = require('../../../helpers.js');
|
||||
|
||||
/**
|
||||
* eggspress() is a factory function for creating express routers.
|
||||
*
|
||||
* @param {*} route the route to the router
|
||||
* @param {*} settings the settings for the router. The following
|
||||
* properties are supported:
|
||||
* - auth: whether or not to use the auth middleware
|
||||
* - fs: whether or not to use the fs middleware
|
||||
* - json: whether or not to use the json middleware
|
||||
* - customArgs: custom arguments to pass to the router
|
||||
* - allowedMethods: the allowed HTTP methods
|
||||
* @param {*} handler the handler for the router
|
||||
* @returns {express.Router} the router
|
||||
*/
|
||||
module.exports = function eggspress (route, settings, handler) {
|
||||
const router = express.Router();
|
||||
const mw = [];
|
||||
const afterMW = [];
|
||||
|
||||
// These flags enable specific middleware.
|
||||
if ( settings.abuse ) mw.push(require('../../../middleware/abuse')(settings.abuse));
|
||||
if ( settings.auth ) mw.push(require('../../../middleware/auth'));
|
||||
if ( settings.auth2 ) mw.push(require('../../../middleware/auth2'));
|
||||
if ( settings.fs ) {
|
||||
mw.push(fsBeforeMW);
|
||||
}
|
||||
if ( settings.verified ) mw.push(require('../../../middleware/verified'));
|
||||
if ( settings.json ) mw.push(express.json());
|
||||
|
||||
// The `files` setting is an array of strings. Each string is the name
|
||||
// of a multipart field that contains files. `multer` is used to parse
|
||||
// the multipart request and store the files in `req.files`.
|
||||
if ( settings.files ) {
|
||||
for ( const key of settings.files ) {
|
||||
mw.push(multer().array(key));
|
||||
}
|
||||
}
|
||||
|
||||
if ( settings.multest ) {
|
||||
mw.push(multest());
|
||||
}
|
||||
|
||||
// The `multipart_jsons` setting is an array of strings. Each string
|
||||
// is the name of a multipart field that contains JSON. This middleware
|
||||
// parses the JSON in each field and stores the result in `req.body`.
|
||||
if ( settings.multipart_jsons ) {
|
||||
for ( const key of settings.multipart_jsons ) {
|
||||
mw.push((req, res, next) => {
|
||||
try {
|
||||
if ( ! Array.isArray(req.body[key]) ) {
|
||||
req.body[key] = [JSON.parse(req.body[key])];
|
||||
} else {
|
||||
req.body[key] = req.body[key].map(JSON.parse);
|
||||
}
|
||||
} catch (e) {
|
||||
return res.status(400).send({
|
||||
error: {
|
||||
message: `Invalid JSON in multipart field ${key}`
|
||||
}
|
||||
});
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// The `alias` setting is an object. Each key is the name of a
|
||||
// parameter. Each value is the name of a parameter that should
|
||||
// be aliased to the key.
|
||||
if ( settings.alias ) {
|
||||
for ( const alias in settings.alias ) {
|
||||
const target = settings.alias[alias];
|
||||
mw.push((req, res, next) => {
|
||||
const values = req.method === 'GET' ? req.query : req.body;
|
||||
if ( values[alias] ) {
|
||||
values[target] = values[alias];
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// The `parameters` setting is an object. Each key is the name of a
|
||||
// parameter. Each value is a `Param` object. The `Param` object
|
||||
// specifies how to validate the parameter.
|
||||
if ( settings.parameters ) {
|
||||
for ( const key in settings.parameters ) {
|
||||
const param = settings.parameters[key];
|
||||
mw.push(async (req, res, next) => {
|
||||
if ( ! req.values ) req.values = {};
|
||||
|
||||
const values = req.method === 'GET' ? req.query : req.body;
|
||||
const getParam = (key) => values[key];
|
||||
try {
|
||||
const result = await param.consolidate({ req, getParam });
|
||||
req.values[key] = result;
|
||||
} catch (e) {
|
||||
api_error_handler(e, req, res, next);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// what if I wanted to pass arguments to, for example, `json`?
|
||||
if ( settings.customArgs ) mw.push(settings.customArgs);
|
||||
|
||||
if ( settings.alarm_timeout ) {
|
||||
mw.push((req, res, next) => {
|
||||
setTimeout(() => {
|
||||
if ( ! res.headersSent ) {
|
||||
const log = req.services.get('log-service').create('eggspress:timeout');
|
||||
const errors = req.services.get('error-service').create(log);
|
||||
let id = Array.isArray(route) ? route[0] : route;
|
||||
id = id.replace(/\//g, '_');
|
||||
errors.report(id, {
|
||||
source: new Error('Response timed out.'),
|
||||
message: 'Response timed out.',
|
||||
trace: true,
|
||||
alarm: true,
|
||||
});
|
||||
}
|
||||
}, settings.alarm_timeout);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
if ( settings.response_timeout ) {
|
||||
mw.push((req, res, next) => {
|
||||
setTimeout(() => {
|
||||
if ( ! res.headersSent ) {
|
||||
api_error_handler(APIError.create('response_timeout'), req, res, next);
|
||||
}
|
||||
}, settings.response_timeout);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
if ( settings.mw ) mw.push(...settings.mw);
|
||||
|
||||
const errorHandledHandler = async function (req, res, next) {
|
||||
if ( settings.subdomain ) {
|
||||
if ( subdomain(req) !== settings.subdomain ) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
try {
|
||||
const expected_ctx = res.locals.ctx;
|
||||
const received_ctx = Context.get(undefined, { allow_fallback: true });
|
||||
|
||||
if ( expected_ctx != received_ctx ) {
|
||||
await expected_ctx.arun(async () => {
|
||||
await handler(req, res, next);
|
||||
});
|
||||
} else await handler(req, res, next);
|
||||
} catch (e) {
|
||||
api_error_handler(e, req, res, next);
|
||||
}
|
||||
};
|
||||
|
||||
if ( settings.allowedMethods.includes('GET') ) {
|
||||
router.get(route, ...mw, errorHandledHandler, ...afterMW);
|
||||
}
|
||||
|
||||
if ( settings.allowedMethods.includes('POST') ) {
|
||||
router.post(route, ...mw, errorHandledHandler, ...afterMW);
|
||||
}
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -25,7 +25,6 @@ const { Context } = require('../../util/context.js');
|
||||
const Busboy = require('busboy');
|
||||
const { TeePromise } = require('../../util/promise.js');
|
||||
const APIError = require('../../api/APIError.js');
|
||||
const api_error_handler = require('../../api/api_error_handler.js');
|
||||
const { valid_file_size } = require('../../util/validutil.js');
|
||||
|
||||
// -----------------------------------------------------------------------//
|
||||
@@ -172,13 +171,8 @@ module.exports = eggspress(['/up', '/write'], {
|
||||
|
||||
const values = req.method === 'GET' ? req.query : req.body;
|
||||
const getParam = (key) => values[key];
|
||||
try {
|
||||
const result = await param.consolidate({ req, getParam });
|
||||
req.values[key] = result;
|
||||
} catch (e) {
|
||||
api_error_handler(e, req, res, next);
|
||||
return;
|
||||
}
|
||||
const result = await param.consolidate({ req, getParam });
|
||||
req.values[key] = result;
|
||||
}
|
||||
|
||||
if ( req.body.size === undefined ) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const { AdvancedBase } = require("@heyputer/putility");
|
||||
const api_error_handler = require("../../api/api_error_handler");
|
||||
const api_error_handler = require("../../modules/web/lib/api_error_handler");
|
||||
const config = require("../../config");
|
||||
const { get_user, get_app, id2path } = require("../../helpers");
|
||||
const { Context } = require("../../util/context");
|
||||
|
||||
Reference in New Issue
Block a user