Merge pull request #2622 from cdr/plugin-additions

This commit is contained in:
Asher 2021-02-10 16:45:00 -06:00 committed by GitHub
commit 8344e2062a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 308 additions and 442 deletions

View File

@ -9,7 +9,7 @@ main() {
# information. We must also run it from the root otherwise coverage will not
# include our source files.
cd "$OLDPWD"
./test/node_modules/.bin/jest "$@"
CS_DISABLE_PLUGINS=true ./test/node_modules/.bin/jest "$@"
}
main "$@"

View File

@ -54,6 +54,7 @@
"doctoc": "^1.4.0",
"eslint": "^7.7.0",
"eslint-config-prettier": "^6.0.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-prettier": "^3.1.0",
"istanbul-badges-readme": "^1.2.0",
@ -139,6 +140,9 @@
"global": {
"lines": 40
}
}
},
"modulePathIgnorePatterns": [
"<rootDir>/release"
]
}
}

View File

@ -5,10 +5,21 @@ import qs from "qs"
import safeCompare from "safe-compare"
import { HttpCode, HttpError } from "../common/http"
import { normalize, Options } from "../common/util"
import { AuthType } from "./cli"
import { AuthType, DefaultedArgs } from "./cli"
import { commit, rootPath } from "./constants"
import { Heart } from "./heart"
import { hash } from "./util"
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Express {
export interface Request {
args: DefaultedArgs
heart: Heart
}
}
}
/**
* Replace common variable strings in HTML templates.
*/

View File

@ -1,13 +1,44 @@
import { Logger, field } from "@coder/logger"
import { field, Level, Logger } from "@coder/logger"
import * as express from "express"
import * as fs from "fs"
import * as path from "path"
import * as semver from "semver"
import * as pluginapi from "../../typings/pluginapi"
import { HttpCode, HttpError } from "../common/http"
import { version } from "./constants"
import { replaceTemplates } from "./http"
import { proxy } from "./proxy"
import * as util from "./util"
import { Router as WsRouter, WebsocketRouter, wss } from "./wsRouter"
const fsp = fs.promises
// Represents a required module which could be anything.
type Module = any
/**
* Inject code-server when `require`d. This is required because the API provides
* more than just types so these need to be provided at run-time.
*/
const originalLoad = require("module")._load
require("module")._load = function (request: string, parent: object, isMain: boolean): Module {
return request === "code-server" ? codeServer : originalLoad.apply(this, [request, parent, isMain])
}
/**
* The module you get when importing "code-server".
*/
export const codeServer = {
express,
field,
HttpCode,
HttpError,
Level,
proxy,
replaceTemplates,
WsRouter,
wss,
}
interface Plugin extends pluginapi.Plugin {
/**
* These fields are populated from the plugin's package.json
@ -26,7 +57,7 @@ interface Application extends pluginapi.Application {
/*
* Clone of the above without functions.
*/
plugin: Omit<Plugin, "init" | "router" | "applications">
plugin: Omit<Plugin, "init" | "deinit" | "router" | "applications">
}
/**
@ -44,6 +75,7 @@ export class PluginAPI {
*/
private readonly csPlugin = "",
private readonly csPluginPath = `${path.join(util.paths.data, "plugins")}:/usr/share/code-server/plugins`,
private readonly workingDirectory: string | undefined = undefined,
) {
this.logger = logger.named("pluginapi")
}
@ -85,22 +117,24 @@ export class PluginAPI {
}
/**
* mount mounts all plugin routers onto r.
* mount mounts all plugin routers onto r and websocket routers onto wr.
*/
public mount(r: express.Router): void {
public mount(r: express.Router, wr: express.Router): void {
for (const [, p] of this.plugins) {
if (!p.router) {
continue
}
if (p.router) {
r.use(`${p.routerPath}`, p.router())
}
if (p.wsRouter) {
wr.use(`${p.routerPath}`, (p.wsRouter() as WebsocketRouter).router)
}
}
}
/**
* loadPlugins loads all plugins based on this.csPlugin,
* this.csPluginPath and the built in plugins.
*/
public async loadPlugins(): Promise<void> {
public async loadPlugins(loadBuiltin = true): Promise<void> {
for (const dir of this.csPlugin.split(":")) {
if (!dir) {
continue
@ -115,9 +149,10 @@ export class PluginAPI {
await this._loadPlugins(dir)
}
// Built-in plugins.
if (loadBuiltin) {
await this._loadPlugins(path.join(__dirname, "../../plugins"))
}
}
/**
* _loadPlugins is the counterpart to loadPlugins.
@ -225,12 +260,28 @@ export class PluginAPI {
p.init({
logger: logger,
workingDirectory: this.workingDirectory,
})
logger.debug("loaded")
return p
}
public async dispose(): Promise<void> {
await Promise.all(
Array.from(this.plugins.values()).map(async (p) => {
if (!p.deinit) {
return
}
try {
await p.deinit()
} catch (error) {
this.logger.error("plugin failed to deinit", field("name", p.name), field("error", error.message))
}
}),
)
}
}
interface PackageJSON {

View File

@ -1,4 +1,5 @@
import { Router } from "express"
import { wss, Router as WsRouter } from "../wsRouter"
export const router = Router()
@ -8,3 +9,19 @@ router.get("/", (req, res) => {
lastHeartbeat: req.heart.lastHeartbeat,
})
})
export const wsRouter = WsRouter()
wsRouter.ws("/", async (req) => {
wss.handleUpgrade(req, req.socket, req.head, (ws) => {
ws.on("message", () => {
ws.send(
JSON.stringify({
event: "health",
status: req.heart.alive() ? "alive" : "expired",
lastHeartbeat: req.heart.lastHeartbeat,
}),
)
})
})
})

View File

@ -6,35 +6,26 @@ import { promises as fs } from "fs"
import http from "http"
import * as path from "path"
import * as tls from "tls"
import * as pluginapi from "../../../typings/pluginapi"
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, redirect } from "../http"
import { redirect, replaceTemplates } from "../http"
import { PluginAPI } from "../plugin"
import { getMediaMime, paths } from "../util"
import { WebsocketRequest } from "../wsRouter"
import { wrapper } from "../wrapper"
import * as apps from "./apps"
import * as domainProxy from "./domainProxy"
import * as health from "./health"
import * as login from "./login"
import * as proxy from "./pathProxy"
import * as pathProxy from "./pathProxy"
// 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.
*/
@ -104,25 +95,34 @@ export const register = async (
wsApp.use("/", domainProxy.wsRouter.router)
app.all("/proxy/(:port)(/*)?", (req, res) => {
proxy.proxy(req, res)
pathProxy.proxy(req, res)
})
wsApp.get("/proxy/(:port)(/*)?", (req, res) => {
proxy.wsProxy(req as WebsocketRequest)
wsApp.get("/proxy/(:port)(/*)?", (req) => {
pathProxy.wsProxy(req as pluginapi.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, {
pathProxy.proxy(req, res, {
passthroughPath: true,
})
})
wsApp.get("/absproxy/(:port)(/*)?", (req, res) => {
proxy.wsProxy(req as WebsocketRequest, {
wsApp.get("/absproxy/(:port)(/*)?", (req) => {
pathProxy.wsProxy(req as pluginapi.WebsocketRequest, {
passthroughPath: true,
})
})
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())
}
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
@ -132,6 +132,7 @@ export const register = async (
wsApp.use("/vscode", vscode.wsRouter.router)
app.use("/healthz", health.router)
wsApp.use("/healthz", health.wsRouter.router)
if (args.auth === AuthType.Password) {
app.use("/login", login.router)
@ -144,11 +145,6 @@ export const register = async (
app.use("/static", _static.router)
app.use("/update", update.router)
const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH)
await papi.loadPlugins()
papi.mount(app)
app.use("/api/applications", apps.router(papi))
app.use(() => {
throw new HttpError("Not Found", HttpCode.NotFound)
})
@ -187,7 +183,7 @@ export const register = async (
const wsErrorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
logger.error(`${err.message} ${err.stack}`)
;(req as WebsocketRequest).ws.end()
;(req as pluginapi.WebsocketRequest).ws.end()
}
wsApp.use(wsErrorHandler)

View File

@ -1,11 +1,11 @@
import { Request, Response } from "express"
import * as path from "path"
import qs from "qs"
import * as pluginapi from "../../../typings/pluginapi"
import { HttpCode, HttpError } from "../../common/http"
import { normalize } from "../../common/util"
import { authenticated, ensureAuthenticated, redirect } from "../http"
import { proxy as _proxy } from "../proxy"
import { WebsocketRequest } from "../wsRouter"
const getProxyTarget = (req: Request, passthroughPath?: boolean): string => {
if (passthroughPath) {
@ -46,7 +46,7 @@ export function proxy(
}
export function wsProxy(
req: WebsocketRequest,
req: pluginapi.WebsocketRequest,
opts?: {
passthroughPath?: boolean
},

View File

@ -234,9 +234,7 @@ export class ParentProcess extends Process {
this.logStdoutStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stdout.log"), opts)
this.logStderrStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stderr.log"), opts)
this.onDispose(() => {
this.disposeChild()
})
this.onDispose(() => this.disposeChild())
this.onChildMessage((message) => {
switch (message.type) {
@ -252,11 +250,15 @@ export class ParentProcess extends Process {
})
}
private disposeChild(): void {
private async disposeChild(): Promise<void> {
this.started = undefined
if (this.child) {
this.child.removeAllListeners()
this.child.kill()
const child = this.child
child.removeAllListeners()
child.kill()
// Wait for the child to exit otherwise its output will be lost which can
// be especially problematic if you're trying to debug why cleanup failed.
await new Promise((r) => child!.on("exit", r))
}
}

View File

@ -1,7 +1,8 @@
import * as express from "express"
import * as expressCore from "express-serve-static-core"
import * as http from "http"
import * as net from "net"
import Websocket from "ws"
import * as pluginapi from "../../typings/pluginapi"
export const handleUpgrade = (app: express.Express, server: http.Server): void => {
server.on("upgrade", (req, socket, head) => {
@ -20,31 +21,24 @@ export const handleUpgrade = (app: express.Express, server: http.Server): void =
})
}
export interface WebsocketRequest extends express.Request {
ws: net.Socket
head: Buffer
}
interface InternalWebsocketRequest extends WebsocketRequest {
interface InternalWebsocketRequest extends pluginapi.WebsocketRequest {
_ws_handled: boolean
}
export type WebSocketHandler = (
req: WebsocketRequest,
res: express.Response,
next: express.NextFunction,
) => void | Promise<void>
export class WebsocketRouter {
public readonly router = express.Router()
public ws(route: expressCore.PathParams, ...handlers: WebSocketHandler[]): void {
/**
* Handle a websocket at this route. Note that websockets are immediately
* paused when they come in.
*/
public ws(route: expressCore.PathParams, ...handlers: pluginapi.WebSocketHandler[]): void {
this.router.get(
route,
...handlers.map((handler) => {
const wrapped: express.Handler = (req, res, next) => {
;(req as InternalWebsocketRequest)._ws_handled = true
return handler(req as WebsocketRequest, res, next)
return handler(req as pluginapi.WebsocketRequest, res, next)
}
return wrapped
}),
@ -55,3 +49,5 @@ export class WebsocketRouter {
export function Router(): WebsocketRouter {
return new WebsocketRouter()
}
export const wss = new Websocket.Server({ noServer: true })

View File

@ -1,7 +1,10 @@
import * as express from "express"
import * as http from "http"
import * as nodeFetch from "node-fetch"
import Websocket from "ws"
import * as util from "../src/common/util"
import { ensureAddress } from "../src/node/app"
import { handleUpgrade } from "../src/node/wsRouter"
// Perhaps an abstraction similar to this should be used in app.ts as well.
export class HttpServer {
@ -39,6 +42,13 @@ export class HttpServer {
})
}
/**
* Send upgrade requests to an Express app.
*/
public listenUpgrade(app: express.Express): void {
handleUpgrade(app, this.hs)
}
/**
* close cleans up the server.
*/
@ -62,6 +72,13 @@ export class HttpServer {
return nodeFetch.default(`${ensureAddress(this.hs)}${requestPath}`, opts)
}
/**
* Open a websocket against the requset path.
*/
public ws(requestPath: string): Websocket {
return new Websocket(`${ensureAddress(this.hs).replace("http:", "ws:")}${requestPath}`)
}
public port(): number {
const addr = this.hs.address()
if (addr && typeof addr === "object") {

View File

@ -2,11 +2,15 @@ import { logger } from "@coder/logger"
import * as express from "express"
import * as fs from "fs"
import * as path from "path"
import { PluginAPI } from "../src/node/plugin"
import { HttpCode } from "../src/common/http"
import { codeServer, PluginAPI } from "../src/node/plugin"
import * as apps from "../src/node/routes/apps"
import * as httpserver from "./httpserver"
const fsp = fs.promises
// Jest overrides `require` so our usual override doesn't work.
jest.mock("code-server", () => codeServer, { virtual: true })
/**
* Use $LOG_LEVEL=debug to see debug logs.
*/
@ -15,15 +19,19 @@ describe("plugin", () => {
let s: httpserver.HttpServer
beforeAll(async () => {
papi = new PluginAPI(logger, `${path.resolve(__dirname, "test-plugin")}:meow`)
await papi.loadPlugins()
// Only include the test plugin to avoid contaminating results with other
// plugins that might be on the filesystem.
papi = new PluginAPI(logger, `${path.resolve(__dirname, "test-plugin")}:meow`, "")
await papi.loadPlugins(false)
const app = express.default()
papi.mount(app)
const wsApp = express.default()
papi.mount(app, wsApp)
app.use("/api/applications", apps.router(papi))
s = new httpserver.HttpServer()
await s.listen(app)
s.listenUpgrade(wsApp)
})
afterAll(async () => {
@ -68,4 +76,18 @@ describe("plugin", () => {
const body = await resp.text()
expect(body).toBe(indexHTML)
})
it("/test-plugin/test-app (websocket)", async () => {
const ws = s.ws("/test-plugin/test-app")
const message = await new Promise((resolve) => {
ws.once("message", (message) => resolve(message))
})
ws.terminate()
expect(message).toBe("hello")
})
it("/test-plugin/error", async () => {
const resp = await s.fetch("/test-plugin/error")
expect(resp.status).toBe(HttpCode.LargePayload)
})
})

View File

@ -0,0 +1,5 @@
settings:
import/resolver:
alias:
map:
- [code-server, ./typings/pluginapi.d.ts]

View File

@ -12,8 +12,5 @@
},
"scripts": {
"build": "tsc"
},
"dependencies": {
"express": "^4.17.1"
}
}

View File

@ -1,8 +1,7 @@
import * as express from "express"
import * as cs from "code-server"
import * as fspath from "path"
import * as pluginapi from "../../../typings/pluginapi"
export const plugin: pluginapi.Plugin = {
export const plugin: cs.Plugin = {
displayName: "Test Plugin",
routerPath: "/test-plugin",
homepageURL: "https://example.com",
@ -13,16 +12,29 @@ export const plugin: pluginapi.Plugin = {
},
router() {
const r = express.Router()
r.get("/test-app", (req, res) => {
const r = cs.express.Router()
r.get("/test-app", (_, res) => {
res.sendFile(fspath.resolve(__dirname, "../public/index.html"))
})
r.get("/goland/icon.svg", (req, res) => {
r.get("/goland/icon.svg", (_, res) => {
res.sendFile(fspath.resolve(__dirname, "../public/icon.svg"))
})
r.get("/error", () => {
throw new cs.HttpError("error", cs.HttpCode.LargePayload)
})
return r
},
wsRouter() {
const wr = cs.WsRouter()
wr.ws("/test-app", (req) => {
cs.wss.handleUpgrade(req, req.socket, req.head, (ws) => {
ws.send("hello")
})
})
return wr
},
applications() {
return [
{

View File

@ -42,8 +42,10 @@
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
"baseUrl": "./" /* Base directory to resolve non-absolute module names. */,
"paths": {
"code-server": ["../../typings/pluginapi"]
} /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */,
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */

View File

@ -64,372 +64,7 @@
"@types/mime" "*"
"@types/node" "*"
accepts@~1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
dependencies:
mime-types "~2.1.24"
negotiator "0.6.2"
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
body-parser@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
dependencies:
bytes "3.1.0"
content-type "~1.0.4"
debug "2.6.9"
depd "~1.1.2"
http-errors "1.7.2"
iconv-lite "0.4.24"
on-finished "~2.3.0"
qs "6.7.0"
raw-body "2.4.0"
type-is "~1.6.17"
bytes@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
content-disposition@0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
dependencies:
safe-buffer "5.1.2"
content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
cookie@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
dependencies:
accepts "~1.3.7"
array-flatten "1.1.1"
body-parser "1.19.0"
content-disposition "0.5.3"
content-type "~1.0.4"
cookie "0.4.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "~1.1.2"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "~1.1.2"
fresh "0.5.2"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "~2.3.0"
parseurl "~1.3.3"
path-to-regexp "0.1.7"
proxy-addr "~2.0.5"
qs "6.7.0"
range-parser "~1.2.1"
safe-buffer "5.1.2"
send "0.17.1"
serve-static "1.14.1"
setprototypeof "1.1.1"
statuses "~1.5.0"
type-is "~1.6.18"
utils-merge "1.0.1"
vary "~1.1.2"
finalhandler@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
dependencies:
debug "2.6.9"
encodeurl "~1.0.2"
escape-html "~1.0.3"
on-finished "~2.3.0"
parseurl "~1.3.3"
statuses "~1.5.0"
unpipe "~1.0.0"
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
http-errors@1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
dependencies:
depd "~1.1.2"
inherits "2.0.3"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
http-errors@~1.7.2:
version "1.7.3"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
dependencies:
depd "~1.1.2"
inherits "2.0.4"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
inherits@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
mime-db@1.44.0:
version "1.44.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
mime-types@~2.1.24:
version "2.1.27"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
dependencies:
mime-db "1.44.0"
mime@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
dependencies:
ee-first "1.1.1"
parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
proxy-addr@~2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==
dependencies:
forwarded "~0.1.2"
ipaddr.js "1.9.1"
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
dependencies:
bytes "3.1.0"
http-errors "1.7.2"
iconv-lite "0.4.24"
unpipe "1.0.0"
safe-buffer@5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
send@0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
dependencies:
debug "2.6.9"
depd "~1.1.2"
destroy "~1.0.4"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "~1.7.2"
mime "1.6.0"
ms "2.1.1"
on-finished "~2.3.0"
range-parser "~1.2.1"
statuses "~1.5.0"
serve-static@1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "0.17.1"
setprototypeof@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
toidentifier@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
dependencies:
media-typer "0.3.0"
mime-types "~2.1.24"
typescript@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389"
integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=

View File

@ -1,8 +1,12 @@
/**
* This file describes the code-server plugin API for adding new applications.
*/
import { Logger } from "@coder/logger"
import { field, Level, Logger } from "@coder/logger"
import * as express from "express"
import * as expressCore from "express-serve-static-core"
import ProxyServer from "http-proxy"
import * as net from "net"
import Websocket from "ws"
/**
* Overlay
@ -78,6 +82,75 @@ import * as express from "express"
* ]
*/
export enum HttpCode {
Ok = 200,
Redirect = 302,
NotFound = 404,
BadRequest = 400,
Unauthorized = 401,
LargePayload = 413,
ServerError = 500,
}
export declare class HttpError extends Error {
constructor(message: string, status: HttpCode, details?: object)
}
export interface WebsocketRequest extends express.Request {
ws: net.Socket
head: Buffer
}
export type WebSocketHandler = (
req: WebsocketRequest,
res: express.Response,
next: express.NextFunction,
) => void | Promise<void>
export interface WebsocketRouter {
readonly router: express.Router
ws(route: expressCore.PathParams, ...handlers: WebSocketHandler[]): void
}
/**
* Create a router for websocket routes.
*/
export function WsRouter(): WebsocketRouter
/**
* The websocket server used by code-server.
*/
export const wss: Websocket.Server
/**
* The Express import used by code-server.
*
* Re-exported so plugins don't have to import duplicate copies of Express and
* to avoid potential version differences or issues caused by running separate
* instances.
*/
export { express }
/**
* Use to add a field to a log.
*
* Re-exported so plugins don't have to import duplicate copies of the logger.
*/
export { field, Level, Logger }
/**
* code-server's proxy server.
*/
export const proxy: ProxyServer
/**
* Replace variables in HTML: TO, BASE, CS_STATIC_BASE, and OPTIONS.
*/
export function replaceTemplates<T extends object>(
req: express.Request,
content: string,
extraOpts?: Omit<T, "base" | "csStaticBase" | "logLevel">,
): string
/**
* Your plugin module must have a top level export "plugin" that implements this interface.
*
@ -125,6 +198,11 @@ export interface Plugin {
*/
init(config: PluginConfig): void
/**
* Called when the plugin should dispose/shutdown everything.
*/
deinit?(): Promise<void>
/**
* Returns the plugin's router.
*
@ -134,6 +212,15 @@ export interface Plugin {
*/
router?(): express.Router
/**
* Returns the plugin's websocket router.
*
* Mounted at <code-sever-root>/<plugin-path>
*
* If not present, the plugin provides no websockets.
*/
wsRouter?(): WebsocketRouter
/**
* code-server uses this to collect the list of applications that
* the plugin can currently provide.
@ -156,6 +243,13 @@ export interface PluginConfig {
* All plugin logs should be logged via this logger.
*/
readonly logger: Logger
/**
* This can be specified by the user on the command line. Plugins should
* default to this directory when applicable. For example, the Jupyter plugin
* uses this to launch in this directory.
*/
readonly workingDirectory?: string
}
/**

View File

@ -2990,6 +2990,11 @@ eslint-config-prettier@^6.0.0:
dependencies:
get-stdin "^6.0.0"
eslint-import-resolver-alias@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz#297062890e31e4d6651eb5eba9534e1f6e68fc97"
integrity sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==
eslint-import-resolver-node@^0.3.4:
version "0.3.4"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717"