UNPKG

code-server

Version:

Run VS Code on a remote server.

362 lines • 15.3 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.self = exports.getCookieOptions = exports.getCookieDomain = exports.redirect = exports.constructRedirectPath = exports.relativeRoot = exports.authenticated = exports.ensureAuthenticated = exports.proxyEnabled = exports.ensureProxyEnabled = exports.replaceTemplates = exports.createClientConfiguration = void 0; exports.disposer = disposer; exports.ensureOrigin = ensureOrigin; exports.authenticateOrigin = authenticateOrigin; exports.getHost = getHost; const logger_1 = require("@coder/logger"); const qs_1 = __importDefault(require("qs")); const http_1 = require("../common/http"); const util_1 = require("../common/util"); const cli_1 = require("./cli"); const constants_1 = require("./constants"); const util_2 = require("./util"); const createClientConfiguration = (req) => { const base = (0, exports.relativeRoot)(req.originalUrl); return { base, csStaticBase: base + "/_static", codeServerVersion: constants_1.version, }; }; exports.createClientConfiguration = createClientConfiguration; /** * Replace common variable strings in HTML templates. */ const replaceTemplates = (req, content, extraOpts) => { const serverOptions = Object.assign(Object.assign({}, (0, exports.createClientConfiguration)(req)), extraOpts); return content .replace(/{{TO}}/g, (typeof req.query.to === "string" && (0, util_2.escapeHtml)(req.query.to)) || "/") .replace(/{{BASE}}/g, serverOptions.base) .replace(/{{CS_STATIC_BASE}}/g, serverOptions.csStaticBase) .replace("{{OPTIONS}}", () => (0, util_2.escapeJSON)(serverOptions)); }; exports.replaceTemplates = replaceTemplates; /** * Throw an error if proxy is not enabled. Call `next` if provided. */ const ensureProxyEnabled = (req, _, next) => { if (!(0, exports.proxyEnabled)(req)) { throw new http_1.HttpError("Forbidden", http_1.HttpCode.Forbidden); } if (next) { next(); } }; exports.ensureProxyEnabled = ensureProxyEnabled; /** * Return true if proxy is enabled. */ const proxyEnabled = (req) => { return !req.args["disable-proxy"]; }; exports.proxyEnabled = proxyEnabled; /** * Throw an error if not authorized. Call `next` if provided. */ const ensureAuthenticated = (req, _, next) => __awaiter(void 0, void 0, void 0, function* () { const isAuthenticated = yield (0, exports.authenticated)(req); if (!isAuthenticated) { throw new http_1.HttpError("Unauthorized", http_1.HttpCode.Unauthorized); } if (next) { next(); } }); exports.ensureAuthenticated = ensureAuthenticated; /** * Return true if authenticated via cookies. */ const authenticated = (req) => __awaiter(void 0, void 0, void 0, function* () { switch (req.args.auth) { case cli_1.AuthType.None: { return true; } case cli_1.AuthType.Password: { // The password is stored in the cookie after being hashed. const hashedPasswordFromArgs = req.args["hashed-password"]; const passwordMethod = (0, util_2.getPasswordMethod)(hashedPasswordFromArgs); const isCookieValidArgs = { passwordMethod, cookieKey: (0, util_2.sanitizeString)(req.cookies[http_1.CookieKeys.Session]), passwordFromArgs: req.args.password || "", hashedPasswordFromArgs: req.args["hashed-password"], }; return yield (0, util_2.isCookieValid)(isCookieValidArgs); } default: { throw new Error(`Unsupported auth type ${req.args.auth}`); } } }); exports.authenticated = authenticated; /** * Get the relative path that will get us to the root of the page. For each * slash we need to go up a directory. Will not have a trailing slash. * * For example: * * / => . * /foo => . * /foo/ => ./.. * /foo/bar => ./.. * /foo/bar/ => ./../.. * * All paths must be relative in order to work behind a reverse proxy since we * we do not know the base path. Anything that needs to be absolute (for * example cookies) must get the base path from the frontend. * * All relative paths must be prefixed with the relative root to ensure they * work no matter the depth at which they happen to appear. * * For Express `req.originalUrl` should be used as they remove the base from the * standard `url` property making it impossible to get the true depth. */ const relativeRoot = (originalUrl) => { const depth = (originalUrl.split("?", 1)[0].match(/\//g) || []).length; return (0, util_1.normalize)("./" + (depth > 1 ? "../".repeat(depth - 1) : "")); }; exports.relativeRoot = relativeRoot; /** * A helper function to construct a redirect path based on * an Express Request, query and a path to redirect to. * * Redirect path is relative to `/${to}`. */ const constructRedirectPath = (req, query, to) => { const relativePath = (0, util_1.normalize)(`${(0, exports.relativeRoot)(req.originalUrl)}/${to}`, true); // %2f or %2F are both equalivent to an encoded slash / const queryString = qs_1.default.stringify(query).replace(/%2[fF]/g, "/"); const redirectPath = `${relativePath}${queryString ? `?${queryString}` : ""}`; return redirectPath; }; exports.constructRedirectPath = constructRedirectPath; /** * Redirect relatively to `/${to}`. Query variables on the current URI will be * preserved. `to` should be a simple path without any query parameters * `override` will merge with the existing query (use `undefined` to unset). */ const redirect = (req, res, to, override = {}) => { const query = Object.assign({}, req.query, override); Object.keys(override).forEach((key) => { if (typeof override[key] === "undefined") { delete query[key]; } }); const redirectPath = (0, exports.constructRedirectPath)(req, query, to); logger_1.logger.debug(`redirecting from ${req.originalUrl} to ${redirectPath}`); res.redirect(redirectPath); }; exports.redirect = redirect; /** * Get the value that should be used for setting a cookie domain. This will * allow the user to authenticate once no matter what sub-domain they use to log * in. This will use the highest level proxy domain (e.g. `coder.com` over * `test.coder.com` if both are specified). */ const getCookieDomain = (host, proxyDomains) => { const idx = host.lastIndexOf(":"); host = idx !== -1 ? host.substring(0, idx) : host; // If any of these are true we will still set cookies but without an explicit // `Domain` attribute on the cookie. if ( // The host can be be blank or missing so there's nothing we can set. !host || // IP addresses can't have subdomains so there's no value in setting the // domain for them. Assume that anything with a : is ipv6 (valid domain name // characters are alphanumeric or dashes)... host.includes(":") || // ...and that anything entirely numbers and dots is ipv4 (currently tlds // cannot be entirely numbers). !/[^0-9.]/.test(host) || // localhost subdomains don't seem to work at all (browser bug?). A cookie // set at dev.localhost cannot be read by 8080.dev.localhost. host.endsWith(".localhost") || // Domains without at least one dot (technically two since domain.tld will // become .domain.tld) are considered invalid according to the spec so don't // set the domain for them. In my testing though localhost is the only // problem (the browser just doesn't store the cookie at all). localhost has // an additional problem which is that a reverse proxy might give // code-server localhost even though the domain is really domain.tld (by // default NGINX does this). !host.includes(".")) { logger_1.logger.debug("no valid cookie domain", (0, logger_1.field)("host", host)); return undefined; } proxyDomains.forEach((domain) => { if (host.endsWith(domain) && domain.length < host.length) { host = domain; } }); logger_1.logger.debug("got cookie domain", (0, logger_1.field)("host", host)); return host || undefined; }; exports.getCookieDomain = getCookieDomain; /** * Return a function capable of fully disposing an HTTP server. */ function disposer(server) { const sockets = new Set(); let cleanupTimeout; server.on("connection", (socket) => { sockets.add(socket); socket.on("close", () => { sockets.delete(socket); if (cleanupTimeout && sockets.size === 0) { clearTimeout(cleanupTimeout); cleanupTimeout = undefined; } }); }); return () => { return new Promise((resolve, reject) => { // The whole reason we need this disposer is because close will not // actually close anything; it only prevents future connections then waits // until everything is closed. server.close((err) => { if (err) { return reject(err); } resolve(); }); // If there are sockets remaining we might need to force close them or // this promise might never resolve. if (sockets.size > 0) { // Give sockets a chance to close up shop. cleanupTimeout = setTimeout(() => { cleanupTimeout = undefined; for (const socket of sockets.values()) { console.warn("a socket was left hanging"); socket.destroy(); } }, 1000); } }); }; } /** * Get the options for setting a cookie. The options must be identical for * setting and unsetting cookies otherwise they are considered separate. */ const getCookieOptions = (req) => { // Normally we set paths relatively. However browsers do not appear to allow // cookies to be set relatively which means we need an absolute path. We // cannot be guaranteed we know the path since a reverse proxy might have // rewritten it. That means we need to get the path from the frontend. var _a, _b; // The reason we need to set the path (as opposed to defaulting to /) is to // avoid code-server instances on different sub-paths clobbering each other or // from accessing each other's tokens (and to prevent other services from // accessing code-server's tokens). // When logging in or out the request must include the href (the full current // URL of that page) and the relative path to the root as given to it by the // backend. Using these two we can determine the true absolute root. const url = new URL(req.query.base || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.base) || "/", req.query.href || ((_b = req.body) === null || _b === void 0 ? void 0 : _b.href) || "http://" + (req.headers.host || "localhost")); return { domain: (0, exports.getCookieDomain)(url.host, req.args["proxy-domain"]), path: (0, util_1.normalize)(url.pathname) || "/", sameSite: "lax", }; }; exports.getCookieOptions = getCookieOptions; /** * Return the full path to the current page, preserving any trailing slash. */ const self = (req) => { return (0, util_1.normalize)(`${req.baseUrl}${req.originalUrl.endsWith("/") ? "/" : ""}`, true); }; exports.self = self; function getFirstHeader(req, headerName) { const val = req.headers[headerName]; return Array.isArray(val) ? val[0] : val; } /** * Throw a forbidden error if origin checks fail. Call `next` if provided. */ function ensureOrigin(req, _, next) { try { authenticateOrigin(req); if (next) { next(); } } catch (error) { logger_1.logger.debug(`${error instanceof Error ? error.message : error}; blocking request to ${req.originalUrl}`); throw new http_1.HttpError("Forbidden", http_1.HttpCode.Forbidden); } } /** * Authenticate the request origin against the host. Throw if invalid. */ function authenticateOrigin(req) { // A missing origin probably means the source is non-browser. Not sure we // have a use case for this but let it through. const originRaw = getFirstHeader(req, "origin"); if (!originRaw) { return; } let origin; try { origin = new URL(originRaw).host.trim().toLowerCase(); } catch (error) { throw new Error(`unable to parse malformed origin "${originRaw}"`); } const trustedOrigins = req.args["trusted-origins"] || []; if (trustedOrigins.includes(origin) || trustedOrigins.includes("*")) { return; } const host = getHost(req); if (typeof host === "undefined") { // A missing host likely means the reverse proxy has not been configured to // forward the host which means we cannot perform the check. Emit an error // so an admin can fix the issue. logger_1.logger.error("No host headers found"); logger_1.logger.error("Are you behind a reverse proxy that does not forward the host?"); throw new Error("no host headers found"); } if (host !== origin) { throw new Error(`host "${host}" does not match origin "${origin}"`); } } /** * Get the host from headers. It will be trimmed and lowercased. */ function getHost(req) { // Honor Forwarded if present. const forwardedRaw = getFirstHeader(req, "forwarded"); if (forwardedRaw) { const parts = forwardedRaw.split(/[;,]/); for (let i = 0; i < parts.length; ++i) { const [key, value] = (0, util_2.splitOnFirstEquals)(parts[i]); if (key.trim().toLowerCase() === "host" && value) { return value.trim().toLowerCase(); } } } // Honor X-Forwarded-Host if present. Some reverse proxies will set multiple // comma-separated hosts. const xHost = getFirstHeader(req, "x-forwarded-host"); if (xHost) { const firstXHost = xHost.split(",")[0]; if (firstXHost) { return firstXHost.trim().toLowerCase(); } } const host = getFirstHeader(req, "host"); return host ? host.trim().toLowerCase() : undefined; } //# sourceMappingURL=http.js.map