Provide WsRouter to plugins
This commit is contained in:
parent
fb37473e72
commit
055e0ef9ec
@ -7,6 +7,7 @@ import * as pluginapi from "../../typings/pluginapi"
|
|||||||
import { version } from "./constants"
|
import { version } from "./constants"
|
||||||
import { proxy } from "./proxy"
|
import { proxy } from "./proxy"
|
||||||
import * as util from "./util"
|
import * as util from "./util"
|
||||||
|
import { Router as WsRouter, WebsocketRouter } from "./wsRouter"
|
||||||
const fsp = fs.promises
|
const fsp = fs.promises
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,6 +22,7 @@ require("module")._load = function (request: string, parent: object, isMain: boo
|
|||||||
express,
|
express,
|
||||||
field,
|
field,
|
||||||
proxy,
|
proxy,
|
||||||
|
WsRouter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return originalLoad.apply(this, [request, parent, isMain])
|
return originalLoad.apply(this, [request, parent, isMain])
|
||||||
@ -103,15 +105,17 @@ 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())
|
r.use(`${p.routerPath}`, p.router())
|
||||||
}
|
}
|
||||||
|
if (p.wsRouter) {
|
||||||
|
wr.use(`${p.routerPath}`, (p.wsRouter() as WebsocketRouter).router)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -6,20 +6,20 @@ 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 * 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"
|
||||||
@ -104,21 +104,21 @@ 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,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -146,7 +146,7 @@ export const register = async (
|
|||||||
|
|
||||||
const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH)
|
const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH)
|
||||||
await papi.loadPlugins()
|
await papi.loadPlugins()
|
||||||
papi.mount(app)
|
papi.mount(app, wsApp)
|
||||||
app.use("/api/applications", apps.router(papi))
|
app.use("/api/applications", apps.router(papi))
|
||||||
|
|
||||||
app.use(() => {
|
app.use(() => {
|
||||||
@ -187,7 +187,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
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
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 * 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 +20,20 @@ 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 {
|
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
|
||||||
}),
|
}),
|
||||||
|
@ -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") {
|
||||||
|
@ -21,11 +21,13 @@ describe("plugin", () => {
|
|||||||
await papi.loadPlugins(false)
|
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 () => {
|
||||||
@ -70,4 +72,13 @@ 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")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import * as cs from "code-server"
|
import * as cs from "code-server"
|
||||||
import * as fspath from "path"
|
import * as fspath from "path"
|
||||||
|
import Websocket from "ws"
|
||||||
|
|
||||||
|
const wss = new Websocket.Server({ noServer: true })
|
||||||
|
|
||||||
export const plugin: cs.Plugin = {
|
export const plugin: cs.Plugin = {
|
||||||
displayName: "Test Plugin",
|
displayName: "Test Plugin",
|
||||||
@ -22,6 +25,16 @@ export const plugin: cs.Plugin = {
|
|||||||
return r
|
return r
|
||||||
},
|
},
|
||||||
|
|
||||||
|
wsRouter() {
|
||||||
|
const wr = cs.WsRouter()
|
||||||
|
wr.ws("/test-app", (req) => {
|
||||||
|
wss.handleUpgrade(req, req.socket, req.head, (ws) => {
|
||||||
|
ws.send("hello")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return wr
|
||||||
|
},
|
||||||
|
|
||||||
applications() {
|
applications() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
33
typings/pluginapi.d.ts
vendored
33
typings/pluginapi.d.ts
vendored
@ -3,6 +3,9 @@
|
|||||||
*/
|
*/
|
||||||
import { field, Logger } from "@coder/logger"
|
import { field, 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"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overlay
|
* Overlay
|
||||||
@ -78,6 +81,27 @@ import * as express from "express"
|
|||||||
* ]
|
* ]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
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 Express import used by code-server.
|
* The Express import used by code-server.
|
||||||
*
|
*
|
||||||
@ -152,6 +176,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.
|
||||||
|
Reference in New Issue
Block a user