2020-10-21 01:05:58 +02:00
|
|
|
import { logger } from "@coder/logger"
|
|
|
|
import bodyParser from "body-parser"
|
|
|
|
import cookieParser from "cookie-parser"
|
2020-11-05 19:58:37 +01:00
|
|
|
import * as express from "express"
|
2020-10-21 01:05:58 +02:00
|
|
|
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"
|
2020-12-14 18:33:36 +01:00
|
|
|
import { replaceTemplates, redirect } from "../http"
|
2020-10-30 08:18:45 +01:00
|
|
|
import { PluginAPI } from "../plugin"
|
2020-10-21 01:05:58 +02:00
|
|
|
import { getMediaMime, paths } from "../util"
|
2020-11-05 19:58:37 +01:00
|
|
|
import { WebsocketRequest } from "../wsRouter"
|
2020-11-04 03:53:16 +01:00
|
|
|
import * as apps from "./apps"
|
2020-11-06 20:46:49 +01:00
|
|
|
import * as domainProxy from "./domainProxy"
|
2020-10-21 01:05:58 +02:00
|
|
|
import * as health from "./health"
|
|
|
|
import * as login from "./login"
|
2020-11-05 23:45:58 +01:00
|
|
|
import * as proxy from "./pathProxy"
|
2020-10-21 01:05:58 +02:00
|
|
|
// 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.
|
|
|
|
*/
|
2020-11-05 19:58:37 +01:00
|
|
|
export const register = async (
|
|
|
|
app: express.Express,
|
|
|
|
wsApp: express.Express,
|
|
|
|
server: http.Server,
|
|
|
|
args: DefaultedArgs,
|
|
|
|
): Promise<void> => {
|
2020-10-21 01:05:58 +02:00
|
|
|
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")
|
2020-11-05 19:58:37 +01:00
|
|
|
wsApp.disable("x-powered-by")
|
2020-10-21 01:05:58 +02:00
|
|
|
|
|
|
|
app.use(cookieParser())
|
2020-11-05 19:58:37 +01:00
|
|
|
wsApp.use(cookieParser())
|
|
|
|
|
2020-10-21 01:05:58 +02:00
|
|
|
app.use(bodyParser.json())
|
|
|
|
app.use(bodyParser.urlencoded({ extended: true }))
|
|
|
|
|
2020-11-05 19:58:37 +01:00
|
|
|
const common: express.RequestHandler = (req, _, next) => {
|
2020-11-18 19:19:08 +01:00
|
|
|
// /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()
|
|
|
|
}
|
2020-10-21 01:05:58 +02:00
|
|
|
|
2020-11-05 19:58:37 +01:00
|
|
|
// 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) => {
|
2020-10-21 01:05:58 +02:00
|
|
|
// 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))
|
|
|
|
}
|
|
|
|
|
2020-11-05 19:58:37 +01:00
|
|
|
next()
|
2020-10-21 01:05:58 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
app.use("/", domainProxy.router)
|
2020-11-05 19:58:37 +01:00
|
|
|
wsApp.use("/", domainProxy.wsRouter.router)
|
|
|
|
|
2020-10-21 01:05:58 +02:00
|
|
|
app.use("/", vscode.router)
|
2020-11-05 19:58:37 +01:00
|
|
|
wsApp.use("/", vscode.wsRouter.router)
|
|
|
|
app.use("/vscode", vscode.router)
|
|
|
|
wsApp.use("/vscode", vscode.wsRouter.router)
|
|
|
|
|
2020-10-21 01:05:58 +02:00
|
|
|
app.use("/healthz", health.router)
|
2020-11-05 19:58:37 +01:00
|
|
|
|
2020-10-21 01:05:58 +02:00
|
|
|
if (args.auth === AuthType.Password) {
|
|
|
|
app.use("/login", login.router)
|
2020-12-14 18:33:36 +01:00
|
|
|
} else {
|
|
|
|
app.all("/login", (req, res) => {
|
|
|
|
redirect(req, res, "/", {})
|
|
|
|
})
|
2020-10-21 01:05:58 +02:00
|
|
|
}
|
2020-11-05 19:58:37 +01:00
|
|
|
|
2020-10-21 01:05:58 +02:00
|
|
|
app.use("/proxy", proxy.router)
|
2020-11-05 19:58:37 +01:00
|
|
|
wsApp.use("/proxy", proxy.wsRouter.router)
|
|
|
|
|
2020-10-21 01:05:58 +02:00
|
|
|
app.use("/static", _static.router)
|
|
|
|
app.use("/update", update.router)
|
|
|
|
|
2020-10-30 08:18:45 +01:00
|
|
|
const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH)
|
|
|
|
await papi.loadPlugins()
|
|
|
|
papi.mount(app)
|
2020-11-04 03:14:14 +01:00
|
|
|
app.use("/api/applications", apps.router(papi))
|
2020-10-21 01:05:58 +02:00
|
|
|
|
|
|
|
app.use(() => {
|
|
|
|
throw new HttpError("Not Found", HttpCode.NotFound)
|
|
|
|
})
|
|
|
|
|
2020-11-13 21:32:47 +01:00
|
|
|
const errorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
|
2020-11-05 22:19:15 +01:00
|
|
|
if (err.code === "ENOENT" || err.code === "EISDIR") {
|
|
|
|
err.status = HttpCode.NotFound
|
|
|
|
}
|
|
|
|
|
|
|
|
const status = err.status ?? err.statusCode ?? 500
|
|
|
|
res.status(status)
|
|
|
|
|
2020-11-13 21:32:47 +01:00
|
|
|
// 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")) {
|
2020-11-05 22:19:15 +01:00
|
|
|
const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html")
|
|
|
|
res.set("Content-Type", getMediaMime(resourcePath))
|
2020-10-21 01:05:58 +02:00
|
|
|
const content = await fs.readFile(resourcePath, "utf8")
|
2020-11-05 22:19:15 +01:00
|
|
|
res.send(
|
2020-10-21 01:05:58 +02:00
|
|
|
replaceTemplates(req, content)
|
2020-10-27 23:18:44 +01:00
|
|
|
.replace(/{{ERROR_TITLE}}/g, status)
|
|
|
|
.replace(/{{ERROR_HEADER}}/g, status)
|
2020-10-21 01:05:58 +02:00
|
|
|
.replace(/{{ERROR_BODY}}/g, err.message),
|
|
|
|
)
|
2020-11-13 21:32:47 +01:00
|
|
|
} else {
|
|
|
|
res.json({
|
|
|
|
error: err.message,
|
|
|
|
...(err.details || {}),
|
|
|
|
})
|
2020-10-21 01:05:58 +02:00
|
|
|
}
|
2020-10-27 23:18:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
app.use(errorHandler)
|
2020-11-05 19:58:37 +01:00
|
|
|
|
|
|
|
const wsErrorHandler: express.ErrorRequestHandler = async (err, req) => {
|
|
|
|
logger.error(`${err.message} ${err.stack}`)
|
2020-11-11 00:52:02 +01:00
|
|
|
;(req as WebsocketRequest).ws.end()
|
2020-11-05 19:58:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
wsApp.use(wsErrorHandler)
|
2020-10-21 01:05:58 +02:00
|
|
|
}
|