Skip to content

Commit 055e0ef

Browse files
committed
Provide WsRouter to plugins
1 parent fb37473 commit 055e0ef

File tree

8 files changed

+101
-34
lines changed

8 files changed

+101
-34
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: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@ 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"
2121
import * as login from "./login"
22-
import * as proxy from "./pathProxy"
22+
import * as pathProxy from "./pathProxy"
2323
// static is a reserved keyword.
2424
import * as _static from "./static"
2525
import * as update from "./update"
@@ -104,21 +104,21 @@ export const register = async (
104104
wsApp.use("/", domainProxy.wsRouter.router)
105105

106106
app.all("/proxy/(:port)(/*)?", (req, res) => {
107-
proxy.proxy(req, res)
107+
pathProxy.proxy(req, res)
108108
})
109-
wsApp.get("/proxy/(:port)(/*)?", (req, res) => {
110-
proxy.wsProxy(req as WebsocketRequest)
109+
wsApp.get("/proxy/(:port)(/*)?", (req) => {
110+
pathProxy.wsProxy(req as pluginapi.WebsocketRequest)
111111
})
112112
// These two routes pass through the path directly.
113113
// So the proxied app must be aware it is running
114114
// under /absproxy/<someport>/
115115
app.all("/absproxy/(:port)(/*)?", (req, res) => {
116-
proxy.proxy(req, res, {
116+
pathProxy.proxy(req, res, {
117117
passthroughPath: true,
118118
})
119119
})
120-
wsApp.get("/absproxy/(:port)(/*)?", (req, res) => {
121-
proxy.wsProxy(req as WebsocketRequest, {
120+
wsApp.get("/absproxy/(:port)(/*)?", (req) => {
121+
pathProxy.wsProxy(req as pluginapi.WebsocketRequest, {
122122
passthroughPath: true,
123123
})
124124
})
@@ -146,7 +146,7 @@ export const register = async (
146146

147147
const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH)
148148
await papi.loadPlugins()
149-
papi.mount(app)
149+
papi.mount(app, wsApp)
150150
app.use("/api/applications", apps.router(papi))
151151

152152
app.use(() => {
@@ -187,7 +187,7 @@ export const register = async (
187187

188188
const wsErrorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
189189
logger.error(`${err.message} ${err.stack}`)
190-
;(req as WebsocketRequest).ws.end()
190+
;(req as pluginapi.WebsocketRequest).ws.end()
191191
}
192192

193193
wsApp.use(wsErrorHandler)

src/node/routes/pathProxy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Request, Response } from "express"
22
import * as path from "path"
33
import qs from "qs"
4+
import * as pluginapi from "../../../typings/pluginapi"
45
import { HttpCode, HttpError } from "../../common/http"
56
import { normalize } from "../../common/util"
67
import { authenticated, ensureAuthenticated, redirect } from "../http"
78
import { proxy as _proxy } from "../proxy"
8-
import { WebsocketRequest } from "../wsRouter"
99

1010
const getProxyTarget = (req: Request, passthroughPath?: boolean): string => {
1111
if (passthroughPath) {
@@ -46,7 +46,7 @@ export function proxy(
4646
}
4747

4848
export function wsProxy(
49-
req: WebsocketRequest,
49+
req: pluginapi.WebsocketRequest,
5050
opts?: {
5151
passthroughPath?: boolean
5252
},

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
@@ -21,11 +21,13 @@ describe("plugin", () => {
2121
await papi.loadPlugins(false)
2222

2323
const app = express.default()
24-
papi.mount(app)
24+
const wsApp = express.default()
25+
papi.mount(app, wsApp)
2526
app.use("/api/applications", apps.router(papi))
2627

2728
s = new httpserver.HttpServer()
2829
await s.listen(app)
30+
s.listenUpgrade(wsApp)
2931
})
3032

3133
afterAll(async () => {
@@ -70,4 +72,13 @@ describe("plugin", () => {
7072
const body = await resp.text()
7173
expect(body).toBe(indexHTML)
7274
})
75+
76+
it("/test-plugin/test-app (websocket)", async () => {
77+
const ws = s.ws("/test-plugin/test-app")
78+
const message = await new Promise((resolve) => {
79+
ws.once("message", (message) => resolve(message))
80+
})
81+
ws.terminate()
82+
expect(message).toBe("hello")
83+
})
7384
})

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)