import { createServer } from 'node:http' import type { Duplex } from 'node:stream' import { getRequestListener } from '@hono/node-server' import type { WebSocketLike } from '@hocuspocus/server' import crossws from 'crossws/adapters/node' import { createApp } from './app.ts' import { SESSION_COOKIE, SessionManager, isSameOrigin, readCookie } from './auth.ts' import { Collab } from './collab.ts' import { loadConfig } from './config.ts' import { Storage } from './db.ts' import { ImageStore } from './images.ts' type ClientConnection = ReturnType const config = loadConfig() const storage = new Storage(config.databaseFile) const sessions = new SessionManager(config.secret, config.password, config.sessionDays) const collab = new Collab(storage, sessions) await collab.start() const app = createApp({ config, sessions, collab, images: new ImageStore(config.imagesDir) }) const server = createServer(getRequestListener(app.fetch)) const connections = new WeakMap() const ws = crossws({ hooks: { open(peer) { const connection = collab.hocuspocus.handleConnection( peer.websocket as unknown as WebSocketLike, peer.request as Request, ) connections.set(peer, connection) }, message(peer, message) { connections.get(peer)?.handleMessage(message.uint8Array()) }, close(peer, event) { connections.get(peer)?.handleClose({ code: event.code ?? 1000, reason: event.reason ?? '' }) connections.delete(peer) }, error(peer, error) { console.error('[boundless] websocket error', error) }, }, }) server.on('upgrade', (request, socket: Duplex, head) => { const url = new URL(request.url ?? '/', 'http://localhost') const forwardedHost = request.headers['x-forwarded-host'] const allowed = url.pathname === '/collab' && isSameOrigin(request.headers.origin, [ Array.isArray(forwardedHost) ? forwardedHost[0] : forwardedHost, request.headers.host, ]) && sessions.verify(readCookie(request.headers.cookie, SESSION_COOKIE)) if (!allowed) { socket.end('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\nContent-Length: 0\r\n\r\n') return } void ws.handleUpgrade(request, socket, head) }) server.listen(config.port, config.host, () => { console.log(`[boundless] 已启动 http://${config.host}:${config.port}`) console.log(`[boundless] 数据目录 ${config.dataDir}`) if (!config.staticDir) console.log('[boundless] 未托管前端页面(开发模式请访问 Vite 地址)') }) let shuttingDown = false async function shutdown() { if (shuttingDown) return shuttingDown = true server.close() // 先把还没写盘的修改落盘,再关闭数据库 await collab.stop() await collab.hocuspocus.closeConnections() await new Promise((resolve) => setTimeout(resolve, 200)) storage.close() process.exit(0) } process.on('SIGINT', () => void shutdown()) process.on('SIGTERM', () => void shutdown())