This commit is contained in:
Alexandre Bodin 2021-09-27 19:27:08 +02:00
parent b63c330c58
commit 1da801ac64
30 changed files with 429 additions and 546 deletions

View File

@ -376,6 +376,8 @@ class Strapi {
// Initialize middlewares.
await initializeMiddlewares(this);
await this.server.initRouting();
await this.runLifecyclesFunctions(LIFECYCLES.BOOTSTRAP);
this.cron.start();

View File

@ -10,7 +10,6 @@ dotenv.config({ path: process.env.ENV_PATH });
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
const getPrefixedDeps = require('../../utils/get-prefixed-dependencies');
const loadConfigDir = require('./config-loader');
const loadFunction = require('./load-functions');
@ -31,15 +30,6 @@ const defaultConfig = {
admin: { autoOpen: false },
},
admin: {},
middleware: {
timeout: 1000,
load: {
before: ['responseTime', 'logger', 'cors', 'responses', 'gzip'],
order: [],
after: ['parser', 'router'],
},
settings: {},
},
};
module.exports = (dir, initialConfig = {}) => {
@ -63,7 +53,6 @@ module.exports = (dir, initialConfig = {}) => {
strapi: strapiVersion,
},
functions: loadFunction(path.join(configDir, 'functions')),
installedMiddlewares: getPrefixedDeps('@strapi/middleware', pkgJSON),
};
const baseConfig = omit('plugins', loadConfigDir(configDir)); // plugin config will be loaded later

View File

@ -9,18 +9,18 @@ const glob = require('../../load/glob');
* Load middlewares
*/
module.exports = async function(strapi) {
const installedMiddlewares = strapi.config.get('installedMiddlewares');
// const installedMiddlewares = strapi.config.get('installedMiddlewares');
const appPath = strapi.config.get('appPath');
let middlewares = {};
const loaders = createLoaders(strapi);
// const loaders = createLoaders(strapi);
await loaders.loadMiddlewareDependencies(installedMiddlewares, middlewares);
// await loaders.loadMiddlewareDependencies(installedMiddlewares, middlewares);
// internal middlewares
await loaders.loadInternalMiddlewares(middlewares);
// await loaders.loadInternalMiddlewares(middlewares);
// local middleware
await loaders.loadLocalMiddlewares(appPath, middlewares);
// await loaders.loadLocalMiddlewares(appPath, middlewares);
return middlewares;
};

View File

@ -0,0 +1,5 @@
'use strict';
const compress = require('koa-compress');
module.exports = (options = {}) => compress(options);

View File

@ -1,5 +0,0 @@
{
"error": {
"enabled": true
}
}

View File

@ -1,147 +0,0 @@
'use strict';
/**
* Boom hook
*/
// Public node modules.
const _ = require('lodash');
const Boom = require('@hapi/boom');
const delegate = require('delegates');
const boomMethods = [
'badRequest',
'unauthorized',
'paymentRequired',
'forbidden',
'notFound',
'methodNotAllowed',
'notAcceptable',
'proxyAuthRequired',
'clientTimeout',
'conflict',
'resourceGone',
'lengthRequired',
'preconditionFailed',
'entityTooLarge',
'uriTooLong',
'unsupportedMediaType',
'rangeNotSatisfiable',
'expectationFailed',
'teapot',
'badData',
'locked',
'failedDependency',
'preconditionRequired',
'tooManyRequests',
'illegal',
'badImplementation',
'notImplemented',
'badGateway',
'serverUnavailable',
'gatewayTimeout',
];
const formatBoomPayload = boomError => {
if (!Boom.isBoom(boomError)) {
boomError = Boom.boomify(boomError, {
statusCode: boomError.status || 500,
});
}
const { output } = boomError;
if (output.statusCode < 500 && !_.isNil(boomError.data)) {
output.payload.data = boomError.data;
}
return { status: output.statusCode, body: output.payload };
};
module.exports = strapi => {
return {
/**
* Initialize the hook
*/
initialize() {
this.delegator = delegate(strapi.server.app.context, 'response');
this.createResponses();
strapi.errors = Boom;
strapi.server.use(async (ctx, next) => {
try {
// App logic.
await next();
} catch (error) {
// emit error if configured
if (strapi.config.get('server.emitErrors', false)) {
strapi.server.app.emit('error', error, ctx);
}
// Log error.
const { status, body } = formatBoomPayload(error);
if (status >= 500) {
strapi.log.error(error);
}
ctx.body = body;
ctx.status = status;
}
});
strapi.server.use(async (ctx, next) => {
await next();
// Empty body is considered as `notFound` response.
if (_.isNil(ctx.body) && (_.isNil(ctx.status) || ctx.status === 404)) {
ctx.notFound();
}
});
},
// Custom function to avoid ctx.body repeat
createResponses() {
boomMethods.forEach(method => {
strapi.server.app.response[method] = function(msg, ...rest) {
const boomError = Boom[method](msg, ...rest) || {};
const { status, body } = formatBoomPayload(boomError);
// keep retro-compatibility for old error formats
body.message = msg || body.data || body.message;
this.body = body;
this.status = status;
};
this.delegator.method(method);
});
strapi.server.app.response.send = function(data, status = 200) {
this.status = status;
this.body = data;
};
strapi.server.app.response.created = function(data) {
this.status = 201;
this.body = data;
};
strapi.server.app.response.deleted = function(data) {
if (_.isNil(data)) {
this.status = 204;
} else {
this.status = 200;
this.body = data;
}
};
this.delegator
.method('send')
.method('created')
.method('deleted');
},
};
};

View File

@ -0,0 +1,134 @@
'use strict';
/**
* Boom hook
*/
// Public node modules.
const _ = require('lodash');
const Boom = require('@hapi/boom');
const delegate = require('delegates');
const boomMethods = [
'badRequest',
'unauthorized',
'paymentRequired',
'forbidden',
'notFound',
'methodNotAllowed',
'notAcceptable',
'proxyAuthRequired',
'clientTimeout',
'conflict',
'resourceGone',
'lengthRequired',
'preconditionFailed',
'entityTooLarge',
'uriTooLong',
'unsupportedMediaType',
'rangeNotSatisfiable',
'expectationFailed',
'teapot',
'badData',
'locked',
'failedDependency',
'preconditionRequired',
'tooManyRequests',
'illegal',
'badImplementation',
'notImplemented',
'badGateway',
'serverUnavailable',
'gatewayTimeout',
];
const formatBoomPayload = boomError => {
if (!Boom.isBoom(boomError)) {
boomError = Boom.boomify(boomError, {
statusCode: boomError.status || 500,
});
}
const { output } = boomError;
if (output.statusCode < 500 && !_.isNil(boomError.data)) {
output.payload.data = boomError.data;
}
return { status: output.statusCode, body: output.payload };
};
const createResponseUtils = () => {
const delegator = delegate(strapi.server.app.context, 'response');
boomMethods.forEach(method => {
strapi.server.app.response[method] = function(msg, ...rest) {
const boomError = Boom[method](msg, ...rest) || {};
const { status, body } = formatBoomPayload(boomError);
// keep retro-compatibility for old error formats
body.message = msg || body.data || body.message;
this.body = body;
this.status = status;
};
delegator.method(method);
});
strapi.server.app.response.send = function(data, status = 200) {
this.status = status;
this.body = data;
};
strapi.server.app.response.created = function(data) {
this.status = 201;
this.body = data;
};
strapi.server.app.response.deleted = function(data) {
if (_.isNil(data)) {
this.status = 204;
} else {
this.status = 200;
this.body = data;
}
};
delegator
.method('send')
.method('created')
.method('deleted');
};
// TODO: inject strapi
module.exports = () => {
createResponseUtils();
strapi.errors = Boom;
return async (ctx, next) => {
try {
// App logic.
await next();
if (_.isNil(ctx.body) && (_.isNil(ctx.status) || ctx.status === 404)) {
ctx.notFound();
}
} catch (error) {
// emit error if configured
if (strapi.config.get('server.emitErrors', false)) {
strapi.server.app.emit('error', error, ctx);
}
const { status, body } = formatBoomPayload(error);
if (status >= 500) {
strapi.log.error(error);
}
ctx.body = body;
ctx.status = status;
}
};
};

View File

@ -0,0 +1,17 @@
'use strict';
const { resolve } = require('path');
const { defaultsDeep } = require('lodash');
const favicon = require('koa-favicon');
const defaults = {
path: 'favicon.ico',
maxAge: 86400000,
};
// TODO: inject strapi
module.exports = options => {
const { maxAge, path: faviconPath } = defaultsDeep(defaults, options);
return favicon(resolve(strapi.dirs.root, faviconPath), { maxAge });
};

View File

@ -1,7 +0,0 @@
{
"favicon": {
"enabled": true,
"path": "favicon.ico",
"maxAge": 86400000
}
}

View File

@ -1,31 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Node.js core.
const { resolve } = require('path');
const favicon = require('koa-favicon');
/**
* Favicon hook
*/
module.exports = strapi => {
return {
/**
* Initialize the hook
*/
initialize() {
const { maxAge, path: faviconPath } = strapi.config.middleware.settings.favicon;
strapi.server.use(
favicon(resolve(strapi.dirs.root, faviconPath), {
maxAge,
})
);
},
};
};

View File

@ -1,6 +0,0 @@
{
"gzip": {
"enabled": false,
"options": {}
}
}

View File

@ -1,19 +0,0 @@
'use strict';
/**
* Gzip hook
*/
const compress = require('koa-compress');
module.exports = strapi => {
return {
/**
* Initialize the hook
*/
initialize() {
const { options = {} } = strapi.config.middleware.settings.gzip;
strapi.server.use(compress(options));
},
};
};

View File

@ -1,18 +0,0 @@
{
"helmet": {
"enabled": true,
"crossOriginEmbedderPolicy": false,
"crossOriginOpenerPolicy": false,
"crossOriginResourcePolicy": false,
"originAgentCluster": false,
"contentSecurityPolicy": false,
"xssFilter": false,
"hsts": {
"maxAge": 31536000,
"includeSubDomains": true
},
"frameguard": {
"action": "sameorigin"
}
}
}

View File

@ -1,120 +1,156 @@
'use strict';
const { uniq, difference, get, isUndefined, merge } = require('lodash');
const compression = require('./compression');
const cors = require('./cors');
const errors = require('./errors');
const favicon = require('./favicon');
const ip = require('./ip');
const logger = require('./logger');
const poweredBy = require('./powered-by');
const requestParser = require('./request-parser');
const responseTime = require('./response-time');
const responseHandlers = require('./response-handlers');
const security = require('./security');
// session: require('./session'),
const publicStatic = require('./public');
const requiredMiddlewares = [
'auth',
'responses',
'router',
'logger',
'error',
'cors',
'cron',
'xframe',
'xss',
'public',
'favicon',
const INTERNAL_MIDDLEWARES = [
errors,
ip,
security,
cors,
responseTime,
poweredBy,
logger,
compression,
responseHandlers,
requestParser,
favicon,
publicStatic,
];
module.exports = async function(strapi) {
/** Utils */
const middlewareConfig = strapi.config.middleware;
module.exports = async strapi => {
for (const middlewareFactory of INTERNAL_MIDDLEWARES) {
// const config = strapi.config.get(`middlwares`);
const middleware = middlewareFactory({});
// check if a middleware exists
const middlewareExists = key => {
return !isUndefined(strapi.middleware[key]);
};
// check if a middleware is enabled
const middlewareEnabled = key => {
return (
requiredMiddlewares.includes(key) ||
get(middlewareConfig, ['settings', key, 'enabled'], false) === true
);
};
// list of enabled middlewares
const enabledMiddlewares = Object.keys(strapi.middleware).filter(middlewareEnabled);
// Method to initialize middlewares and emit an event.
const initialize = middlewareKey => {
if (strapi.middleware[middlewareKey].loaded === true) return;
const module = strapi.middleware[middlewareKey].load;
return new Promise((resolve, reject) => {
const timeout = setTimeout(
() => reject(`(middleware: ${middlewareKey}) is taking too long to load.`),
middlewareConfig.timeout || 1000
);
strapi.middleware[middlewareKey] = merge(strapi.middleware[middlewareKey], module);
Promise.resolve()
.then(() => module.initialize(strapi))
.then(() => {
clearTimeout(timeout);
strapi.middleware[middlewareKey].loaded = true;
resolve();
})
.catch(err => {
clearTimeout(timeout);
if (err) {
return reject(err);
}
});
});
};
/**
* Run init functions
*/
// Run beforeInitialize of every middleware
await Promise.all(
enabledMiddlewares.map(key => {
const { beforeInitialize } = strapi.middleware[key].load;
if (typeof beforeInitialize === 'function') {
return beforeInitialize(strapi);
}
})
);
// run the initialization of an array of middlewares sequentially
const initMiddlewaresSeq = async middlewareArr => {
for (let key of uniq(middlewareArr)) {
await initialize(key);
}
};
const middlewaresBefore = get(middlewareConfig, 'load.before', [])
.filter(middlewareExists)
.filter(middlewareEnabled);
const middlewaresAfter = get(middlewareConfig, 'load.after', [])
.filter(middlewareExists)
.filter(middlewareEnabled);
const middlewaresOrder = get(middlewareConfig, 'load.order', [])
.filter(middlewareExists)
.filter(middlewareEnabled);
const unspecifiedMiddlewares = difference(
enabledMiddlewares,
middlewaresBefore,
middlewaresOrder,
middlewaresAfter
);
// before
await initMiddlewaresSeq(middlewaresBefore);
// ordered // rest of middlewares
await Promise.all([
initMiddlewaresSeq(middlewaresOrder),
Promise.all(unspecifiedMiddlewares.map(initialize)),
]);
// after
await initMiddlewaresSeq(middlewaresAfter);
strapi.server.use(middleware);
}
};
// const requiredMiddlewares = [
// 'auth',
// 'responses',
// 'router',
// 'logger',
// 'error',
// 'cors',
// 'cron',
// 'xframe',
// 'xss',
// 'public',
// 'favicon',
// ];
// module.exports = async function(strapi) {
// /** Utils */
// const middlewareConfig = strapi.config.middleware;
// // check if a middleware exists
// const middlewareExists = key => {
// return !isUndefined(strapi.middleware[key]);
// };
// // check if a middleware is enabled
// const middlewareEnabled = key => {
// return (
// requiredMiddlewares.includes(key) ||
// get(middlewareConfig, ['settings', key, 'enabled'], false) === true
// );
// };
// // list of enabled middlewares
// const enabledMiddlewares = Object.keys(strapi.middleware).filter(middlewareEnabled);
// // Method to initialize middlewares and emit an event.
// const initialize = middlewareKey => {
// if (strapi.middleware[middlewareKey].loaded === true) return;
// const module = strapi.middleware[middlewareKey].load;
// return new Promise((resolve, reject) => {
// const timeout = setTimeout(
// () => reject(`(middleware: ${middlewareKey}) is taking too long to load.`),
// middlewareConfig.timeout || 1000
// );
// strapi.middleware[middlewareKey] = merge(strapi.middleware[middlewareKey], module);
// Promise.resolve()
// .then(() => module.initialize(strapi))
// .then(() => {
// clearTimeout(timeout);
// strapi.middleware[middlewareKey].loaded = true;
// resolve();
// })
// .catch(err => {
// clearTimeout(timeout);
// if (err) {
// return reject(err);
// }
// });
// });
// };
// /**
// * Run init functions
// */
// // Run beforeInitialize of every middleware
// await Promise.all(
// enabledMiddlewares.map(key => {
// const { beforeInitialize } = strapi.middleware[key].load;
// if (typeof beforeInitialize === 'function') {
// return beforeInitialize(strapi);
// }
// })
// );
// // run the initialization of an array of middlewares sequentially
// const initMiddlewaresSeq = async middlewareArr => {
// for (let key of uniq(middlewareArr)) {
// await initialize(key);
// }
// };
// const middlewaresBefore = get(middlewareConfig, 'load.before', [])
// .filter(middlewareExists)
// .filter(middlewareEnabled);
// const middlewaresAfter = get(middlewareConfig, 'load.after', [])
// .filter(middlewareExists)
// .filter(middlewareEnabled);
// const middlewaresOrder = get(middlewareConfig, 'load.order', [])
// .filter(middlewareExists)
// .filter(middlewareEnabled);
// const unspecifiedMiddlewares = difference(
// enabledMiddlewares,
// middlewaresBefore,
// middlewaresOrder,
// middlewaresAfter
// );
// // before
// await initMiddlewaresSeq(middlewaresBefore);
// // ordered // rest of middlewares
// await Promise.all([
// initMiddlewaresSeq(middlewaresOrder),
// Promise.all(unspecifiedMiddlewares.map(initialize)),
// ]);
// // after
// await initMiddlewaresSeq(middlewaresAfter);
// };

View File

@ -1,11 +0,0 @@
{
"parser": {
"enabled": true,
"multipart": true,
"queryStringParser": {
"strictNullHandling": true,
"arrayLimit": 100,
"depth": 20
}
}
}

View File

@ -1,75 +0,0 @@
'use strict';
const body = require('koa-body');
const qs = require('qs');
const { omit } = require('lodash');
/**
* Body parser hook
*/
const addQsParser = (app, settings) => {
Object.defineProperty(app.request, 'query', {
configurable: false,
enumerable: true,
/*
* Get parsed query-string.
*/
get() {
const qstr = this.querystring;
const cache = (this._querycache = this._querycache || {});
return cache[qstr] || (cache[qstr] = qs.parse(qstr, settings));
},
/*
* Set query-string as an object.
*/
set(obj) {
this.querystring = qs.stringify(obj);
},
});
return app;
};
module.exports = strapi => {
return {
/**
* Initialize the hook
*/
initialize() {
strapi.server.use(async (ctx, next) => {
// disable for graphql
// TODO: find a better way later
if (ctx.url === '/graphql') {
return next();
}
try {
const res = await body({
patchKoa: true,
...omit(strapi.config.middleware.settings.parser, 'queryStringParser'),
})(ctx, next);
return res;
} catch (e) {
if ((e || {}).message && e.message.includes('maxFileSize exceeded')) {
throw strapi.errors.entityTooLarge('FileTooBig', {
errors: [
{
id: 'Upload.status.sizeLimit',
message: `file is bigger than the limit size!`,
},
],
});
}
throw e;
}
});
addQsParser(
strapi.server.app,
strapi.config.get('middleware.settings.parser.queryStringParser')
);
},
};
};

View File

@ -18,6 +18,12 @@ const serveStatic = require('./serve-static');
*/
module.exports = strapi => {
return async (ctx, next) => {
return next();
}
return {
/**
* Initialize the hook

View File

@ -0,0 +1,73 @@
'use strict';
const { defaultsDeep } = require('lodash/fp');
const body = require('koa-body');
const qs = require('qs');
const defaults = {
multipart: true,
queryStringParser: {
strictNullHandling: true,
arrayLimit: 100,
depth: 20,
},
};
/**
* Body parser hook
*/
const addQsParser = (app, settings) => {
Object.defineProperty(app.request, 'query', {
configurable: false,
enumerable: true,
/*
* Get parsed query-string.
*/
get() {
const qstr = this.querystring;
const cache = (this._querycache = this._querycache || {});
return cache[qstr] || (cache[qstr] = qs.parse(qstr, settings));
},
/*
* Set query-string as an object.
*/
set(obj) {
this.querystring = qs.stringify(obj);
},
});
return app;
};
// TODO: inject strapi
module.exports = options => {
const { queryStringParser, ...bodyOptions } = defaultsDeep(defaults, options);
addQsParser(strapi.server.app, queryStringParser);
return async (ctx, next) => {
// disable for graphql
// TODO: find a better way later
if (ctx.url === '/graphql') {
return next();
}
try {
return body({ patchKoa: true, ...bodyOptions })(ctx, next);
} catch (e) {
if ((e || {}).message && e.message.includes('maxFileSize exceeded')) {
throw strapi.errors.entityTooLarge('FileTooBig', {
errors: [
{
id: 'parser.file.status.sizeLimit',
message: `file is bigger than the limit size!`,
},
],
});
}
throw e;
}
};
};

View File

@ -0,0 +1,16 @@
'use strict';
const { prop, isFunction } = require('lodash/fp');
module.exports = (options = {}) => {
return async (ctx, next) => {
await next();
const status = ctx.status;
const handler = prop(`handlers.${status}`, options);
if (isFunction(handler)) {
await handler(ctx);
}
};
};

View File

@ -0,0 +1,12 @@
'use strict';
module.exports = () => {
return async (ctx, next) => {
const start = Date.now();
await next();
const delta = Math.ceil(Date.now() - start);
ctx.set('X-Response-Time', delta + 'ms');
};
};

View File

@ -1,5 +0,0 @@
{
"responseTime": {
"enabled": true
}
}

View File

@ -1,25 +0,0 @@
'use strict';
/**
* X-Response-Time hook
*/
module.exports = strapi => {
return {
/**
* Initialize the hook
*/
initialize() {
strapi.server.use(async (ctx, next) => {
const start = Date.now();
await next();
const delta = Math.ceil(Date.now() - start);
ctx.set('X-Response-Time', delta + 'ms'); // eslint-disable-line prefer-template
});
},
};
};

View File

@ -1,5 +0,0 @@
{
"responses": {
"enabled": true
}
}

View File

@ -1,19 +0,0 @@
'use strict';
const _ = require('lodash');
module.exports = strapi => {
return {
initialize() {
strapi.server.use(async (ctx, next) => {
await next();
const status = ctx.status;
const responseFn = strapi.config.get(`middleware.settings.responses.handlers.${status}`);
if (_.isFunction(responseFn)) {
await responseFn(ctx);
}
});
},
};
};

View File

@ -1,7 +0,0 @@
{
"router": {
"enabled": true,
"prefix": "",
"routes": {}
}
}

View File

@ -7,6 +7,7 @@ const { createHTTPServer } = require('./http-server');
const { createRouteManager } = require('./routing');
const { createAdminAPI } = require('./admin-api');
const { createContentAPI } = require('./content-api');
const registerAllRoutes = require('./register-routes');
const healthCheck = async ctx => {
ctx.set('strapi', 'You are so French!');
@ -89,6 +90,10 @@ const createServer = strapi => {
return this;
},
initRouting() {
registerAllRoutes(strapi);
},
listen(...args) {
if (!state.mounted) {
this.mount();

View File

@ -19,7 +19,7 @@ const createRouteScopeGenerator = namespace => route => {
}
};
module.exports = strapi => {
module.exports = (server, strapi) => {
const registerAdminRoutes = () => {
const generateRouteScope = createRouteScopeGenerator(`admin::`);
@ -28,7 +28,7 @@ module.exports = strapi => {
route.info = { pluginName: 'admin' };
});
strapi.server.routes({
server.routes({
type: 'admin',
prefix: '/admin',
routes: strapi.admin.routes,
@ -47,7 +47,7 @@ module.exports = strapi => {
route.info = { pluginName };
});
strapi.server.routes({
server.routes({
type: 'admin',
prefix: `/${pluginName}`,
routes: plugin.routes,
@ -61,7 +61,7 @@ module.exports = strapi => {
route.info = { pluginName };
});
strapi.server.routes(router);
server.routes(router);
});
}
}
@ -82,7 +82,7 @@ module.exports = strapi => {
route.info = { apiName };
});
return strapi.server.routes(router);
return server.routes(router);
});
}
};

View File

@ -1,25 +0,0 @@
'use strict';
const getPrefixedDeps = require('../get-prefixed-dependencies');
describe('getPrefixedDeps', () => {
test('Returns a list of the dependencies and removes the prefix', () => {
expect(
getPrefixedDeps('test-prefix', {
dependencies: {
'test-prefix-packagename': '1',
},
})
).toEqual(['packagename']);
});
test('Ignores exact names', () => {
expect(
getPrefixedDeps('test-prefix', {
dependencies: {
'test-prefix': '1',
},
})
).toEqual([]);
});
});

View File

@ -1,7 +0,0 @@
'use strict';
module.exports = (prefix, pkgJSON) => {
return Object.keys(pkgJSON.dependencies)
.filter(d => d.startsWith(prefix) && d.length > prefix.length)
.map(pkgName => pkgName.substring(prefix.length + 1));
};