|
1 | | -import { createServer, type IncomingMessage, type Server } from 'node:http'; |
| 1 | +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'; |
| 2 | +import { createServer as createHttpsServer, type Server as HttpsServer } from 'node:https'; |
| 3 | +import { readFileSync } from 'node:fs'; |
2 | 4 | import { performance } from 'node:perf_hooks'; |
3 | 5 |
|
4 | 6 | import { ErrorCodes, TelagentError } from '@telagent/protocol'; |
@@ -96,18 +98,15 @@ function requireGlobalAuth(req: IncomingMessage, pathname: string, ctx: RuntimeC |
96 | 98 |
|
97 | 99 | export class ApiServer { |
98 | 100 | private server: Server | null = null; |
| 101 | + private secureServer: HttpsServer | null = null; |
99 | 102 | private readonly router: Router; |
100 | 103 |
|
101 | 104 | constructor(private readonly ctx: RuntimeContext) { |
102 | 105 | this.router = buildRouter(ctx); |
103 | 106 | } |
104 | 107 |
|
105 | | - async start(): Promise<void> { |
106 | | - if (this.server) { |
107 | | - return; |
108 | | - } |
109 | | - |
110 | | - this.server = createServer(async (req, res) => { |
| 108 | + private createRequestHandler() { |
| 109 | + return async (req: IncomingMessage, res: ServerResponse) => { |
111 | 110 | const parsedUrl = new URL(req.url || '/', 'http://127.0.0.1'); |
112 | 111 | const startedAt = performance.now(); |
113 | 112 | res.once('finish', () => { |
@@ -161,25 +160,68 @@ export class ApiServer { |
161 | 160 | const internal = error instanceof TelagentError ? error : new TelagentError(ErrorCodes.INTERNAL, error instanceof Error ? error.message : 'Unexpected error'); |
162 | 161 | problem(res, internal.toProblem(req.url)); |
163 | 162 | } |
164 | | - }); |
| 163 | + }; |
| 164 | + } |
165 | 165 |
|
166 | | - await new Promise<void>((resolve) => { |
167 | | - this.server!.listen(this.ctx.config.port, this.ctx.config.host, resolve); |
168 | | - }); |
| 166 | + async start(): Promise<void> { |
| 167 | + if (this.server || this.secureServer) { |
| 168 | + return; |
| 169 | + } |
| 170 | + |
| 171 | + const { tls } = this.ctx.config; |
| 172 | + |
| 173 | + if (tls) { |
| 174 | + // ── HTTPS mode: main traffic on HTTPS, HTTP redirects to HTTPS ── |
| 175 | + const cert = readFileSync(tls.certPath); |
| 176 | + const key = readFileSync(tls.keyPath); |
| 177 | + |
| 178 | + this.secureServer = createHttpsServer({ cert, key }, this.createRequestHandler()); |
| 179 | + await new Promise<void>((resolve) => { |
| 180 | + this.secureServer!.listen(tls.httpsPort, this.ctx.config.host, resolve); |
| 181 | + }); |
| 182 | + |
| 183 | + // HTTP → HTTPS redirect server |
| 184 | + const httpsPort = tls.httpsPort; |
| 185 | + const redirectHost = this.ctx.config.host; |
| 186 | + this.server = createServer((req, res) => { |
| 187 | + const host = req.headers.host?.replace(/:\d+$/, '') || redirectHost; |
| 188 | + const location = `https://${host}${httpsPort === 443 ? '' : ':' + httpsPort}${req.url || '/'}`; |
| 189 | + res.writeHead(301, { Location: location }); |
| 190 | + res.end(); |
| 191 | + }); |
| 192 | + await new Promise<void>((resolve) => { |
| 193 | + this.server!.listen(this.ctx.config.port, this.ctx.config.host, resolve); |
| 194 | + }); |
| 195 | + } else { |
| 196 | + // ── Plain HTTP mode (default, unchanged) ── |
| 197 | + this.server = createServer(this.createRequestHandler()); |
| 198 | + await new Promise<void>((resolve) => { |
| 199 | + this.server!.listen(this.ctx.config.port, this.ctx.config.host, resolve); |
| 200 | + }); |
| 201 | + } |
169 | 202 | } |
170 | 203 |
|
171 | 204 | async stop(): Promise<void> { |
172 | | - if (!this.server) { |
173 | | - return; |
| 205 | + const closeServer = (s: Server | HttpsServer) => |
| 206 | + new Promise<void>((resolve) => { s.close(() => resolve()); }); |
| 207 | + |
| 208 | + const tasks: Promise<void>[] = []; |
| 209 | + if (this.secureServer) { |
| 210 | + tasks.push(closeServer(this.secureServer)); |
| 211 | + this.secureServer = null; |
| 212 | + } |
| 213 | + if (this.server) { |
| 214 | + tasks.push(closeServer(this.server)); |
| 215 | + this.server = null; |
174 | 216 | } |
175 | | - const current = this.server; |
176 | | - this.server = null; |
177 | | - await new Promise<void>((resolve) => { |
178 | | - current.close(() => resolve()); |
179 | | - }); |
| 217 | + await Promise.all(tasks); |
180 | 218 | } |
181 | 219 |
|
182 | 220 | get httpServer(): Server | null { |
183 | 221 | return this.server; |
184 | 222 | } |
| 223 | + |
| 224 | + get httpsServer(): HttpsServer | null { |
| 225 | + return this.secureServer; |
| 226 | + } |
185 | 227 | } |
0 commit comments