http.ts 5.2 KB
Newer Older
A
Asher 已提交
1
import { field, logger } from "@coder/logger"
A
Asher 已提交
2 3 4
import * as express from "express"
import * as expressCore from "express-serve-static-core"
import qs from "qs"
A
Asher 已提交
5 6
import safeCompare from "safe-compare"
import { HttpCode, HttpError } from "../common/http"
A
Asher 已提交
7
import { normalize, Options } from "../common/util"
A
Asher 已提交
8
import { AuthType } from "./cli"
A
Asher 已提交
9 10
import { commit, rootPath } from "./constants"
import { hash } from "./util"
A
Asher 已提交
11 12

/**
A
Asher 已提交
13
 * Replace common variable strings in HTML templates.
A
Asher 已提交
14
 */
A
Asher 已提交
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
export const replaceTemplates = <T extends object>(
  req: express.Request,
  content: string,
  extraOpts?: Omit<T, "base" | "csStaticBase" | "logLevel">,
): string => {
  const base = relativeRoot(req)
  const options: Options = {
    base,
    csStaticBase: base + "/static/" + commit + rootPath,
    logLevel: logger.level,
    ...extraOpts,
  }
  return content
    .replace(/{{TO}}/g, (typeof req.query.to === "string" && req.query.to) || "/")
    .replace(/{{BASE}}/g, options.base)
    .replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase)
    .replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`)
A
Asher 已提交
32 33 34
}

/**
A
Asher 已提交
35
 * Throw an error if not authorized. Call `next` if provided.
A
Asher 已提交
36
 */
A
Asher 已提交
37
export const ensureAuthenticated = (req: express.Request, _?: express.Response, next?: express.NextFunction): void => {
A
Asher 已提交
38 39
  if (!authenticated(req)) {
    throw new HttpError("Unauthorized", HttpCode.Unauthorized)
40
  }
A
Asher 已提交
41 42 43
  if (next) {
    next()
  }
A
Asher 已提交
44 45
}

A
Asher 已提交
46 47 48 49 50 51 52 53 54
/**
 * Return true if authenticated via cookies.
 */
export const authenticated = (req: express.Request): boolean => {
  switch (req.args.auth) {
    case AuthType.None:
      return true
    case AuthType.Password:
      // The password is stored in the cookie after being hashed.
S
SPGoding 已提交
55 56
      return !!(
        req.cookies.key &&
57 58
        (req.args["hashed-password"]
          ? safeCompare(req.cookies.key, req.args["hashed-password"])
S
SPGoding 已提交
59 60
          : req.args.password && safeCompare(req.cookies.key, hash(req.args.password)))
      )
A
Asher 已提交
61 62 63
    default:
      throw new Error(`Unsupported auth type ${req.args.auth}`)
  }
A
Asher 已提交
64 65
}

A
Asher 已提交
66 67 68 69 70 71 72 73 74 75 76 77
/**
 * Get the relative path that will get us to the root of the page. For each
 * slash we need to go up a directory. For example:
 * / => .
 * /foo => .
 * /foo/ => ./..
 * /foo/bar => ./..
 * /foo/bar/ => ./../..
 */
export const relativeRoot = (req: express.Request): string => {
  const depth = (req.originalUrl.split("?", 1)[0].match(/\//g) || []).length
  return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : ""))
A
Asher 已提交
78 79
}

A
Asher 已提交
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
/**
 * Redirect relatively to `/${to}`. Query variables will be preserved.
 * `override` will merge with the existing query (use `undefined` to unset).
 */
export const redirect = (
  req: express.Request,
  res: express.Response,
  to: string,
  override: expressCore.Query = {},
): void => {
  const query = Object.assign({}, req.query, override)
  Object.keys(override).forEach((key) => {
    if (typeof override[key] === "undefined") {
      delete query[key]
    }
  })

  const relativePath = normalize(`${relativeRoot(req)}/${to}`, true)
  const queryString = qs.stringify(query)
  const redirectPath = `${relativePath}${queryString ? `?${queryString}` : ""}`
  logger.debug(`redirecting from ${req.originalUrl} to ${redirectPath}`)
  res.redirect(redirectPath)
102 103
}

A
Asher 已提交
104
/**
A
Asher 已提交
105
 * Get the value that should be used for setting a cookie domain. This will
106 107 108
 * 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).
A
Asher 已提交
109
 */
A
Asher 已提交
110 111 112
export const getCookieDomain = (host: string, proxyDomains: string[]): string | undefined => {
  const idx = host.lastIndexOf(":")
  host = idx !== -1 ? host.substring(0, idx) : host
113 114
  // If any of these are true we will still set cookies but without an explicit
  // `Domain` attribute on the cookie.
A
Asher 已提交
115
  if (
116
    // The host can be be blank or missing so there's nothing we can set.
A
Asher 已提交
117 118
    !host ||
    // IP addresses can't have subdomains so there's no value in setting the
119 120
    // domain for them. Assume that anything with a : is ipv6 (valid domain name
    // characters are alphanumeric or dashes)...
A
Asher 已提交
121
    host.includes(":") ||
122
    // ...and that anything entirely numbers and dots is ipv4 (currently tlds
A
Asher 已提交
123 124
    // cannot be entirely numbers).
    !/[^0-9.]/.test(host) ||
125 126
    // localhost subdomains don't seem to work at all (browser bug?). A cookie
    // set at dev.localhost cannot be read by 8080.dev.localhost.
A
Asher 已提交
127
    host.endsWith(".localhost") ||
128 129 130 131 132 133 134 135
    // 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(".")
A
Asher 已提交
136 137 138 139 140 141 142 143 144 145 146 147
  ) {
    logger.debug("no valid cookie doman", field("host", host))
    return undefined
  }

  proxyDomains.forEach((domain) => {
    if (host.endsWith(domain) && domain.length < host.length) {
      host = domain
    }
  })

  logger.debug("got cookie doman", field("host", host))
A
Asher 已提交
148
  return host || undefined
A
Asher 已提交
149
}