Start server without paths.

feature/typescript
SamTV12345 2023-06-25 16:49:17 +02:00
parent 798543fb45
commit 8926677a66
No known key found for this signature in database
GPG Key ID: E63EEC7466038043
13 changed files with 2657 additions and 74 deletions

3
src/.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env"]
}

View File

@ -32,4 +32,4 @@ src/bin/installDeps.sh "$@" || exit 1
# Move to the node folder and start # Move to the node folder and start
log "Starting Etherpad..." log "Starting Etherpad..."
exec node src/node/server.js "$@" exec node src/dist/node/server.js "$@"

View File

@ -109,11 +109,15 @@ export const createServer = async () => {
'variable to production by using: export NODE_ENV=production'); 'variable to production by using: export NODE_ENV=production');
} }
}; };
export const app = express();
import http from 'http'
import https from 'https'
export const restartServer = async () => { export const restartServer = async () => {
await closeServer(); await closeServer();
const app = express(); // New syntax for express v3 // New syntax for express v3
if (ssl) { if (ssl) {
console.log('SSL -- enabled'); console.log('SSL -- enabled');
@ -133,11 +137,8 @@ export const restartServer = async () => {
options.ca.push(fs.readFileSync(caFileName)); options.ca.push(fs.readFileSync(caFileName));
} }
} }
const https = require('https');
server = https.createServer(options, app); server = https.createServer(options, app);
} else { } else {
const http = require('http');
server = http.createServer(app); server = http.createServer(app);
} }

View File

@ -44,5 +44,5 @@ export const expressPreSession = async (hookName, {app}) => {
// Provide a possibility to query the latest available API version // Provide a possibility to query the latest available API version
app.get('/api', (req, res) => { app.get('/api', (req, res) => {
res.json({currentVersion: latestApiVersion}); res.json({currentVersion: latestApiVersion});
}); })
}; }

View File

@ -51,7 +51,7 @@ export const userCanModify = (padId, req) => {
}; };
// Exported so that tests can set this to 0 to avoid unnecessary test slowness. // Exported so that tests can set this to 0 to avoid unnecessary test slowness.
export const authnFailureDelayMs = 1000; export let authnFailureDelayMs = 1000;
export const checkAccess = async (req, res, next) => { export const checkAccess = async (req, res, next) => {
const requireAdmin = req.path.toLowerCase().startsWith('/admin'); const requireAdmin = req.path.toLowerCase().startsWith('/admin');
@ -204,3 +204,8 @@ export const checkAccess = async (req, res, next) => {
export const checkAccess2 = (req, res, next) => { export const checkAccess2 = (req, res, next) => {
checkAccess(req, res, next).catch((err) => next(err || new Error(err))); checkAccess(req, res, next).catch((err) => next(err || new Error(err)));
}; };
// Setters
export const setauthnFailureDelayMs = (value) => {
authnFailureDelayMs = value;
}

View File

@ -51,9 +51,7 @@ import {createServer, server} from './hooks/express';
import hooks = require('../static/js/pluginfw/hooks'); import hooks = require('../static/js/pluginfw/hooks');
import pluginDefs = require('../static/js/pluginfw/plugin_defs'); import pluginDefs = require('../static/js/pluginfw/plugin_defs');
import plugins = require('../static/js/pluginfw/plugins'); import plugins = require('../static/js/pluginfw/plugins');
import stats = require('./stats');
import {createCollection} from "./stats"; import {createCollection} from "./stats";
const logger = log4js.getLogger('server'); const logger = log4js.getLogger('server');
console.log = logger.info.bind(logger); // do the same for others - console.debug, etc. console.log = logger.info.bind(logger); // do the same for others - console.debug, etc.
@ -152,7 +150,7 @@ export const start = async () => {
logger.debug(`Installed parts:\n${plugins.formatParts()}`); logger.debug(`Installed parts:\n${plugins.formatParts()}`);
logger.debug(`Installed server-side hooks:\n${plugins.formatHooks('hooks', false)}`); logger.debug(`Installed server-side hooks:\n${plugins.formatHooks('hooks', false)}`);
await hooks.aCallAll('loadSettings', {settings}); await hooks.aCallAll('loadSettings', {settings});
await hooks.aCallAll(createServer()) await hooks.aCallAll(createServer());
} catch (err) { } catch (err) {
logger.error('Error occurred while starting Etherpad'); logger.error('Error occurred while starting Etherpad');
state = State.STATE_TRANSITION_FAILED; state = State.STATE_TRANSITION_FAILED;

View File

@ -109,12 +109,12 @@ export const skinVariants = 'super-light-toolbar super-light-editor light-backgr
/** /**
* The IP ep-lite should listen to * The IP ep-lite should listen to
*/ */
export const ip:String = '0.0.0.0'; export let ip:String = '0.0.0.0';
/** /**
* The Port ep-lite should listen to * The Port ep-lite should listen to
*/ */
export const port = process.env.PORT || 9001; export let port = process.env.PORT || 9001;
/** /**
* Should we suppress Error messages from being in Pad Contents * Should we suppress Error messages from being in Pad Contents
@ -125,7 +125,7 @@ export const suppressErrorsInPadText = false;
* The SSL signed server key and the Certificate Authority's own certificate * The SSL signed server key and the Certificate Authority's own certificate
* default case: ep-lite does *not* use SSL. A signed server key is not required in this case. * default case: ep-lite does *not* use SSL. A signed server key is not required in this case.
*/ */
export const ssl = false; export let ssl = false;
export const sslKeys = { export const sslKeys = {
cert: undefined, cert: undefined,
@ -319,7 +319,7 @@ export let sessionKey: string|boolean = false;
/* /*
* Trust Proxy, whether or not trust the x-forwarded-for header. * Trust Proxy, whether or not trust the x-forwarded-for header.
*/ */
export const trustProxy = false; export let trustProxy = false;
/* /*
* Settings controlling the session cookie issued by Etherpad. * Settings controlling the session cookie issued by Etherpad.
@ -412,7 +412,7 @@ export const setUsers = (newUsers:any) => {
* *
* See https://github.com/nfriedly/express-rate-limit for more options * See https://github.com/nfriedly/express-rate-limit for more options
*/ */
export const importExportRateLimiting = { export let importExportRateLimiting = {
// duration of the rate limit window (milliseconds) // duration of the rate limit window (milliseconds)
windowMs: 90000, windowMs: 90000,
@ -429,7 +429,7 @@ export const importExportRateLimiting = {
* *
* See https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#websocket-single-connection-prevent-flooding for more options * See https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#websocket-single-connection-prevent-flooding for more options
*/ */
export const commitRateLimiting = { export let commitRateLimiting = {
// duration of the rate limit window (seconds) // duration of the rate limit window (seconds)
duration: 1, duration: 1,
@ -884,3 +884,27 @@ export const exportedForTestingOnly = {
reloadSettings(); reloadSettings();
// Setters
export const setPort = (value: number) => {
port = value;
}
export const setIp = (value: string) => {
ip = value;
}
export const setTrustProxy = (value: boolean) => {
trustProxy = value;
}
export const setSsl = (value: boolean) => {
ssl = value;
}
export const setimportExportRateLimiting = (value: any) => {
importExportRateLimiting = value;
}
export const setCommitRateLimiting = (value: any) => {
commitRateLimiting = value;
}

2568
src/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -78,18 +78,22 @@
"etherpad-lite": "node/server.js" "etherpad-lite": "node/server.js"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.22.5",
"@babel/core": "^7.22.5",
"@babel/preset-env": "^7.22.5",
"@babel/register": "^7.22.5",
"@types/cross-spawn": "^6.0.2", "@types/cross-spawn": "^6.0.2",
"@types/express": "4.17.17", "@types/express": "4.17.17",
"@types/jquery": "^3.5.16", "@types/jquery": "^3.5.16",
"@types/js-cookie": "^3.0.3", "@types/js-cookie": "^3.0.3",
"i18next": "^23.2.3",
"i18next-fs-backend": "^2.1.5",
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"@types/underscore": "^1.11.5", "@types/underscore": "^1.11.5",
"concurrently": "^8.2.0", "concurrently": "^8.2.0",
"eslint": "^8.14.0", "eslint": "^8.14.0",
"eslint-config-etherpad": "^3.0.13", "eslint-config-etherpad": "^3.0.13",
"etherpad-cli-client": "^2.0.1", "etherpad-cli-client": "^2.0.1",
"i18next": "^23.2.3",
"i18next-fs-backend": "^2.1.5",
"mocha": "^9.2.2", "mocha": "^9.2.2",
"mocha-froth": "^0.2.10", "mocha-froth": "^0.2.10",
"nodeify": "^1.0.1", "nodeify": "^1.0.1",
@ -112,7 +116,7 @@
}, },
"scripts": { "scripts": {
"lint": "eslint .", "lint": "eslint .",
"test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", "test": "mocha --require @babel/register --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
"test-container": "mocha --timeout 5000 tests/container/specs/api", "test-container": "mocha --timeout 5000 tests/container/specs/api",
"dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/server.js\"" "dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/server.js\""
}, },

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
import {hooks} from './plugin_defs'; import {hooks} from './plugin_defs';
import {getLogger} from "log4js";
// Maps the name of a server-side hook to a string explaining the deprecation // Maps the name of a server-side hook to a string explaining the deprecation
// (e.g., 'use the foo hook instead'). // (e.g., 'use the foo hook instead').
@ -347,10 +348,15 @@ const callHookFnAsync = async (hook, context) => {
// If cb is non-null, this function resolves to the value returned by cb. // If cb is non-null, this function resolves to the value returned by cb.
export const aCallAll = async (hookName, context?, cb = null) => { export const aCallAll = async (hookName, context?, cb = null) => {
if (cb != null) return await attachCallback(aCallAll(hookName, context), cb); if (cb != null) return await attachCallback(aCallAll(hookName, context), cb);
if (context == null) context = {}; if (context == null) {
context = {};
}
const hooksResult = hooks[hookName] || []; const hooksResult = hooks[hookName] || [];
const results = await Promise.all( const results = await Promise.all(
hooksResult.map(async (hook) => normalizeValue(await callHookFnAsync(hook, context)))); hooksResult.map(async (hook) => {
getLogger().info(`Calling hook ${hook.hook_name} asynchronously`);
return normalizeValue(await callHookFnAsync(hook, context))
}));
return flatten1(results); return flatten1(results);
}; };

View File

@ -105,7 +105,6 @@ export const update = async () => {
})); }));
logger.info(`Loaded ${Object.keys(packages).length} plugins`); logger.info(`Loaded ${Object.keys(packages).length} plugins`);
logger.info(parts)
setPlugins(plugins); setPlugins(plugins);
setParts(sortParts(parts)) setParts(sortParts(parts))
setHooks(extractHooks(parts, 'hooks', pathNormalization)); setHooks(extractHooks(parts, 'hooks', pathNormalization));
@ -129,12 +128,13 @@ const getPackages = async () => {
logger.info("After exportCMD") logger.info("After exportCMD")
const {dependencies = {}} = JSON.parse(cmdReturn as string); const {dependencies = {}} = JSON.parse(cmdReturn as string);
await Promise.all(Object.entries(dependencies).map(async ([pkg, info]) => { await Promise.all(Object.entries(dependencies).map(async ([pkg, info]) => {
logger.info(`Found plugin ${pkg}`)
if (!pkg.startsWith(prefix)) { if (!pkg.startsWith(prefix)) {
delete dependencies[pkg]; delete dependencies[pkg];
return; return;
} }
const mappedInfo = info as PluginInfo const mappedInfo = info as PluginInfo
logger.info(`Found plugin ${pkg} at ${mappedInfo.path}`)
mappedInfo.realPath = await fs.realpath(mappedInfo.path); mappedInfo.realPath = await fs.realpath(mappedInfo.path);
})); }));
return dependencies; return dependencies;

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
import {parts} from './plugin_defs'; import {parts} from './plugin_defs';
import {getLogger} from "log4js";
const disabledHookReasons = { const disabledHookReasons = {
hooks: { hooks: {
@ -35,6 +36,7 @@ export const loadFn = (path, hookName) => {
export const extractHooks = (parts: any[], hookSetName, normalizer) => { export const extractHooks = (parts: any[], hookSetName, normalizer) => {
const hooks = {}; const hooks = {};
const logger = getLogger('pluginfw:shared')
for (const part of parts) { for (const part of parts) {
for (const [hookName, regHookFnName] of Object.entries(part[hookSetName] || {})) { for (const [hookName, regHookFnName] of Object.entries(part[hookSetName] || {})) {
/* On the server side, you can't just /* On the server side, you can't just
@ -45,10 +47,10 @@ export const extractHooks = (parts: any[], hookSetName, normalizer) => {
const disabledReason = (disabledHookReasons[hookSetName] || {})[hookName]; const disabledReason = (disabledHookReasons[hookSetName] || {})[hookName];
if (disabledReason) { if (disabledReason) {
console.error(`Hook ${hookSetName}/${hookName} is disabled. Reason: ${disabledReason}`); logger.error(`Hook ${hookSetName}/${hookName} is disabled. Reason: ${disabledReason}`);
console.error(`The hook function ${hookFnName} from plugin ${part.plugin} ` + logger.error(`The hook function ${hookFnName} from plugin ${part.plugin} ` +
'will never be called, which may cause the plugin to fail'); 'will never be called, which may cause the plugin to fail');
console.error(`Please update the ${part.plugin} plugin to not use the ${hookName} hook`); logger.error(`Please update the ${part.plugin} plugin to not use the ${hookName} hook`);
return; return;
} }
let hookFn; let hookFn;
@ -56,7 +58,7 @@ export const extractHooks = (parts: any[], hookSetName, normalizer) => {
hookFn = loadFn(hookFnName, hookName); hookFn = loadFn(hookFnName, hookName);
if (!hookFn) throw new Error('Not a function'); if (!hookFn) throw new Error('Not a function');
} catch (err) { } catch (err) {
console.error(`Failed to load hook function "${hookFnName}" for plugin "${part.plugin}" ` + logger.error(`Failed to load hook function "${hookFnName}" for plugin "${part.plugin}" ` +
`part "${part.name}" hook set "${hookSetName}" hook "${hookName}": ` + `part "${part.name}" hook set "${hookSetName}" hook "${hookName}": ` +
`${err.stack || err}`); `${err.stack || err}`);
} }

View File

@ -1,22 +1,28 @@
'use strict'; 'use strict';
const AttributePool = require('../../static/js/AttributePool'); import {AttributePool} from '../../static/js/AttributePool';
const apiHandler = require('../../node/handler/APIHandler'); import {exportedForTestingOnly as aExportedForTestingOnly} from '../../node/handler/APIHandler';
const assert = require('assert').strict; import assert, {strict} from 'assert'
const io = require('socket.io-client'); import io from 'socket.io-client';
const log4js = require('log4js'); import log4js from 'log4js';
const {padutils} = require('../../static/js/pad_utils'); import {padutils} from '../../static/js/pad_utils';
const process = require('process'); import processA from 'process';
const server = require('../../node/server'); import {} from '../../node/server';
const setCookieParser = require('set-cookie-parser'); import setCookieParser from 'set-cookie-parser';
const settings = require('../../node/utils/Settings'); import {setCommitRateLimiting, setimportExportRateLimiting, setIp, setPort} from '../../node/utils/Settings';
const supertest = require('supertest'); import supertest from 'supertest';
const webaccess = require('../../node/hooks/express/webaccess'); import {authnFailureDelayMs, setauthnFailureDelayMs} from '../../node/hooks/express/webaccess';
import { before,after } from 'mocha';
import * as settings from '../../node/utils/Settings';
import {server} from "../../node/hooks/express";
const backups = {}; const backups:{
settings?:any,
authnFailureDelayMs?:any
} = {};
let agentPromise = null; let agentPromise = null;
exports.apiKey = apiHandler.exportedForTestingOnly.apiKey; exports.apiKey = aExportedForTestingOnly.apiKey;
exports.agent = null; exports.agent = null;
exports.baseUrl = null; exports.baseUrl = null;
exports.httpServer = null; exports.httpServer = null;
@ -27,7 +33,7 @@ const logLevel = logger.level;
// Mocha doesn't monitor unhandled Promise rejections, so convert them to uncaught exceptions. // Mocha doesn't monitor unhandled Promise rejections, so convert them to uncaught exceptions.
// https://github.com/mochajs/mocha/issues/2640 // https://github.com/mochajs/mocha/issues/2640
process.on('unhandledRejection', (reason, promise) => { throw reason; }); processA.on('unhandledRejection', (reason, promise) => { throw reason; });
before(async function () { before(async function () {
this.timeout(60000); this.timeout(60000);
@ -42,31 +48,29 @@ exports.init = async function () {
if (!logLevel.isLessThanOrEqualTo(log4js.levels.DEBUG)) { if (!logLevel.isLessThanOrEqualTo(log4js.levels.DEBUG)) {
logger.warn('Disabling non-test logging for the duration of the test. ' + logger.warn('Disabling non-test logging for the duration of the test. ' +
'To enable non-test logging, change the loglevel setting to DEBUG.'); 'To enable non-test logging, change the loglevel setting to DEBUG.');
log4js.setGlobalLogLevel(log4js.levels.OFF);
logger.setLevel(logLevel); logger.setLevel(logLevel);
} }
// Note: This is only a shallow backup. // Note: This is only a shallow backup.
backups.settings = Object.assign({}, settings); backups.settings = Object.assign({}, settings);
// Start the Etherpad server on a random unused port. // Start the Etherpad server on a random unused port.
settings.port = 0; setPort(0)
settings.ip = 'localhost'; setIp('localhost')
settings.importExportRateLimiting = {max: 0}; setimportExportRateLimiting({max: 0})
settings.commitRateLimiting = {duration: 0.001, points: 1e6}; setCommitRateLimiting({duration: 0.001, points: 1e6});
exports.httpServer = await server.start(); exports.httpServer = await server.start();
exports.baseUrl = `http://localhost:${exports.httpServer.address().port}`; exports.baseUrl = `http://localhost:${exports.httpServer.address().port}`;
logger.debug(`HTTP server at ${exports.baseUrl}`); logger.debug(`HTTP server at ${exports.baseUrl}`);
// Create a supertest user agent for the HTTP server. // Create a supertest user agent for the HTTP server.
exports.agent = supertest(exports.baseUrl); exports.agent = supertest(exports.baseUrl);
// Speed up authn tests. // Speed up authn tests.
backups.authnFailureDelayMs = webaccess.authnFailureDelayMs; backups.authnFailureDelayMs = authnFailureDelayMs;
webaccess.authnFailureDelayMs = 0; setauthnFailureDelayMs(0)
after(async function () { after(async function () {
webaccess.authnFailureDelayMs = backups.authnFailureDelayMs; setauthnFailureDelayMs(backups.authnFailureDelayMs);
// Note: This does not unset settings that were added. // Note: This does not unset settings that were added.
Object.assign(settings, backups.settings); Object.assign(settings, backups.settings);
log4js.setGlobalLogLevel(logLevel);
await server.exit(); await server.exit();
}); });
@ -93,7 +97,7 @@ exports.waitForSocketEvent = async (socket, event) => {
const handlers = new Map(); const handlers = new Map();
let cancelTimeout; let cancelTimeout;
try { try {
const timeoutP = new Promise((resolve, reject) => { const timeoutP = new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
reject(new Error(`timed out waiting for ${event} event`)); reject(new Error(`timed out waiting for ${event} event`));
cancelTimeout = () => {}; cancelTimeout = () => {};
@ -139,9 +143,11 @@ exports.waitForSocketEvent = async (socket, event) => {
*/ */
exports.connect = async (res = null) => { exports.connect = async (res = null) => {
// Convert the `set-cookie` header(s) into a `cookie` header. // Convert the `set-cookie` header(s) into a `cookie` header.
const resCookies = (res == null) ? {} : setCookieParser.parse(res, {map: true}); const resCookies:{
const reqCookieHdr = Object.entries(resCookies).map( [key:string]:{value:string}
([name, cookie]) => `${name}=${encodeURIComponent(cookie.value)}`).join('; '); } = (res == null) ? {} : setCookieParser.parse(res, {map: true});
const reqCookieHdr = Object.entries(resCookies)
.map(([name, cookie]) => `${name}=${encodeURIComponent(cookie.value)}`).join('; ');
logger.debug('socket.io connecting...'); logger.debug('socket.io connecting...');
let padId = null; let padId = null;
@ -191,7 +197,7 @@ exports.handshake = async (socket, padId, token = padutils.generateAuthorToken()
/** /**
* Convenience wrapper around `socket.send()` that waits for acknowledgement. * Convenience wrapper around `socket.send()` that waits for acknowledgement.
*/ */
exports.sendMessage = async (socket, message) => await new Promise((resolve, reject) => { exports.sendMessage = async (socket, message) => await new Promise<void>((resolve, reject) => {
socket.send(message, (errInfo) => { socket.send(message, (errInfo) => {
if (errInfo != null) { if (errInfo != null) {
const {name, message} = errInfo; const {name, message} = errInfo;