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"
|
2021-01-20 21:11:08 +01:00
|
|
|
import * as pluginapi from "../../../typings/pluginapi"
|
2020-10-21 01:05:58 +02:00
|
|
|
import { HttpCode, HttpError } from "../../common/http"
|
|
|
|
import { plural } from "../../common/util"
|
|
|
|
import { AuthType, DefaultedArgs } from "../cli"
|
|
|
|
import { rootPath } from "../constants"
|
|
|
|
import { Heart } from "../heart"
|
2021-01-20 21:11:08 +01:00
|
|
|
import { redirect, replaceTemplates } 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"
|
2021-01-20 22:48:35 +01:00
|
|
|
import { wrapper } from "../wrapper"
|
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"
|
2021-01-20 21:11:08 +01:00
|
|
|
import * as pathProxy 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"
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
heart.ts: Fix leak when server closes
This had me very confused for quite a while until I did a binary search
inspection on route/index.ts. Only with the heart.beat line commented
out did my tests pass without leaking.
They weren't leaking fds but just this heartbeat timer and node of
course prints just fds that are active when it detects some sort of leak
I guess and that made the whole thing very confusing. These fds are not
leaked and will close when node's event loop detects there are no more
callbacks to run.
no of handles 3
tcp stream {
fd: 20,
readable: false,
writable: true,
address: {},
serverAddr: null
}
tcp stream {
fd: 22,
readable: false,
writable: true,
address: {},
serverAddr: null
}
tcp stream {
fd: 23,
readable: true,
writable: false,
address: {},
serverAddr: null
}
It kept printing the above text again and again for 60s and then the
test binary times out I think. I'm not sure if it was node printing the
stuff above or if it was a mocha thing. But it was really confusing...
cc @code-asher for thoughts on what was going on.
edit: It was the leaked-handles import in socket.test.ts!!!
Not sure if we should keep it, this was really confusing and misleading.
2021-01-18 14:30:00 +01:00
|
|
|
server.on("close", () => {
|
|
|
|
heart.dispose()
|
|
|
|
})
|
2020-10-21 01:05:58 +02:00
|
|
|
|
|
|
|
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())
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
2021-02-04 23:29:44 +01:00
|
|
|
app.all("/proxy/(:port)(/*)?", (req, res) => {
|
2021-01-20 21:11:08 +01:00
|
|
|
pathProxy.proxy(req, res)
|
2021-02-04 23:29:44 +01:00
|
|
|
})
|
2021-01-20 21:11:08 +01:00
|
|
|
wsApp.get("/proxy/(:port)(/*)?", (req) => {
|
|
|
|
pathProxy.wsProxy(req as pluginapi.WebsocketRequest)
|
2021-02-04 23:29:44 +01:00
|
|
|
})
|
|
|
|
// 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) => {
|
2021-01-20 21:11:08 +01:00
|
|
|
pathProxy.proxy(req, res, {
|
2021-02-04 23:29:44 +01:00
|
|
|
passthroughPath: true,
|
|
|
|
})
|
|
|
|
})
|
2021-01-20 21:11:08 +01:00
|
|
|
wsApp.get("/absproxy/(:port)(/*)?", (req) => {
|
|
|
|
pathProxy.wsProxy(req as pluginapi.WebsocketRequest, {
|
2021-02-04 23:29:44 +01:00
|
|
|
passthroughPath: true,
|
|
|
|
})
|
|
|
|
})
|
2021-01-20 23:32:47 +01:00
|
|
|
|
2021-02-09 22:36:05 +01:00
|
|
|
if (!process.env.CS_DISABLE_PLUGINS) {
|
|
|
|
const workingDir = args._ && args._.length > 0 ? path.resolve(args._[args._.length - 1]) : undefined
|
|
|
|
const pluginApi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH, workingDir)
|
|
|
|
await pluginApi.loadPlugins()
|
|
|
|
pluginApi.mount(app, wsApp)
|
|
|
|
app.use("/api/applications", apps.router(pluginApi))
|
|
|
|
wrapper.onDispose(() => pluginApi.dispose())
|
|
|
|
}
|
2021-02-09 19:26:00 +01:00
|
|
|
|
2021-01-20 23:32:47 +01:00
|
|
|
app.use(bodyParser.json())
|
|
|
|
app.use(bodyParser.urlencoded({ extended: true }))
|
|
|
|
|
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)
|
2021-01-28 19:47:50 +01:00
|
|
|
wsApp.use("/healthz", health.wsRouter.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("/static", _static.router)
|
|
|
|
app.use("/update", update.router)
|
|
|
|
|
|
|
|
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
|
|
|
|
2021-01-11 20:33:46 +01:00
|
|
|
const wsErrorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
|
2020-11-05 19:58:37 +01:00
|
|
|
logger.error(`${err.message} ${err.stack}`)
|
2021-01-20 21:11:08 +01:00
|
|
|
;(req as pluginapi.WebsocketRequest).ws.end()
|
2020-11-05 19:58:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
wsApp.use(wsErrorHandler)
|
2020-10-21 01:05:58 +02:00
|
|
|
}
|