nephele
Version:
Highly customizable and extensible WebDAV server for Node.js and Express.
295 lines • 12.3 kB
JavaScript
import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import express from 'express';
import cookieParser from 'cookie-parser';
import createDebug from 'debug';
import { nanoid } from 'nanoid';
import { defaults, getAdapter, getAuthenticator, getPlugins, } from './Options.js';
import { catchErrors } from './catchErrors.js';
import { ForbiddenError, UnauthorizedError } from './Errors/index.js';
import { COPY, DELETE, GET_HEAD, LOCK, MKCOL, MOVE, OPTIONS, PROPFIND, PROPPATCH, PUT, UNLOCK, Method, } from './Methods/index.js';
const debug = createDebug('nephele:server');
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json')).toString());
export default function createServer({ adapter, authenticator, plugins }, options = {}) {
const opts = Object.assign({}, defaults, options);
const app = express();
app.disable('etag');
app.use(cookieParser());
async function debugLogger(request, response, next) {
response.locals.requestId = nanoid(5);
response.locals.debug = debug.extend(`${response.locals.requestId}`);
response.locals.debug(`IP: ${request.ip}, Method: ${request.method}, URL: ${request.originalUrl}`);
response.locals.errors = [];
next();
}
app.use(debugLogger);
async function debugLoggerEnd(_request, response, next) {
response.on('close', () => {
response.locals.debug(`Response: ${response.statusCode} ${response.statusMessage || ''}`);
for (let error of response.locals.errors) {
response.locals.debug('Error Message: %s', 'message' in error ? error.message : error);
}
});
next();
}
app.use(debugLoggerEnd);
async function loadPlugins(request, response, next) {
if (plugins == null) {
response.locals.pluginsConfig = plugins;
response.locals.plugins = [];
}
else {
const pluginArray = typeof plugins === 'function'
? await plugins(request, response)
: plugins;
response.locals.pluginsConfig = pluginArray;
const parsedPlugins = getPlugins(decodeURIComponent(request.path).replace(/\/?$/, () => '/'), pluginArray);
const baseUrl = new URL(path.join(request.baseUrl || '/', parsedPlugins.baseUrl), `${request.protocol}://${request.headers.host}`);
response.locals.plugins = parsedPlugins.plugins;
response.locals.plugins.forEach((plugin) => (plugin.baseUrl = baseUrl));
}
next();
}
app.use(loadPlugins);
app.use(async (request, response, next) => {
let ended = false;
await catchErrors(async () => {
for (let plugin of response.locals.plugins) {
if ('prepare' in plugin && plugin.prepare) {
const result = await plugin.prepare(request, response);
if (result === false) {
ended = true;
}
}
}
}, async (code, message, error) => {
await opts.errorHandler(code, message, request, response, error);
ended = true;
})();
if (!ended) {
next();
}
});
async function loadAuthenticator(request, response, next) {
const auth = typeof authenticator === 'function'
? await authenticator(request, response)
: authenticator;
response.locals.authenticatorConfig = auth;
response.locals.authenticator = getAuthenticator(decodeURIComponent(request.path).replace(/\/?$/, () => '/'), auth);
next();
}
app.use(loadAuthenticator);
async function loadAdapter(request, response, next) {
const adapt = typeof adapter === 'function'
? await adapter(request, response)
: adapter;
response.locals.adapterConfig = adapt;
const parsedAdapter = await getAdapter(decodeURIComponent(request.path).replace(/\/?$/, () => '/'), adapt, {
request,
response,
plugins: response.locals.plugins,
});
response.locals.adapter = parsedAdapter.adapter;
response.locals.baseUrl = new URL(path.join(request.baseUrl || '/', parsedAdapter.baseUrl), `${request.protocol}://${request.headers.host}`);
next();
}
app.use(loadAdapter);
app.use(async (request, response, next) => {
let ended = false;
await catchErrors(async () => {
for (let plugin of response.locals.plugins) {
if ('beforeAuth' in plugin && plugin.beforeAuth) {
const result = await plugin.beforeAuth(request, response);
if (result === false) {
ended = true;
}
}
}
}, async (code, message, error) => {
await opts.errorHandler(code, message, request, response, error);
ended = true;
})();
if (!ended) {
next();
}
});
async function authenticate(request, response, next) {
try {
if (request.method === 'OPTIONS') {
response.locals.debug(`Skipping authentication for OPTIONS request.`);
}
else {
response.locals.debug(`Authenticating user.`);
response.locals.user = await response.locals.authenticator.authenticate(request, response);
}
}
catch (e) {
response.locals.debug(`Auth failed.`);
response.locals.errors.push(e);
if (e instanceof UnauthorizedError) {
response.status(401);
opts.errorHandler(401, 'Unauthorized.', request, response, e);
return;
}
if (e instanceof ForbiddenError) {
response.status(403);
opts.errorHandler(403, 'Forbidden.', request, response, e);
return;
}
response.locals.debug('Error: %o', e);
response.status(500);
opts.errorHandler(500, 'Internal server error.', request, response, e);
return;
}
next();
}
app.use(authenticate);
app.use(async (request, response, next) => {
let ended = false;
await catchErrors(async () => {
for (let plugin of response.locals.plugins) {
if ('afterAuth' in plugin && plugin.afterAuth) {
const result = await plugin.afterAuth(request, response);
if (result === false) {
ended = true;
}
}
}
}, async (code, message, error) => {
await opts.errorHandler(code, message, request, response, error);
ended = true;
})();
if (!ended) {
next();
}
});
async function unauthenticate(request, response, next) {
response.on('close', async () => {
try {
await response.locals.authenticator.cleanAuthentication(request, response);
}
catch (e) {
response.locals.debug('Error during authentication cleanup: %o', e);
}
});
next();
}
app.use(unauthenticate);
async function addServerHeader(_request, response, next) {
response.set({
Server: `${pkg.name}/${pkg.version}`,
});
next();
}
app.use(addServerHeader);
async function checkRequestPath(request, response, next) {
const splitPath = request.path.split('/');
if (splitPath.includes('..') || splitPath.includes('.')) {
response.status(400);
opts.errorHandler(400, 'Bad request.', request, response);
return;
}
next();
}
app.use(checkRequestPath);
const runMethodCatchErrors = (method) => {
return catchErrors(method.run.bind(method), async (code, message, error, [request, response]) => {
await opts.errorHandler(code, message, request, response, error);
});
};
app.use(loadAdapter);
app.use(async (request, response, next) => {
let ended = false;
await catchErrors(async () => {
for (let plugin of response.locals.plugins) {
if ('begin' in plugin && plugin.begin) {
const result = await plugin.begin(request, response);
if (result === false) {
ended = true;
}
}
}
}, async (code, message, error) => {
await opts.errorHandler(code, message, request, response, error);
ended = true;
})();
if (!ended) {
next();
}
});
app.use(async (request, response, next) => {
response.on('close', async () => {
await catchErrors(async () => {
for (let plugin of response.locals.plugins) {
if ('close' in plugin && plugin.close) {
await plugin.close(request, response);
}
}
}, async (code, message, error) => {
await opts.errorHandler(code, message, request, response, error);
})();
});
next();
});
app.options('/{*splat}', runMethodCatchErrors(new OPTIONS(opts)));
app.get('/{*splat}', runMethodCatchErrors(new GET_HEAD(opts)));
app.head('/{*splat}', runMethodCatchErrors(new GET_HEAD(opts)));
app.put('/{*splat}', runMethodCatchErrors(new PUT(opts)));
app.delete('/{*splat}', runMethodCatchErrors(new DELETE(opts)));
app.copy('/{*splat}', runMethodCatchErrors(new COPY(opts)));
app.move('/{*splat}', runMethodCatchErrors(new MOVE(opts)));
app.mkcol('/{*splat}', runMethodCatchErrors(new MKCOL(opts)));
app.lock('/{*splat}', runMethodCatchErrors(new LOCK(opts)));
app.unlock('/{*splat}', runMethodCatchErrors(new UNLOCK(opts)));
const propfind = runMethodCatchErrors(new PROPFIND(opts));
const proppatch = runMethodCatchErrors(new PROPPATCH(opts));
app.all('/{*splat}', async (request, response) => {
switch (request.method) {
case 'PROPFIND':
await propfind(request, response);
break;
case 'PROPPATCH':
await proppatch(request, response);
break;
default:
const run = catchErrors(async () => {
const pluginMethod = new Method(opts);
let { url } = pluginMethod.getRequestData(request, response);
if (await pluginMethod.runPlugins(request, response, 'beginMethod', {
method: request.method,
url,
})) {
return;
}
if (await pluginMethod.runPlugins(request, response, 'preMethod', {
method: request.method,
url,
})) {
return;
}
const MethodClass = response.locals.adapter.getMethod(request.method);
const method = new MethodClass(opts);
if (await method.runPlugins(request, response, 'beforeMethod', {
method,
url,
})) {
return;
}
await method.run(request, response);
await method.runPlugins(request, response, 'afterMethod', {
method,
url,
});
}, async (code, message, error) => {
await opts.errorHandler(code, message, request, response, error);
});
await run();
break;
}
});
debug('Nephele server set up. Ready to start listening.');
return app;
}
//# sourceMappingURL=createServer.js.map