Merge pull request #2622 from cdr/plugin-additions
This commit is contained in:
commit
8344e2062a
|
@ -9,7 +9,7 @@ main() {
|
||||||
# information. We must also run it from the root otherwise coverage will not
|
# information. We must also run it from the root otherwise coverage will not
|
||||||
# include our source files.
|
# include our source files.
|
||||||
cd "$OLDPWD"
|
cd "$OLDPWD"
|
||||||
./test/node_modules/.bin/jest "$@"
|
CS_DISABLE_PLUGINS=true ./test/node_modules/.bin/jest "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|
|
@ -54,6 +54,7 @@
|
||||||
"doctoc": "^1.4.0",
|
"doctoc": "^1.4.0",
|
||||||
"eslint": "^7.7.0",
|
"eslint": "^7.7.0",
|
||||||
"eslint-config-prettier": "^6.0.0",
|
"eslint-config-prettier": "^6.0.0",
|
||||||
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
"eslint-plugin-import": "^2.18.2",
|
"eslint-plugin-import": "^2.18.2",
|
||||||
"eslint-plugin-prettier": "^3.1.0",
|
"eslint-plugin-prettier": "^3.1.0",
|
||||||
"istanbul-badges-readme": "^1.2.0",
|
"istanbul-badges-readme": "^1.2.0",
|
||||||
|
@ -139,6 +140,9 @@
|
||||||
"global": {
|
"global": {
|
||||||
"lines": 40
|
"lines": 40
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"modulePathIgnorePatterns": [
|
||||||
|
"<rootDir>/release"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,21 @@ import qs from "qs"
|
||||||
import safeCompare from "safe-compare"
|
import safeCompare from "safe-compare"
|
||||||
import { HttpCode, HttpError } from "../common/http"
|
import { HttpCode, HttpError } from "../common/http"
|
||||||
import { normalize, Options } from "../common/util"
|
import { normalize, Options } from "../common/util"
|
||||||
import { AuthType } from "./cli"
|
import { AuthType, DefaultedArgs } from "./cli"
|
||||||
import { commit, rootPath } from "./constants"
|
import { commit, rootPath } from "./constants"
|
||||||
|
import { Heart } from "./heart"
|
||||||
import { hash } from "./util"
|
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.
|
* Replace common variable strings in HTML templates.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,13 +1,44 @@
|
||||||
import { Logger, field } from "@coder/logger"
|
import { field, Level, Logger } from "@coder/logger"
|
||||||
import * as express from "express"
|
import * as express from "express"
|
||||||
import * as fs from "fs"
|
import * as fs from "fs"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as semver from "semver"
|
import * as semver from "semver"
|
||||||
import * as pluginapi from "../../typings/pluginapi"
|
import * as pluginapi from "../../typings/pluginapi"
|
||||||
|
import { HttpCode, HttpError } from "../common/http"
|
||||||
import { version } from "./constants"
|
import { version } from "./constants"
|
||||||
|
import { replaceTemplates } from "./http"
|
||||||
|
import { proxy } from "./proxy"
|
||||||
import * as util from "./util"
|
import * as util from "./util"
|
||||||
|
import { Router as WsRouter, WebsocketRouter, wss } from "./wsRouter"
|
||||||
const fsp = fs.promises
|
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 {
|
interface Plugin extends pluginapi.Plugin {
|
||||||
/**
|
/**
|
||||||
* These fields are populated from the plugin's package.json
|
* 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.
|
* 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 csPlugin = "",
|
||||||
private readonly csPluginPath = `${path.join(util.paths.data, "plugins")}:/usr/share/code-server/plugins`,
|
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")
|
this.logger = logger.named("pluginapi")
|
||||||
}
|
}
|
||||||
|
@ -85,14 +117,16 @@ 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) {
|
for (const [, p] of this.plugins) {
|
||||||
if (!p.router) {
|
if (p.router) {
|
||||||
continue
|
r.use(`${p.routerPath}`, p.router())
|
||||||
|
}
|
||||||
|
if (p.wsRouter) {
|
||||||
|
wr.use(`${p.routerPath}`, (p.wsRouter() as WebsocketRouter).router)
|
||||||
}
|
}
|
||||||
r.use(`${p.routerPath}`, p.router())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +134,7 @@ export class PluginAPI {
|
||||||
* loadPlugins loads all plugins based on this.csPlugin,
|
* loadPlugins loads all plugins based on this.csPlugin,
|
||||||
* this.csPluginPath and the built in plugins.
|
* 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(":")) {
|
for (const dir of this.csPlugin.split(":")) {
|
||||||
if (!dir) {
|
if (!dir) {
|
||||||
continue
|
continue
|
||||||
|
@ -115,8 +149,9 @@ export class PluginAPI {
|
||||||
await this._loadPlugins(dir)
|
await this._loadPlugins(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Built-in plugins.
|
if (loadBuiltin) {
|
||||||
await this._loadPlugins(path.join(__dirname, "../../plugins"))
|
await this._loadPlugins(path.join(__dirname, "../../plugins"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -225,12 +260,28 @@ export class PluginAPI {
|
||||||
|
|
||||||
p.init({
|
p.init({
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
workingDirectory: this.workingDirectory,
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.debug("loaded")
|
logger.debug("loaded")
|
||||||
|
|
||||||
return p
|
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 {
|
interface PackageJSON {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Router } from "express"
|
import { Router } from "express"
|
||||||
|
import { wss, Router as WsRouter } from "../wsRouter"
|
||||||
|
|
||||||
export const router = Router()
|
export const router = Router()
|
||||||
|
|
||||||
|
@ -8,3 +9,19 @@ router.get("/", (req, res) => {
|
||||||
lastHeartbeat: req.heart.lastHeartbeat,
|
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,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -6,35 +6,26 @@ import { promises as fs } from "fs"
|
||||||
import http from "http"
|
import http from "http"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as tls from "tls"
|
import * as tls from "tls"
|
||||||
|
import * as pluginapi from "../../../typings/pluginapi"
|
||||||
import { HttpCode, HttpError } from "../../common/http"
|
import { HttpCode, HttpError } from "../../common/http"
|
||||||
import { plural } from "../../common/util"
|
import { plural } from "../../common/util"
|
||||||
import { AuthType, DefaultedArgs } from "../cli"
|
import { AuthType, DefaultedArgs } from "../cli"
|
||||||
import { rootPath } from "../constants"
|
import { rootPath } from "../constants"
|
||||||
import { Heart } from "../heart"
|
import { Heart } from "../heart"
|
||||||
import { replaceTemplates, redirect } from "../http"
|
import { redirect, replaceTemplates } from "../http"
|
||||||
import { PluginAPI } from "../plugin"
|
import { PluginAPI } from "../plugin"
|
||||||
import { getMediaMime, paths } from "../util"
|
import { getMediaMime, paths } from "../util"
|
||||||
import { WebsocketRequest } from "../wsRouter"
|
import { wrapper } from "../wrapper"
|
||||||
import * as apps from "./apps"
|
import * as apps from "./apps"
|
||||||
import * as domainProxy from "./domainProxy"
|
import * as domainProxy from "./domainProxy"
|
||||||
import * as health from "./health"
|
import * as health from "./health"
|
||||||
import * as login from "./login"
|
import * as login from "./login"
|
||||||
import * as proxy from "./pathProxy"
|
import * as pathProxy from "./pathProxy"
|
||||||
// static is a reserved keyword.
|
// static is a reserved keyword.
|
||||||
import * as _static from "./static"
|
import * as _static from "./static"
|
||||||
import * as update from "./update"
|
import * as update from "./update"
|
||||||
import * as vscode from "./vscode"
|
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.
|
* Register all routes and middleware.
|
||||||
*/
|
*/
|
||||||
|
@ -104,25 +95,34 @@ export const register = async (
|
||||||
wsApp.use("/", domainProxy.wsRouter.router)
|
wsApp.use("/", domainProxy.wsRouter.router)
|
||||||
|
|
||||||
app.all("/proxy/(:port)(/*)?", (req, res) => {
|
app.all("/proxy/(:port)(/*)?", (req, res) => {
|
||||||
proxy.proxy(req, res)
|
pathProxy.proxy(req, res)
|
||||||
})
|
})
|
||||||
wsApp.get("/proxy/(:port)(/*)?", (req, res) => {
|
wsApp.get("/proxy/(:port)(/*)?", (req) => {
|
||||||
proxy.wsProxy(req as WebsocketRequest)
|
pathProxy.wsProxy(req as pluginapi.WebsocketRequest)
|
||||||
})
|
})
|
||||||
// These two routes pass through the path directly.
|
// These two routes pass through the path directly.
|
||||||
// So the proxied app must be aware it is running
|
// So the proxied app must be aware it is running
|
||||||
// under /absproxy/<someport>/
|
// under /absproxy/<someport>/
|
||||||
app.all("/absproxy/(:port)(/*)?", (req, res) => {
|
app.all("/absproxy/(:port)(/*)?", (req, res) => {
|
||||||
proxy.proxy(req, res, {
|
pathProxy.proxy(req, res, {
|
||||||
passthroughPath: true,
|
passthroughPath: true,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
wsApp.get("/absproxy/(:port)(/*)?", (req, res) => {
|
wsApp.get("/absproxy/(:port)(/*)?", (req) => {
|
||||||
proxy.wsProxy(req as WebsocketRequest, {
|
pathProxy.wsProxy(req as pluginapi.WebsocketRequest, {
|
||||||
passthroughPath: true,
|
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.json())
|
||||||
app.use(bodyParser.urlencoded({ extended: true }))
|
app.use(bodyParser.urlencoded({ extended: true }))
|
||||||
|
|
||||||
|
@ -132,6 +132,7 @@ export const register = async (
|
||||||
wsApp.use("/vscode", vscode.wsRouter.router)
|
wsApp.use("/vscode", vscode.wsRouter.router)
|
||||||
|
|
||||||
app.use("/healthz", health.router)
|
app.use("/healthz", health.router)
|
||||||
|
wsApp.use("/healthz", health.wsRouter.router)
|
||||||
|
|
||||||
if (args.auth === AuthType.Password) {
|
if (args.auth === AuthType.Password) {
|
||||||
app.use("/login", login.router)
|
app.use("/login", login.router)
|
||||||
|
@ -144,11 +145,6 @@ export const register = async (
|
||||||
app.use("/static", _static.router)
|
app.use("/static", _static.router)
|
||||||
app.use("/update", update.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(() => {
|
app.use(() => {
|
||||||
throw new HttpError("Not Found", HttpCode.NotFound)
|
throw new HttpError("Not Found", HttpCode.NotFound)
|
||||||
})
|
})
|
||||||
|
@ -187,7 +183,7 @@ export const register = async (
|
||||||
|
|
||||||
const wsErrorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
|
const wsErrorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
|
||||||
logger.error(`${err.message} ${err.stack}`)
|
logger.error(`${err.message} ${err.stack}`)
|
||||||
;(req as WebsocketRequest).ws.end()
|
;(req as pluginapi.WebsocketRequest).ws.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
wsApp.use(wsErrorHandler)
|
wsApp.use(wsErrorHandler)
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Request, Response } from "express"
|
import { Request, Response } from "express"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import qs from "qs"
|
import qs from "qs"
|
||||||
|
import * as pluginapi from "../../../typings/pluginapi"
|
||||||
import { HttpCode, HttpError } from "../../common/http"
|
import { HttpCode, HttpError } from "../../common/http"
|
||||||
import { normalize } from "../../common/util"
|
import { normalize } from "../../common/util"
|
||||||
import { authenticated, ensureAuthenticated, redirect } from "../http"
|
import { authenticated, ensureAuthenticated, redirect } from "../http"
|
||||||
import { proxy as _proxy } from "../proxy"
|
import { proxy as _proxy } from "../proxy"
|
||||||
import { WebsocketRequest } from "../wsRouter"
|
|
||||||
|
|
||||||
const getProxyTarget = (req: Request, passthroughPath?: boolean): string => {
|
const getProxyTarget = (req: Request, passthroughPath?: boolean): string => {
|
||||||
if (passthroughPath) {
|
if (passthroughPath) {
|
||||||
|
@ -46,7 +46,7 @@ export function proxy(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wsProxy(
|
export function wsProxy(
|
||||||
req: WebsocketRequest,
|
req: pluginapi.WebsocketRequest,
|
||||||
opts?: {
|
opts?: {
|
||||||
passthroughPath?: boolean
|
passthroughPath?: boolean
|
||||||
},
|
},
|
||||||
|
|
|
@ -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.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.logStderrStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stderr.log"), opts)
|
||||||
|
|
||||||
this.onDispose(() => {
|
this.onDispose(() => this.disposeChild())
|
||||||
this.disposeChild()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.onChildMessage((message) => {
|
this.onChildMessage((message) => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
|
@ -252,11 +250,15 @@ export class ParentProcess extends Process {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private disposeChild(): void {
|
private async disposeChild(): Promise<void> {
|
||||||
this.started = undefined
|
this.started = undefined
|
||||||
if (this.child) {
|
if (this.child) {
|
||||||
this.child.removeAllListeners()
|
const child = this.child
|
||||||
this.child.kill()
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import * as express from "express"
|
import * as express from "express"
|
||||||
import * as expressCore from "express-serve-static-core"
|
import * as expressCore from "express-serve-static-core"
|
||||||
import * as http from "http"
|
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 => {
|
export const handleUpgrade = (app: express.Express, server: http.Server): void => {
|
||||||
server.on("upgrade", (req, socket, head) => {
|
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 {
|
interface InternalWebsocketRequest extends pluginapi.WebsocketRequest {
|
||||||
ws: net.Socket
|
|
||||||
head: Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InternalWebsocketRequest extends WebsocketRequest {
|
|
||||||
_ws_handled: boolean
|
_ws_handled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WebSocketHandler = (
|
|
||||||
req: WebsocketRequest,
|
|
||||||
res: express.Response,
|
|
||||||
next: express.NextFunction,
|
|
||||||
) => void | Promise<void>
|
|
||||||
|
|
||||||
export class WebsocketRouter {
|
export class WebsocketRouter {
|
||||||
public readonly router = express.Router()
|
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(
|
this.router.get(
|
||||||
route,
|
route,
|
||||||
...handlers.map((handler) => {
|
...handlers.map((handler) => {
|
||||||
const wrapped: express.Handler = (req, res, next) => {
|
const wrapped: express.Handler = (req, res, next) => {
|
||||||
;(req as InternalWebsocketRequest)._ws_handled = true
|
;(req as InternalWebsocketRequest)._ws_handled = true
|
||||||
return handler(req as WebsocketRequest, res, next)
|
return handler(req as pluginapi.WebsocketRequest, res, next)
|
||||||
}
|
}
|
||||||
return wrapped
|
return wrapped
|
||||||
}),
|
}),
|
||||||
|
@ -55,3 +49,5 @@ export class WebsocketRouter {
|
||||||
export function Router(): WebsocketRouter {
|
export function Router(): WebsocketRouter {
|
||||||
return new WebsocketRouter()
|
return new WebsocketRouter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const wss = new Websocket.Server({ noServer: true })
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
import * as express from "express"
|
||||||
import * as http from "http"
|
import * as http from "http"
|
||||||
import * as nodeFetch from "node-fetch"
|
import * as nodeFetch from "node-fetch"
|
||||||
|
import Websocket from "ws"
|
||||||
import * as util from "../src/common/util"
|
import * as util from "../src/common/util"
|
||||||
import { ensureAddress } from "../src/node/app"
|
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.
|
// Perhaps an abstraction similar to this should be used in app.ts as well.
|
||||||
export class HttpServer {
|
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.
|
* close cleans up the server.
|
||||||
*/
|
*/
|
||||||
|
@ -62,6 +72,13 @@ export class HttpServer {
|
||||||
return nodeFetch.default(`${ensureAddress(this.hs)}${requestPath}`, opts)
|
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 {
|
public port(): number {
|
||||||
const addr = this.hs.address()
|
const addr = this.hs.address()
|
||||||
if (addr && typeof addr === "object") {
|
if (addr && typeof addr === "object") {
|
||||||
|
|
|
@ -2,11 +2,15 @@ import { logger } from "@coder/logger"
|
||||||
import * as express from "express"
|
import * as express from "express"
|
||||||
import * as fs from "fs"
|
import * as fs from "fs"
|
||||||
import * as path from "path"
|
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 apps from "../src/node/routes/apps"
|
||||||
import * as httpserver from "./httpserver"
|
import * as httpserver from "./httpserver"
|
||||||
const fsp = fs.promises
|
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.
|
* Use $LOG_LEVEL=debug to see debug logs.
|
||||||
*/
|
*/
|
||||||
|
@ -15,15 +19,19 @@ describe("plugin", () => {
|
||||||
let s: httpserver.HttpServer
|
let s: httpserver.HttpServer
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
papi = new PluginAPI(logger, `${path.resolve(__dirname, "test-plugin")}:meow`)
|
// Only include the test plugin to avoid contaminating results with other
|
||||||
await papi.loadPlugins()
|
// 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()
|
const app = express.default()
|
||||||
papi.mount(app)
|
const wsApp = express.default()
|
||||||
|
papi.mount(app, wsApp)
|
||||||
app.use("/api/applications", apps.router(papi))
|
app.use("/api/applications", apps.router(papi))
|
||||||
|
|
||||||
s = new httpserver.HttpServer()
|
s = new httpserver.HttpServer()
|
||||||
await s.listen(app)
|
await s.listen(app)
|
||||||
|
s.listenUpgrade(wsApp)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -68,4 +76,18 @@ describe("plugin", () => {
|
||||||
const body = await resp.text()
|
const body = await resp.text()
|
||||||
expect(body).toBe(indexHTML)
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
settings:
|
||||||
|
import/resolver:
|
||||||
|
alias:
|
||||||
|
map:
|
||||||
|
- [code-server, ./typings/pluginapi.d.ts]
|
|
@ -12,8 +12,5 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"express": "^4.17.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import * as express from "express"
|
import * as cs from "code-server"
|
||||||
import * as fspath from "path"
|
import * as fspath from "path"
|
||||||
import * as pluginapi from "../../../typings/pluginapi"
|
|
||||||
|
|
||||||
export const plugin: pluginapi.Plugin = {
|
export const plugin: cs.Plugin = {
|
||||||
displayName: "Test Plugin",
|
displayName: "Test Plugin",
|
||||||
routerPath: "/test-plugin",
|
routerPath: "/test-plugin",
|
||||||
homepageURL: "https://example.com",
|
homepageURL: "https://example.com",
|
||||||
|
@ -13,16 +12,29 @@ export const plugin: pluginapi.Plugin = {
|
||||||
},
|
},
|
||||||
|
|
||||||
router() {
|
router() {
|
||||||
const r = express.Router()
|
const r = cs.express.Router()
|
||||||
r.get("/test-app", (req, res) => {
|
r.get("/test-app", (_, res) => {
|
||||||
res.sendFile(fspath.resolve(__dirname, "../public/index.html"))
|
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"))
|
res.sendFile(fspath.resolve(__dirname, "../public/icon.svg"))
|
||||||
})
|
})
|
||||||
|
r.get("/error", () => {
|
||||||
|
throw new cs.HttpError("error", cs.HttpCode.LargePayload)
|
||||||
|
})
|
||||||
return r
|
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() {
|
applications() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
|
@ -42,8 +42,10 @@
|
||||||
|
|
||||||
/* Module Resolution Options */
|
/* Module Resolution Options */
|
||||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
"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'. */
|
"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. */
|
// "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. */
|
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||||
// "types": [], /* Type declaration files to be included in compilation. */
|
// "types": [], /* Type declaration files to be included in compilation. */
|
||||||
|
|
|
@ -64,372 +64,7 @@
|
||||||
"@types/mime" "*"
|
"@types/mime" "*"
|
||||||
"@types/node" "*"
|
"@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:
|
typescript@^4.0.5:
|
||||||
version "4.0.5"
|
version "4.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389"
|
||||||
integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==
|
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=
|
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
/**
|
/**
|
||||||
* This file describes the code-server plugin API for adding new applications.
|
* 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 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
|
* 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.
|
* 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
|
init(config: PluginConfig): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the plugin should dispose/shutdown everything.
|
||||||
|
*/
|
||||||
|
deinit?(): Promise<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the plugin's router.
|
* Returns the plugin's router.
|
||||||
*
|
*
|
||||||
|
@ -134,6 +212,15 @@ export interface Plugin {
|
||||||
*/
|
*/
|
||||||
router?(): express.Router
|
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
|
* code-server uses this to collect the list of applications that
|
||||||
* the plugin can currently provide.
|
* the plugin can currently provide.
|
||||||
|
@ -156,6 +243,13 @@ export interface PluginConfig {
|
||||||
* All plugin logs should be logged via this logger.
|
* All plugin logs should be logged via this logger.
|
||||||
*/
|
*/
|
||||||
readonly logger: 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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2990,6 +2990,11 @@ eslint-config-prettier@^6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
get-stdin "^6.0.0"
|
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:
|
eslint-import-resolver-node@^0.3.4:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717"
|
||||||
|
|
Loading…
Reference in New Issue