2024-05-09 20:51:25 +08:00
|
|
|
import type { Hono } from 'hono'
|
|
|
|
import type { UpgradeWebSocket, WSContext } from 'hono/ws'
|
2024-07-04 13:17:51 +08:00
|
|
|
import type { WebSocket } from 'ws'
|
2024-05-09 20:51:25 +08:00
|
|
|
import { WebSocketServer } from 'ws'
|
2024-07-04 13:17:51 +08:00
|
|
|
import type { IncomingMessage } from 'http'
|
2024-12-25 17:08:43 +08:00
|
|
|
import type { Server } from 'node:http'
|
|
|
|
import type { Http2SecureServer, Http2Server } from 'node:http2'
|
2025-02-23 14:31:44 +08:00
|
|
|
import type { Duplex } from 'node:stream'
|
2024-07-19 06:10:07 +08:00
|
|
|
import { CloseEvent } from './events'
|
2024-05-09 20:51:25 +08:00
|
|
|
|
|
|
|
export interface NodeWebSocket {
|
2025-01-21 17:39:56 +08:00
|
|
|
upgradeWebSocket: UpgradeWebSocket<WebSocket>
|
2024-05-09 20:51:25 +08:00
|
|
|
injectWebSocket(server: Server | Http2Server | Http2SecureServer): void
|
|
|
|
}
|
|
|
|
export interface NodeWebSocketInit {
|
2025-02-02 20:25:54 +08:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
app: Hono<any, any, any>
|
2024-05-09 20:51:25 +08:00
|
|
|
baseUrl?: string | URL
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create WebSockets for Node.js
|
|
|
|
* @param init Options
|
|
|
|
* @returns NodeWebSocket
|
|
|
|
*/
|
|
|
|
export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => {
|
2024-07-04 13:17:51 +08:00
|
|
|
const wss = new WebSocketServer({ noServer: true })
|
2025-04-01 20:35:45 +08:00
|
|
|
const waiterMap = new Map<
|
|
|
|
IncomingMessage,
|
|
|
|
{ resolve: (ws: WebSocket) => void; response: Response }
|
|
|
|
>()
|
2024-07-04 13:17:51 +08:00
|
|
|
|
|
|
|
wss.on('connection', (ws, request) => {
|
2025-02-23 14:31:44 +08:00
|
|
|
const waiter = waiterMap.get(request)
|
|
|
|
if (waiter) {
|
|
|
|
waiter.resolve(ws)
|
|
|
|
waiterMap.delete(request)
|
2024-07-04 13:17:51 +08:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2025-02-23 14:31:44 +08:00
|
|
|
const nodeUpgradeWebSocket = (request: IncomingMessage, response: Response) => {
|
2024-07-04 13:17:51 +08:00
|
|
|
return new Promise<WebSocket>((resolve) => {
|
2025-02-23 14:31:44 +08:00
|
|
|
waiterMap.set(request, { resolve, response })
|
2024-07-04 13:17:51 +08:00
|
|
|
})
|
|
|
|
}
|
2024-05-26 10:36:11 +08:00
|
|
|
|
2024-05-09 20:51:25 +08:00
|
|
|
return {
|
|
|
|
injectWebSocket(server) {
|
2025-02-23 14:31:44 +08:00
|
|
|
server.on('upgrade', async (request, socket: Duplex, head) => {
|
2024-05-09 20:51:25 +08:00
|
|
|
const url = new URL(request.url ?? '/', init.baseUrl ?? 'http://localhost')
|
|
|
|
const headers = new Headers()
|
|
|
|
for (const key in request.headers) {
|
|
|
|
const value = request.headers[key]
|
|
|
|
if (!value) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
headers.append(key, Array.isArray(value) ? value[0] : value)
|
|
|
|
}
|
2025-02-23 14:31:44 +08:00
|
|
|
|
|
|
|
const response = await init.app.request(
|
2024-07-04 13:17:51 +08:00
|
|
|
url,
|
|
|
|
{ headers: headers },
|
|
|
|
{ incoming: request, outgoing: undefined }
|
|
|
|
)
|
2025-02-23 14:31:44 +08:00
|
|
|
|
|
|
|
const waiter = waiterMap.get(request)
|
|
|
|
if (!waiter || waiter.response !== response) {
|
|
|
|
socket.end(
|
|
|
|
'HTTP/1.1 400 Bad Request\r\n' +
|
2025-04-01 20:35:45 +08:00
|
|
|
'Connection: close\r\n' +
|
|
|
|
'Content-Length: 0\r\n' +
|
|
|
|
'\r\n'
|
2025-02-23 14:31:44 +08:00
|
|
|
)
|
|
|
|
waiterMap.delete(request)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-05-26 10:36:11 +08:00
|
|
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
|
|
wss.emit('connection', ws, request)
|
2024-05-09 20:51:25 +08:00
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
upgradeWebSocket: (createEvents) =>
|
|
|
|
async function upgradeWebSocket(c, next) {
|
2024-12-10 11:58:31 +08:00
|
|
|
if (c.req.header('upgrade')?.toLowerCase() !== 'websocket') {
|
2024-05-09 20:51:25 +08:00
|
|
|
// Not websocket
|
|
|
|
await next()
|
|
|
|
return
|
|
|
|
}
|
2024-07-04 13:17:51 +08:00
|
|
|
|
2025-02-23 14:31:44 +08:00
|
|
|
const response = new Response()
|
2024-07-04 13:17:51 +08:00
|
|
|
;(async () => {
|
2025-02-23 14:31:44 +08:00
|
|
|
const ws = await nodeUpgradeWebSocket(c.env.incoming, response)
|
2025-02-10 17:10:03 +08:00
|
|
|
const events = await createEvents(c)
|
2024-07-04 13:17:51 +08:00
|
|
|
|
2025-01-21 17:39:56 +08:00
|
|
|
const ctx: WSContext<WebSocket> = {
|
2024-05-09 20:51:25 +08:00
|
|
|
binaryType: 'arraybuffer',
|
|
|
|
close(code, reason) {
|
|
|
|
ws.close(code, reason)
|
|
|
|
},
|
|
|
|
protocol: ws.protocol,
|
|
|
|
raw: ws,
|
|
|
|
get readyState() {
|
|
|
|
return ws.readyState
|
|
|
|
},
|
|
|
|
send(source, opts) {
|
|
|
|
ws.send(source, {
|
|
|
|
compress: opts?.compress,
|
|
|
|
})
|
|
|
|
},
|
|
|
|
url: new URL(c.req.url),
|
|
|
|
}
|
|
|
|
events.onOpen?.(new Event('open'), ctx)
|
|
|
|
ws.on('message', (data, isBinary) => {
|
|
|
|
const datas = Array.isArray(data) ? data : [data]
|
|
|
|
for (const data of datas) {
|
|
|
|
events.onMessage?.(
|
|
|
|
new MessageEvent('message', {
|
2025-04-01 20:35:45 +08:00
|
|
|
data: isBinary
|
|
|
|
? data instanceof ArrayBuffer
|
|
|
|
? data
|
|
|
|
: data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)
|
|
|
|
: data.toString('utf-8'),
|
2024-05-09 20:51:25 +08:00
|
|
|
}),
|
|
|
|
ctx
|
|
|
|
)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
ws.on('close', () => {
|
|
|
|
events.onClose?.(new CloseEvent('close'), ctx)
|
|
|
|
})
|
|
|
|
ws.on('error', (error) => {
|
|
|
|
events.onError?.(
|
|
|
|
new ErrorEvent('error', {
|
|
|
|
error: error,
|
|
|
|
}),
|
|
|
|
ctx
|
|
|
|
)
|
|
|
|
})
|
2024-07-04 13:17:51 +08:00
|
|
|
})()
|
2024-05-26 10:36:11 +08:00
|
|
|
|
2025-02-23 14:31:44 +08:00
|
|
|
return response
|
2024-05-09 20:51:25 +08:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|