liefern
Version:
Node Webserver without dependencies
314 lines • 13.4 kB
JavaScript
import * as https from 'https';
import { readFileSync, existsSync } from 'node:fs';
import * as http from 'node:http';
import { cors } from './handleCORS.js';
import { handleNonStatic } from './handleNonStatic.js';
import { body } from './utils/bodyHelper.js';
import { getRequestHost } from './utils/getRequestHost.js';
import { createLogger } from './utils/logger.js';
import { adressInUseLogMessage, adressNotAvailableLogMessage, gracefullyShutdownLogMessage, unhandledStartUpErrorMessage, } from './messages.js';
import { regexUrlMatcher } from './utils/regexUrlMatcher.js';
import { handleStatic } from './handleStatic.js';
import { internalServerError, notFound } from './utils/sendHelper.js';
import { isAbsolutPath } from './utils/isAbsolutPath.js';
import { errorHasCode } from './utils/typeCuards.js';
import { createSecureContext } from 'node:tls';
export const Liefern = (options = {}) => {
const _static = [];
const _middlewares = [];
const _get = [];
const _head = [];
const _put = [];
const _post = [];
const _delete = [];
const _patch = [];
const _connect = [];
const _trace = [];
const _options = [];
let _host = 'localhost';
const _name = options.name ?? 'Liefern.io';
const logger = options.logger ?? createLogger(_name);
const urlMatcher = options.urlMatcher ?? regexUrlMatcher;
const _corsDefaults = {
allowMethods: ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH', 'OPTIONS'],
maxAge: 5,
};
const listener = async (request, response) => {
const requestHost = getRequestHost(request);
const preferedConnectionType = 'close';
try {
response.setHeader('Connection', preferedConnectionType);
const bodyData = await body(request);
await handleNonStatic(request, response, urlMatcher.handler, _middlewares, _get, _head, _post, _put, _delete, _patch, _connect, _trace, _options, bodyData);
if (!response.writableEnded) {
await handleStatic(request, response, _static, bodyData);
}
if (!response.writableEnded) {
notFound(response);
}
}
catch (e) {
logger.error(e);
if (!response.writableEnded) {
internalServerError(response);
}
}
finally {
// maybe this is not neccessary because node handles this by itself for header "Connection: close"
if (preferedConnectionType === 'close') {
response.socket?.destroy();
}
logger.info(requestHost, response.statusCode, request.method, request.url);
}
};
const protocol = options.https ? 'https' : 'http';
if (options.https) {
console.log('TSL option detected - starting https');
}
const httpsCacheCtx = {};
const httpServer = options.https
? https.createServer({
cert: 'cert' in options.https &&
typeof options.https.cert === 'string' &&
existsSync(options.https.cert)
? readFileSync(options.https.cert)
: undefined,
key: 'key' in options.https &&
typeof options.https.key === 'string' &&
existsSync(options.https.key)
? readFileSync(options.https.key)
: undefined,
SNICallback: typeof options.https === 'object' &&
typeof options.https !== 'undefined' &&
(!('cert' in options.https) ||
typeof options.https.cert !== 'string') &&
(!('key' in options.https) || typeof options.https.key !== 'string')
? function (domain, cb) {
if (httpsCacheCtx[domain]) {
cb(null, httpsCacheCtx[domain]);
return;
}
const map = options.https;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (!map[domain]) {
const msg = `no certs specified for domain ${domain}`;
console.warn(msg);
return cb(new Error(msg));
}
const cert = map[domain].cert;
const key = map[domain].key;
if (!existsSync(key)) {
const msg = `no key file exists for domain ${domain} - ${key}`;
console.warn(msg);
return cb(new Error(msg));
}
if (!existsSync(cert)) {
const msg = `no cert file exists for domain ${domain} - ${cert}`;
console.warn(msg);
return cb(new Error(msg));
}
const ctx = createSecureContext({
cert: readFileSync(cert),
key: readFileSync(key),
}).context;
httpsCacheCtx[domain] = ctx;
cb(null, ctx);
}
: undefined,
}, listener)
: http.createServer(listener);
const start = async (port = 1337, host = 'localhost') => {
_host = host;
return new Promise((resolve, reject) => {
const startUpErrorHandler = async (e) => {
if (!errorHasCode(e)) {
logger.error(unhandledStartUpErrorMessage);
reject(unhandledStartUpErrorMessage);
return;
}
if (e.code === 'EADDRINUSE') {
logger.error(adressInUseLogMessage);
reject(adressInUseLogMessage);
}
else if (e.code === 'EADDRNOTAVAIL') {
logger.error(adressNotAvailableLogMessage);
reject(`${adressNotAvailableLogMessage}`);
}
else {
logger.error(unhandledStartUpErrorMessage);
reject(unhandledStartUpErrorMessage);
}
};
httpServer.addListener('error', startUpErrorHandler);
httpServer.listen(port, undefined, undefined, () => {
httpServer.removeListener('error', startUpErrorHandler);
logger.info('started at', `${protocol}://${_host}:${port}`);
process.addListener('SIGTERM', sigtermHandler);
process.addListener('SIGINT', sigintHandler);
process.addListener('SIGBREAK', sigbreakHandler);
process.addListener('SIGQUIT', sigquitHandler);
resolve();
});
});
};
const stop = async () => {
return new Promise((resolve) => {
process.removeListener('SIGTERM', sigtermHandler);
process.removeListener('SIGINT', sigintHandler);
process.removeListener('SIGBREAK', sigbreakHandler);
process.removeListener('SIGQUIT', sigquitHandler);
httpServer.close(() => {
logger.info('shut down');
resolve();
});
});
};
const sigtermHandler = async () => {
logger.info('[SIGTERM]', gracefullyShutdownLogMessage);
await stop();
};
const sigintHandler = async () => {
logger.info('[SIGINT]', gracefullyShutdownLogMessage);
await stop();
};
const sigbreakHandler = async () => {
logger.info('[SIGBREAK]', gracefullyShutdownLogMessage);
await stop();
};
const sigquitHandler = async () => {
logger.info('[SIGQUIT]', gracefullyShutdownLogMessage);
await stop();
};
return {
get logger() {
return logger;
},
get httpServer() {
return httpServer;
},
useWithUrl: (urlPattern, middleware) => {
_middlewares.push(['*', urlPattern, middleware]);
},
use: (middleware) => {
_middlewares.push(['*', urlMatcher.chatchAllIndicator, middleware]);
},
cors: (config) => {
_middlewares.unshift([
'*',
urlMatcher.chatchAllIndicator,
cors({
..._corsDefaults,
...config,
}),
]);
},
all: (urlPattern, ...controller) => {
_get.push(['*', urlPattern, ...controller]);
_head.push(['*', urlPattern, ...controller]);
_post.push(['*', urlPattern, ...controller]);
_put.push(['*', urlPattern, ...controller]);
_delete.push(['*', urlPattern, ...controller]);
_patch.push(['*', urlPattern, ...controller]);
_connect.push(['*', urlPattern, ...controller]);
_trace.push(['*', urlPattern, ...controller]);
_options.push(['*', urlPattern, ...controller]);
},
get: (urlPattern, ...controller) => {
_get.push(['*', urlPattern, ...controller]);
},
head: (urlPattern, ...controller) => {
_head.push(['*', urlPattern, ...controller]);
},
post: (urlPattern, ...controller) => {
_post.push(['*', urlPattern, ...controller]);
},
put: (urlPattern, ...controller) => {
_put.push(['*', urlPattern, ...controller]);
},
delete: (urlPattern, ...controller) => {
_delete.push(['*', urlPattern, ...controller]);
},
patch: (urlPattern, ...controller) => {
_patch.push(['*', urlPattern, ...controller]);
},
connect: (urlPattern, ...controller) => {
_connect.push(['*', urlPattern, ...controller]);
},
trace: (urlPattern, ...controller) => {
_trace.push(['*', urlPattern, ...controller]);
},
options: (urlPattern, ...controller) => {
_options.push(['*', urlPattern, ...controller]);
},
// with domain
useWithUrlOnlyOnDomain: (domain, urlPattern, middleware) => {
_middlewares.push([domain, urlPattern, middleware]);
},
useOnlyOnDomain: (domain, middleware) => {
_middlewares.push([domain, urlMatcher.chatchAllIndicator, middleware]);
},
corsOnlyOnDomain: (domain, config) => {
_middlewares.unshift([
domain,
urlMatcher.chatchAllIndicator,
cors({
..._corsDefaults,
...config,
}),
]);
},
allOnlyOnDomain: (domain, urlPattern, ...controller) => {
_get.push([domain, urlPattern, ...controller]);
_head.push([domain, urlPattern, ...controller]);
_post.push([domain, urlPattern, ...controller]);
_put.push([domain, urlPattern, ...controller]);
_delete.push([domain, urlPattern, ...controller]);
_patch.push([domain, urlPattern, ...controller]);
_connect.push([domain, urlPattern, ...controller]);
_trace.push([domain, urlPattern, ...controller]);
_options.push([domain, urlPattern, ...controller]);
},
getOnlyOnDomain: (domain, urlPattern, ...controller) => {
_get.push([domain, urlPattern, ...controller]);
},
headOnlyOnDomain: (domain, urlPattern, ...controller) => {
_head.push([domain, urlPattern, ...controller]);
},
postOnlyOnDomain: (domain, urlPattern, ...controller) => {
_post.push([domain, urlPattern, ...controller]);
},
putOnlyOnDomain: (domain, urlPattern, ...controller) => {
_put.push([domain, urlPattern, ...controller]);
},
deleteOnlyOnDomain: (domain, urlPattern, ...controller) => {
_delete.push([domain, urlPattern, ...controller]);
},
patchOnlyOnDomain: (domain, urlPattern, ...controller) => {
_patch.push([domain, urlPattern, ...controller]);
},
connectOnlyOnDomain: (domain, urlPattern, ...controller) => {
_connect.push([domain, urlPattern, ...controller]);
},
traceOnlyOnDomain: (domain, urlPattern, ...controller) => {
_trace.push([domain, urlPattern, ...controller]);
},
optionsOnlyOnDomain: (domain, urlPattern, ...controller) => {
_options.push([domain, urlPattern, ...controller]);
},
start,
stop,
static: (urlPattern, absolutPath, ...middleware) => {
if (!isAbsolutPath(absolutPath)) {
return logger.error(`could not add static route, because path to directory is not absolute "${absolutPath}"`);
}
_static.push(['*', urlPattern, absolutPath, ...middleware]);
},
staticOnlyOnDomain: (domain, urlPattern, absolutPath, ...middleware) => {
if (!isAbsolutPath(absolutPath)) {
return logger.error(`could not add static route, because path to directory is not absolute "${absolutPath}"`);
}
_static.push([domain, urlPattern, absolutPath, ...middleware]);
},
};
};
//# sourceMappingURL=index.js.map