Remove long-unused internal plugin system
We are trying to update Express to fix a vulnerability. We would have to update the plugins as well, but since we are no longer using the plugin system, we can just delete it instead.
This commit is contained in:
parent
949aed1cef
commit
2809245dda
@ -44,10 +44,6 @@ bundle_code_server() {
|
|||||||
rsync src/browser/pages/*.css "$RELEASE_PATH/src/browser/pages"
|
rsync src/browser/pages/*.css "$RELEASE_PATH/src/browser/pages"
|
||||||
rsync src/browser/robots.txt "$RELEASE_PATH/src/browser"
|
rsync src/browser/robots.txt "$RELEASE_PATH/src/browser"
|
||||||
|
|
||||||
# Add typings for plugins
|
|
||||||
mkdir -p "$RELEASE_PATH/typings"
|
|
||||||
rsync typings/pluginapi.d.ts "$RELEASE_PATH/typings"
|
|
||||||
|
|
||||||
# Adds the commit to package.json
|
# Adds the commit to package.json
|
||||||
jq --slurp '(.[0] | del(.scripts,.jest,.devDependencies)) * .[1]' package.json <(
|
jq --slurp '(.[0] | del(.scripts,.jest,.devDependencies)) * .[1]' package.json <(
|
||||||
cat << EOF
|
cat << EOF
|
||||||
|
@ -33,7 +33,7 @@ main() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
CODE_SERVER_PATH="$path" CS_DISABLE_PLUGINS=true ./test/node_modules/.bin/jest "$@" --coverage=false --testRegex "./test/integration" --testPathIgnorePatterns "./test/integration/fixtures"
|
CODE_SERVER_PATH="$path" ./test/node_modules/.bin/jest "$@" --coverage=false --testRegex "./test/integration" --testPathIgnorePatterns "./test/integration/fixtures"
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
@ -6,15 +6,10 @@ main() {
|
|||||||
|
|
||||||
source ./ci/lib.sh
|
source ./ci/lib.sh
|
||||||
|
|
||||||
echo "Building test plugin"
|
|
||||||
pushd test/unit/node/test-plugin
|
|
||||||
make -s out/index.js
|
|
||||||
popd
|
|
||||||
|
|
||||||
# We must keep jest in a sub-directory. See ../../test/package.json for more
|
# We must keep jest in a sub-directory. See ../../test/package.json for more
|
||||||
# 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.
|
||||||
CS_DISABLE_PLUGINS=true ./test/node_modules/.bin/jest "$@" --testRegex "./test/unit/.*ts" --testPathIgnorePatterns "./test/unit/node/test-plugin"
|
./test/node_modules/.bin/jest "$@" --testRegex "./test/unit/.*ts"
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
"publish:docker": "./ci/steps/docker-buildx-push.sh",
|
"publish:docker": "./ci/steps/docker-buildx-push.sh",
|
||||||
"fmt": "npm run prettier && ./ci/dev/doctoc.sh",
|
"fmt": "npm run prettier && ./ci/dev/doctoc.sh",
|
||||||
"lint:scripts": "./ci/dev/lint-scripts.sh",
|
"lint:scripts": "./ci/dev/lint-scripts.sh",
|
||||||
"lint:ts": "eslint --max-warnings=0 --fix $(git ls-files '*.ts' '*.js' | grep -v 'lib/vscode' | grep -v test-plugin)",
|
"lint:ts": "eslint --max-warnings=0 --fix $(git ls-files '*.ts' '*.js' | grep -v 'lib/vscode')",
|
||||||
"test": "echo 'Run npm run test:unit or npm run test:e2e' && exit 1",
|
"test": "echo 'Run npm run test:unit or npm run test:e2e' && exit 1",
|
||||||
"watch": "VSCODE_DEV=1 VSCODE_IPC_HOOK_CLI= NODE_OPTIONS='--max_old_space_size=32384 --trace-warnings' ts-node ./ci/dev/watch.ts",
|
"watch": "VSCODE_DEV=1 VSCODE_IPC_HOOK_CLI= NODE_OPTIONS='--max_old_space_size=32384 --trace-warnings' ts-node ./ci/dev/watch.ts",
|
||||||
"icons": "./ci/dev/gen_icons.sh"
|
"icons": "./ci/dev/gen_icons.sh"
|
||||||
|
@ -1,302 +0,0 @@
|
|||||||
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 { authenticated, ensureAuthenticated, 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 = {
|
|
||||||
HttpCode,
|
|
||||||
HttpError,
|
|
||||||
Level,
|
|
||||||
authenticated,
|
|
||||||
ensureAuthenticated,
|
|
||||||
express,
|
|
||||||
field,
|
|
||||||
proxy,
|
|
||||||
replaceTemplates,
|
|
||||||
WsRouter,
|
|
||||||
wss,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Plugin extends pluginapi.Plugin {
|
|
||||||
/**
|
|
||||||
* These fields are populated from the plugin's package.json
|
|
||||||
* and now guaranteed to exist.
|
|
||||||
*/
|
|
||||||
name: string
|
|
||||||
version: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* path to the node module on the disk.
|
|
||||||
*/
|
|
||||||
modulePath: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Application extends pluginapi.Application {
|
|
||||||
/*
|
|
||||||
* Clone of the above without functions.
|
|
||||||
*/
|
|
||||||
plugin: Omit<Plugin, "init" | "deinit" | "router" | "applications">
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PluginAPI implements the plugin API described in typings/pluginapi.d.ts
|
|
||||||
* Please see that file for details.
|
|
||||||
*/
|
|
||||||
export class PluginAPI {
|
|
||||||
private readonly plugins = new Map<string, Plugin>()
|
|
||||||
private readonly logger: Logger
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
logger: Logger,
|
|
||||||
/**
|
|
||||||
* These correspond to $CS_PLUGIN_PATH and $CS_PLUGIN respectively.
|
|
||||||
*/
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* applications grabs the full list of applications from
|
|
||||||
* all loaded plugins.
|
|
||||||
*/
|
|
||||||
public async applications(): Promise<Application[]> {
|
|
||||||
const apps = new Array<Application>()
|
|
||||||
for (const [, p] of this.plugins) {
|
|
||||||
if (!p.applications) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const pluginApps = await p.applications()
|
|
||||||
|
|
||||||
// Add plugin key to each app.
|
|
||||||
apps.push(
|
|
||||||
...pluginApps.map((app) => {
|
|
||||||
app = { ...app, path: path.join(p.routerPath, app.path || "") }
|
|
||||||
app = { ...app, iconPath: path.join(app.path || "", app.iconPath) }
|
|
||||||
return {
|
|
||||||
...app,
|
|
||||||
plugin: {
|
|
||||||
name: p.name,
|
|
||||||
version: p.version,
|
|
||||||
modulePath: p.modulePath,
|
|
||||||
|
|
||||||
displayName: p.displayName,
|
|
||||||
description: p.description,
|
|
||||||
routerPath: p.routerPath,
|
|
||||||
homepageURL: p.homepageURL,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return apps
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* mount mounts all plugin routers onto r and websocket routers onto wr.
|
|
||||||
*/
|
|
||||||
public mount(r: express.Router, wr: express.Router): void {
|
|
||||||
for (const [, p] of this.plugins) {
|
|
||||||
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(loadBuiltin = true): Promise<void> {
|
|
||||||
for (const dir of this.csPlugin.split(":")) {
|
|
||||||
if (!dir) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
await this.loadPlugin(dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const dir of this.csPluginPath.split(":")) {
|
|
||||||
if (!dir) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
await this._loadPlugins(dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loadBuiltin) {
|
|
||||||
await this._loadPlugins(path.join(__dirname, "../../plugins"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* _loadPlugins is the counterpart to loadPlugins.
|
|
||||||
*
|
|
||||||
* It differs in that it loads all plugins in a single
|
|
||||||
* directory whereas loadPlugins uses all available directories
|
|
||||||
* as documented.
|
|
||||||
*/
|
|
||||||
private async _loadPlugins(dir: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
const entries = await fsp.readdir(dir, { withFileTypes: true })
|
|
||||||
for (const ent of entries) {
|
|
||||||
if (!ent.isDirectory()) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
await this.loadPlugin(path.join(dir, ent.name))
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.code !== "ENOENT") {
|
|
||||||
this.logger.warn(`failed to load plugins from ${q(dir)}: ${error.message}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadPlugin(dir: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
const str = await fsp.readFile(path.join(dir, "package.json"), {
|
|
||||||
encoding: "utf8",
|
|
||||||
})
|
|
||||||
const packageJSON: PackageJSON = JSON.parse(str)
|
|
||||||
for (const [, p] of this.plugins) {
|
|
||||||
if (p.name === packageJSON.name) {
|
|
||||||
this.logger.warn(
|
|
||||||
`ignoring duplicate plugin ${q(p.name)} at ${q(dir)}, using previously loaded ${q(p.modulePath)}`,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const p = this._loadPlugin(dir, packageJSON)
|
|
||||||
this.plugins.set(p.name, p)
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.code !== "ENOENT") {
|
|
||||||
this.logger.warn(`failed to load plugin: ${error.stack}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* _loadPlugin is the counterpart to loadPlugin and actually
|
|
||||||
* loads the plugin now that we know there is no duplicate
|
|
||||||
* and that the package.json has been read.
|
|
||||||
*/
|
|
||||||
private _loadPlugin(dir: string, packageJSON: PackageJSON): Plugin {
|
|
||||||
dir = path.resolve(dir)
|
|
||||||
|
|
||||||
const logger = this.logger.named(packageJSON.name)
|
|
||||||
logger.debug("loading plugin", field("plugin_dir", dir), field("package_json", packageJSON))
|
|
||||||
|
|
||||||
if (!packageJSON.name) {
|
|
||||||
throw new Error("plugin package.json missing name")
|
|
||||||
}
|
|
||||||
if (!packageJSON.version) {
|
|
||||||
throw new Error("plugin package.json missing version")
|
|
||||||
}
|
|
||||||
if (!packageJSON.engines || !packageJSON.engines["code-server"]) {
|
|
||||||
throw new Error(`plugin package.json missing code-server range like:
|
|
||||||
"engines": {
|
|
||||||
"code-server": "^3.7.0"
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
if (!semver.satisfies(version, packageJSON.engines["code-server"])) {
|
|
||||||
this.logger.warn(
|
|
||||||
`plugin range ${q(packageJSON.engines["code-server"])} incompatible` + ` with code-server version ${version}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const pluginModule = require(dir)
|
|
||||||
if (!pluginModule.plugin) {
|
|
||||||
throw new Error("plugin module does not export a plugin")
|
|
||||||
}
|
|
||||||
|
|
||||||
const p = {
|
|
||||||
name: packageJSON.name,
|
|
||||||
version: packageJSON.version,
|
|
||||||
modulePath: dir,
|
|
||||||
...pluginModule.plugin,
|
|
||||||
} as Plugin
|
|
||||||
|
|
||||||
if (!p.displayName) {
|
|
||||||
throw new Error("plugin missing displayName")
|
|
||||||
}
|
|
||||||
if (!p.description) {
|
|
||||||
throw new Error("plugin missing description")
|
|
||||||
}
|
|
||||||
if (!p.routerPath) {
|
|
||||||
throw new Error("plugin missing router path")
|
|
||||||
}
|
|
||||||
if (!p.routerPath.startsWith("/")) {
|
|
||||||
throw new Error(`plugin router path ${q(p.routerPath)}: invalid`)
|
|
||||||
}
|
|
||||||
if (!p.homepageURL) {
|
|
||||||
throw new Error("plugin missing homepage")
|
|
||||||
}
|
|
||||||
|
|
||||||
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: any) {
|
|
||||||
this.logger.error("plugin failed to deinit", field("name", p.name), field("error", error.message))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PackageJSON {
|
|
||||||
name: string
|
|
||||||
version: string
|
|
||||||
engines: {
|
|
||||||
"code-server": string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function q(s: string | undefined): string {
|
|
||||||
if (s === undefined) {
|
|
||||||
s = "undefined"
|
|
||||||
}
|
|
||||||
return JSON.stringify(s)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
import * as express from "express"
|
|
||||||
import { PluginAPI } from "../plugin"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements the /api/applications endpoint
|
|
||||||
*
|
|
||||||
* See typings/pluginapi.d.ts for details.
|
|
||||||
*/
|
|
||||||
export function router(papi: PluginAPI): express.Router {
|
|
||||||
const router = express.Router()
|
|
||||||
|
|
||||||
router.get("/", async (req, res) => {
|
|
||||||
res.json(await papi.applications())
|
|
||||||
})
|
|
||||||
|
|
||||||
return router
|
|
||||||
}
|
|
@ -2,8 +2,8 @@ import { logger } from "@coder/logger"
|
|||||||
import express from "express"
|
import express from "express"
|
||||||
import { promises as fs } from "fs"
|
import { promises as fs } from "fs"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { WebsocketRequest } from "../../../typings/pluginapi"
|
|
||||||
import { HttpCode } from "../../common/http"
|
import { HttpCode } from "../../common/http"
|
||||||
|
import type { WebsocketRequest } from "../wsRouter"
|
||||||
import { rootPath } from "../constants"
|
import { rootPath } from "../constants"
|
||||||
import { replaceTemplates } from "../http"
|
import { replaceTemplates } from "../http"
|
||||||
import { escapeHtml, getMediaMime } from "../util"
|
import { escapeHtml, getMediaMime } from "../util"
|
||||||
|
@ -4,7 +4,6 @@ import * as express from "express"
|
|||||||
import { promises as fs } from "fs"
|
import { promises as fs } from "fs"
|
||||||
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 { Disposable } from "../../common/emitter"
|
import { Disposable } from "../../common/emitter"
|
||||||
import { HttpCode, HttpError } from "../../common/http"
|
import { HttpCode, HttpError } from "../../common/http"
|
||||||
import { plural } from "../../common/util"
|
import { plural } from "../../common/util"
|
||||||
@ -12,12 +11,11 @@ import { App } from "../app"
|
|||||||
import { AuthType, DefaultedArgs } from "../cli"
|
import { AuthType, DefaultedArgs } from "../cli"
|
||||||
import { commit, rootPath } from "../constants"
|
import { commit, rootPath } from "../constants"
|
||||||
import { Heart } from "../heart"
|
import { Heart } from "../heart"
|
||||||
import { ensureAuthenticated, redirect } from "../http"
|
import { redirect } from "../http"
|
||||||
import { PluginAPI } from "../plugin"
|
|
||||||
import { CoderSettings, SettingsProvider } from "../settings"
|
import { CoderSettings, SettingsProvider } from "../settings"
|
||||||
import { UpdateProvider } from "../update"
|
import { UpdateProvider } from "../update"
|
||||||
|
import type { WebsocketRequest } from "../wsRouter"
|
||||||
import { getMediaMime, paths } from "../util"
|
import { getMediaMime, paths } from "../util"
|
||||||
import * as apps from "./apps"
|
|
||||||
import * as domainProxy from "./domainProxy"
|
import * as domainProxy from "./domainProxy"
|
||||||
import { errorHandler, wsErrorHandler } from "./errors"
|
import { errorHandler, wsErrorHandler } from "./errors"
|
||||||
import * as health from "./health"
|
import * as health from "./health"
|
||||||
@ -113,7 +111,7 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl
|
|||||||
await pathProxy.proxy(req, res)
|
await pathProxy.proxy(req, res)
|
||||||
})
|
})
|
||||||
app.wsRouter.get("/proxy/:port/:path(.*)?", async (req) => {
|
app.wsRouter.get("/proxy/:port/:path(.*)?", async (req) => {
|
||||||
await pathProxy.wsProxy(req as pluginapi.WebsocketRequest)
|
await pathProxy.wsProxy(req as 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
|
||||||
@ -125,21 +123,12 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
app.wsRouter.get("/absproxy/:port/:path(.*)?", async (req) => {
|
app.wsRouter.get("/absproxy/:port/:path(.*)?", async (req) => {
|
||||||
await pathProxy.wsProxy(req as pluginapi.WebsocketRequest, {
|
await pathProxy.wsProxy(req as WebsocketRequest, {
|
||||||
passthroughPath: true,
|
passthroughPath: true,
|
||||||
proxyBasePath: args["abs-proxy-base-path"],
|
proxyBasePath: args["abs-proxy-base-path"],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
let pluginApi: PluginAPI
|
|
||||||
if (!process.env.CS_DISABLE_PLUGINS) {
|
|
||||||
const workingDir = args._ && args._.length > 0 ? path.resolve(args._[args._.length - 1]) : undefined
|
|
||||||
pluginApi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH, workingDir)
|
|
||||||
await pluginApi.loadPlugins()
|
|
||||||
pluginApi.mount(app.router, app.wsRouter)
|
|
||||||
app.router.use("/api/applications", ensureAuthenticated, apps.router(pluginApi))
|
|
||||||
}
|
|
||||||
|
|
||||||
app.router.use(express.json())
|
app.router.use(express.json())
|
||||||
app.router.use(express.urlencoded({ extended: true }))
|
app.router.use(express.urlencoded({ extended: true }))
|
||||||
|
|
||||||
@ -172,7 +161,9 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl
|
|||||||
|
|
||||||
app.router.use("/update", update.router)
|
app.router.use("/update", update.router)
|
||||||
|
|
||||||
// Note that the root route is replaced in Coder Enterprise by the plugin API.
|
// For historic reasons we also load at /vscode because the root was replaced
|
||||||
|
// by a plugin in v1 of Coder. The plugin system (which was for internal use
|
||||||
|
// only) has been removed, but leave the additional route for now.
|
||||||
for (const routePrefix of ["/vscode", "/"]) {
|
for (const routePrefix of ["/vscode", "/"]) {
|
||||||
app.router.use(routePrefix, vscode.router)
|
app.router.use(routePrefix, vscode.router)
|
||||||
app.wsRouter.use(routePrefix, vscode.wsRouter.router)
|
app.wsRouter.use(routePrefix, vscode.wsRouter.router)
|
||||||
@ -187,7 +178,6 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
heart.dispose()
|
heart.dispose()
|
||||||
pluginApi?.dispose()
|
|
||||||
vscode.dispose()
|
vscode.dispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Request, Response } from "express"
|
import { Request, Response } from "express"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as pluginapi from "../../../typings/pluginapi"
|
|
||||||
import { HttpCode, HttpError } from "../../common/http"
|
import { HttpCode, HttpError } from "../../common/http"
|
||||||
import { ensureProxyEnabled, authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http"
|
import { ensureProxyEnabled, authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http"
|
||||||
import { proxy as _proxy } from "../proxy"
|
import { proxy as _proxy } from "../proxy"
|
||||||
|
import type { WebsocketRequest } from "../wsRouter"
|
||||||
|
|
||||||
const getProxyTarget = (
|
const getProxyTarget = (
|
||||||
req: Request,
|
req: Request,
|
||||||
@ -49,7 +49,7 @@ export async function proxy(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function wsProxy(
|
export async function wsProxy(
|
||||||
req: pluginapi.WebsocketRequest,
|
req: WebsocketRequest,
|
||||||
opts?: {
|
opts?: {
|
||||||
passthroughPath?: boolean
|
passthroughPath?: boolean
|
||||||
proxyBasePath?: string
|
proxyBasePath?: string
|
||||||
|
@ -6,14 +6,13 @@ import * as http from "http"
|
|||||||
import * as net from "net"
|
import * as net from "net"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as os from "os"
|
import * as os from "os"
|
||||||
import { WebsocketRequest } from "../../../typings/pluginapi"
|
|
||||||
import { logError } from "../../common/util"
|
import { logError } from "../../common/util"
|
||||||
import { CodeArgs, toCodeArgs } from "../cli"
|
import { CodeArgs, toCodeArgs } from "../cli"
|
||||||
import { isDevMode, vsRootPath } from "../constants"
|
import { isDevMode, vsRootPath } from "../constants"
|
||||||
import { authenticated, ensureAuthenticated, ensureOrigin, redirect, replaceTemplates, self } from "../http"
|
import { authenticated, ensureAuthenticated, ensureOrigin, redirect, replaceTemplates, self } from "../http"
|
||||||
import { SocketProxyProvider } from "../socket"
|
import { SocketProxyProvider } from "../socket"
|
||||||
import { isFile } from "../util"
|
import { isFile } from "../util"
|
||||||
import { Router as WsRouter } from "../wsRouter"
|
import { type WebsocketRequest, Router as WsRouter } from "../wsRouter"
|
||||||
|
|
||||||
export const router = express.Router()
|
export const router = express.Router()
|
||||||
|
|
||||||
|
@ -1,8 +1,17 @@
|
|||||||
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 stream from "stream"
|
||||||
import Websocket from "ws"
|
import Websocket from "ws"
|
||||||
import * as pluginapi from "../../typings/pluginapi"
|
|
||||||
|
export interface WebsocketRequest extends express.Request {
|
||||||
|
ws: stream.Duplex
|
||||||
|
head: Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InternalWebsocketRequest extends WebsocketRequest {
|
||||||
|
_ws_handled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
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) => {
|
||||||
@ -22,9 +31,11 @@ export const handleUpgrade = (app: express.Express, server: http.Server): void =
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InternalWebsocketRequest extends pluginapi.WebsocketRequest {
|
export type WebSocketHandler = (
|
||||||
_ws_handled: boolean
|
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()
|
||||||
@ -36,13 +47,13 @@ export class WebsocketRouter {
|
|||||||
* If the origin header exists it must match the host or the connection will
|
* If the origin header exists it must match the host or the connection will
|
||||||
* be prevented.
|
* be prevented.
|
||||||
*/
|
*/
|
||||||
public ws(route: expressCore.PathParams, ...handlers: pluginapi.WebSocketHandler[]): void {
|
public ws(route: expressCore.PathParams, ...handlers: 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 pluginapi.WebsocketRequest, res, next)
|
return handler(req as WebsocketRequest, res, next)
|
||||||
}
|
}
|
||||||
return wrapped
|
return wrapped
|
||||||
}),
|
}),
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"extends": "../tsconfig.json",
|
"extends": "../tsconfig.json",
|
||||||
"include": ["./**/*.ts"],
|
"include": ["./**/*.ts"]
|
||||||
"exclude": ["./unit/node/test-plugin"]
|
|
||||||
}
|
}
|
||||||
|
@ -1,118 +0,0 @@
|
|||||||
import { logger } from "@coder/logger"
|
|
||||||
import * as express from "express"
|
|
||||||
import * as fs from "fs"
|
|
||||||
import * as path from "path"
|
|
||||||
import { HttpCode } from "../../../src/common/http"
|
|
||||||
import { AuthType } from "../../../src/node/cli"
|
|
||||||
import { codeServer, PluginAPI } from "../../../src/node/plugin"
|
|
||||||
import * as apps from "../../../src/node/routes/apps"
|
|
||||||
import * as httpserver from "../../utils/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.
|
|
||||||
*/
|
|
||||||
describe("plugin", () => {
|
|
||||||
let papi: PluginAPI
|
|
||||||
let s: httpserver.HttpServer
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
// 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()
|
|
||||||
const wsApp = express.default()
|
|
||||||
|
|
||||||
const common: express.RequestHandler = (req, _, next) => {
|
|
||||||
// Routes might use these arguments.
|
|
||||||
req.args = {
|
|
||||||
_: [],
|
|
||||||
auth: AuthType.None,
|
|
||||||
host: "localhost",
|
|
||||||
port: 8080,
|
|
||||||
"proxy-domain": [],
|
|
||||||
config: "~/.config/code-server/config.yaml",
|
|
||||||
verbose: false,
|
|
||||||
"disable-file-downloads": false,
|
|
||||||
usingEnvPassword: false,
|
|
||||||
usingEnvHashedPassword: false,
|
|
||||||
"extensions-dir": "",
|
|
||||||
"user-data-dir": "",
|
|
||||||
"session-socket": "",
|
|
||||||
}
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
|
|
||||||
app.use(common)
|
|
||||||
wsApp.use(common)
|
|
||||||
|
|
||||||
papi.mount(app, wsApp)
|
|
||||||
app.use("/api/applications", apps.router(papi))
|
|
||||||
|
|
||||||
s = new httpserver.HttpServer()
|
|
||||||
await s.listen(app)
|
|
||||||
s.listenUpgrade(wsApp)
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await s.dispose()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("/api/applications", async () => {
|
|
||||||
const resp = await s.fetch("/api/applications")
|
|
||||||
expect(resp.status).toBe(200)
|
|
||||||
const body = await resp.json()
|
|
||||||
logger.debug(`${JSON.stringify(body)}`)
|
|
||||||
expect(body).toStrictEqual([
|
|
||||||
{
|
|
||||||
name: "Test App",
|
|
||||||
version: "4.0.1",
|
|
||||||
|
|
||||||
description: "This app does XYZ.",
|
|
||||||
iconPath: "/test-plugin/test-app/icon.svg",
|
|
||||||
homepageURL: "https://example.com",
|
|
||||||
path: "/test-plugin/test-app",
|
|
||||||
|
|
||||||
plugin: {
|
|
||||||
name: "test-plugin",
|
|
||||||
version: "1.0.0",
|
|
||||||
modulePath: path.join(__dirname, "test-plugin"),
|
|
||||||
|
|
||||||
displayName: "Test Plugin",
|
|
||||||
description: "Plugin used in code-server tests.",
|
|
||||||
routerPath: "/test-plugin",
|
|
||||||
homepageURL: "https://example.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it("/test-plugin/test-app", async () => {
|
|
||||||
const indexHTML = await fsp.readFile(path.join(__dirname, "test-plugin/public/index.html"), {
|
|
||||||
encoding: "utf8",
|
|
||||||
})
|
|
||||||
const resp = await s.fetch("/test-plugin/test-app")
|
|
||||||
expect(resp.status).toBe(200)
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,9 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
settings: {
|
|
||||||
"import/resolver": {
|
|
||||||
typescript: {
|
|
||||||
project: __dirname,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
1
test/unit/node/test-plugin/.gitignore
vendored
1
test/unit/node/test-plugin/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
out
|
|
@ -1,6 +0,0 @@
|
|||||||
out/index.js: src/index.ts
|
|
||||||
# Typescript always emits, even on errors.
|
|
||||||
npm run build || rm out/index.js
|
|
||||||
|
|
||||||
node_modules: package.json package-lock.json
|
|
||||||
npm install
|
|
90
test/unit/node/test-plugin/package-lock.json
generated
90
test/unit/node/test-plugin/package-lock.json
generated
@ -1,90 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "test-plugin",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"lockfileVersion": 1,
|
|
||||||
"requires": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/body-parser": {
|
|
||||||
"version": "1.19.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
|
|
||||||
"integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
|
|
||||||
"requires": {
|
|
||||||
"@types/connect": "*",
|
|
||||||
"@types/node": "*"
|
|
||||||
},
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/connect": {
|
|
||||||
"version": "3.4.33",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
|
|
||||||
"integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==",
|
|
||||||
"requires": {
|
|
||||||
"@types/node": "*"
|
|
||||||
},
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/express": {
|
|
||||||
"version": "4.17.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.8.tgz",
|
|
||||||
"integrity": "sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ==",
|
|
||||||
"requires": {
|
|
||||||
"@types/body-parser": "*",
|
|
||||||
"@types/express-serve-static-core": "*",
|
|
||||||
"@types/qs": "*",
|
|
||||||
"@types/serve-static": "*"
|
|
||||||
},
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/express-serve-static-core": {
|
|
||||||
"version": "4.17.13",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz",
|
|
||||||
"integrity": "sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA==",
|
|
||||||
"requires": {
|
|
||||||
"@types/node": "*",
|
|
||||||
"@types/qs": "*",
|
|
||||||
"@types/range-parser": "*"
|
|
||||||
},
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/mime": {
|
|
||||||
"version": "2.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
|
|
||||||
"integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/node": {
|
|
||||||
"version": "14.14.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.6.tgz",
|
|
||||||
"integrity": "sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/qs": {
|
|
||||||
"version": "6.9.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz",
|
|
||||||
"integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/range-parser": {
|
|
||||||
"version": "1.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
|
|
||||||
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/serve-static": {
|
|
||||||
"version": "1.13.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.6.tgz",
|
|
||||||
"integrity": "sha512-nuRJmv7jW7VmCVTn+IgYDkkbbDGyIINOeu/G0d74X3lm6E5KfMeQPJhxIt1ayQeQB3cSxvYs1RA/wipYoFB4EA==",
|
|
||||||
"requires": {
|
|
||||||
"@types/mime": "*",
|
|
||||||
"@types/node": "*"
|
|
||||||
},
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"typescript": {
|
|
||||||
"version": "4.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.5.tgz",
|
|
||||||
"integrity": "sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==",
|
|
||||||
"dev": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"private": true,
|
|
||||||
"name": "test-plugin",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"engines": {
|
|
||||||
"code-server": "*"
|
|
||||||
},
|
|
||||||
"main": "out/index.js",
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/express": "^4.17.8",
|
|
||||||
"typescript": "^4.0.5"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
<svg width="121" height="131" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="9.612%" y1="66.482%" x2="89.899%" y2="33.523%" id="a"><stop stop-color="#FCEE39" offset="0%"/><stop stop-color="#F37B3D" offset="100%"/></linearGradient><linearGradient x1="8.601%" y1="15.03%" x2="99.641%" y2="89.058%" id="b"><stop stop-color="#EF5A6B" offset="0%"/><stop stop-color="#F26F4E" offset="57%"/><stop stop-color="#F37B3D" offset="100%"/></linearGradient><linearGradient x1="90.118%" y1="69.931%" x2="17.938%" y2="38.628%" id="c"><stop stop-color="#7C59A4" offset="0%"/><stop stop-color="#AF4C92" offset="38.52%"/><stop stop-color="#DC4183" offset="76.54%"/><stop stop-color="#ED3D7D" offset="95.7%"/></linearGradient><linearGradient x1="91.376%" y1="19.144%" x2="18.895%" y2="70.21%" id="d"><stop stop-color="#EF5A6B" offset="0%"/><stop stop-color="#EE4E72" offset="36.4%"/><stop stop-color="#ED3D7D" offset="100%"/></linearGradient></defs><g fill="none"><path d="M118.623 71.8c.9-.8 1.4-1.9 1.5-3.2.1-2.6-1.8-4.7-4.4-4.9-1.2-.1-2.4.4-3.3 1.1l-83.8 45.9c-1.9.8-3.6 2.2-4.7 4.1-2.9 4.8-1.3 11 3.6 13.9 3.4 2 7.5 1.8 10.7-.2.2-.2.5-.3.7-.5l78-54.8c.4-.3 1.5-1.1 1.7-1.4z" fill="url(#a)" transform="translate(-.023)"/><path d="M118.823 65.1l-63.8-62.6c-1.4-1.5-3.4-2.5-5.7-2.5-4.3 0-7.7 3.5-7.7 7.7 0 2.1.8 3.9 2.1 5.3.4.4.8.7 1.2 1l67.4 57.7c.8.7 1.8 1.2 3 1.3 2.6.1 4.7-1.8 4.9-4.4 0-1.3-.5-2.6-1.4-3.5z" fill="url(#b)" transform="translate(-.023)"/><path d="M57.123 59.5c-.1 0-39.4-31-40.2-31.5l-1.8-.9c-5.8-2.2-12.2.8-14.4 6.6-1.9 5.1.2 10.7 4.6 13.4.7.4 1.3.7 2 .9.4.2 45.4 18.8 45.4 18.8 1.8.8 3.9.3 5.1-1.2 1.5-1.9 1.2-4.6-.7-6.1z" fill="url(#c)" transform="translate(-.023)"/><path d="M49.323 0c-1.7 0-3.3.6-4.6 1.5l-39.8 26.8c-.1.1-.2.1-.2.2h-.1c-1.7 1.2-3.1 3-3.9 5.1-2.2 5.8.8 12.3 6.6 14.4 3.6 1.4 7.5.7 10.4-1.4.7-.5 1.3-1 1.8-1.6l34.6-31.2c1.8-1.4 3-3.6 3-6.1 0-4.2-3.5-7.7-7.8-7.7z" fill="url(#d)" transform="translate(-.023)"/><path fill="#000" d="M34.6 37.4h51v51h-51z"/><path fill="#FFF" d="M39 78.8h19.1V82H39zm-.2-28l1.5-1.4c.4.5.8.8 1.3.8.6 0 .9-.4.9-1.2v-5.3h2.3V49c0 1-.3 1.8-.8 2.3-.5.5-1.3.8-2.3.8-1.5.1-2.3-.5-2.9-1.3zm6.5-7H52v1.9h-4.4V47h4v1.8h-4v1.3h4.5v2h-6.7zm9.7 2h-2.5v-2h7.3v2h-2.5v6.3H55zM39 54h4.3c1 0 1.8.3 2.3.7.3.3.5.8.5 1.4 0 1-.5 1.5-1.3 1.9 1 .3 1.6.9 1.6 2 0 1.4-1.2 2.3-3.1 2.3H39V54zm4.8 2.6c0-.5-.4-.7-1-.7h-1.5v1.5h1.4c.7-.1 1.1-.3 1.1-.8zM43 59h-1.8v1.5H43c.7 0 1.1-.3 1.1-.8s-.4-.7-1.1-.7zm3.8-5h3.9c1.3 0 2.1.3 2.7.9.5.5.7 1.1.7 1.9 0 1.3-.7 2.1-1.7 2.6l2 2.9h-2.6l-1.7-2.5h-1v2.5h-2.3V54zm3.8 4c.8 0 1.2-.4 1.2-1 0-.7-.5-1-1.2-1h-1.5v2h1.5z"/><path d="M56.8 54H59l3.5 8.4H60l-.6-1.5h-3.2l-.6 1.5h-2.4l3.6-8.4zm2 5l-.9-2.3L57 59h1.8zm4-5h2.3v8.3h-2.3zm2.9 0h2.1l3.4 4.4V54h2.3v8.3h-2L68 57.8v4.6h-2.3zm8 7.1l1.3-1.5c.8.7 1.7 1 2.7 1 .6 0 1-.2 1-.6 0-.4-.3-.5-1.4-.8-1.8-.4-3.1-.9-3.1-2.6 0-1.5 1.2-2.7 3.2-2.7 1.4 0 2.5.4 3.4 1.1l-1.2 1.6c-.8-.5-1.6-.8-2.3-.8-.6 0-.8.2-.8.5 0 .4.3.5 1.4.8 1.9.4 3.1 1 3.1 2.6 0 1.7-1.3 2.7-3.4 2.7-1.5.1-2.9-.4-3.9-1.3z" fill="#FFF"/></g></svg>
|
|
Before Width: | Height: | Size: 3.0 KiB |
@ -1,10 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>Test Plugin</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<p>Welcome to the test plugin!</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,52 +0,0 @@
|
|||||||
import * as cs from "code-server"
|
|
||||||
import * as fspath from "path"
|
|
||||||
|
|
||||||
export const plugin: cs.Plugin = {
|
|
||||||
displayName: "Test Plugin",
|
|
||||||
routerPath: "/test-plugin",
|
|
||||||
homepageURL: "https://example.com",
|
|
||||||
description: "Plugin used in code-server tests.",
|
|
||||||
|
|
||||||
init(config) {
|
|
||||||
config.logger.debug("test-plugin loaded!")
|
|
||||||
},
|
|
||||||
|
|
||||||
router() {
|
|
||||||
const r = cs.express.Router()
|
|
||||||
r.get("/test-app", (_, res) => {
|
|
||||||
res.sendFile(fspath.resolve(__dirname, "../public/index.html"))
|
|
||||||
})
|
|
||||||
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.ws, req.head, (ws) => {
|
|
||||||
req.ws.resume()
|
|
||||||
ws.send("hello")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return wr
|
|
||||||
},
|
|
||||||
|
|
||||||
applications() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: "Test App",
|
|
||||||
version: "4.0.1",
|
|
||||||
iconPath: "/icon.svg",
|
|
||||||
path: "/test-app",
|
|
||||||
|
|
||||||
description: "This app does XYZ.",
|
|
||||||
homepageURL: "https://example.com",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
|
||||||
|
|
||||||
/* Basic Options */
|
|
||||||
// "incremental": true, /* Enable incremental compilation */
|
|
||||||
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
|
|
||||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
|
|
||||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
|
||||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
|
||||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
|
||||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
|
||||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
|
||||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
|
||||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
|
||||||
"outDir": "./out" /* Redirect output structure to the directory. */,
|
|
||||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
|
||||||
// "composite": true, /* Enable project compilation */
|
|
||||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
|
||||||
// "removeComments": true, /* Do not emit comments to output. */
|
|
||||||
// "noEmit": true, /* Do not emit outputs. */
|
|
||||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
|
||||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
|
||||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
|
||||||
|
|
||||||
/* Strict Type-Checking Options */
|
|
||||||
"strict": true /* Enable all strict type-checking options. */,
|
|
||||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
|
||||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
|
||||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
|
||||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
|
||||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
|
||||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
|
||||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
|
||||||
|
|
||||||
/* Additional Checks */
|
|
||||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
|
||||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
|
||||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
|
||||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
|
||||||
|
|
||||||
/* 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": {
|
|
||||||
"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. */
|
|
||||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
|
||||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
|
||||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
|
||||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
|
||||||
|
|
||||||
/* Source Map Options */
|
|
||||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
|
||||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
|
||||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
|
||||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
|
||||||
|
|
||||||
/* Experimental Options */
|
|
||||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
|
||||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
|
||||||
|
|
||||||
/* Advanced Options */
|
|
||||||
"skipLibCheck": true /* Skip type checking of declaration files. */,
|
|
||||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
|
||||||
}
|
|
||||||
}
|
|
297
typings/pluginapi.d.ts
vendored
297
typings/pluginapi.d.ts
vendored
@ -1,297 +0,0 @@
|
|||||||
/**
|
|
||||||
* This file describes the code-server plugin API for adding new applications.
|
|
||||||
*/
|
|
||||||
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 stream from "stream"
|
|
||||||
import Websocket from "ws"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overlay
|
|
||||||
*
|
|
||||||
* The homepage of code-server will launch into VS Code. However, there will be an overlay
|
|
||||||
* button that when clicked, will show all available applications with their names,
|
|
||||||
* icons and provider plugins. When one clicks on an app's icon, they will be directed
|
|
||||||
* to <code-server-root>/<plugin-path>/<app-path> to access the application.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugins
|
|
||||||
*
|
|
||||||
* Plugins are just node modules that contain a top level export "plugin" that implements
|
|
||||||
* the Plugin interface.
|
|
||||||
*
|
|
||||||
* 1. code-server uses $CS_PLUGIN to find plugins.
|
|
||||||
*
|
|
||||||
* e.g. CS_PLUGIN=/tmp/will:/tmp/teffen will cause code-server to load
|
|
||||||
* /tmp/will and /tmp/teffen as plugins.
|
|
||||||
*
|
|
||||||
* 2. code-server uses $CS_PLUGIN_PATH to find plugins. Each subdirectory in
|
|
||||||
* $CS_PLUGIN_PATH with a package.json where the engine is code-server is
|
|
||||||
* a valid plugin.
|
|
||||||
*
|
|
||||||
* e.g. CS_PLUGIN_PATH=/tmp/nhooyr:/tmp/ash will cause code-server to search
|
|
||||||
* /tmp/nhooyr and then /tmp/ash for plugins.
|
|
||||||
*
|
|
||||||
* CS_PLUGIN_PATH defaults to
|
|
||||||
* ~/.local/share/code-server/plugins:/usr/share/code-server/plugins
|
|
||||||
* if unset.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* 3. Built in plugins are loaded from __dirname/../plugins
|
|
||||||
*
|
|
||||||
* Plugins are required as soon as they are found and then initialized.
|
|
||||||
* See the Plugin interface for details.
|
|
||||||
*
|
|
||||||
* If two plugins are found with the exact same name, then code-server will
|
|
||||||
* use the first one and emit a warning.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Programmability
|
|
||||||
*
|
|
||||||
* There is also a /api/applications endpoint to allow programmatic access to all
|
|
||||||
* available applications. It could be used to create a custom application dashboard
|
|
||||||
* for example. An important difference with the API is that all application paths
|
|
||||||
* will be absolute (i.e have the plugin path prepended) so that they may be used
|
|
||||||
* directly.
|
|
||||||
*
|
|
||||||
* Example output:
|
|
||||||
*
|
|
||||||
* [
|
|
||||||
* {
|
|
||||||
* "name": "Test App",
|
|
||||||
* "version": "4.0.1",
|
|
||||||
* "iconPath": "/test-plugin/test-app/icon.svg",
|
|
||||||
* "path": "/test-plugin/test-app",
|
|
||||||
* "description": "This app does XYZ.",
|
|
||||||
* "homepageURL": "https://example.com",
|
|
||||||
* "plugin": {
|
|
||||||
* "name": "test-plugin",
|
|
||||||
* "version": "1.0.0",
|
|
||||||
* "modulePath": "/Users/nhooyr/src/coder/code-server/test/test-plugin",
|
|
||||||
* "displayName": "Test Plugin",
|
|
||||||
* "description": "Plugin used in code-server tests.",
|
|
||||||
* "routerPath": "/test-plugin",
|
|
||||||
* "homepageURL": "https://example.com"
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* ]
|
|
||||||
*/
|
|
||||||
|
|
||||||
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: stream.Duplex
|
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Middleware to ensure the user is authenticated. Throws if they are not.
|
|
||||||
*/
|
|
||||||
export function ensureAuthenticated(
|
|
||||||
req: express.Request,
|
|
||||||
res?: express.Response,
|
|
||||||
next?: express.NextFunction,
|
|
||||||
): Promise<void>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the user is authenticated.
|
|
||||||
*/
|
|
||||||
export function authenticated(req: express.Request): Promise<void>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*
|
|
||||||
* The plugin's router will be mounted at <code-sever-root>/<plugin-path>
|
|
||||||
*/
|
|
||||||
export interface Plugin {
|
|
||||||
/**
|
|
||||||
* name is used as the plugin's unique identifier.
|
|
||||||
* No two plugins may share the same name.
|
|
||||||
*
|
|
||||||
* Fetched from package.json.
|
|
||||||
*/
|
|
||||||
readonly name?: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The version for the plugin in the overlay.
|
|
||||||
*
|
|
||||||
* Fetched from package.json.
|
|
||||||
*/
|
|
||||||
readonly version?: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name used in the overlay.
|
|
||||||
*/
|
|
||||||
readonly displayName: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used in overlay.
|
|
||||||
* Should be a full sentence describing the plugin.
|
|
||||||
*/
|
|
||||||
readonly description: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The path at which the plugin router is to be registered.
|
|
||||||
*/
|
|
||||||
readonly routerPath: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Link to plugin homepage.
|
|
||||||
*/
|
|
||||||
readonly homepageURL: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* init is called so that the plugin may initialize itself with the config.
|
|
||||||
*/
|
|
||||||
init(config: PluginConfig): void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the plugin should dispose/shutdown everything.
|
|
||||||
*/
|
|
||||||
deinit?(): Promise<void>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the plugin's router.
|
|
||||||
*
|
|
||||||
* Mounted at <code-sever-root>/<plugin-path>
|
|
||||||
*
|
|
||||||
* If not present, the plugin provides no routes.
|
|
||||||
*/
|
|
||||||
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.
|
|
||||||
* It is called when /api/applications is hit or the overlay needs to
|
|
||||||
* refresh the list of applications
|
|
||||||
*
|
|
||||||
* Ensure this is as fast as possible.
|
|
||||||
*
|
|
||||||
* If not present, the plugin provides no applications.
|
|
||||||
*/
|
|
||||||
applications?(): Application[] | Promise<Application[]>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PluginConfig contains the configuration required for initializing
|
|
||||||
* a plugin.
|
|
||||||
*/
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Application represents a user accessible application.
|
|
||||||
*/
|
|
||||||
export interface Application {
|
|
||||||
readonly name: string
|
|
||||||
readonly version: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When the user clicks on the icon in the overlay, they will be
|
|
||||||
* redirected to <code-server-root>/<plugin-path>/<app-path>
|
|
||||||
* where the application should be accessible.
|
|
||||||
*
|
|
||||||
* If undefined, then <code-server-root>/<plugin-path> is used.
|
|
||||||
*/
|
|
||||||
readonly path?: string
|
|
||||||
|
|
||||||
readonly description?: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The path at which the icon for this application can be accessed.
|
|
||||||
* <code-server-root>/<plugin-path>/<app-path>/<icon-path>
|
|
||||||
*/
|
|
||||||
readonly iconPath: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Link to application homepage.
|
|
||||||
*/
|
|
||||||
readonly homepageURL: string
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user