express: Move static handlers to expressPreSession
This avoids the need to exempt the paths from authentication checks, and it eliminates unnecessary express-session state.
This commit is contained in:
parent
72f4ae444d
commit
649fbdccf5
10 changed files with 65 additions and 85 deletions
|
@ -8,6 +8,9 @@
|
||||||
* `padOptions.showChat`
|
* `padOptions.showChat`
|
||||||
* `padOptions.userColor`
|
* `padOptions.userColor`
|
||||||
* `padOptions.userName`
|
* `padOptions.userName`
|
||||||
|
* Requests for static content (e.g., `/robots.txt`) and special pages (e.g., the
|
||||||
|
HTTP API, `/stats`) no longer cause the server to generate database records
|
||||||
|
intended to manage browser sessions (`sessionstorage:*`).
|
||||||
* Fixed the return value of the `getText` HTTP API when called with a specific
|
* Fixed the return value of the `getText` HTTP API when called with a specific
|
||||||
revision.
|
revision.
|
||||||
* Fixed a potential attribute pool corruption bug with `copyPadWithoutHistory`.
|
* Fixed a potential attribute pool corruption bug with `copyPadWithoutHistory`.
|
||||||
|
|
13
src/ep.json
13
src/ep.json
|
@ -23,7 +23,7 @@
|
||||||
{
|
{
|
||||||
"name": "static",
|
"name": "static",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/static"
|
"expressPreSession": "ep_etherpad-lite/node/hooks/express/static"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -35,13 +35,14 @@
|
||||||
{
|
{
|
||||||
"name": "i18n",
|
"name": "i18n",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/i18n"
|
"expressPreSession": "ep_etherpad-lite/node/hooks/i18n"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "specialpages",
|
"name": "specialpages",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages"
|
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages",
|
||||||
|
"expressPreSession": "ep_etherpad-lite/node/hooks/express/specialpages"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -53,7 +54,7 @@
|
||||||
{
|
{
|
||||||
"name": "apicalls",
|
"name": "apicalls",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/apicalls"
|
"expressPreSession": "ep_etherpad-lite/node/hooks/express/apicalls"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -79,7 +80,7 @@
|
||||||
{
|
{
|
||||||
"name": "tests",
|
"name": "tests",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/tests"
|
"expressPreSession": "ep_etherpad-lite/node/hooks/express/tests"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -105,7 +106,7 @@
|
||||||
{
|
{
|
||||||
"name": "openapi",
|
"name": "openapi",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/openapi"
|
"expressPreSession": "ep_etherpad-lite/node/hooks/express/openapi"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,9 +6,9 @@ const formidable = require('formidable');
|
||||||
const apiHandler = require('../../handler/APIHandler');
|
const apiHandler = require('../../handler/APIHandler');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
|
|
||||||
exports.expressCreateServer = (hookName, args, cb) => {
|
exports.expressPreSession = async (hookName, {app}) => {
|
||||||
// The Etherpad client side sends information about how a disconnect happened
|
// The Etherpad client side sends information about how a disconnect happened
|
||||||
args.app.post('/ep/pad/connection-diagnostic-info', (req, res) => {
|
app.post('/ep/pad/connection-diagnostic-info', (req, res) => {
|
||||||
new formidable.IncomingForm().parse(req, (err, fields, files) => {
|
new formidable.IncomingForm().parse(req, (err, fields, files) => {
|
||||||
clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`);
|
clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`);
|
||||||
res.end('OK');
|
res.end('OK');
|
||||||
|
@ -23,7 +23,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// The Etherpad client side sends information about client side javscript errors
|
// The Etherpad client side sends information about client side javscript errors
|
||||||
args.app.post('/jserror', (req, res, next) => {
|
app.post('/jserror', (req, res, next) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const data = JSON.parse(await parseJserrorForm(req));
|
const data = JSON.parse(await parseJserrorForm(req));
|
||||||
clientLogger.warn(`${data.msg} --`, {
|
clientLogger.warn(`${data.msg} --`, {
|
||||||
|
@ -38,9 +38,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Provide a possibility to query the latest available API version
|
// Provide a possibility to query the latest available API version
|
||||||
args.app.get('/api', (req, res) => {
|
app.get('/api', (req, res) => {
|
||||||
res.json({currentVersion: apiHandler.latestApiVersion});
|
res.json({currentVersion: apiHandler.latestApiVersion});
|
||||||
});
|
});
|
||||||
|
|
||||||
return cb();
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -540,9 +540,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
|
||||||
return definition;
|
return definition;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.expressCreateServer = (hookName, args, cb) => {
|
exports.expressPreSession = async (hookName, {app}) => {
|
||||||
const {app} = args;
|
|
||||||
|
|
||||||
// create openapi-backend handlers for each api version under /api/{version}/*
|
// create openapi-backend handlers for each api version under /api/{version}/*
|
||||||
for (const version of Object.keys(apiHandler.version)) {
|
for (const version of Object.keys(apiHandler.version)) {
|
||||||
// we support two different styles of api: flat + rest
|
// we support two different styles of api: flat + rest
|
||||||
|
@ -690,7 +688,6 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cb();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// helper to get api root
|
// helper to get api root
|
||||||
|
|
|
@ -10,25 +10,16 @@ const settings = require('../../utils/Settings');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const webaccess = require('./webaccess');
|
const webaccess = require('./webaccess');
|
||||||
|
|
||||||
exports.expressCreateServer = (hookName, args, cb) => {
|
exports.expressPreSession = async (hookName, {app}) => {
|
||||||
// expose current stats
|
app.get('/stats', (req, res) => {
|
||||||
args.app.get('/stats', (req, res) => {
|
|
||||||
res.json(require('../../stats').toJSON());
|
res.json(require('../../stats').toJSON());
|
||||||
});
|
});
|
||||||
|
|
||||||
// serve index.html under /
|
app.get('/javascript', (req, res) => {
|
||||||
args.app.get('/', (req, res) => {
|
|
||||||
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req}));
|
|
||||||
});
|
|
||||||
|
|
||||||
// serve javascript.html
|
|
||||||
args.app.get('/javascript', (req, res) => {
|
|
||||||
res.send(eejs.require('ep_etherpad-lite/templates/javascript.html', {req}));
|
res.send(eejs.require('ep_etherpad-lite/templates/javascript.html', {req}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/robots.txt', (req, res) => {
|
||||||
// serve robots.txt
|
|
||||||
args.app.get('/robots.txt', (req, res) => {
|
|
||||||
let filePath = path.join(
|
let filePath = path.join(
|
||||||
settings.root,
|
settings.root,
|
||||||
'src',
|
'src',
|
||||||
|
@ -46,6 +37,34 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/favicon.ico', (req, res, next) => {
|
||||||
|
(async () => {
|
||||||
|
const fns = [
|
||||||
|
...(settings.favicon ? [path.resolve(settings.root, settings.favicon)] : []),
|
||||||
|
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'),
|
||||||
|
path.join(settings.root, 'src', 'static', 'favicon.ico'),
|
||||||
|
];
|
||||||
|
for (const fn of fns) {
|
||||||
|
try {
|
||||||
|
await fsp.access(fn, fs.constants.R_OK);
|
||||||
|
} catch (err) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
||||||
|
await util.promisify(res.sendFile.bind(res))(fn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
})().catch((err) => next(err || new Error(err)));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.expressCreateServer = (hookName, args, cb) => {
|
||||||
|
// serve index.html under /
|
||||||
|
args.app.get('/', (req, res) => {
|
||||||
|
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req}));
|
||||||
|
});
|
||||||
|
|
||||||
// serve pad.html under /p
|
// serve pad.html under /p
|
||||||
args.app.get('/p/:pad', (req, res, next) => {
|
args.app.get('/p/:pad', (req, res, next) => {
|
||||||
// The below might break for pads being rewritten
|
// The below might break for pads being rewritten
|
||||||
|
@ -77,26 +96,5 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
args.app.get('/favicon.ico', (req, res, next) => {
|
|
||||||
(async () => {
|
|
||||||
const fns = [
|
|
||||||
...(settings.favicon ? [path.resolve(settings.root, settings.favicon)] : []),
|
|
||||||
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'),
|
|
||||||
path.join(settings.root, 'src', 'static', 'favicon.ico'),
|
|
||||||
];
|
|
||||||
for (const fn of fns) {
|
|
||||||
try {
|
|
||||||
await fsp.access(fn, fs.constants.R_OK);
|
|
||||||
} catch (err) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
|
||||||
await util.promisify(res.sendFile.bind(res))(fn);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
})().catch((err) => next(err || new Error(err)));
|
|
||||||
});
|
|
||||||
|
|
||||||
return cb();
|
return cb();
|
||||||
};
|
};
|
||||||
|
|
|
@ -28,14 +28,14 @@ const getTar = async () => {
|
||||||
return tar;
|
return tar;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.expressCreateServer = async (hookName, args) => {
|
exports.expressPreSession = async (hookName, {app}) => {
|
||||||
// Cache both minified and static.
|
// Cache both minified and static.
|
||||||
const assetCache = new CachingMiddleware();
|
const assetCache = new CachingMiddleware();
|
||||||
args.app.all(/\/javascripts\/(.*)/, assetCache.handle.bind(assetCache));
|
app.all(/\/javascripts\/(.*)/, assetCache.handle.bind(assetCache));
|
||||||
|
|
||||||
// Minify will serve static files compressed (minify enabled). It also has
|
// Minify will serve static files compressed (minify enabled). It also has
|
||||||
// file-specific hacks for ace/require-kernel/etc.
|
// file-specific hacks for ace/require-kernel/etc.
|
||||||
args.app.all('/static/:filename(*)', minify.minify);
|
app.all('/static/:filename(*)', minify.minify);
|
||||||
|
|
||||||
// Setup middleware that will package JavaScript files served by minify for
|
// Setup middleware that will package JavaScript files served by minify for
|
||||||
// CommonJS loader on the client-side.
|
// CommonJS loader on the client-side.
|
||||||
|
@ -53,12 +53,12 @@ exports.expressCreateServer = async (hookName, args) => {
|
||||||
const associator = new StaticAssociator(associations);
|
const associator = new StaticAssociator(associations);
|
||||||
jsServer.setAssociator(associator);
|
jsServer.setAssociator(associator);
|
||||||
|
|
||||||
args.app.use(jsServer.handle.bind(jsServer));
|
app.use(jsServer.handle.bind(jsServer));
|
||||||
|
|
||||||
// serve plugin definitions
|
// serve plugin definitions
|
||||||
// not very static, but served here so that client can do
|
// not very static, but served here so that client can do
|
||||||
// require("pluginfw/static/js/plugin-definitions.js");
|
// require("pluginfw/static/js/plugin-definitions.js");
|
||||||
args.app.get('/pluginfw/plugin-definitions.json', (req, res, next) => {
|
app.get('/pluginfw/plugin-definitions.json', (req, res, next) => {
|
||||||
const clientParts = plugins.parts.filter((part) => part.client_hooks != null);
|
const clientParts = plugins.parts.filter((part) => part.client_hooks != null);
|
||||||
const clientPlugins = {};
|
const clientPlugins = {};
|
||||||
for (const name of new Set(clientParts.map((part) => part.plugin))) {
|
for (const name of new Set(clientParts.map((part) => part.plugin))) {
|
||||||
|
|
|
@ -29,8 +29,8 @@ const findSpecs = async (specDir) => {
|
||||||
return specs;
|
return specs;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.expressCreateServer = (hookName, args, cb) => {
|
exports.expressPreSession = async (hookName, {app}) => {
|
||||||
args.app.get('/tests/frontend/frontendTestSpecs.json', (req, res, next) => {
|
app.get('/tests/frontend/frontendTestSpecs.json', (req, res, next) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const modules = [];
|
const modules = [];
|
||||||
await Promise.all(Object.entries(plugins.plugins).map(async ([plugin, def]) => {
|
await Promise.all(Object.entries(plugins.plugins).map(async ([plugin, def]) => {
|
||||||
|
@ -59,14 +59,14 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
||||||
|
|
||||||
const rootTestFolder = path.join(settings.root, 'src/tests/frontend/');
|
const rootTestFolder = path.join(settings.root, 'src/tests/frontend/');
|
||||||
|
|
||||||
args.app.get('/tests/frontend/index.html', (req, res) => {
|
app.get('/tests/frontend/index.html', (req, res) => {
|
||||||
res.redirect(['./', ...req.url.split('?').slice(1)].join('?'));
|
res.redirect(['./', ...req.url.split('?').slice(1)].join('?'));
|
||||||
});
|
});
|
||||||
|
|
||||||
// The regexp /[\d\D]{0,}/ is equivalent to the regexp /.*/. The Express route path used here
|
// The regexp /[\d\D]{0,}/ is equivalent to the regexp /.*/. The Express route path used here
|
||||||
// uses the more verbose /[\d\D]{0,}/ pattern instead of /.*/ because path-to-regexp v0.1.7 (the
|
// uses the more verbose /[\d\D]{0,}/ pattern instead of /.*/ because path-to-regexp v0.1.7 (the
|
||||||
// version used with Express v4.x) interprets '.' and '*' differently than regexp.
|
// version used with Express v4.x) interprets '.' and '*' differently than regexp.
|
||||||
args.app.get('/tests/frontend/:file([\\d\\D]{0,})', (req, res, next) => {
|
app.get('/tests/frontend/:file([\\d\\D]{0,})', (req, res, next) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
let file = sanitizePathname(req.params.file);
|
let file = sanitizePathname(req.params.file);
|
||||||
if (['', '.', './'].includes(file)) file = 'index.html';
|
if (['', '.', './'].includes(file)) file = 'index.html';
|
||||||
|
@ -74,9 +74,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
||||||
})().catch((err) => next(err || new Error(err)));
|
})().catch((err) => next(err || new Error(err)));
|
||||||
});
|
});
|
||||||
|
|
||||||
args.app.get('/tests/frontend', (req, res) => {
|
app.get('/tests/frontend', (req, res) => {
|
||||||
res.redirect(['./frontend/', ...req.url.split('?').slice(1)].join('?'));
|
res.redirect(['./frontend/', ...req.url.split('?').slice(1)].join('?'));
|
||||||
});
|
});
|
||||||
|
|
||||||
return cb();
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,23 +9,6 @@ const readOnlyManager = require('../../db/ReadOnlyManager');
|
||||||
|
|
||||||
hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
|
hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
|
||||||
|
|
||||||
const staticPathsRE = new RegExp(`^/(?:${[
|
|
||||||
'api(?:/.*)?',
|
|
||||||
'favicon\\.ico',
|
|
||||||
'ep/pad/connection-diagnostic-info',
|
|
||||||
'javascript',
|
|
||||||
'javascripts/.*',
|
|
||||||
'jserror/?',
|
|
||||||
'locales\\.json',
|
|
||||||
'locales/.*',
|
|
||||||
'rest/.*',
|
|
||||||
'pluginfw/.*',
|
|
||||||
'robots.txt',
|
|
||||||
'static/.*',
|
|
||||||
'stats/?',
|
|
||||||
'tests/frontend(?:/.*)?',
|
|
||||||
].join('|')})$`);
|
|
||||||
|
|
||||||
// Promisified wrapper around hooks.aCallFirst.
|
// Promisified wrapper around hooks.aCallFirst.
|
||||||
const aCallFirst = (hookName, context, pred = null) => new Promise((resolve, reject) => {
|
const aCallFirst = (hookName, context, pred = null) => new Promise((resolve, reject) => {
|
||||||
hooks.aCallFirst(hookName, context, (err, r) => err != null ? reject(err) : resolve(r), pred);
|
hooks.aCallFirst(hookName, context, (err, r) => err != null ? reject(err) : resolve(r), pred);
|
||||||
|
@ -90,7 +73,6 @@ const preAuthorize = async (req, res, next) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (locals.skip) return;
|
if (locals.skip) return;
|
||||||
if (staticPathsRE.test(req.path)) results.push(true);
|
|
||||||
if (requireAdmin) {
|
if (requireAdmin) {
|
||||||
// Filter out all 'true' entries to prevent plugin authors from accidentally granting admin
|
// Filter out all 'true' entries to prevent plugin authors from accidentally granting admin
|
||||||
// privileges to the general public.
|
// privileges to the general public.
|
||||||
|
|
|
@ -100,13 +100,13 @@ const generateLocaleIndex = (locales) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
exports.expressCreateServer = (n, args, cb) => {
|
exports.expressPreSession = async (hookName, {app}) => {
|
||||||
// regenerate locales on server restart
|
// regenerate locales on server restart
|
||||||
const locales = getAllLocales();
|
const locales = getAllLocales();
|
||||||
const localeIndex = generateLocaleIndex(locales);
|
const localeIndex = generateLocaleIndex(locales);
|
||||||
exports.availableLangs = getAvailableLangs(locales);
|
exports.availableLangs = getAvailableLangs(locales);
|
||||||
|
|
||||||
args.app.get('/locales/:locale', (req, res) => {
|
app.get('/locales/:locale', (req, res) => {
|
||||||
// works with /locale/en and /locale/en.json requests
|
// works with /locale/en and /locale/en.json requests
|
||||||
const locale = req.params.locale.split('.')[0];
|
const locale = req.params.locale.split('.')[0];
|
||||||
if (Object.prototype.hasOwnProperty.call(exports.availableLangs, locale)) {
|
if (Object.prototype.hasOwnProperty.call(exports.availableLangs, locale)) {
|
||||||
|
@ -118,11 +118,9 @@ exports.expressCreateServer = (n, args, cb) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
args.app.get('/locales.json', (req, res) => {
|
app.get('/locales.json', (req, res) => {
|
||||||
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
||||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
res.send(localeIndex);
|
res.send(localeIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
return cb();
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -77,6 +77,11 @@ describe(__filename, function () {
|
||||||
settings.requireAuthorization = false;
|
settings.requireAuthorization = false;
|
||||||
await agent.get('/admin/').auth('admin', 'admin-password').expect(200);
|
await agent.get('/admin/').auth('admin', 'admin-password').expect(200);
|
||||||
});
|
});
|
||||||
|
it('authn authz anonymous /robots.txt -> 200', async function () {
|
||||||
|
settings.requireAuthentication = true;
|
||||||
|
settings.requireAuthorization = true;
|
||||||
|
await agent.get('/robots.txt').expect(200);
|
||||||
|
});
|
||||||
it('authn authz user / -> 403', async function () {
|
it('authn authz user / -> 403', async function () {
|
||||||
settings.requireAuthentication = true;
|
settings.requireAuthentication = true;
|
||||||
settings.requireAuthorization = true;
|
settings.requireAuthorization = true;
|
||||||
|
|
Loading…
Reference in a new issue