index.ts 3.7 KB
Newer Older
A
Asher 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
import { logger } from "@coder/logger"
import bodyParser from "body-parser"
import cookieParser from "cookie-parser"
import { Express } from "express"
import { promises as fs } from "fs"
import http from "http"
import * as path from "path"
import * as tls from "tls"
import { HttpCode, HttpError } from "../../common/http"
import { plural } from "../../common/util"
import { AuthType, DefaultedArgs } from "../cli"
import { rootPath } from "../constants"
import { Heart } from "../heart"
import { replaceTemplates } from "../http"
import { loadPlugins } from "../plugin"
import * as domainProxy from "../proxy"
import { getMediaMime, paths } from "../util"
import * as health from "./health"
import * as login from "./login"
import * as proxy from "./proxy"
// static is a reserved keyword.
import * as _static from "./static"
import * as update from "./update"
import * as vscode from "./vscode"

declare global {
  // eslint-disable-next-line @typescript-eslint/no-namespace
  namespace Express {
    export interface Request {
      args: DefaultedArgs
      heart: Heart
    }
  }
}

/**
 * Register all routes and middleware.
 */
export const register = async (app: Express, server: http.Server, args: DefaultedArgs): Promise<void> => {
  const heart = new Heart(path.join(paths.data, "heartbeat"), async () => {
    return new Promise((resolve, reject) => {
      server.getConnections((error, count) => {
        if (error) {
          return reject(error)
        }
        logger.trace(plural(count, `${count} active connection`))
        resolve(count > 0)
      })
    })
  })

  app.disable("x-powered-by")

  app.use(cookieParser())
  app.use(bodyParser.json())
  app.use(bodyParser.urlencoded({ extended: true }))

  server.on("upgrade", () => {
    heart.beat()
  })

  app.use(async (req, res, next) => {
    heart.beat()

    // If we're handling TLS ensure all requests are redirected to HTTPS.
    // TODO: This does *NOT* work if you have a base path since to specify the
    // protocol we need to specify the whole path.
    if (args.cert && !(req.connection as tls.TLSSocket).encrypted) {
      return res.redirect(`https://${req.headers.host}${req.originalUrl}`)
    }

    // Return robots.txt.
    if (req.originalUrl === "/robots.txt") {
      const resourcePath = path.resolve(rootPath, "src/browser/robots.txt")
      res.set("Content-Type", getMediaMime(resourcePath))
      return res.send(await fs.readFile(resourcePath))
    }

    // Add common variables routes can use.
    req.args = args
    req.heart = heart

    return next()
  })

  app.use("/", domainProxy.router)
  app.use("/", vscode.router)
  app.use("/healthz", health.router)
  if (args.auth === AuthType.Password) {
    app.use("/login", login.router)
  }
  app.use("/proxy", proxy.router)
  app.use("/static", _static.router)
  app.use("/update", update.router)
  app.use("/vscode", vscode.router)

  await loadPlugins(app, args)

  app.use(() => {
    throw new HttpError("Not Found", HttpCode.NotFound)
  })

  // Handle errors.
  // TODO: The types are broken; says they're all implicitly `any`.
  app.use(async (err: any, req: any, res: any, next: any) => {
    const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html")
    res.set("Content-Type", getMediaMime(resourcePath))
    try {
      const content = await fs.readFile(resourcePath, "utf8")
      if (err.code === "ENOENT" || err.code === "EISDIR") {
        err.status = HttpCode.NotFound
      }
      res.status(err.status || 500).send(
        replaceTemplates(req, content)
          .replace(/{{ERROR_TITLE}}/g, err.status || "Error")
          .replace(/{{ERROR_HEADER}}/g, err.status || "Error")
          .replace(/{{ERROR_BODY}}/g, err.message),
      )
    } catch (error) {
      next(error)
    }
  })
}