UNPKG

liefern

Version:

Node Webserver without dependencies

314 lines 13.4 kB
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