Added more types.

This commit is contained in:
SamTV12345 2024-01-24 20:18:46 +01:00
parent 545c7c6060
commit 03e73053cb
16 changed files with 167 additions and 81 deletions

View file

@ -1,7 +1,9 @@
'use strict'; 'use strict';
import {Socket} from "node:net";
import {MapArrayType} from "../types/MapType";
const _ = require('underscore'); const _ = require('underscore');
const SecretRotator = require('../security/SecretRotator');
const cookieParser = require('cookie-parser'); const cookieParser = require('cookie-parser');
const events = require('events'); const events = require('events');
const express = require('express'); const express = require('express');
@ -15,11 +17,14 @@ const stats = require('../stats');
const util = require('util'); const util = require('util');
const webaccess = require('./express/webaccess'); const webaccess = require('./express/webaccess');
let secretRotator = null; const SecretRotator = require('../security/SecretRotator')
// TODO once we have ESM we can use the type of the class
let secretRotator: any|null = null;
const logger = log4js.getLogger('http'); const logger = log4js.getLogger('http');
let serverName; let serverName:string;
let sessionStore; let sessionStore: { shutdown: () => void; } | null;
const sockets = new Set(); const sockets:Set<Socket> = new Set();
const socketsEvents = new events.EventEmitter(); const socketsEvents = new events.EventEmitter();
const startTime = stats.settableGauge('httpStartTime'); const startTime = stats.settableGauge('httpStartTime');
@ -101,7 +106,7 @@ exports.restartServer = async () => {
console.log(`SSL -- server key file: ${settings.ssl.key}`); console.log(`SSL -- server key file: ${settings.ssl.key}`);
console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`); console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`);
const options = { const options: MapArrayType<any> = {
key: fs.readFileSync(settings.ssl.key), key: fs.readFileSync(settings.ssl.key),
cert: fs.readFileSync(settings.ssl.cert), cert: fs.readFileSync(settings.ssl.cert),
}; };
@ -121,7 +126,7 @@ exports.restartServer = async () => {
exports.server = http.createServer(app); exports.server = http.createServer(app);
} }
app.use((req, res, next) => { app.use((req:any, res:any, next:Function) => {
// res.header("X-Frame-Options", "deny"); // breaks embedded pads // res.header("X-Frame-Options", "deny"); // breaks embedded pads
if (settings.ssl) { if (settings.ssl) {
// we use SSL // we use SSL
@ -160,10 +165,10 @@ exports.restartServer = async () => {
} }
// Measure response time // Measure response time
app.use((req, res, next) => { app.use((req:any, res:any, next:Function) => {
const stopWatch = stats.timer('httpRequests').start(); const stopWatch = stats.timer('httpRequests').start();
const sendFn = res.send.bind(res); const sendFn = res.send.bind(res);
res.send = (...args) => { stopWatch.end(); sendFn(...args); }; res.send = (...args: any) => { stopWatch.end(); sendFn(...args); };
next(); next();
}); });
@ -237,7 +242,7 @@ exports.restartServer = async () => {
hooks.aCallAll('expressConfigure', {app}), hooks.aCallAll('expressConfigure', {app}),
hooks.aCallAll('expressCreateServer', {app, server: exports.server}), hooks.aCallAll('expressCreateServer', {app, server: exports.server}),
]); ]);
exports.server.on('connection', (socket) => { exports.server.on('connection', (socket:Socket) => {
sockets.add(socket); sockets.add(socket);
socketsEvents.emit('updated'); socketsEvents.emit('updated');
socket.on('close', () => { socket.on('close', () => {
@ -250,6 +255,6 @@ exports.restartServer = async () => {
logger.info('HTTP server listening for connections'); logger.info('HTTP server listening for connections');
}; };
exports.shutdown = async (hookName, context) => { exports.shutdown = async (hookName:string, context: any) => {
await closeServer(); await closeServer();
}; };

View file

@ -1,6 +1,7 @@
'use strict'; 'use strict';
const eejs = require('../../eejs'); import {ArgsExpressType} from "../../types/ArgsExpressType";
const eejs = require('../../eejs');
/** /**
* Add the admin navigation link * Add the admin navigation link
@ -9,8 +10,8 @@ const eejs = require('../../eejs');
* @param {Function} cb the callback function * @param {Function} cb the callback function
* @return {*} * @return {*}
*/ */
exports.expressCreateServer = (hookName, args, cb) => { exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Function): any => {
args.app.get('/admin', (req, res) => { args.app.get('/admin', (req:any, res:any) => {
if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/'); if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/');
res.send(eejs.require('ep_etherpad-lite/templates/admin/index.html', {req})); res.send(eejs.require('ep_etherpad-lite/templates/admin/index.html', {req}));
}); });

View file

@ -1,5 +1,11 @@
'use strict'; 'use strict';
import {ArgsExpressType} from "../../types/ArgsExpressType";
import {Socket} from "node:net";
import {ErrorCaused} from "../../types/ErrorCaused";
import {QueryType} from "../../types/QueryType";
import {PluginType} from "../../types/Plugin";
const eejs = require('../../eejs'); const eejs = require('../../eejs');
const settings = require('../../utils/Settings'); const settings = require('../../utils/Settings');
const installer = require('../../../static/js/pluginfw/installer'); const installer = require('../../../static/js/pluginfw/installer');
@ -8,8 +14,8 @@ const plugins = require('../../../static/js/pluginfw/plugins');
const semver = require('semver'); const semver = require('semver');
const UpdateCheck = require('../../utils/UpdateCheck'); const UpdateCheck = require('../../utils/UpdateCheck');
exports.expressCreateServer = (hookName, args, cb) => { exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Function) => {
args.app.get('/admin/plugins', (req, res) => { args.app.get('/admin/plugins', (req:any, res:any) => {
res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins.html', { res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins.html', {
plugins: pluginDefs.plugins, plugins: pluginDefs.plugins,
req, req,
@ -17,7 +23,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
})); }));
}); });
args.app.get('/admin/plugins/info', (req, res) => { args.app.get('/admin/plugins/info', (req:any, res:any) => {
const gitCommit = settings.getGitCommit(); const gitCommit = settings.getGitCommit();
const epVersion = settings.getEpVersion(); const epVersion = settings.getEpVersion();
@ -36,13 +42,14 @@ exports.expressCreateServer = (hookName, args, cb) => {
return cb(); return cb();
}; };
exports.socketio = (hookName, args, cb) => { exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
const io = args.io.of('/pluginfw/installer'); const io = args.io.of('/pluginfw/installer');
io.on('connection', (socket) => { io.on('connection', (socket:any) => {
// @ts-ignore
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request; const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
if (!isAdmin) return; if (!isAdmin) return;
socket.on('getInstalled', (query) => { socket.on('getInstalled', (query:string) => {
// send currently installed plugins // send currently installed plugins
const installed = const installed =
Object.keys(pluginDefs.plugins).map((plugin) => pluginDefs.plugins[plugin].package); Object.keys(pluginDefs.plugins).map((plugin) => pluginDefs.plugins[plugin].package);
@ -66,13 +73,14 @@ exports.socketio = (hookName, args, cb) => {
socket.emit('results:updatable', {updatable}); socket.emit('results:updatable', {updatable});
} catch (err) { } catch (err) {
console.warn(err.stack || err.toString()); const errc = err as ErrorCaused
console.warn(errc.stack || errc.toString());
socket.emit('results:updatable', {updatable: {}}); socket.emit('results:updatable', {updatable: {}});
} }
}); });
socket.on('getAvailable', async (query) => { socket.on('getAvailable', async (query:string) => {
try { try {
const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ false); const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ false);
socket.emit('results:available', results); socket.emit('results:available', results);
@ -82,7 +90,7 @@ exports.socketio = (hookName, args, cb) => {
} }
}); });
socket.on('search', async (query) => { socket.on('search', async (query: QueryType) => {
try { try {
const results = await installer.search(query.searchTerm, /* maxCacheAge:*/ 60 * 10); const results = await installer.search(query.searchTerm, /* maxCacheAge:*/ 60 * 10);
let res = Object.keys(results) let res = Object.keys(results)
@ -98,8 +106,8 @@ exports.socketio = (hookName, args, cb) => {
} }
}); });
socket.on('install', (pluginName) => { socket.on('install', (pluginName: string) => {
installer.install(pluginName, (err) => { installer.install(pluginName, (err: ErrorCaused) => {
if (err) console.warn(err.stack || err.toString()); if (err) console.warn(err.stack || err.toString());
socket.emit('finished:install', { socket.emit('finished:install', {
@ -110,8 +118,8 @@ exports.socketio = (hookName, args, cb) => {
}); });
}); });
socket.on('uninstall', (pluginName) => { socket.on('uninstall', (pluginName:string) => {
installer.uninstall(pluginName, (err) => { installer.uninstall(pluginName, (err:ErrorCaused) => {
if (err) console.warn(err.stack || err.toString()); if (err) console.warn(err.stack || err.toString());
socket.emit('finished:uninstall', {plugin: pluginName, error: err ? err.message : null}); socket.emit('finished:uninstall', {plugin: pluginName, error: err ? err.message : null});
@ -128,11 +136,13 @@ exports.socketio = (hookName, args, cb) => {
* @param {String} dir The directory of the plugin * @param {String} dir The directory of the plugin
* @return {Object[]} * @return {Object[]}
*/ */
const sortPluginList = (plugins, property, /* ASC?*/dir) => plugins.sort((a, b) => { const sortPluginList = (plugins:PluginType[], property:string, /* ASC?*/dir:string): object[] => plugins.sort((a, b) => {
// @ts-ignore
if (a[property] < b[property]) { if (a[property] < b[property]) {
return dir ? -1 : 1; return dir ? -1 : 1;
} }
// @ts-ignore
if (a[property] > b[property]) { if (a[property] > b[property]) {
return dir ? 1 : -1; return dir ? 1 : -1;
} }

View file

@ -6,8 +6,8 @@ const hooks = require('../../../static/js/pluginfw/hooks');
const plugins = require('../../../static/js/pluginfw/plugins'); const plugins = require('../../../static/js/pluginfw/plugins');
const settings = require('../../utils/Settings'); const settings = require('../../utils/Settings');
exports.expressCreateServer = (hookName, {app}) => { exports.expressCreateServer = (hookName:string, {app}:any) => {
app.get('/admin/settings', (req, res) => { app.get('/admin/settings', (req:any, res:any) => {
res.send(eejs.require('ep_etherpad-lite/templates/admin/settings.html', { res.send(eejs.require('ep_etherpad-lite/templates/admin/settings.html', {
req, req,
settings: '', settings: '',
@ -16,12 +16,13 @@ exports.expressCreateServer = (hookName, {app}) => {
}); });
}; };
exports.socketio = (hookName, {io}) => { exports.socketio = (hookName:string, {io}:any) => {
io.of('/settings').on('connection', (socket) => { io.of('/settings').on('connection', (socket: any ) => {
// @ts-ignore
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request; const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
if (!isAdmin) return; if (!isAdmin) return;
socket.on('load', async (query) => { socket.on('load', async (query:string):Promise<any> => {
let data; let data;
try { try {
data = await fsp.readFile(settings.settingsFilename, 'utf8'); data = await fsp.readFile(settings.settingsFilename, 'utf8');
@ -36,7 +37,7 @@ exports.socketio = (hookName, {io}) => {
} }
}); });
socket.on('saveSettings', async (newSettings) => { socket.on('saveSettings', async (newSettings:string) => {
await fsp.writeFile(settings.settingsFilename, newSettings); await fsp.writeFile(settings.settingsFilename, newSettings);
socket.emit('saveprogress', 'saved'); socket.emit('saveprogress', 'saved');
}); });

View file

@ -6,15 +6,15 @@ const {Formidable} = require('formidable');
const apiHandler = require('../../handler/APIHandler'); const apiHandler = require('../../handler/APIHandler');
const util = require('util'); const util = require('util');
exports.expressPreSession = async (hookName, {app}) => { exports.expressPreSession = async (hookName:string, {app}:any) => {
// The Etherpad client side sends information about how a disconnect happened // The Etherpad client side sends information about how a disconnect happened
app.post('/ep/pad/connection-diagnostic-info', async (req, res) => { app.post('/ep/pad/connection-diagnostic-info', async (req:any, res:any) => {
const [fields, files] = await (new Formidable({})).parse(req); const [fields, files] = await (new Formidable({})).parse(req);
clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`); clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`);
res.end('OK'); res.end('OK');
}); });
const parseJserrorForm = async (req) => { const parseJserrorForm = async (req:any) => {
const form = new Formidable({ const form = new Formidable({
maxFileSize: 1, // Files are not expected. Not sure if 0 means unlimited, so 1 is used. maxFileSize: 1, // Files are not expected. Not sure if 0 means unlimited, so 1 is used.
}); });
@ -23,11 +23,11 @@ exports.expressPreSession = async (hookName, {app}) => {
}; };
// The Etherpad client side sends information about client side javscript errors // The Etherpad client side sends information about client side javscript errors
app.post('/jserror', (req, res, next) => { app.post('/jserror', (req:any, res:any, next:Function) => {
(async () => { (async () => {
const data = JSON.parse(await parseJserrorForm(req)); const data = JSON.parse(await parseJserrorForm(req));
clientLogger.warn(`${data.msg} --`, { clientLogger.warn(`${data.msg} --`, {
[util.inspect.custom]: (depth, options) => { [util.inspect.custom]: (depth: number, options:any) => {
// Depth is forced to infinity to ensure that all of the provided data is logged. // Depth is forced to infinity to ensure that all of the provided data is logged.
options = Object.assign({}, options, {depth: Infinity, colors: true}); options = Object.assign({}, options, {depth: Infinity, colors: true});
return util.inspect(data, options); return util.inspect(data, options);
@ -38,7 +38,7 @@ exports.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:any, res:any) => {
res.json({currentVersion: apiHandler.latestApiVersion}); res.json({currentVersion: apiHandler.latestApiVersion});
}); });
}; };

View file

@ -1,12 +1,15 @@
'use strict'; 'use strict';
import {ArgsExpressType} from "../../types/ArgsExpressType";
import {ErrorCaused} from "../../types/ErrorCaused";
const stats = require('../../stats'); const stats = require('../../stats');
exports.expressCreateServer = (hook_name, args, cb) => { exports.expressCreateServer = (hook_name:string, args: ArgsExpressType, cb:Function) => {
exports.app = args.app; exports.app = args.app;
// Handle errors // Handle errors
args.app.use((err, req, res, next) => { args.app.use((err:ErrorCaused, req:any, res:any, next:Function) => {
// if an error occurs Connect will pass it down // if an error occurs Connect will pass it down
// through these "error-handling" middleware // through these "error-handling" middleware
// allowing you to respond however you like // allowing you to respond however you like

View file

@ -1,5 +1,7 @@
'use strict'; 'use strict';
import {ArgsExpressType} from "../../types/ArgsExpressType";
const hasPadAccess = require('../../padaccess'); const hasPadAccess = require('../../padaccess');
const settings = require('../../utils/Settings'); const settings = require('../../utils/Settings');
const exportHandler = require('../../handler/ExportHandler'); const exportHandler = require('../../handler/ExportHandler');
@ -10,10 +12,10 @@ const rateLimit = require('express-rate-limit');
const securityManager = require('../../db/SecurityManager'); const securityManager = require('../../db/SecurityManager');
const webaccess = require('./webaccess'); const webaccess = require('./webaccess');
exports.expressCreateServer = (hookName, args, cb) => { exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
const limiter = rateLimit({ const limiter = rateLimit({
...settings.importExportRateLimiting, ...settings.importExportRateLimiting,
handler: (request, response, next, options) => { handler: (request:any) => {
if (request.rateLimit.current === request.rateLimit.limit + 1) { if (request.rateLimit.current === request.rateLimit.limit + 1) {
// when the rate limiter triggers, write a warning in the logs // when the rate limiter triggers, write a warning in the logs
console.warn('Import/Export rate limiter triggered on ' + console.warn('Import/Export rate limiter triggered on ' +
@ -24,7 +26,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
// handle export requests // handle export requests
args.app.use('/p/:pad/:rev?/export/:type', limiter); args.app.use('/p/:pad/:rev?/export/:type', limiter);
args.app.get('/p/:pad/:rev?/export/:type', (req, res, next) => { args.app.get('/p/:pad/:rev?/export/:type', (req:any, res:any, next:Function) => {
(async () => { (async () => {
const types = ['pdf', 'doc', 'txt', 'html', 'odt', 'etherpad']; const types = ['pdf', 'doc', 'txt', 'html', 'odt', 'etherpad'];
// send a 404 if we don't support this filetype // send a 404 if we don't support this filetype
@ -70,8 +72,9 @@ exports.expressCreateServer = (hookName, args, cb) => {
// handle import requests // handle import requests
args.app.use('/p/:pad/import', limiter); args.app.use('/p/:pad/import', limiter);
args.app.post('/p/:pad/import', (req, res, next) => { args.app.post('/p/:pad/import', (req:any, res:any, next:Function) => {
(async () => { (async () => {
// @ts-ignore
const {session: {user} = {}} = req; const {session: {user} = {}} = req;
const {accessStatus, authorID: authorId} = await securityManager.checkAccess( const {accessStatus, authorID: authorId} = await securityManager.checkAccess(
req.params.pad, req.cookies.sessionID, req.cookies.token, user); req.params.pad, req.cookies.sessionID, req.cookies.token, user);

View file

@ -1,5 +1,9 @@
'use strict'; 'use strict';
import {OpenAPIOperations, OpenAPISuccessResponse, SwaggerUIResource} from "../../types/SwaggerUIResource";
import {MapArrayType} from "../../types/MapType";
import {ErrorCaused} from "../../types/ErrorCaused";
/** /**
* node/hooks/express/openapi.js * node/hooks/express/openapi.js
* *
@ -52,8 +56,9 @@ const APIPathStyle = {
REST: 'rest', // restful paths e.g. /rest/group/create REST: 'rest', // restful paths e.g. /rest/group/create
}; };
// API resources - describe your API endpoints here // API resources - describe your API endpoints here
const resources = { const resources:SwaggerUIResource = {
// Group // Group
group: { group: {
create: { create: {
@ -372,7 +377,7 @@ const defaultResponses = {
}, },
}; };
const defaultResponseRefs = { const defaultResponseRefs:OpenAPISuccessResponse = {
200: { 200: {
$ref: '#/components/responses/Success', $ref: '#/components/responses/Success',
}, },
@ -388,16 +393,16 @@ const defaultResponseRefs = {
}; };
// convert to a dictionary of operation objects // convert to a dictionary of operation objects
const operations = {}; const operations: OpenAPIOperations = {};
for (const [resource, actions] of Object.entries(resources)) { for (const [resource, actions] of Object.entries(resources)) {
for (const [action, spec] of Object.entries(actions)) { for (const [action, spec] of Object.entries(actions)) {
const {operationId,responseSchema, ...operation} = spec; const {operationId,responseSchema, ...operation} = spec;
// add response objects // add response objects
const responses = {...defaultResponseRefs}; const responses:OpenAPISuccessResponse = {...defaultResponseRefs};
if (responseSchema) { if (responseSchema) {
responses[200] = cloneDeep(defaultResponses.Success); responses[200] = cloneDeep(defaultResponses.Success);
responses[200].content['application/json'].schema.properties.data = { responses[200].content!['application/json'].schema.properties.data = {
type: 'object', type: 'object',
properties: responseSchema, properties: responseSchema,
}; };
@ -414,7 +419,7 @@ for (const [resource, actions] of Object.entries(resources)) {
} }
} }
const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => { const generateDefinitionForVersion = (version:string, style = APIPathStyle.FLAT) => {
const definition = { const definition = {
openapi: OPENAPI_VERSION, openapi: OPENAPI_VERSION,
info, info,
@ -490,7 +495,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
// build operations // build operations
for (const funcName of Object.keys(apiHandler.version[version])) { for (const funcName of Object.keys(apiHandler.version[version])) {
let operation = {}; let operation:OpenAPIOperations = {};
if (operations[funcName]) { if (operations[funcName]) {
operation = {...operations[funcName]}; operation = {...operations[funcName]};
} else { } else {
@ -505,7 +510,9 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
operation.parameters = operation.parameters || []; operation.parameters = operation.parameters || [];
for (const paramName of apiHandler.version[version][funcName]) { for (const paramName of apiHandler.version[version][funcName]) {
operation.parameters.push({$ref: `#/components/parameters/${paramName}`}); operation.parameters.push({$ref: `#/components/parameters/${paramName}`});
// @ts-ignore
if (!definition.components.parameters[paramName]) { if (!definition.components.parameters[paramName]) {
// @ts-ignore
definition.components.parameters[paramName] = { definition.components.parameters[paramName] = {
name: paramName, name: paramName,
in: 'query', in: 'query',
@ -525,6 +532,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
// add to definition // add to definition
// NOTE: It may be confusing that every operation can be called with both GET and POST // NOTE: It may be confusing that every operation can be called with both GET and POST
// @ts-ignore
definition.paths[path] = { definition.paths[path] = {
get: { get: {
...operation, ...operation,
@ -539,7 +547,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
return definition; return definition;
}; };
exports.expressPreSession = async (hookName, {app}) => { exports.expressPreSession = async (hookName:string, {app}:any) => {
// 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
@ -552,7 +560,7 @@ exports.expressPreSession = async (hookName, {app}) => {
const definition = generateDefinitionForVersion(version, style); const definition = generateDefinitionForVersion(version, style);
// serve version specific openapi definition // serve version specific openapi definition
app.get(`${apiRoot}/openapi.json`, (req, res) => { app.get(`${apiRoot}/openapi.json`, (req:any, res:any) => {
// For openapi definitions, wide CORS is probably fine // For openapi definitions, wide CORS is probably fine
res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Origin', '*');
res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]}); res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]});
@ -561,7 +569,7 @@ exports.expressPreSession = async (hookName, {app}) => {
// serve latest openapi definition file under /api/openapi.json // serve latest openapi definition file under /api/openapi.json
const isLatestAPIVersion = version === apiHandler.latestApiVersion; const isLatestAPIVersion = version === apiHandler.latestApiVersion;
if (isLatestAPIVersion) { if (isLatestAPIVersion) {
app.get(`/${style}/openapi.json`, (req, res) => { app.get(`/${style}/openapi.json`, (req:any, res:any) => {
res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Origin', '*');
res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]}); res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]});
}); });
@ -588,12 +596,12 @@ exports.expressPreSession = async (hookName, {app}) => {
// register operation handlers // register operation handlers
for (const funcName of Object.keys(apiHandler.version[version])) { for (const funcName of Object.keys(apiHandler.version[version])) {
const handler = async (c, req, res) => { const handler = async (c: any, req:any, res:any) => {
// parse fields from request // parse fields from request
const {header, params, query} = c.request; const {header, params, query} = c.request;
// read form data if method was POST // read form data if method was POST
let formData = {}; let formData:MapArrayType<any> = {};
if (c.request.method === 'post') { if (c.request.method === 'post') {
const form = new IncomingForm(); const form = new IncomingForm();
formData = (await form.parse(req))[0]; formData = (await form.parse(req))[0];
@ -615,18 +623,19 @@ exports.expressPreSession = async (hookName, {app}) => {
try { try {
data = await apiHandler.handle(version, funcName, fields, req, res); data = await apiHandler.handle(version, funcName, fields, req, res);
} catch (err) { } catch (err) {
const errCaused = err as ErrorCaused
// convert all errors to http errors // convert all errors to http errors
if (createHTTPError.isHttpError(err)) { if (createHTTPError.isHttpError(err)) {
// pass http errors thrown by handler forward // pass http errors thrown by handler forward
throw err; throw err;
} else if (err.name === 'apierror') { } else if (errCaused.name === 'apierror') {
// parameters were wrong and the api stopped execution, pass the error // parameters were wrong and the api stopped execution, pass the error
// convert to http error // convert to http error
throw new createHTTPError.BadRequest(err.message); throw new createHTTPError.BadRequest(errCaused.message);
} else { } else {
// an unknown error happened // an unknown error happened
// log it and throw internal error // log it and throw internal error
logger.error(err.stack || err.toString()); logger.error(errCaused.stack || errCaused.toString());
throw new createHTTPError.InternalError('internal error'); throw new createHTTPError.InternalError('internal error');
} }
} }
@ -649,7 +658,7 @@ exports.expressPreSession = async (hookName, {app}) => {
// start and bind to express // start and bind to express
api.init(); api.init();
app.use(apiRoot, async (req, res) => { app.use(apiRoot, async (req:any, res:any) => {
let response = null; let response = null;
try { try {
if (style === APIPathStyle.REST) { if (style === APIPathStyle.REST) {
@ -660,31 +669,33 @@ exports.expressPreSession = async (hookName, {app}) => {
// pass to openapi-backend handler // pass to openapi-backend handler
response = await api.handleRequest(req, req, res); response = await api.handleRequest(req, req, res);
} catch (err) { } catch (err) {
const errCaused = err as ErrorCaused
// handle http errors // handle http errors
res.statusCode = err.statusCode || 500; // @ts-ignore
res.statusCode = errCaused.statusCode || 500;
// convert to our json response format // convert to our json response format
// https://github.com/ether/etherpad-lite/tree/master/doc/api/http_api.md#response-format // https://github.com/ether/etherpad-lite/tree/master/doc/api/http_api.md#response-format
switch (res.statusCode) { switch (res.statusCode) {
case 403: // forbidden case 403: // forbidden
response = {code: 4, message: err.message, data: null}; response = {code: 4, message: errCaused.message, data: null};
break; break;
case 401: // unauthorized (no or wrong api key) case 401: // unauthorized (no or wrong api key)
response = {code: 4, message: err.message, data: null}; response = {code: 4, message: errCaused.message, data: null};
break; break;
case 404: // not found (no such function) case 404: // not found (no such function)
response = {code: 3, message: err.message, data: null}; response = {code: 3, message: errCaused.message, data: null};
break; break;
case 500: // server error (internal error) case 500: // server error (internal error)
response = {code: 2, message: err.message, data: null}; response = {code: 2, message: errCaused.message, data: null};
break; break;
case 400: // bad request (wrong parameters) case 400: // bad request (wrong parameters)
// respond with 200 OK to keep old behavior and pass tests // respond with 200 OK to keep old behavior and pass tests
res.statusCode = 200; // @TODO: this is bad api design res.statusCode = 200; // @TODO: this is bad api design
response = {code: 1, message: err.message, data: null}; response = {code: 1, message: errCaused.message, data: null};
break; break;
default: default:
response = {code: 1, message: err.message, data: null}; response = {code: 1, message: errCaused.message, data: null};
break; break;
} }
} }
@ -702,7 +713,7 @@ exports.expressPreSession = async (hookName, {app}) => {
* @param {APIPathStyle} style The style of the API path * @param {APIPathStyle} style The style of the API path
* @return {String} The root path for the API version * @return {String} The root path for the API version
*/ */
const getApiRootForVersion = (version, style = APIPathStyle.FLAT) => `/${style}/${version}`; const getApiRootForVersion = (version:string, style:any = APIPathStyle.FLAT): string => `/${style}/${version}`;
/** /**
* Helper to generate an OpenAPI server object when serving definitions * Helper to generate an OpenAPI server object when serving definitions
@ -710,6 +721,8 @@ const getApiRootForVersion = (version, style = APIPathStyle.FLAT) => `/${style}/
* @param {Request} req The express request object * @param {Request} req The express request object
* @return {url: String} The server object for the OpenAPI definition location * @return {url: String} The server object for the OpenAPI definition location
*/ */
const generateServerForApiVersion = (apiRoot, req) => ({ const generateServerForApiVersion = (apiRoot:string, req:any): {
url:string
} => ({
url: `${settings.ssl ? 'https' : 'http'}://${req.headers.host}${apiRoot}`, url: `${settings.ssl ? 'https' : 'http'}://${req.headers.host}${apiRoot}`,
}); });

View file

@ -1,10 +1,12 @@
'use strict'; 'use strict';
import {ArgsExpressType} from "../../types/ArgsExpressType";
const padManager = require('../../db/PadManager'); const padManager = require('../../db/PadManager');
exports.expressCreateServer = (hookName, args, cb) => { exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
// redirects browser to the pad's sanitized url if needed. otherwise, renders the html // redirects browser to the pad's sanitized url if needed. otherwise, renders the html
args.app.param('pad', (req, res, next, padId) => { args.app.param('pad', (req:any, res:any, next:Function, padId:string) => {
(async () => { (async () => {
// ensure the padname is valid and the url doesn't end with a / // ensure the padname is valid and the url doesn't end with a /
if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) { if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) {

View file

@ -1,5 +1,7 @@
'use strict'; 'use strict';
import {ArgsExpressType} from "../../types/ArgsExpressType";
const events = require('events'); const events = require('events');
const express = require('../express'); const express = require('../express');
const log4js = require('log4js'); const log4js = require('log4js');
@ -10,7 +12,7 @@ const socketIORouter = require('../../handler/SocketIORouter');
const hooks = require('../../../static/js/pluginfw/hooks'); const hooks = require('../../../static/js/pluginfw/hooks');
const padMessageHandler = require('../../handler/PadMessageHandler'); const padMessageHandler = require('../../handler/PadMessageHandler');
let io; let io:any;
const logger = log4js.getLogger('socket.io'); const logger = log4js.getLogger('socket.io');
const sockets = new Set(); const sockets = new Set();
const socketsEvents = new events.EventEmitter(); const socketsEvents = new events.EventEmitter();
@ -46,7 +48,7 @@ exports.expressCloseServer = async () => {
logger.info('All socket.io clients have disconnected'); logger.info('All socket.io clients have disconnected');
}; };
exports.expressCreateServer = (hookName, args, cb) => { exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
// init socket.io and redirect all requests to the MessageHandler // init socket.io and redirect all requests to the MessageHandler
// there shouldn't be a browser that isn't compatible to all // there shouldn't be a browser that isn't compatible to all
// transports in this list at once // transports in this list at once
@ -77,7 +79,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
maxHttpBufferSize: settings.socketIo.maxHttpBufferSize, maxHttpBufferSize: settings.socketIo.maxHttpBufferSize,
}); });
io.on('connect', (socket) => { io.on('connect', (socket:any) => {
sockets.add(socket); sockets.add(socket);
socketsEvents.emit('updated'); socketsEvents.emit('updated');
socket.on('disconnect', () => { socket.on('disconnect', () => {
@ -86,7 +88,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
}); });
}); });
io.use((socket, next) => { io.use((socket:any, next: Function) => {
const req = socket.request; const req = socket.request;
// Express sets req.ip but socket.io does not. Replicate Express's behavior here. // Express sets req.ip but socket.io does not. Replicate Express's behavior here.
if (req.ip == null) { if (req.ip == null) {
@ -105,8 +107,8 @@ exports.expressCreateServer = (hookName, args, cb) => {
express.sessionMiddleware(req, {}, next); express.sessionMiddleware(req, {}, next);
}); });
io.use((socket, next) => { io.use((socket:any, next:Function) => {
socket.conn.on('packet', (packet) => { socket.conn.on('packet', (packet:string) => {
// Tell express-session that the session is still active. The session store can use these // Tell express-session that the session is still active. The session store can use these
// touch events to defer automatic session cleanup, and if express-session is configured with // touch events to defer automatic session cleanup, and if express-session is configured with
// rolling=true the cookie's expiration time will be renewed. (Note that WebSockets does not // rolling=true the cookie's expiration time will be renewed. (Note that WebSockets does not

View file

@ -63,7 +63,7 @@ const intervalStart = (t:number, interval:number) => t - mod(t, interval);
* The secrets are generated using a key derivation function (KDF) with input keying material coming * The secrets are generated using a key derivation function (KDF) with input keying material coming
* from a long-lived secret stored in the database (generated if missing). * from a long-lived secret stored in the database (generated if missing).
*/ */
class SecretRotator { export class SecretRotator {
private readonly secrets: string[]; private readonly secrets: string[];
private readonly _dbPrefix private readonly _dbPrefix
private readonly _interval private readonly _interval

View file

@ -0,0 +1,5 @@
export type ArgsExpressType = {
app:any,
io: any,
server:any
}

View file

@ -1,5 +1,6 @@
export class ErrorCaused extends Error { export class ErrorCaused extends Error {
cause: Error; cause: Error;
code: any;
constructor(message: string, cause: Error) { constructor(message: string, cause: Error) {
super(); super();
this.cause = cause this.cause = cause

View file

@ -0,0 +1,3 @@
export type QueryType = {
searchTerm: string; sortBy: string; sortDir: string; offset: number; limit: number;
}

View file

@ -0,0 +1,3 @@
export type SecretRotatorType = {
stop: ()=>void
}

View file

@ -0,0 +1,34 @@
export type SwaggerUIResource = {
[key: string]: {
[secondKey: string]: {
operationId: string,
summary?: string,
description?:string
responseSchema?: object
}
}
}
export type OpenAPISuccessResponse = {
[key: number] :{
$ref: string,
content?: {
[key: string]: {
schema: {
properties: {
data: {
type: string,
properties: object
}
}
}
}
}
}
}
export type OpenAPIOperations = {
[key:string]: any
}