UNPKG

@webda/shell

Version:

Deploy a Webda app or configure it

354 lines 12.7 kB
import { HttpContext, ResourceService, WaitFor, WaitLinearDelay, Core as Webda, WebdaError } from "@webda/core"; import { serialize as cookieSerialize } from "cookie"; import * as http from "http"; import { createChecker } from "is-in-subnet"; export var ServerStatus; (function (ServerStatus) { ServerStatus["Stopped"] = "STOPPED"; ServerStatus["Stopping"] = "STOPPING"; ServerStatus["Starting"] = "STARTING"; ServerStatus["Started"] = "STARTED"; })(ServerStatus || (ServerStatus = {})); export class WebdaServer extends Webda { constructor() { super(...arguments); this.serverStatus = ServerStatus.Stopped; } /** * Toggle DevMode * * In DevMode CORS is allowed * @param devMode */ setDevMode(devMode) { this.devMode = devMode; if (devMode) { this.output("Dev mode activated : wildcard CORS enabled"); } } /** * Return true if devMode is enabled * @returns */ isDebug() { return this.devMode ?? false; } output(...args) { this.log("INFO", ...args); } /** * Check if a proxy is a trusted proxy * @param ip * @returns */ isProxyTrusted(ip) { // ipv4 mapped to v6 return this.subnetChecker(ip); } /** * Return a Context object based on a request * @param req to initiate object from * @param res to add for body * @returns */ async getContextFromRequest(req, res) { // Wait for Webda to be ready await this.init(); // Handle reverse proxy let vhost = req.headers.host.match(/:/g) ? req.headers.host.slice(0, req.headers.host.indexOf(":")) : req.headers.host; if ((req.headers["x-forwarded-for"] || req.headers["x-forwarded-host"] || req.headers["x-forwarded-proto"] || req.headers["x-forwarded-port"]) && !this.isProxyTrusted(req.socket.remoteAddress)) { // Do not even let the query go through this.log("WARN", `X-Forwarded-* headers set from an unknown source: ${req.socket.remoteAddress}`); res.writeHead(400); return; } // Might want to add some whitelisting if (req.headers["x-forwarded-host"] !== undefined) { vhost = req.headers["x-forwarded-host"]; } let protocol = "http"; if (req.headers["x-forwarded-proto"] !== undefined) { protocol = req.headers["x-forwarded-proto"]; } let method = req.method; let port; if (req.socket && req.socket.address()) { port = req.socket.address().port; } if (req.headers["x-forwarded-port"] !== undefined) { port = parseInt(req.headers["x-forwarded-port"]); } else if (req.headers["x-forwarded-proto"] !== undefined) { // GCP send a proto without port so fallback on default port port = protocol === "http" ? 80 : 443; } let httpContext = new HttpContext(vhost, method, req.url, protocol, port, req.headers); httpContext.setClientIp(httpContext.getUniqueHeader("x-forwarded-for", req.socket.remoteAddress)); // https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods if (["PUT", "PATCH", "POST", "DELETE"].includes(method)) { httpContext.setBody(req); } return this.newWebContext(httpContext, res, true); } /** * Manage the request * * @param req * @param res * @param next */ async handleRequest(req, res) { let ctx; let emitResult = false; try { res.on("error", this.log.bind(this, "ERROR")); ctx = await this.getContextFromRequest(req, res); // @ts-ignore req.webdaContext = ctx; } catch (err) { this.log("ERROR", err); } // If no context, we are in error if (!ctx) { if (res.statusCode < 400) { res.writeHead(500); } res.end(); return; } try { let httpContext = ctx.getHttpContext(); if (!this.updateContextWithRoute(ctx) && httpContext.getMethod() !== "OPTIONS") { // Static served should not be reachable via XHR if (httpContext.getMethod() !== "GET" || !this.resourceService) { ctx.writeHead(404); return; } // Try to serve static resource await ctx.init(); ctx.getParameters()["resource"] = ctx.getHttpContext().getUrl().substring(1); await this.resourceService._serve(ctx); return; } await ctx.init(); const origin = (req.headers.Origin || req.headers.origin); // Set predefined headers for CORS if (this.devMode || !origin || (await this.checkCORSRequest(ctx))) { ctx.setHeader("Access-Control-Allow-Origin", origin); } else { throw new WebdaError.Unauthorized(`CORS denied from ${origin}`); } // Verify if request is authorized if (!(await this.checkRequest(ctx))) { this.log("WARN", "Request refused"); throw new WebdaError.Forbidden("Request refused"); } if (httpContext.getProtocol() === "https:") { // Add the HSTS header ctx.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload"); } // Add correct headers for X-scripting if (req.headers["x-forwarded-server"] === undefined) { if (this.devMode && req.headers["origin"]) { ctx.setHeader("Access-Control-Allow-Origin", req.headers["origin"]); } } // Handle OPTIONS if (req.method === "OPTIONS") { let routes = this.router.getRouteMethodsFromUrl(httpContext.getRelativeUri()); // OPTIONS on unknown route should return 404 if (routes.length === 0) { ctx.writeHead(404); return; } routes.push("OPTIONS"); ctx.setHeader("Access-Control-Allow-Credentials", "true"); ctx.setHeader("Access-Control-Allow-Headers", req.headers["access-control-request-headers"] || "content-type"); ctx.setHeader("Access-Control-Allow-Methods", routes.join(",")); ctx.setHeader("Access-Control-Max-Age", 86400); ctx.setHeader("Allow", routes.join(",")); ctx.writeHead(200); return; } await this.emitSync("Webda.Request", { context: ctx }); ctx.setHeader("Access-Control-Allow-Credentials", "true"); emitResult = true; this.log("DEBUG", "Execute", ctx.getHttpContext().getMethod(), ctx.getHttpContext().getUrl()); await ctx.execute(); await this.emitSync("Webda.Result", { context: ctx }); } catch (err) { err = typeof err === "number" ? new WebdaError.HttpError("Wrapped", err) : err; if (err instanceof WebdaError.HttpError) { ctx.statusCode = err.getResponseCode(); // Handle redirect if (err instanceof WebdaError.Redirect) { ctx.setHeader("Location", err.location); } this.log("TRACE", `${err.getResponseCode()}: ${err.message}`); } else { ctx.statusCode = 500; } // If we have a context, we can send the error emitResult && (await this.emitSync("Webda.Result", { context: ctx })); if (ctx.statusCode >= 500) { this.log("ERROR", err); } } finally { await ctx.end(); } } /** * @override */ flushHeaders(ctx) { if (ctx.hasFlushedHeaders()) { return; } ctx.setFlushedHeaders(true); const res = ctx.getStream(); const headers = ctx.getResponseHeaders(); const cookies = ctx.getResponseCookies(); try { for (let i in cookies) { res.setHeader("Set-Cookie", cookieSerialize(cookies[i].name, cookies[i].value, cookies[i].options)); } res.writeHead(ctx.statusCode, headers); } catch (err) { this.log("ERROR", err); } } /** * @override */ flush(ctx) { const res = ctx._stream; const body = ctx.getResponseBody(); if (body !== undefined && body) { res.write(body); } } /** * @override */ async init() { var _a; // Avoid reinit everytime if (this._init) { return this._init; } await super.init(); (_a = this.getGlobalParams()).trustedProxies ?? (_a.trustedProxies = "127.0.0.1"); if (typeof this.getGlobalParams().trustedProxies === "string") { this.getGlobalParams().trustedProxies = this.getGlobalParams().trustedProxies.split(","); } this.subnetChecker = createChecker(this.getGlobalParams().trustedProxies.map(n => (n.indexOf("/") < 0 ? `${n.trim()}/32` : n.trim()))); if (this.getGlobalParams().website && this.getGlobalParams().website.path && !this.resourceService) { this.resourceService = await new ResourceService(this, "websiteResource", { folder: this.getAppPath(this.getGlobalParams().website.path) }) .resolve() .init(); } } /** * Start listening to serve request * * @param port to listen to * @param bind address to bind */ async serve(port = 18080, bind = undefined) { this.serverStatus = ServerStatus.Starting; try { this.http = http .createServer(async (req, res) => { this.handleRequest(req, res).finally(() => { res.end(); }); }) .listen(port, bind); process.on("SIGINT", this.onSIGINT.bind(this)); this.http.on("close", () => { this.serverStatus = ServerStatus.Stopped; }); this.http.on("error", err => { this.log("ERROR", err.message); this.serverStatus = ServerStatus.Stopped; }); this.emit("Webda.Init.Http", this.http); this.logger.logTitle(`Server running at http://0.0.0.0:${port}`); this.serverStatus = ServerStatus.Started; } catch (err) { this.log("ERROR", err); this.serverStatus = ServerStatus.Stopped; throw err; } } /** * Close server and exit */ onSIGINT() { if (this.http) { this.http.close(); } } /** * Get server status */ getServerStatus() { return this.serverStatus; } /** * Wait for the server to be in a desired state * * @param status to wait for * @param timeout max number of ms to wait for */ async waitForStatus(status, timeout = 60000) { return WaitFor(async (resolve) => { if (this.getServerStatus() === status) { resolve(); return true; } }, timeout / 1000, "Waiting for server status", undefined, WaitLinearDelay(1000)); } async stopHttp() { if (this.http && this.serverStatus === ServerStatus.Starting) { await this.waitForStatus(ServerStatus.Started); } this.serverStatus = ServerStatus.Stopping; if (this.http) { await new Promise((resolve, reject) => { this.http.close(err => { if (err) { reject(err); } else { resolve(); } }); }); } this.serverStatus = ServerStatus.Stopped; this.http = undefined; } /** * Stop the http server */ async stop() { await Promise.all([super.stop(), this.stopHttp()]); } } //# sourceMappingURL=http.js.map