2021-05-04 03:42:24 +08:00
|
|
|
import { logger } from "@coder/logger"
|
2021-01-21 04:11:08 +08:00
|
|
|
import * as express from "express"
|
2021-01-14 22:34:51 +08:00
|
|
|
import * as http from "http"
|
2021-10-29 04:27:17 +08:00
|
|
|
import nodeFetch, { RequestInit, Response } from "node-fetch"
|
2021-01-21 04:11:08 +08:00
|
|
|
import Websocket from "ws"
|
2021-10-29 04:27:17 +08:00
|
|
|
import { Disposable } from "../../src/common/emitter"
|
2021-03-10 07:33:39 +08:00
|
|
|
import * as util from "../../src/common/util"
|
|
|
|
import { ensureAddress } from "../../src/node/app"
|
2021-10-29 04:27:17 +08:00
|
|
|
import { disposer } from "../../src/node/http"
|
2021-03-10 07:33:39 +08:00
|
|
|
import { handleUpgrade } from "../../src/node/wsRouter"
|
2021-01-14 22:34:51 +08:00
|
|
|
|
2021-01-14 23:53:58 +08:00
|
|
|
// Perhaps an abstraction similar to this should be used in app.ts as well.
|
2021-01-14 22:34:51 +08:00
|
|
|
export class HttpServer {
|
2021-10-29 04:27:17 +08:00
|
|
|
private hs: http.Server
|
|
|
|
public dispose: Disposable["dispose"]
|
2021-01-14 22:34:51 +08:00
|
|
|
|
2021-10-29 04:27:17 +08:00
|
|
|
/**
|
|
|
|
* Expects a server and a disposal that cleans up the server (and anything
|
|
|
|
* else that may need cleanup).
|
|
|
|
*
|
|
|
|
* Otherwise a new server is created.
|
|
|
|
*/
|
|
|
|
public constructor(server?: { server: http.Server; dispose: Disposable["dispose"] }) {
|
|
|
|
this.hs = server?.server || http.createServer()
|
|
|
|
this.dispose = server?.dispose || disposer(this.hs)
|
2021-01-14 23:53:58 +08:00
|
|
|
}
|
|
|
|
|
2021-01-14 22:34:51 +08:00
|
|
|
/**
|
|
|
|
* listen starts the server on a random localhost port.
|
|
|
|
* Use close to cleanup when done.
|
|
|
|
*/
|
|
|
|
public listen(fn: http.RequestListener): Promise<void> {
|
|
|
|
this.hs.on("request", fn)
|
|
|
|
|
2021-10-29 04:27:17 +08:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
this.hs.on("error", reject)
|
|
|
|
|
2023-09-22 08:13:34 +08:00
|
|
|
this.hs.listen(0, "127.0.0.1", () => {
|
2021-10-29 04:27:17 +08:00
|
|
|
this.hs.off("error", reject)
|
|
|
|
resolve()
|
2021-01-14 22:34:51 +08:00
|
|
|
|
2021-10-29 04:27:17 +08:00
|
|
|
this.hs.on("error", (err) => {
|
2021-01-14 22:34:51 +08:00
|
|
|
// Promise resolved earlier so this is some other error.
|
2021-05-04 03:42:24 +08:00
|
|
|
util.logError(logger, "http server error", err)
|
2021-10-29 04:27:17 +08:00
|
|
|
})
|
2021-01-14 22:34:51 +08:00
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-01-21 04:11:08 +08:00
|
|
|
/**
|
|
|
|
* Send upgrade requests to an Express app.
|
|
|
|
*/
|
|
|
|
public listenUpgrade(app: express.Express): void {
|
|
|
|
handleUpgrade(app, this.hs)
|
|
|
|
}
|
|
|
|
|
2021-01-14 22:34:51 +08:00
|
|
|
/**
|
|
|
|
* fetch fetches the request path.
|
|
|
|
* The request path must be rooted!
|
|
|
|
*/
|
2021-12-18 03:06:52 +08:00
|
|
|
public fetch(requestPath: string, opts?: RequestInit, query?: { [key: string]: string }): Promise<Response> {
|
2021-10-29 04:27:17 +08:00
|
|
|
const address = ensureAddress(this.hs, "http")
|
|
|
|
if (typeof address === "string") {
|
|
|
|
throw new Error("Cannot fetch socket path")
|
|
|
|
}
|
|
|
|
address.pathname = requestPath
|
2021-12-18 03:06:52 +08:00
|
|
|
if (query) {
|
|
|
|
Object.keys(query).forEach((key) => {
|
|
|
|
address.searchParams.append(key, query[key])
|
|
|
|
})
|
|
|
|
}
|
2021-10-29 04:27:17 +08:00
|
|
|
return nodeFetch(address.toString(), opts)
|
2021-01-14 22:34:51 +08:00
|
|
|
}
|
2021-01-14 23:53:58 +08:00
|
|
|
|
2021-01-21 04:11:08 +08:00
|
|
|
/**
|
2021-10-29 04:27:17 +08:00
|
|
|
* Open a websocket against the request path.
|
2021-01-21 04:11:08 +08:00
|
|
|
*/
|
2023-03-03 17:12:34 +08:00
|
|
|
public ws(requestPath: string, options?: Websocket.ClientOptions): Websocket {
|
2021-10-29 04:27:17 +08:00
|
|
|
const address = ensureAddress(this.hs, "ws")
|
|
|
|
if (typeof address === "string") {
|
|
|
|
throw new Error("Cannot open websocket to socket path")
|
|
|
|
}
|
|
|
|
address.pathname = requestPath
|
|
|
|
|
2023-03-03 17:12:34 +08:00
|
|
|
return new Websocket(address.toString(), options)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Open a websocket and wait for it to fully open.
|
|
|
|
*/
|
|
|
|
public wsWait(requestPath: string, options?: Websocket.ClientOptions): Promise<Websocket> {
|
|
|
|
const ws = this.ws(requestPath, options)
|
|
|
|
return new Promise<Websocket>((resolve, reject) => {
|
|
|
|
ws.on("error", (err) => reject(err))
|
|
|
|
ws.on("open", () => resolve(ws))
|
|
|
|
})
|
2021-01-21 04:11:08 +08:00
|
|
|
}
|
|
|
|
|
2021-01-14 23:53:58 +08:00
|
|
|
public port(): number {
|
|
|
|
const addr = this.hs.address()
|
2021-01-14 23:55:19 +08:00
|
|
|
if (addr && typeof addr === "object") {
|
2021-01-14 23:53:58 +08:00
|
|
|
return addr.port
|
|
|
|
}
|
|
|
|
throw new Error("server not listening or listening on unix socket")
|
|
|
|
}
|
2021-01-14 22:34:51 +08:00
|
|
|
}
|