Skip to content

Commit 40cd2f7

Browse files
committed
Provide WsRouter to plugins
1 parent 118507a commit 40cd2f7

File tree

7 files changed

+92
-25
lines changed

7 files changed

+92
-25
lines changed

src/node/plugin.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as pluginapi from "../../typings/pluginapi"
77
import { version } from "./constants"
88
import { proxy } from "./proxy"
99
import * as util from "./util"
10+
import { Router as WsRouter, WebsocketRouter } from "./wsRouter"
1011
const fsp = fs.promises
1112

1213
/**
@@ -21,6 +22,7 @@ require("module")._load = function (request: string, parent: object, isMain: boo
2122
express,
2223
field,
2324
proxy,
25+
WsRouter,
2426
}
2527
}
2628
return originalLoad.apply(this, [request, parent, isMain])
@@ -103,14 +105,16 @@ export class PluginAPI {
103105
}
104106

105107
/**
106-
* mount mounts all plugin routers onto r.
108+
* mount mounts all plugin routers onto r and websocket routers onto wr.
107109
*/
108-
public mount(r: express.Router): void {
110+
public mount(r: express.Router, wr: express.Router): void {
109111
for (const [, p] of this.plugins) {
110-
if (!p.router) {
111-
continue
112+
if (p.router) {
113+
r.use(`${p.routerPath}`, p.router())
114+
}
115+
if (p.wsRouter) {
116+
wr.use(`${p.routerPath}`, (p.wsRouter() as WebsocketRouter).router)
112117
}
113-
r.use(`${p.routerPath}`, p.router())
114118
}
115119
}
116120

src/node/routes/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ import { promises as fs } from "fs"
66
import http from "http"
77
import * as path from "path"
88
import * as tls from "tls"
9+
import * as pluginapi from "../../../typings/pluginapi"
910
import { HttpCode, HttpError } from "../../common/http"
1011
import { plural } from "../../common/util"
1112
import { AuthType, DefaultedArgs } from "../cli"
1213
import { rootPath } from "../constants"
1314
import { Heart } from "../heart"
14-
import { replaceTemplates, redirect } from "../http"
15+
import { redirect, replaceTemplates } from "../http"
1516
import { PluginAPI } from "../plugin"
1617
import { getMediaMime, paths } from "../util"
17-
import { WebsocketRequest } from "../wsRouter"
1818
import * as apps from "./apps"
1919
import * as domainProxy from "./domainProxy"
2020
import * as health from "./health"
@@ -129,7 +129,7 @@ export const register = async (
129129

130130
const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH)
131131
await papi.loadPlugins()
132-
papi.mount(app)
132+
papi.mount(app, wsApp)
133133
app.use("/api/applications", apps.router(papi))
134134

135135
app.use(() => {
@@ -170,7 +170,7 @@ export const register = async (
170170

171171
const wsErrorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
172172
logger.error(`${err.message} ${err.stack}`)
173-
;(req as WebsocketRequest).ws.end()
173+
;(req as pluginapi.WebsocketRequest).ws.end()
174174
}
175175

176176
wsApp.use(wsErrorHandler)

src/node/wsRouter.ts

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as express from "express"
22
import * as expressCore from "express-serve-static-core"
33
import * as http from "http"
4-
import * as net from "net"
4+
import * as pluginapi from "../../typings/pluginapi"
55

66
export const handleUpgrade = (app: express.Express, server: http.Server): void => {
77
server.on("upgrade", (req, socket, head) => {
@@ -20,31 +20,20 @@ export const handleUpgrade = (app: express.Express, server: http.Server): void =
2020
})
2121
}
2222

23-
export interface WebsocketRequest extends express.Request {
24-
ws: net.Socket
25-
head: Buffer
26-
}
27-
28-
interface InternalWebsocketRequest extends WebsocketRequest {
23+
interface InternalWebsocketRequest extends pluginapi.WebsocketRequest {
2924
_ws_handled: boolean
3025
}
3126

32-
export type WebSocketHandler = (
33-
req: WebsocketRequest,
34-
res: express.Response,
35-
next: express.NextFunction,
36-
) => void | Promise<void>
37-
3827
export class WebsocketRouter {
3928
public readonly router = express.Router()
4029

41-
public ws(route: expressCore.PathParams, ...handlers: WebSocketHandler[]): void {
30+
public ws(route: expressCore.PathParams, ...handlers: pluginapi.WebSocketHandler[]): void {
4231
this.router.get(
4332
route,
4433
...handlers.map((handler) => {
4534
const wrapped: express.Handler = (req, res, next) => {
4635
;(req as InternalWebsocketRequest)._ws_handled = true
47-
return handler(req as WebsocketRequest, res, next)
36+
return handler(req as pluginapi.WebsocketRequest, res, next)
4837
}
4938
return wrapped
5039
}),

test/httpserver.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import * as express from "express"
12
import * as http from "http"
23
import * as nodeFetch from "node-fetch"
4+
import Websocket from "ws"
35
import * as util from "../src/common/util"
46
import { ensureAddress } from "../src/node/app"
7+
import { handleUpgrade } from "../src/node/wsRouter"
58

69
// Perhaps an abstraction similar to this should be used in app.ts as well.
710
export class HttpServer {
@@ -39,6 +42,13 @@ export class HttpServer {
3942
})
4043
}
4144

45+
/**
46+
* Send upgrade requests to an Express app.
47+
*/
48+
public listenUpgrade(app: express.Express): void {
49+
handleUpgrade(app, this.hs)
50+
}
51+
4252
/**
4353
* close cleans up the server.
4454
*/
@@ -62,6 +72,13 @@ export class HttpServer {
6272
return nodeFetch.default(`${ensureAddress(this.hs)}${requestPath}`, opts)
6373
}
6474

75+
/**
76+
* Open a websocket against the requset path.
77+
*/
78+
public ws(requestPath: string): Websocket {
79+
return new Websocket(`${ensureAddress(this.hs).replace("http:", "ws:")}${requestPath}`)
80+
}
81+
6582
public port(): number {
6683
const addr = this.hs.address()
6784
if (addr && typeof addr === "object") {

test/plugin.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ describe("plugin", () => {
2323
await papi.loadPlugins(false)
2424

2525
const app = express.default()
26-
papi.mount(app)
26+
const wsApp = express.default()
27+
papi.mount(app, wsApp)
2728
app.use("/api/applications", apps.router(papi))
2829

2930
s = new httpserver.HttpServer()
3031
await s.listen(app)
32+
s.listenUpgrade(wsApp)
3133
})
3234

3335
after(async () => {
@@ -72,4 +74,13 @@ describe("plugin", () => {
7274
const body = await resp.text()
7375
assert.equal(body, indexHTML)
7476
})
77+
78+
it("/test-plugin/test-app (websocket)", async () => {
79+
const ws = s.ws("/test-plugin/test-app")
80+
const message = await new Promise((resolve) => {
81+
ws.once("message", (message) => resolve(message))
82+
})
83+
ws.terminate()
84+
assert.strictEqual(message, "hello")
85+
})
7586
})

test/test-plugin/src/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import * as cs from "code-server"
22
import * as fspath from "path"
3+
import Websocket from "ws"
4+
5+
const wss = new Websocket.Server({ noServer: true })
36

47
export const plugin: cs.Plugin = {
58
displayName: "Test Plugin",
@@ -22,6 +25,16 @@ export const plugin: cs.Plugin = {
2225
return r
2326
},
2427

28+
wsRouter() {
29+
const wr = cs.WsRouter()
30+
wr.ws("/test-app", (req) => {
31+
wss.handleUpgrade(req, req.socket, req.head, (ws) => {
32+
ws.send("hello")
33+
})
34+
})
35+
return wr
36+
},
37+
2538
applications() {
2639
return [
2740
{

typings/pluginapi.d.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
*/
44
import { field, Logger } from "@coder/logger"
55
import * as express from "express"
6+
import * as expressCore from "express-serve-static-core"
7+
import ProxyServer from "http-proxy"
8+
import * as net from "net"
69

710
/**
811
* Overlay
@@ -78,6 +81,27 @@ import * as express from "express"
7881
* ]
7982
*/
8083

84+
export interface WebsocketRequest extends express.Request {
85+
ws: net.Socket
86+
head: Buffer
87+
}
88+
89+
export type WebSocketHandler = (
90+
req: WebsocketRequest,
91+
res: express.Response,
92+
next: express.NextFunction,
93+
) => void | Promise<void>
94+
95+
export interface WebsocketRouter {
96+
readonly router: express.Router
97+
ws(route: expressCore.PathParams, ...handlers: WebSocketHandler[]): void
98+
}
99+
100+
/**
101+
* Create a router for websocket routes.
102+
*/
103+
export function WsRouter(): WebsocketRouter
104+
81105
/**
82106
* The Express import used by code-server.
83107
*
@@ -152,6 +176,15 @@ export interface Plugin {
152176
*/
153177
router?(): express.Router
154178

179+
/**
180+
* Returns the plugin's websocket router.
181+
*
182+
* Mounted at <code-sever-root>/<plugin-path>
183+
*
184+
* If not present, the plugin provides no websockets.
185+
*/
186+
wsRouter?(): WebsocketRouter
187+
155188
/**
156189
* code-server uses this to collect the list of applications that
157190
* the plugin can currently provide.

0 commit comments

Comments
 (0)