quickwin 1.0.2 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +204 -204
- package/README.md +203 -203
- package/examples/PdfCanvas.tsx +83 -83
- package/examples/exec_server.ts +35 -35
- package/examples/pdf_preview2.ts +375 -375
- package/examples/pdf_viewer.tsx +279 -279
- package/examples/test_gallery.tsx +285 -285
- package/examples/test_react_lazy.tsx +63 -63
- package/examples/test_react_listbox.tsx +71 -71
- package/examples/test_root_window.tsx +41 -41
- package/examples/test_tab.tsx +75 -75
- package/examples/tray_demo.tsx +81 -81
- package/lib/assert.ts +6 -6
- package/lib/cache-utils.ts +40 -40
- package/lib/dumpRects.ts +25 -25
- package/lib/fetch.ts +502 -502
- package/lib/http-common.ts +399 -399
- package/lib/http-server.ts +424 -424
- package/lib/polyfill.ts +171 -171
- package/lib/react-qw/components/index.ts +17 -17
- package/lib/react-qw/index.ts +90 -90
- package/lib/react-qw/jsx.d.ts +34 -34
- package/lib/react-qw/layout.ts +111 -111
- package/lib/react-qw/props.ts +36 -36
- package/lib/react-qw/reconciler.ts +373 -373
- package/lib/stream.ts +21 -21
- package/lib/text-codec.ts +171 -171
- package/lib/text-measure.ts +54 -54
- package/lib/url.ts +201 -201
- package/lib/vendor/web-streams/ponyfill.d.mts +829 -829
- package/lib/vendor/web-streams/ponyfill.mjs +8 -8
- package/lib/websocket.js +8 -0
- package/lib/websocket.ts +563 -555
- package/package.json +50 -44
- package/qwin-nowasm-x86.exe +0 -0
- package/qwin-nowasm.exe +0 -0
- package/qwin-x86.exe +0 -0
- package/qwin.exe +0 -0
- package/test/run.ts +96 -96
- package/test/test_basic.ts +9 -9
- package/test/test_basic_fetch.ts +33 -33
- package/test/test_brotli.ts +52 -52
- package/test/test_fetch_cache.ts +187 -187
- package/test/test_ffi.ts +174 -174
- package/test/test_frame_encoding.ts +132 -132
- package/test/test_helper.ts +80 -80
- package/test/test_http_import.ts +74 -74
- package/test/test_http_server.ts +136 -136
- package/test/test_import_dir.ts +5 -5
- package/test/test_ipv6.ts +129 -129
- package/test/test_meta_url.ts +4 -4
- package/test/test_mupdf_render.ts +74 -74
- package/test/test_mupdf_twice.ts +81 -81
- package/test/test_mupdf_wasm.ts +30 -30
- package/test/test_net_event.ts +55 -55
- package/test/test_net_fetch.ts +238 -238
- package/test/test_net_websocket.ts +164 -164
- package/test/test_polyfill.ts +159 -159
- package/test/test_url.ts +183 -183
- package/test/test_wasm_basic.ts +73 -73
- package/test/test_wasm_import_global.ts +45 -45
- package/test/test_wasm_sjlj.ts +125 -125
- package/test/test_wasm_types.ts +124 -124
- package/test/test_worker.ts +24 -24
- package/test/test_worker_fetch.ts +27 -27
- package/test/test_worker_net.ts +54 -54
- package/test/test_worker_wasm_concurrent.ts +45 -45
- package/test/worker_fetch.ts +13 -13
- package/test/worker_helper.ts +1 -1
- package/test/worker_http.ts +20 -20
- package/test/worker_import_lib.ts +13 -13
- package/test/worker_module.ts +17 -17
- package/test/worker_wasm_concurrent.ts +22 -22
- package/tsconfig.json +27 -27
- package/vendor/mupdf-wasm/mupdf-wasm.d.ts +571 -571
- package/vendor/mupdf-wasm/mupdf-wasm.js +2749 -2749
- package/vendor/mupdf-wasm/mupdf.d.ts +939 -939
- package/vendor/mupdf-wasm/mupdf.js +3317 -3317
package/lib/http-server.ts
CHANGED
|
@@ -1,424 +1,424 @@
|
|
|
1
|
-
import './polyfill.js'
|
|
2
|
-
import * as sock from 'sock'
|
|
3
|
-
import { HeadersImpl, RequestImpl, ResponseImpl, _readStream } from './http-common.js'
|
|
4
|
-
|
|
5
|
-
type SockHandle = import('sock').SockHandle
|
|
6
|
-
|
|
7
|
-
// ── Public API ──
|
|
8
|
-
|
|
9
|
-
export type ServerHandler = (req: Request) => Promise<Response>
|
|
10
|
-
|
|
11
|
-
// ── Internal types ──
|
|
12
|
-
|
|
13
|
-
interface NetEvent { lNetworkEvents: number; iErrorCode: number[] }
|
|
14
|
-
|
|
15
|
-
interface ParsedHeader {
|
|
16
|
-
method: string
|
|
17
|
-
target: string
|
|
18
|
-
version: string
|
|
19
|
-
headers: Record<string, string>
|
|
20
|
-
chunked: boolean
|
|
21
|
-
contentLength: number
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface PendingReq {
|
|
25
|
-
parsed: ParsedHeader
|
|
26
|
-
bodyStart: number
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface ActiveReq {
|
|
30
|
-
req: RequestImpl
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface Conn {
|
|
34
|
-
sock: SockHandle
|
|
35
|
-
parts: Uint8Array[]
|
|
36
|
-
pending: PendingReq | null
|
|
37
|
-
current: ActiveReq | null
|
|
38
|
-
outQueue: Uint8Array | null
|
|
39
|
-
closed: boolean
|
|
40
|
-
closeAfterFlush: boolean
|
|
41
|
-
keepAlive: boolean
|
|
42
|
-
owner: HttpServer
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const STATUS_TEXT: Record<number, string> = {
|
|
46
|
-
100: 'Continue', 101: 'Switching Protocols',
|
|
47
|
-
200: 'OK', 201: 'Created', 202: 'Accepted', 204: 'No Content',
|
|
48
|
-
301: 'Moved Permanently', 302: 'Found', 304: 'Not Modified',
|
|
49
|
-
400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found',
|
|
50
|
-
405: 'Method Not Allowed', 408: 'Request Timeout', 413: 'Payload Too Large',
|
|
51
|
-
500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable',
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ── Byte helpers ──
|
|
55
|
-
|
|
56
|
-
function concatU8(parts: Uint8Array[]): Uint8Array {
|
|
57
|
-
let total = 0
|
|
58
|
-
for (const p of parts) total += p.length
|
|
59
|
-
const out = new Uint8Array(total)
|
|
60
|
-
let off = 0
|
|
61
|
-
for (const p of parts) { out.set(p, off); off += p.length }
|
|
62
|
-
return out
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function toArrayBuffer(u8: Uint8Array): ArrayBuffer {
|
|
66
|
-
return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function findCrLfCrLf(buf: Uint8Array): number {
|
|
70
|
-
const n = buf.length - 3
|
|
71
|
-
for (let i = 0; i < n; i++) {
|
|
72
|
-
if (buf[i] === 13 && buf[i + 1] === 10 && buf[i + 2] === 13 && buf[i + 3] === 10) return i
|
|
73
|
-
}
|
|
74
|
-
return -1
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Decode a chunked-transfer body starting at `start`. Returns the decoded
|
|
78
|
-
* data and the offset just past the terminating CRLF, or null if the buffer
|
|
79
|
-
* does not yet contain the complete chunked body. */
|
|
80
|
-
function decodeChunked(buf: Uint8Array, start: number): { data: Uint8Array; end: number } | null {
|
|
81
|
-
let pos = start
|
|
82
|
-
const parts: Uint8Array[] = []
|
|
83
|
-
const decoder = new TextDecoder('utf-8')
|
|
84
|
-
while (pos < buf.length) {
|
|
85
|
-
let eol = pos
|
|
86
|
-
while (eol < buf.length && buf[eol] !== 13) eol++
|
|
87
|
-
if (eol >= buf.length || eol + 1 >= buf.length || buf[eol + 1] !== 10) return null
|
|
88
|
-
const sizeLine = decoder.decode(buf.subarray(pos, eol)).trim()
|
|
89
|
-
const semi = sizeLine.indexOf(';')
|
|
90
|
-
const hex = (semi >= 0 ? sizeLine.slice(0, semi) : sizeLine).trim()
|
|
91
|
-
const size = parseInt(hex, 16)
|
|
92
|
-
if (isNaN(size) || size < 0) return null
|
|
93
|
-
pos = eol + 2
|
|
94
|
-
if (size === 0) {
|
|
95
|
-
while (true) {
|
|
96
|
-
if (pos >= buf.length) return null
|
|
97
|
-
if (buf[pos] === 13) {
|
|
98
|
-
if (pos + 1 >= buf.length || buf[pos + 1] !== 10) return null
|
|
99
|
-
pos += 2
|
|
100
|
-
return { data: concatU8(parts), end: pos }
|
|
101
|
-
}
|
|
102
|
-
let t = pos
|
|
103
|
-
while (t < buf.length && buf[t] !== 13) t++
|
|
104
|
-
if (t >= buf.length || t + 1 >= buf.length || buf[t + 1] !== 10) return null
|
|
105
|
-
pos = t + 2
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (pos + size > buf.length) return null
|
|
109
|
-
parts.push(buf.subarray(pos, pos + size))
|
|
110
|
-
pos += size
|
|
111
|
-
if (pos + 1 >= buf.length || buf[pos] !== 13 || buf[pos + 1] !== 10) return null
|
|
112
|
-
pos += 2
|
|
113
|
-
}
|
|
114
|
-
return null
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ── Parsing ──
|
|
118
|
-
|
|
119
|
-
function parseHeader(headerStr: string): ParsedHeader | null {
|
|
120
|
-
const lines = headerStr.split('\r\n')
|
|
121
|
-
const first = lines[0]
|
|
122
|
-
if (!first) return null
|
|
123
|
-
const sp1 = first.indexOf(' ')
|
|
124
|
-
const sp2 = first.lastIndexOf(' ')
|
|
125
|
-
if (sp1 <= 0 || sp2 <= sp1) return null
|
|
126
|
-
const method = first.slice(0, sp1)
|
|
127
|
-
const target = first.slice(sp1 + 1, sp2)
|
|
128
|
-
const version = first.slice(sp2 + 1).trim()
|
|
129
|
-
|
|
130
|
-
const headers: Record<string, string> = {}
|
|
131
|
-
for (let i = 1; i < lines.length; i++) {
|
|
132
|
-
const line = lines[i]
|
|
133
|
-
if (!line) continue
|
|
134
|
-
const ci = line.indexOf(':')
|
|
135
|
-
if (ci <= 0) continue
|
|
136
|
-
const name = line.slice(0, ci).trim().toLowerCase()
|
|
137
|
-
const value = line.slice(ci + 1).trim()
|
|
138
|
-
if (headers[name] !== undefined) headers[name] = headers[name] + ', ' + value
|
|
139
|
-
else headers[name] = value
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const te = (headers['transfer-encoding'] || '').toLowerCase()
|
|
143
|
-
const chunked = te.indexOf('chunked') >= 0
|
|
144
|
-
const contentLength = parseInt(headers['content-length'] || '0', 10) || 0
|
|
145
|
-
return { method, target, version, headers, chunked, contentLength }
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ── Server ──
|
|
149
|
-
|
|
150
|
-
export class HttpServer {
|
|
151
|
-
readonly handler: ServerHandler
|
|
152
|
-
private serverSock: SockHandle | null = null
|
|
153
|
-
readonly conns = new Map<SockHandle, Conn>()
|
|
154
|
-
private closed = false
|
|
155
|
-
|
|
156
|
-
constructor(handler: ServerHandler) {
|
|
157
|
-
this.handler = handler
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** Actual bound address, or null if not listening. */
|
|
161
|
-
address(): { addr: string; port: number } | null {
|
|
162
|
-
if (this.serverSock === null) return null
|
|
163
|
-
return sock.getsockname(this.serverSock)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Bind and start listening. Returns 0 on success, -1 on failure. */
|
|
167
|
-
listen(port: number, host?: string, backlog?: number): number {
|
|
168
|
-
if (this.serverSock !== null) return -1
|
|
169
|
-
const s = sock.socket()
|
|
170
|
-
if (s < 0) return -1
|
|
171
|
-
if (sock.bind(s, host ?? null, port) !== 0) { sock.closesocket(s); return -1 }
|
|
172
|
-
if (sock.listen(s, backlog ?? 64) !== 0) { sock.closesocket(s); return -1 }
|
|
173
|
-
this.serverSock = s
|
|
174
|
-
sock.set_on_event(s, (event: NetEvent) => {
|
|
175
|
-
if (event.lNetworkEvents & sock.FdEvent.FD_ACCEPT) this.acceptLoop()
|
|
176
|
-
})
|
|
177
|
-
return 0
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
close(): void {
|
|
181
|
-
if (this.closed) return
|
|
182
|
-
this.closed = true
|
|
183
|
-
for (const c of Array.from(this.conns.values())) closeConn(c)
|
|
184
|
-
this.conns.clear()
|
|
185
|
-
if (this.serverSock !== null) { sock.closesocket(this.serverSock); this.serverSock = null }
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
private acceptLoop(): void {
|
|
189
|
-
if (this.closed || this.serverSock === null) return
|
|
190
|
-
while (true) {
|
|
191
|
-
const ac = sock.accept(this.serverSock)
|
|
192
|
-
if (!ac) break
|
|
193
|
-
const c: Conn = {
|
|
194
|
-
sock: ac.handle,
|
|
195
|
-
parts: [],
|
|
196
|
-
pending: null,
|
|
197
|
-
current: null,
|
|
198
|
-
outQueue: null,
|
|
199
|
-
closed: false,
|
|
200
|
-
closeAfterFlush: false,
|
|
201
|
-
keepAlive: true,
|
|
202
|
-
owner: this,
|
|
203
|
-
}
|
|
204
|
-
this.conns.set(c.sock, c)
|
|
205
|
-
sock.set_on_event(c.sock, (event: NetEvent) => this.onConnEvent(c, event))
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
private onConnEvent(c: Conn, event: NetEvent): void {
|
|
210
|
-
if (c.closed) return
|
|
211
|
-
if (event.lNetworkEvents & sock.FdEvent.FD_READ) onRead(c)
|
|
212
|
-
if (event.lNetworkEvents & sock.FdEvent.FD_WRITE) flushQueue(c)
|
|
213
|
-
if (event.lNetworkEvents & sock.FdEvent.FD_CLOSE) {
|
|
214
|
-
onRead(c)
|
|
215
|
-
if (!c.current && !c.pending) closeConn(c)
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export function createServer(handler: ServerHandler): HttpServer {
|
|
221
|
-
return new HttpServer(handler)
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// ── Connection I/O ──
|
|
225
|
-
|
|
226
|
-
function onRead(c: Conn): void {
|
|
227
|
-
if (c.closed) return
|
|
228
|
-
while (true) {
|
|
229
|
-
const d = sock.recv(c.sock, 8192)
|
|
230
|
-
if (!d) break
|
|
231
|
-
c.parts.push(new Uint8Array(d))
|
|
232
|
-
}
|
|
233
|
-
if (!c.closed) tryProcess(c)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function queueSend(c: Conn, data: Uint8Array): void {
|
|
237
|
-
if (c.closed || data.length === 0) return
|
|
238
|
-
if (c.outQueue && c.outQueue.length > 0) {
|
|
239
|
-
c.outQueue = concatU8([c.outQueue, data])
|
|
240
|
-
return
|
|
241
|
-
}
|
|
242
|
-
const ret = sock.send(c.sock, toArrayBuffer(data))
|
|
243
|
-
if (ret < 0) {
|
|
244
|
-
c.outQueue = data
|
|
245
|
-
return
|
|
246
|
-
}
|
|
247
|
-
if (ret < data.length) {
|
|
248
|
-
c.outQueue = data.subarray(ret)
|
|
249
|
-
return
|
|
250
|
-
}
|
|
251
|
-
if (c.closeAfterFlush) closeConn(c)
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function flushQueue(c: Conn): void {
|
|
255
|
-
if (c.closed) return
|
|
256
|
-
if (!c.outQueue || c.outQueue.length === 0) {
|
|
257
|
-
if (c.closeAfterFlush) closeConn(c)
|
|
258
|
-
return
|
|
259
|
-
}
|
|
260
|
-
const q = c.outQueue
|
|
261
|
-
const ret = sock.send(c.sock, toArrayBuffer(q))
|
|
262
|
-
if (ret < 0) return
|
|
263
|
-
if (ret < q.length) {
|
|
264
|
-
c.outQueue = q.subarray(ret)
|
|
265
|
-
return
|
|
266
|
-
}
|
|
267
|
-
c.outQueue = null
|
|
268
|
-
if (c.closeAfterFlush) closeConn(c)
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function closeConn(c: Conn): void {
|
|
272
|
-
if (c.closed) return
|
|
273
|
-
c.closed = true
|
|
274
|
-
if (c.sock >= 0) sock.closesocket(c.sock)
|
|
275
|
-
c.owner.conns.delete(c.sock)
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// ── Request state machine ──
|
|
279
|
-
|
|
280
|
-
function whole(c: Conn): Uint8Array {
|
|
281
|
-
return concatU8(c.parts)
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function trimParts(c: Conn, buf: Uint8Array, consumed: number): void {
|
|
285
|
-
if (consumed >= buf.length) {
|
|
286
|
-
c.parts = []
|
|
287
|
-
} else {
|
|
288
|
-
c.parts = [buf.subarray(consumed)]
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function tryProcess(c: Conn): void {
|
|
293
|
-
if (c.closed || c.current) return
|
|
294
|
-
const buf = whole(c)
|
|
295
|
-
|
|
296
|
-
if (c.pending) {
|
|
297
|
-
const bodyStart = c.pending.bodyStart
|
|
298
|
-
const parsed = c.pending.parsed
|
|
299
|
-
let body: Uint8Array
|
|
300
|
-
let end: number
|
|
301
|
-
if (parsed.chunked) {
|
|
302
|
-
const dec = decodeChunked(buf, bodyStart)
|
|
303
|
-
if (!dec) return
|
|
304
|
-
body = dec.data
|
|
305
|
-
end = dec.end
|
|
306
|
-
} else {
|
|
307
|
-
const needed = parsed.contentLength
|
|
308
|
-
if (bodyStart + needed > buf.length) return
|
|
309
|
-
body = buf.subarray(bodyStart, bodyStart + needed)
|
|
310
|
-
end = bodyStart + needed
|
|
311
|
-
}
|
|
312
|
-
c.pending = null
|
|
313
|
-
dispatchRequest(c, parsed, body, end)
|
|
314
|
-
return
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const headerEnd = findCrLfCrLf(buf)
|
|
318
|
-
if (headerEnd < 0) return
|
|
319
|
-
const absEnd = headerEnd + 4
|
|
320
|
-
const headerStr = new TextDecoder('utf-8').decode(buf.subarray(0, absEnd))
|
|
321
|
-
const parsed = parseHeader(headerStr)
|
|
322
|
-
if (!parsed) { closeConn(c); return }
|
|
323
|
-
|
|
324
|
-
const bodyStart = absEnd
|
|
325
|
-
if (parsed.chunked) {
|
|
326
|
-
const dec = decodeChunked(buf, bodyStart)
|
|
327
|
-
if (!dec) {
|
|
328
|
-
c.pending = { parsed, bodyStart }
|
|
329
|
-
return
|
|
330
|
-
}
|
|
331
|
-
dispatchRequest(c, parsed, dec.data, dec.end)
|
|
332
|
-
} else if (parsed.contentLength > 0) {
|
|
333
|
-
if (bodyStart + parsed.contentLength > buf.length) {
|
|
334
|
-
c.pending = { parsed, bodyStart }
|
|
335
|
-
return
|
|
336
|
-
}
|
|
337
|
-
dispatchRequest(c, parsed, buf.subarray(bodyStart, bodyStart + parsed.contentLength), bodyStart + parsed.contentLength)
|
|
338
|
-
} else {
|
|
339
|
-
dispatchRequest(c, parsed, new Uint8Array(0), bodyStart)
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function dispatchRequest(c: Conn, parsed: ParsedHeader, body: Uint8Array, end: number): void {
|
|
344
|
-
const connHeader = (parsed.headers['connection'] || '').toLowerCase()
|
|
345
|
-
if (parsed.version === 'HTTP/1.1') c.keepAlive = connHeader.indexOf('close') < 0
|
|
346
|
-
else c.keepAlive = connHeader.indexOf('keep-alive') >= 0
|
|
347
|
-
|
|
348
|
-
let host = parsed.headers['host']
|
|
349
|
-
if (!host) {
|
|
350
|
-
const a = c.owner.address()
|
|
351
|
-
const addrStr = a ? (a.addr.includes(':') ? `[${a.addr}]` : a.addr) : 'localhost'
|
|
352
|
-
host = a ? addrStr + ':' + a.port : 'localhost'
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const isAbsolute = parsed.target.startsWith('http://') || parsed.target.startsWith('https://')
|
|
356
|
-
const url = isAbsolute ? parsed.target : 'http://' + host + parsed.target
|
|
357
|
-
|
|
358
|
-
const req = new RequestImpl(url, {
|
|
359
|
-
method: parsed.method,
|
|
360
|
-
headers: new HeadersImpl(parsed.headers),
|
|
361
|
-
body: body.length > 0 ? body : undefined,
|
|
362
|
-
})
|
|
363
|
-
|
|
364
|
-
c.current = { req }
|
|
365
|
-
trimParts(c, whole(c), end)
|
|
366
|
-
void handleRequest(c)
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
async function handleRequest(c: Conn): Promise<void> {
|
|
370
|
-
const req = c.current?.req
|
|
371
|
-
if (!req) { c.current = null; return }
|
|
372
|
-
|
|
373
|
-
let res: Response
|
|
374
|
-
try {
|
|
375
|
-
const result = await c.owner.handler(req)
|
|
376
|
-
if (result instanceof ResponseImpl) {
|
|
377
|
-
res = result
|
|
378
|
-
} else {
|
|
379
|
-
console.error('[http-server] handler must return a Response')
|
|
380
|
-
res = new ResponseImpl('Internal Server Error', { status: 500 })
|
|
381
|
-
}
|
|
382
|
-
} catch (e) {
|
|
383
|
-
console.error('[http-server] handler error:', e)
|
|
384
|
-
res = new ResponseImpl('Internal Server Error', { status: 500 })
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (c.closed) return
|
|
388
|
-
await sendResponse(c, req.method, res)
|
|
389
|
-
if (c.closed) return
|
|
390
|
-
|
|
391
|
-
c.current = null
|
|
392
|
-
if (!c.closeAfterFlush) tryProcess(c)
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
async function sendResponse(c: Conn, method: string, res: Response): Promise<void> {
|
|
396
|
-
const status = res.status
|
|
397
|
-
const statusText = res.statusText || STATUS_TEXT[status] || 'OK'
|
|
398
|
-
const headers = new HeadersImpl(res.headers)
|
|
399
|
-
|
|
400
|
-
let body: Uint8Array
|
|
401
|
-
try {
|
|
402
|
-
body = await _readStream(res.body)
|
|
403
|
-
} catch {
|
|
404
|
-
body = new Uint8Array(0)
|
|
405
|
-
}
|
|
406
|
-
if (c.closed) return
|
|
407
|
-
|
|
408
|
-
const headOnly = method === 'HEAD'
|
|
409
|
-
const bodyless = (status >= 100 && status < 200) || status === 204 || status === 304
|
|
410
|
-
|
|
411
|
-
if (!headers.has('content-length') && !bodyless) headers.set('Content-Length', String(body.length))
|
|
412
|
-
if (!headers.has('connection')) headers.set('Connection', c.keepAlive ? 'keep-alive' : 'close')
|
|
413
|
-
|
|
414
|
-
const connHdr = (headers.get('connection') || '').toLowerCase()
|
|
415
|
-
if (!c.keepAlive || connHdr === 'close') c.closeAfterFlush = true
|
|
416
|
-
|
|
417
|
-
let head = 'HTTP/1.1 ' + status + ' ' + statusText + '\r\n'
|
|
418
|
-
for (const [name, value] of headers) head += name + ': ' + value + '\r\n'
|
|
419
|
-
head += '\r\n'
|
|
420
|
-
|
|
421
|
-
const headBytes = new TextEncoder().encode(head)
|
|
422
|
-
const payload = headOnly || body.length === 0 ? headBytes : concatU8([headBytes, body])
|
|
423
|
-
queueSend(c, payload)
|
|
424
|
-
}
|
|
1
|
+
import './polyfill.js'
|
|
2
|
+
import * as sock from 'sock'
|
|
3
|
+
import { HeadersImpl, RequestImpl, ResponseImpl, _readStream } from './http-common.js'
|
|
4
|
+
|
|
5
|
+
type SockHandle = import('sock').SockHandle
|
|
6
|
+
|
|
7
|
+
// ── Public API ──
|
|
8
|
+
|
|
9
|
+
export type ServerHandler = (req: Request) => Promise<Response>
|
|
10
|
+
|
|
11
|
+
// ── Internal types ──
|
|
12
|
+
|
|
13
|
+
interface NetEvent { lNetworkEvents: number; iErrorCode: number[] }
|
|
14
|
+
|
|
15
|
+
interface ParsedHeader {
|
|
16
|
+
method: string
|
|
17
|
+
target: string
|
|
18
|
+
version: string
|
|
19
|
+
headers: Record<string, string>
|
|
20
|
+
chunked: boolean
|
|
21
|
+
contentLength: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface PendingReq {
|
|
25
|
+
parsed: ParsedHeader
|
|
26
|
+
bodyStart: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ActiveReq {
|
|
30
|
+
req: RequestImpl
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Conn {
|
|
34
|
+
sock: SockHandle
|
|
35
|
+
parts: Uint8Array[]
|
|
36
|
+
pending: PendingReq | null
|
|
37
|
+
current: ActiveReq | null
|
|
38
|
+
outQueue: Uint8Array | null
|
|
39
|
+
closed: boolean
|
|
40
|
+
closeAfterFlush: boolean
|
|
41
|
+
keepAlive: boolean
|
|
42
|
+
owner: HttpServer
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const STATUS_TEXT: Record<number, string> = {
|
|
46
|
+
100: 'Continue', 101: 'Switching Protocols',
|
|
47
|
+
200: 'OK', 201: 'Created', 202: 'Accepted', 204: 'No Content',
|
|
48
|
+
301: 'Moved Permanently', 302: 'Found', 304: 'Not Modified',
|
|
49
|
+
400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found',
|
|
50
|
+
405: 'Method Not Allowed', 408: 'Request Timeout', 413: 'Payload Too Large',
|
|
51
|
+
500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Byte helpers ──
|
|
55
|
+
|
|
56
|
+
function concatU8(parts: Uint8Array[]): Uint8Array {
|
|
57
|
+
let total = 0
|
|
58
|
+
for (const p of parts) total += p.length
|
|
59
|
+
const out = new Uint8Array(total)
|
|
60
|
+
let off = 0
|
|
61
|
+
for (const p of parts) { out.set(p, off); off += p.length }
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function toArrayBuffer(u8: Uint8Array): ArrayBuffer {
|
|
66
|
+
return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function findCrLfCrLf(buf: Uint8Array): number {
|
|
70
|
+
const n = buf.length - 3
|
|
71
|
+
for (let i = 0; i < n; i++) {
|
|
72
|
+
if (buf[i] === 13 && buf[i + 1] === 10 && buf[i + 2] === 13 && buf[i + 3] === 10) return i
|
|
73
|
+
}
|
|
74
|
+
return -1
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Decode a chunked-transfer body starting at `start`. Returns the decoded
|
|
78
|
+
* data and the offset just past the terminating CRLF, or null if the buffer
|
|
79
|
+
* does not yet contain the complete chunked body. */
|
|
80
|
+
function decodeChunked(buf: Uint8Array, start: number): { data: Uint8Array; end: number } | null {
|
|
81
|
+
let pos = start
|
|
82
|
+
const parts: Uint8Array[] = []
|
|
83
|
+
const decoder = new TextDecoder('utf-8')
|
|
84
|
+
while (pos < buf.length) {
|
|
85
|
+
let eol = pos
|
|
86
|
+
while (eol < buf.length && buf[eol] !== 13) eol++
|
|
87
|
+
if (eol >= buf.length || eol + 1 >= buf.length || buf[eol + 1] !== 10) return null
|
|
88
|
+
const sizeLine = decoder.decode(buf.subarray(pos, eol)).trim()
|
|
89
|
+
const semi = sizeLine.indexOf(';')
|
|
90
|
+
const hex = (semi >= 0 ? sizeLine.slice(0, semi) : sizeLine).trim()
|
|
91
|
+
const size = parseInt(hex, 16)
|
|
92
|
+
if (isNaN(size) || size < 0) return null
|
|
93
|
+
pos = eol + 2
|
|
94
|
+
if (size === 0) {
|
|
95
|
+
while (true) {
|
|
96
|
+
if (pos >= buf.length) return null
|
|
97
|
+
if (buf[pos] === 13) {
|
|
98
|
+
if (pos + 1 >= buf.length || buf[pos + 1] !== 10) return null
|
|
99
|
+
pos += 2
|
|
100
|
+
return { data: concatU8(parts), end: pos }
|
|
101
|
+
}
|
|
102
|
+
let t = pos
|
|
103
|
+
while (t < buf.length && buf[t] !== 13) t++
|
|
104
|
+
if (t >= buf.length || t + 1 >= buf.length || buf[t + 1] !== 10) return null
|
|
105
|
+
pos = t + 2
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (pos + size > buf.length) return null
|
|
109
|
+
parts.push(buf.subarray(pos, pos + size))
|
|
110
|
+
pos += size
|
|
111
|
+
if (pos + 1 >= buf.length || buf[pos] !== 13 || buf[pos + 1] !== 10) return null
|
|
112
|
+
pos += 2
|
|
113
|
+
}
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Parsing ──
|
|
118
|
+
|
|
119
|
+
function parseHeader(headerStr: string): ParsedHeader | null {
|
|
120
|
+
const lines = headerStr.split('\r\n')
|
|
121
|
+
const first = lines[0]
|
|
122
|
+
if (!first) return null
|
|
123
|
+
const sp1 = first.indexOf(' ')
|
|
124
|
+
const sp2 = first.lastIndexOf(' ')
|
|
125
|
+
if (sp1 <= 0 || sp2 <= sp1) return null
|
|
126
|
+
const method = first.slice(0, sp1)
|
|
127
|
+
const target = first.slice(sp1 + 1, sp2)
|
|
128
|
+
const version = first.slice(sp2 + 1).trim()
|
|
129
|
+
|
|
130
|
+
const headers: Record<string, string> = {}
|
|
131
|
+
for (let i = 1; i < lines.length; i++) {
|
|
132
|
+
const line = lines[i]
|
|
133
|
+
if (!line) continue
|
|
134
|
+
const ci = line.indexOf(':')
|
|
135
|
+
if (ci <= 0) continue
|
|
136
|
+
const name = line.slice(0, ci).trim().toLowerCase()
|
|
137
|
+
const value = line.slice(ci + 1).trim()
|
|
138
|
+
if (headers[name] !== undefined) headers[name] = headers[name] + ', ' + value
|
|
139
|
+
else headers[name] = value
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const te = (headers['transfer-encoding'] || '').toLowerCase()
|
|
143
|
+
const chunked = te.indexOf('chunked') >= 0
|
|
144
|
+
const contentLength = parseInt(headers['content-length'] || '0', 10) || 0
|
|
145
|
+
return { method, target, version, headers, chunked, contentLength }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Server ──
|
|
149
|
+
|
|
150
|
+
export class HttpServer {
|
|
151
|
+
readonly handler: ServerHandler
|
|
152
|
+
private serverSock: SockHandle | null = null
|
|
153
|
+
readonly conns = new Map<SockHandle, Conn>()
|
|
154
|
+
private closed = false
|
|
155
|
+
|
|
156
|
+
constructor(handler: ServerHandler) {
|
|
157
|
+
this.handler = handler
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Actual bound address, or null if not listening. */
|
|
161
|
+
address(): { addr: string; port: number } | null {
|
|
162
|
+
if (this.serverSock === null) return null
|
|
163
|
+
return sock.getsockname(this.serverSock)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Bind and start listening. Returns 0 on success, -1 on failure. */
|
|
167
|
+
listen(port: number, host?: string, backlog?: number): number {
|
|
168
|
+
if (this.serverSock !== null) return -1
|
|
169
|
+
const s = sock.socket()
|
|
170
|
+
if (s < 0) return -1
|
|
171
|
+
if (sock.bind(s, host ?? null, port) !== 0) { sock.closesocket(s); return -1 }
|
|
172
|
+
if (sock.listen(s, backlog ?? 64) !== 0) { sock.closesocket(s); return -1 }
|
|
173
|
+
this.serverSock = s
|
|
174
|
+
sock.set_on_event(s, (event: NetEvent) => {
|
|
175
|
+
if (event.lNetworkEvents & sock.FdEvent.FD_ACCEPT) this.acceptLoop()
|
|
176
|
+
})
|
|
177
|
+
return 0
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
close(): void {
|
|
181
|
+
if (this.closed) return
|
|
182
|
+
this.closed = true
|
|
183
|
+
for (const c of Array.from(this.conns.values())) closeConn(c)
|
|
184
|
+
this.conns.clear()
|
|
185
|
+
if (this.serverSock !== null) { sock.closesocket(this.serverSock); this.serverSock = null }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private acceptLoop(): void {
|
|
189
|
+
if (this.closed || this.serverSock === null) return
|
|
190
|
+
while (true) {
|
|
191
|
+
const ac = sock.accept(this.serverSock)
|
|
192
|
+
if (!ac) break
|
|
193
|
+
const c: Conn = {
|
|
194
|
+
sock: ac.handle,
|
|
195
|
+
parts: [],
|
|
196
|
+
pending: null,
|
|
197
|
+
current: null,
|
|
198
|
+
outQueue: null,
|
|
199
|
+
closed: false,
|
|
200
|
+
closeAfterFlush: false,
|
|
201
|
+
keepAlive: true,
|
|
202
|
+
owner: this,
|
|
203
|
+
}
|
|
204
|
+
this.conns.set(c.sock, c)
|
|
205
|
+
sock.set_on_event(c.sock, (event: NetEvent) => this.onConnEvent(c, event))
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private onConnEvent(c: Conn, event: NetEvent): void {
|
|
210
|
+
if (c.closed) return
|
|
211
|
+
if (event.lNetworkEvents & sock.FdEvent.FD_READ) onRead(c)
|
|
212
|
+
if (event.lNetworkEvents & sock.FdEvent.FD_WRITE) flushQueue(c)
|
|
213
|
+
if (event.lNetworkEvents & sock.FdEvent.FD_CLOSE) {
|
|
214
|
+
onRead(c)
|
|
215
|
+
if (!c.current && !c.pending) closeConn(c)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function createServer(handler: ServerHandler): HttpServer {
|
|
221
|
+
return new HttpServer(handler)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Connection I/O ──
|
|
225
|
+
|
|
226
|
+
function onRead(c: Conn): void {
|
|
227
|
+
if (c.closed) return
|
|
228
|
+
while (true) {
|
|
229
|
+
const d = sock.recv(c.sock, 8192)
|
|
230
|
+
if (!d) break
|
|
231
|
+
c.parts.push(new Uint8Array(d))
|
|
232
|
+
}
|
|
233
|
+
if (!c.closed) tryProcess(c)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function queueSend(c: Conn, data: Uint8Array): void {
|
|
237
|
+
if (c.closed || data.length === 0) return
|
|
238
|
+
if (c.outQueue && c.outQueue.length > 0) {
|
|
239
|
+
c.outQueue = concatU8([c.outQueue, data])
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
const ret = sock.send(c.sock, toArrayBuffer(data))
|
|
243
|
+
if (ret < 0) {
|
|
244
|
+
c.outQueue = data
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
if (ret < data.length) {
|
|
248
|
+
c.outQueue = data.subarray(ret)
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
if (c.closeAfterFlush) closeConn(c)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function flushQueue(c: Conn): void {
|
|
255
|
+
if (c.closed) return
|
|
256
|
+
if (!c.outQueue || c.outQueue.length === 0) {
|
|
257
|
+
if (c.closeAfterFlush) closeConn(c)
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
const q = c.outQueue
|
|
261
|
+
const ret = sock.send(c.sock, toArrayBuffer(q))
|
|
262
|
+
if (ret < 0) return
|
|
263
|
+
if (ret < q.length) {
|
|
264
|
+
c.outQueue = q.subarray(ret)
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
c.outQueue = null
|
|
268
|
+
if (c.closeAfterFlush) closeConn(c)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function closeConn(c: Conn): void {
|
|
272
|
+
if (c.closed) return
|
|
273
|
+
c.closed = true
|
|
274
|
+
if (c.sock >= 0) sock.closesocket(c.sock)
|
|
275
|
+
c.owner.conns.delete(c.sock)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── Request state machine ──
|
|
279
|
+
|
|
280
|
+
function whole(c: Conn): Uint8Array {
|
|
281
|
+
return concatU8(c.parts)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function trimParts(c: Conn, buf: Uint8Array, consumed: number): void {
|
|
285
|
+
if (consumed >= buf.length) {
|
|
286
|
+
c.parts = []
|
|
287
|
+
} else {
|
|
288
|
+
c.parts = [buf.subarray(consumed)]
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function tryProcess(c: Conn): void {
|
|
293
|
+
if (c.closed || c.current) return
|
|
294
|
+
const buf = whole(c)
|
|
295
|
+
|
|
296
|
+
if (c.pending) {
|
|
297
|
+
const bodyStart = c.pending.bodyStart
|
|
298
|
+
const parsed = c.pending.parsed
|
|
299
|
+
let body: Uint8Array
|
|
300
|
+
let end: number
|
|
301
|
+
if (parsed.chunked) {
|
|
302
|
+
const dec = decodeChunked(buf, bodyStart)
|
|
303
|
+
if (!dec) return
|
|
304
|
+
body = dec.data
|
|
305
|
+
end = dec.end
|
|
306
|
+
} else {
|
|
307
|
+
const needed = parsed.contentLength
|
|
308
|
+
if (bodyStart + needed > buf.length) return
|
|
309
|
+
body = buf.subarray(bodyStart, bodyStart + needed)
|
|
310
|
+
end = bodyStart + needed
|
|
311
|
+
}
|
|
312
|
+
c.pending = null
|
|
313
|
+
dispatchRequest(c, parsed, body, end)
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const headerEnd = findCrLfCrLf(buf)
|
|
318
|
+
if (headerEnd < 0) return
|
|
319
|
+
const absEnd = headerEnd + 4
|
|
320
|
+
const headerStr = new TextDecoder('utf-8').decode(buf.subarray(0, absEnd))
|
|
321
|
+
const parsed = parseHeader(headerStr)
|
|
322
|
+
if (!parsed) { closeConn(c); return }
|
|
323
|
+
|
|
324
|
+
const bodyStart = absEnd
|
|
325
|
+
if (parsed.chunked) {
|
|
326
|
+
const dec = decodeChunked(buf, bodyStart)
|
|
327
|
+
if (!dec) {
|
|
328
|
+
c.pending = { parsed, bodyStart }
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
dispatchRequest(c, parsed, dec.data, dec.end)
|
|
332
|
+
} else if (parsed.contentLength > 0) {
|
|
333
|
+
if (bodyStart + parsed.contentLength > buf.length) {
|
|
334
|
+
c.pending = { parsed, bodyStart }
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
dispatchRequest(c, parsed, buf.subarray(bodyStart, bodyStart + parsed.contentLength), bodyStart + parsed.contentLength)
|
|
338
|
+
} else {
|
|
339
|
+
dispatchRequest(c, parsed, new Uint8Array(0), bodyStart)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function dispatchRequest(c: Conn, parsed: ParsedHeader, body: Uint8Array, end: number): void {
|
|
344
|
+
const connHeader = (parsed.headers['connection'] || '').toLowerCase()
|
|
345
|
+
if (parsed.version === 'HTTP/1.1') c.keepAlive = connHeader.indexOf('close') < 0
|
|
346
|
+
else c.keepAlive = connHeader.indexOf('keep-alive') >= 0
|
|
347
|
+
|
|
348
|
+
let host = parsed.headers['host']
|
|
349
|
+
if (!host) {
|
|
350
|
+
const a = c.owner.address()
|
|
351
|
+
const addrStr = a ? (a.addr.includes(':') ? `[${a.addr}]` : a.addr) : 'localhost'
|
|
352
|
+
host = a ? addrStr + ':' + a.port : 'localhost'
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const isAbsolute = parsed.target.startsWith('http://') || parsed.target.startsWith('https://')
|
|
356
|
+
const url = isAbsolute ? parsed.target : 'http://' + host + parsed.target
|
|
357
|
+
|
|
358
|
+
const req = new RequestImpl(url, {
|
|
359
|
+
method: parsed.method,
|
|
360
|
+
headers: new HeadersImpl(parsed.headers),
|
|
361
|
+
body: body.length > 0 ? body : undefined,
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
c.current = { req }
|
|
365
|
+
trimParts(c, whole(c), end)
|
|
366
|
+
void handleRequest(c)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function handleRequest(c: Conn): Promise<void> {
|
|
370
|
+
const req = c.current?.req
|
|
371
|
+
if (!req) { c.current = null; return }
|
|
372
|
+
|
|
373
|
+
let res: Response
|
|
374
|
+
try {
|
|
375
|
+
const result = await c.owner.handler(req)
|
|
376
|
+
if (result instanceof ResponseImpl) {
|
|
377
|
+
res = result
|
|
378
|
+
} else {
|
|
379
|
+
console.error('[http-server] handler must return a Response')
|
|
380
|
+
res = new ResponseImpl('Internal Server Error', { status: 500 })
|
|
381
|
+
}
|
|
382
|
+
} catch (e) {
|
|
383
|
+
console.error('[http-server] handler error:', e)
|
|
384
|
+
res = new ResponseImpl('Internal Server Error', { status: 500 })
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (c.closed) return
|
|
388
|
+
await sendResponse(c, req.method, res)
|
|
389
|
+
if (c.closed) return
|
|
390
|
+
|
|
391
|
+
c.current = null
|
|
392
|
+
if (!c.closeAfterFlush) tryProcess(c)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function sendResponse(c: Conn, method: string, res: Response): Promise<void> {
|
|
396
|
+
const status = res.status
|
|
397
|
+
const statusText = res.statusText || STATUS_TEXT[status] || 'OK'
|
|
398
|
+
const headers = new HeadersImpl(res.headers)
|
|
399
|
+
|
|
400
|
+
let body: Uint8Array
|
|
401
|
+
try {
|
|
402
|
+
body = await _readStream(res.body)
|
|
403
|
+
} catch {
|
|
404
|
+
body = new Uint8Array(0)
|
|
405
|
+
}
|
|
406
|
+
if (c.closed) return
|
|
407
|
+
|
|
408
|
+
const headOnly = method === 'HEAD'
|
|
409
|
+
const bodyless = (status >= 100 && status < 200) || status === 204 || status === 304
|
|
410
|
+
|
|
411
|
+
if (!headers.has('content-length') && !bodyless) headers.set('Content-Length', String(body.length))
|
|
412
|
+
if (!headers.has('connection')) headers.set('Connection', c.keepAlive ? 'keep-alive' : 'close')
|
|
413
|
+
|
|
414
|
+
const connHdr = (headers.get('connection') || '').toLowerCase()
|
|
415
|
+
if (!c.keepAlive || connHdr === 'close') c.closeAfterFlush = true
|
|
416
|
+
|
|
417
|
+
let head = 'HTTP/1.1 ' + status + ' ' + statusText + '\r\n'
|
|
418
|
+
for (const [name, value] of headers) head += name + ': ' + value + '\r\n'
|
|
419
|
+
head += '\r\n'
|
|
420
|
+
|
|
421
|
+
const headBytes = new TextEncoder().encode(head)
|
|
422
|
+
const payload = headOnly || body.length === 0 ? headBytes : concatU8([headBytes, body])
|
|
423
|
+
queueSend(c, payload)
|
|
424
|
+
}
|