index.ts 5.7 KB
Newer Older
A
Asher 已提交
1 2 3
import { logger } from "@coder/logger"
import bodyParser from "body-parser"
import cookieParser from "cookie-parser"
A
Asher 已提交
4
import * as express from "express"
A
Asher 已提交
5 6 7 8 9 10 11 12 13
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"
14
import { replaceTemplates, redirect } from "../http"
15
import { PluginAPI } from "../plugin"
A
Asher 已提交
16
import { getMediaMime, paths } from "../util"
A
Asher 已提交
17
import { WebsocketRequest } from "../wsRouter"
A
Anmol Sethi 已提交
18
import * as apps from "./apps"
A
Anmol Sethi 已提交
19
import * as domainProxy from "./domainProxy"
A
Asher 已提交
20 21
import * as health from "./health"
import * as login from "./login"
A
Asher 已提交
22
import * as proxy from "./pathProxy"
A
Asher 已提交
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
// 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.
 */
A
Asher 已提交
41 42 43 44 45 46
export const register = async (
  app: express.Express,
  wsApp: express.Express,
  server: http.Server,
  args: DefaultedArgs,
): Promise<void> => {
A
Asher 已提交
47 48 49 50 51 52 53 54 55 56 57
  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)
      })
    })
  })
58 59 60
  server.on("close", () => {
    heart.dispose()
  })
A
Asher 已提交
61 62

  app.disable("x-powered-by")
A
Asher 已提交
63
  wsApp.disable("x-powered-by")
A
Asher 已提交
64 65

  app.use(cookieParser())
A
Asher 已提交
66 67 68
  wsApp.use(cookieParser())

  const common: express.RequestHandler = (req, _, next) => {
69 70 71 72 73
    // /healthz|/healthz/ needs to be excluded otherwise health checks will make
    // it look like code-server is always in use.
    if (!/^\/healthz\/?$/.test(req.url)) {
      heart.beat()
    }
A
Asher 已提交
74

A
Asher 已提交
75 76 77 78 79 80 81 82 83 84 85
    // Add common variables routes can use.
    req.args = args
    req.heart = heart

    next()
  }

  app.use(common)
  wsApp.use(common)

  app.use(async (req, res, next) => {
A
Asher 已提交
86 87 88 89 90 91 92 93 94 95 96 97 98 99
    // 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))
    }

A
Asher 已提交
100
    next()
A
Asher 已提交
101 102 103
  })

  app.use("/", domainProxy.router)
A
Asher 已提交
104 105
  wsApp.use("/", domainProxy.wsRouter.router)

106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
  app.all("/proxy/(:port)(/*)?", (req, res) => {
    proxy.proxy(req, res)
  })
  wsApp.get("/proxy/(:port)(/*)?", (req, res) => {
    proxy.wsProxy(req as WebsocketRequest)
  })
  // These two routes pass through the path directly.
  // So the proxied app must be aware it is running
  // under /absproxy/<someport>/
  app.all("/absproxy/(:port)(/*)?", (req, res) => {
    proxy.proxy(req, res, {
      passthroughPath: true,
    })
  })
  wsApp.get("/absproxy/(:port)(/*)?", (req, res) => {
    proxy.wsProxy(req as WebsocketRequest, {
      passthroughPath: true,
    })
  })
125 126 127 128

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

A
Asher 已提交
129
  app.use("/", vscode.router)
A
Asher 已提交
130 131 132 133
  wsApp.use("/", vscode.wsRouter.router)
  app.use("/vscode", vscode.router)
  wsApp.use("/vscode", vscode.wsRouter.router)

A
Asher 已提交
134
  app.use("/healthz", health.router)
A
Asher 已提交
135

A
Asher 已提交
136 137
  if (args.auth === AuthType.Password) {
    app.use("/login", login.router)
138 139 140 141
  } else {
    app.all("/login", (req, res) => {
      redirect(req, res, "/", {})
    })
A
Asher 已提交
142
  }
A
Asher 已提交
143

A
Asher 已提交
144 145 146
  app.use("/static", _static.router)
  app.use("/update", update.router)

147 148 149
  const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH)
  await papi.loadPlugins()
  papi.mount(app)
150
  app.use("/api/applications", apps.router(papi))
A
Asher 已提交
151 152 153 154 155

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

A
Anmol Sethi 已提交
156
  const errorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
A
Asher 已提交
157 158 159 160 161 162 163
    if (err.code === "ENOENT" || err.code === "EISDIR") {
      err.status = HttpCode.NotFound
    }

    const status = err.status ?? err.statusCode ?? 500
    res.status(status)

A
Anmol Sethi 已提交
164 165 166 167 168
    // Assume anything that explicitly accepts text/html is a user browsing a
    // page (as opposed to an xhr request). Don't use `req.accepts()` since
    // *every* request that I've seen (in Firefox and Chromium at least)
    // includes `*/*` making it always truthy. Even for css/javascript.
    if (req.headers.accept && req.headers.accept.includes("text/html")) {
A
Asher 已提交
169 170
      const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html")
      res.set("Content-Type", getMediaMime(resourcePath))
A
Asher 已提交
171
      const content = await fs.readFile(resourcePath, "utf8")
A
Asher 已提交
172
      res.send(
A
Asher 已提交
173
        replaceTemplates(req, content)
A
Asher 已提交
174 175
          .replace(/{{ERROR_TITLE}}/g, status)
          .replace(/{{ERROR_HEADER}}/g, status)
A
Asher 已提交
176 177
          .replace(/{{ERROR_BODY}}/g, err.message),
      )
A
Anmol Sethi 已提交
178 179 180 181 182
    } else {
      res.json({
        error: err.message,
        ...(err.details || {}),
      })
A
Asher 已提交
183
    }
A
Asher 已提交
184 185 186
  }

  app.use(errorHandler)
A
Asher 已提交
187

188
  const wsErrorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
A
Asher 已提交
189
    logger.error(`${err.message} ${err.stack}`)
A
Asher 已提交
190
    ;(req as WebsocketRequest).ws.end()
A
Asher 已提交
191 192 193
  }

  wsApp.use(wsErrorHandler)
A
Asher 已提交
194
}