@amritk/nish-x86_64-linux 0.26.0 → 0.28.0

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.
@@ -0,0 +1,1645 @@
1
+ /**
2
+ * `nish/net/http-client` — a blocking HTTP/1.1 client for command-line
3
+ * programs: `httpFetch(request, options)` dials, sends, and answers the
4
+ * response's head with a reader for its body, and an `HttpClient` does the
5
+ * same while keeping one connection alive between fetches.
6
+ *
7
+ * import { HttpOptions, HttpRequest, HttpResponse, httpFetch } from "nish/net/http-client";
8
+ *
9
+ * const req = new HttpRequest("GET", "http://example.com/");
10
+ * req.header("Accept", "application/json");
11
+ * const res: HttpResponse | null = httpFetch(req, new HttpOptions());
12
+ * if (res !== null) {
13
+ * const body: u8[] | null = res.body.readAll(1048576);
14
+ * }
15
+ *
16
+ * **It blocks the thread** at every step, the way a CLI wants it to: the
17
+ * lookup, the connect, each write and each read. A server, which must not
18
+ * stall every connection for one slow peer, drives `nish/net/http1-client`'s
19
+ * parser from its own loop instead. Every wait is bounded: `connectTimeoutMs`
20
+ * for the lookup and the connect (one budget across every address the name
21
+ * has), `readTimeoutMs` for each wait on the peer, and `totalTimeoutMs`, when
22
+ * set, for the whole fetch, redirects and body included, so a server that
23
+ * drips one byte a second cannot hold a fetch forever.
24
+ *
25
+ * **Framing is the parser's.** The head is written by `http1RequestHead` and
26
+ * every byte read goes through `Http1ResponseParser`, so a value holding a CR
27
+ * or LF is refused rather than sent, and every response-smuggling shape is
28
+ * refused with the parser's own code (see `docs/security/http1-client.md`).
29
+ *
30
+ * **Keep-alive** holds one connection, to the last origin (or proxy) used. A
31
+ * fetch to the same place reuses it once the last body has been read to its
32
+ * end; a fetch while a body is unread closes it, and the old reader answers an
33
+ * error. A reused connection the server closed while it sat idle is the one
34
+ * failure a client cannot avoid, so a request with an idempotent method that
35
+ * fails on it before any response byte arrives is sent once more on a fresh
36
+ * connection (RFC 9110 §9.2.2). A POST is never sent twice.
37
+ *
38
+ * **Redirects** are not followed unless `followRedirects` is set, and then
39
+ * only to the same origin unless `crossOriginRedirects` is set too. A hop to
40
+ * another origin drops `Authorization`, `Proxy-Authorization`, `Cookie` and
41
+ * the caller's `Host`; a hop from https to http is always refused; 303 turns
42
+ * any method but HEAD into a GET without a body, as do 301 and 302 after a
43
+ * POST, and 307 and 308 keep the method and the body (RFC 9110 §15.4). Past
44
+ * `maxRedirects` hops the fetch fails.
45
+ *
46
+ * **Proxies** come from the environment when `useEnvProxy` is set:
47
+ * `http_proxy` (or `HTTP_PROXY`), `https_proxy` (or `HTTPS_PROXY`) and
48
+ * `no_proxy` (or `NO_PROXY`), the lower-case name first. `HTTP_PROXY` is
49
+ * ignored when `REQUEST_METHOD` is set, since a CGI program finds a request's
50
+ * `Proxy:` header there under that name (the "httpoxy" hole). Only an
51
+ * `http://` proxy is accepted. An http request goes to it in absolute form; an
52
+ * https one goes through a `CONNECT` tunnel.
53
+ *
54
+ * Every failure answers null with a distinct `HTTP_E_*` code in the client's
55
+ * `error` and a sentence in `errorText`; a body reader answers the code
56
+ * negated.
57
+ *
58
+ * Private helpers share the importing program's flat symbol namespace
59
+ * (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
60
+ *
61
+ * Written from RFC 9110 and RFC 9112, not ported from another implementation.
62
+ */
63
+ import { base64Encode } from "nish/base64"
64
+ import { copyBytes } from "nish/bytes"
65
+ import {
66
+ HTTP1_BODY,
67
+ HTTP1_CLOSED,
68
+ HTTP1_END,
69
+ HTTP1_ERROR,
70
+ HTTP1_HEAD,
71
+ HTTP1_NEED_MORE,
72
+ HTTP1_UPGRADE,
73
+ } from "nish/net/http1"
74
+ import {
75
+ HTTP1_E_BODY_LIMIT,
76
+ HTTP1_E_TRUNCATED,
77
+ HTTP1_INFORMATIONAL,
78
+ Http1ResponseParser,
79
+ http1RequestHead,
80
+ } from "nish/net/http1-client"
81
+ import { httpFieldBytes } from "nish/net/http-fields"
82
+ import { Url, urlDecode, urlDefaultPort, urlOrigin, urlParse, urlResolve } from "nish/net/url"
83
+ import { resolveHost } from "nish/net/dns"
84
+ import { DNS_ADDRESS_BYTES } from "nish/net/dns-message"
85
+ import { DNS_RESOLVE_MAX_ADDRESSES, DNS_RESOLVE_TIMEOUT } from "nish/net/dns-resolver"
86
+ import {
87
+ connectResult,
88
+ netClose,
89
+ netRead,
90
+ netWrite,
91
+ pollAdd,
92
+ pollCreate,
93
+ pollModify,
94
+ pollRemove,
95
+ pollWait,
96
+ tcpConnect,
97
+ } from "nish:net"
98
+ import { getenv, monotonicNanos, wallClockNanos } from "nish:process"
99
+ import { PkiRoots, pkiLoadRoots, pkiLoadSystemRoots } from "nish/crypto/pki-roots"
100
+ import { TlsClient, TlsClientConfig, tlsClientIsIpLiteral } from "nish/net/tls/client"
101
+ import { TLS_RECORD_AUTH, TlsRecordClient } from "nish/net/tls/record-client"
102
+ import { TLS_RECORD_AGAIN, TLS_RECORD_STATE_FAILED, TLS_RECORD_STATE_OPEN } from "nish/net/tls/record-server"
103
+ import { TlsClientTrust, tlsClientTrust, tlsClientVerdict } from "nish/net/tls/verify"
104
+
105
+ /** No error. */
106
+ export const HTTP_E_NONE: i32 = 0
107
+ /** The request's URL is not an absolute http or https URL. */
108
+ export const HTTP_E_URL: i32 = 1
109
+ /** The host's name could not be looked up, or has no address. */
110
+ export const HTTP_E_DNS: i32 = 2
111
+ /** Every address of the host refused the connection, or could not be reached. */
112
+ export const HTTP_E_REFUSED: i32 = 3
113
+ /** No address of the host answered within `connectTimeoutMs`. */
114
+ export const HTTP_E_CONNECT_TIMEOUT: i32 = 4
115
+ /** The server sent nothing, or took nothing, for `readTimeoutMs`. */
116
+ export const HTTP_E_READ_TIMEOUT: i32 = 5
117
+ /** The fetch ran past `totalTimeoutMs`. */
118
+ export const HTTP_E_TOTAL_TIMEOUT: i32 = 6
119
+ /** The response broke HTTP/1.1; `errorText` names the parser's code. */
120
+ export const HTTP_E_PROTOCOL: i32 = 7
121
+ /** A redirect past `maxRedirects`. */
122
+ export const HTTP_E_TOO_MANY_REDIRECTS: i32 = 8
123
+ /** A redirect to another origin, from https to http, or to a URL that is not http or https. */
124
+ export const HTTP_E_REDIRECT_REFUSED: i32 = 9
125
+ /** A body over `maxBodyBytes`, or over the limit given to `readAll`. */
126
+ export const HTTP_E_BODY_TOO_LARGE: i32 = 10
127
+ /** The proxy answered its CONNECT with something other than 2xx. */
128
+ export const HTTP_E_PROXY_REFUSED: i32 = 11
129
+ /** The TLS handshake or a TLS record failed. */
130
+ export const HTTP_E_TLS: i32 = 12
131
+ /** The server's certificate chain was not accepted for its host. */
132
+ export const HTTP_E_CERTIFICATE: i32 = 13
133
+ /** The request itself was refused before a byte was sent: a bad method, header or target. */
134
+ export const HTTP_E_REQUEST: i32 = 14
135
+ /** A socket call failed, or the server closed the connection before it answered. */
136
+ export const HTTP_E_IO: i32 = 15
137
+ /** A proxy setting that is not an `http://` URL with a host. */
138
+ export const HTTP_E_PROXY: i32 = 16
139
+
140
+ /** A typed zero: a bare literal is an `f64` under `--number-mode f64`. */
141
+ const HTTP_CLIENT_ZERO: i32 = 0
142
+ /** The read size `drain` asks a body for. */
143
+ const HTTP_CLIENT_DRAIN_CHUNK: i32 = 4096
144
+
145
+ /** The size of the client's read buffer: one netRead fills at most this. */
146
+ const HTTP_CLIENT_READ_BYTES: i32 = 16384
147
+ /** The most header fields a response may carry. */
148
+ const HTTP_CLIENT_MAX_HEADERS: i32 = 256
149
+ /** The most body bytes a redirect may carry and still leave its connection kept. */
150
+ const HTTP_CLIENT_DRAIN_BYTES: i32 = 65536
151
+ /** The port a proxy URL without one is dialled on, as curl does. */
152
+ const HTTP_CLIENT_PROXY_PORT: i32 = 1080
153
+ /** The longest single poll wait, so a deadline far away never overflows an `i32` of milliseconds. */
154
+ const HTTP_CLIENT_MAX_WAIT_MS: i64 = 3600000
155
+ /** Nanoseconds in a millisecond. */
156
+ const HTTP_CLIENT_NS_PER_MS: i64 = 1000000
157
+ /** A deadline that never comes: 146 years of monotonic nanoseconds, far below the overflow of a sum with a timeout. */
158
+ const HTTP_CLIENT_NEVER: i64 = 4611686018 * 1000000000
159
+ /** netRead's and netWrite's answer when the socket has nothing ready. */
160
+ const HTTP_CLIENT_WOULD_BLOCK: i32 = -11
161
+ /** pollAdd's events: readable, writable. */
162
+ const HTTP_CLIENT_READABLE: i32 = 1
163
+ const HTTP_CLIENT_WRITABLE: i32 = 2
164
+ /** The size of the client random and of each ephemeral private key a TLS handshake draws. */
165
+ const HTTP_CLIENT_TLS_KEY_BYTES: i32 = 32
166
+
167
+ /** A short name for an `HTTP_E_*` code, for messages: "dns", "read timeout", ... */
168
+ export const httpErrorName = (code: i32): string => {
169
+ // Indexed by code: the order of the HTTP_E_* constants above.
170
+ const names: string[] = [
171
+ "none",
172
+ "url",
173
+ "dns",
174
+ "refused",
175
+ "connect timeout",
176
+ "read timeout",
177
+ "total timeout",
178
+ "protocol",
179
+ "too many redirects",
180
+ "redirect refused",
181
+ "body too large",
182
+ "proxy refused",
183
+ "tls",
184
+ "certificate",
185
+ "request",
186
+ "io",
187
+ "proxy",
188
+ ]
189
+ return code >= 0 && code < toI32(names.length) ? names[code] : "unknown"
190
+ }
191
+
192
+ /**
193
+ * One request: a method, an absolute `http://` or `https://` URL, header
194
+ * fields in the order they are sent, and an optional body. A `Host` field is
195
+ * added from the URL unless the caller gives one, and `Content-Length` from
196
+ * the body, so a caller never frames the body itself. Userinfo in the URL is
197
+ * not sent anywhere: a program that means to authenticate sets its own
198
+ * `Authorization` field, so a password is never sent by accident.
199
+ */
200
+ export class HttpRequest {
201
+ method: string
202
+ url: string
203
+ names: string[]
204
+ values: string[]
205
+ /** Null for no body at all; an empty array sends `Content-Length: 0`. */
206
+ body: u8[] | null = null
207
+
208
+ constructor(method: string, url: string) {
209
+ this.method = method
210
+ this.url = url
211
+ this.names = []
212
+ this.values = []
213
+ }
214
+
215
+ /** Appends one header field; nothing is checked until the request is written. */
216
+ header(name: string, value: string): void {
217
+ this.names.push(name)
218
+ this.values.push(value)
219
+ }
220
+ }
221
+
222
+ /**
223
+ * How a client behaves. Every field has a default meant for a CLI talking to
224
+ * an API; raise `maxBodyBytes` to download something large, and set
225
+ * `readTimeoutMs` to 0 for an event stream that may be quiet for a long time.
226
+ */
227
+ export class HttpOptions {
228
+ // Declared widest first, so no byte of a value is padding.
229
+ /** 256 MiB by default; a body past it fails with `HTTP_E_BODY_TOO_LARGE`. */
230
+ maxBodyBytes: i64 = 268435456
231
+ /** A PEM file of the roots an https server's chain must end at; null for the system's. */
232
+ rootsPemPath: string | null = null
233
+ /** The budget for the lookup and the connect together; 0 or less for none but the total. */
234
+ connectTimeoutMs: i32 = 10000
235
+ /** The longest wait for the server to send or take a byte; 0 or less for none. */
236
+ readTimeoutMs: i32 = 30000
237
+ /** The budget for a whole fetch, its body included; 0 or less for none. */
238
+ totalTimeoutMs: i32 = 0
239
+ maxRedirects: i32 = 5
240
+ /** The status line and the header section, each at most this. */
241
+ maxHeaderBytes: i32 = 65536
242
+ followRedirects: boolean = false
243
+ crossOriginRedirects: boolean = false
244
+ /** Read the proxy settings from the environment (see the module comment). */
245
+ useEnvProxy: boolean = true
246
+ }
247
+
248
+ /**
249
+ * A response's head, as the server sent it, and a reader for its body. `url`
250
+ * is the URL that answered, which differs from the request's after a redirect.
251
+ * `names` are lower-cased and in the order they arrived, beside `values`.
252
+ */
253
+ export class HttpResponse {
254
+ status: i32 = 0
255
+ reason: string = ""
256
+ url: string = ""
257
+ names: string[]
258
+ values: string[]
259
+ body: HttpBodyReader
260
+
261
+ constructor(body: HttpBodyReader) {
262
+ this.names = []
263
+ this.values = []
264
+ this.body = body
265
+ }
266
+
267
+ /** The first value of the field `name`, in any case; null when the response has none. */
268
+ header(name: string): string | null {
269
+ const lower: string = httpClientLower(name)
270
+ for (let i: i32 = 0; i < toI32(this.names.length) && i < toI32(this.values.length); i += 1) {
271
+ if (this.names[i] === lower) {
272
+ return this.values[i]
273
+ }
274
+ }
275
+ return null
276
+ }
277
+ }
278
+
279
+ /**
280
+ * A response's body, read as it arrives: de-chunked, bounded by
281
+ * `maxBodyBytes`, and ending where the framing says it ends, so a cut-short
282
+ * body is an error and never taken for a whole one. It reads through the
283
+ * client that made it, and only until that client starts its next fetch.
284
+ */
285
+ export class HttpBodyReader {
286
+ client: HttpClient
287
+ generation: i32
288
+
289
+ constructor(client: HttpClient, generation: i32) {
290
+ this.client = client
291
+ this.generation = generation
292
+ }
293
+
294
+ /**
295
+ * Up to `n` body bytes into `buf[off ..]`: how many (at least one), 0 at
296
+ * the end of the body, or a negated `HTTP_E_*` code, the client's
297
+ * `errorText` saying why. It waits for at least one byte, no more, so an
298
+ * event stream is read event by event. A window outside `buf` panics; an
299
+ * empty one answers 0 at once, reading nothing, so 0 means the end only
300
+ * when `n` is positive.
301
+ */
302
+ read(buf: u8[], off: i32, n: i32): i32 {
303
+ if (off < 0 || n < 0 || toI64(off) + toI64(n) > toI64(buf.length)) {
304
+ panic("HttpBodyReader.read: the window is outside the buffer")
305
+ }
306
+ if (n === 0) {
307
+ return 0
308
+ }
309
+ return this.client.readBody(this.generation, buf, off, n)
310
+ }
311
+
312
+ /**
313
+ * The rest of the body in one array, or null on an error or once it passes
314
+ * `limit` bytes (`HTTP_E_BODY_TOO_LARGE`, and the connection is closed).
315
+ */
316
+ readAll(limit: i32): u8[] | null {
317
+ let size: i32 = 4096
318
+ if (limit < 0) {
319
+ size = 0
320
+ } else if (limit < 4096) {
321
+ size = limit
322
+ }
323
+ let out: u8[] = new Array<u8>(size)
324
+ let len: i32 = 0
325
+ const chunk: u8[] = new Array<u8>(HTTP_CLIENT_READ_BYTES)
326
+ let n: i32 = this.read(chunk, HTTP_CLIENT_ZERO, HTTP_CLIENT_READ_BYTES)
327
+ while (n > 0) {
328
+ if (n > limit - len) {
329
+ this.client.refuseBody(limit)
330
+ return null
331
+ }
332
+ if (len + n > toI32(out.length)) {
333
+ let cap: i32 = toI32(out.length) * 2
334
+ if (cap < len + n) {
335
+ cap = len + n
336
+ }
337
+ if (cap > limit) {
338
+ cap = limit
339
+ }
340
+ const grown: u8[] = new Array<u8>(cap)
341
+ copyBytes(grown, 0, out, 0, len)
342
+ out = grown
343
+ }
344
+ copyBytes(out, len, chunk, 0, n)
345
+ len += n
346
+ n = this.read(chunk, HTTP_CLIENT_ZERO, HTTP_CLIENT_READ_BYTES)
347
+ }
348
+ if (n < 0) {
349
+ return null
350
+ }
351
+ if (len === toI32(out.length)) {
352
+ return out
353
+ }
354
+ const exact: u8[] = new Array<u8>(len)
355
+ copyBytes(exact, 0, out, 0, len)
356
+ return exact
357
+ }
358
+ }
359
+
360
+ /**
361
+ * A client: the options, the proxy settings, and at most one open
362
+ * connection, kept between fetches. Not for two threads at once.
363
+ */
364
+ export class HttpClient {
365
+ // Declared widest first, so no byte of a client is padding.
366
+ options: HttpOptions
367
+ errorText: string = ""
368
+ /** The proxy settings in use; "" for none. `setProxies` replaces them. */
369
+ httpProxy: string = ""
370
+ httpsProxy: string = ""
371
+ noProxy: string = ""
372
+ /** Where `fd` leads: an origin, or the proxy an http request went through. */
373
+ route: string = ""
374
+ parser: Http1ResponseParser
375
+ /** The TLS connection over `fd` to an https origin; null over plain http. */
376
+ tls: TlsRecordClient | null = null
377
+ /** The roots an https server's chain must end at, read on the client's first https fetch. */
378
+ trust: TlsClientTrust | null = null
379
+ ready: i32[]
380
+ rbuf: u8[]
381
+ /** Where TLS application data is read before the parser takes it; empty until the first https connection. */
382
+ plain: u8[]
383
+ /** The fetch's end in monotonic nanoseconds; `HTTP_CLIENT_NEVER` for none. */
384
+ deadline: i64 = 0
385
+ /** The last failure's `HTTP_E_*` code; `HTTP_E_NONE` after a fetch that answered. */
386
+ error: i32 = 0
387
+ fd: i32 = -1
388
+ /** The fd registered in `loop`, and for what. */
389
+ loop: i32 = -1
390
+ watched: i32 = -1
391
+ watchedEvents: i32 = 0
392
+ /** Counts responses; a reader reads only while its own is the open one. */
393
+ generation: i32 = 0
394
+ /** How the latest response's body failed, `HTTP_E_NONE` while it has not; its reader answers it again. */
395
+ bodyError: i32 = 0
396
+ /** Where the latest response's unread body bytes start in `parser`, and how many. */
397
+ pendOff: i32 = 0
398
+ pendLen: i32 = 0
399
+ /** Whether `fd` has carried a response before the one in flight. */
400
+ reused: boolean = false
401
+ /** Whether any byte of the response in flight has arrived. */
402
+ gotByte: boolean = false
403
+ /** Whether the failure in hand was the connection going away, which a retry may cure. */
404
+ lost: boolean = false
405
+ /** Whether the latest response's body is unread. */
406
+ bodyOpen: boolean = false
407
+ /** Whether this is `httpFetch`'s client, closed whole once its one body is done with. */
408
+ once: boolean = false
409
+
410
+ constructor(options: HttpOptions) {
411
+ this.options = options
412
+ this.parser = httpClientParser(options)
413
+ this.ready = new Array<i32>(8)
414
+ this.rbuf = new Array<u8>(HTTP_CLIENT_READ_BYTES)
415
+ this.plain = []
416
+ if (options.useEnvProxy) {
417
+ const cgi: boolean = getenv("REQUEST_METHOD") !== null
418
+ this.httpProxy = httpChooseProxy(getenv("http_proxy"), getenv("HTTP_PROXY"), !cgi)
419
+ this.httpsProxy = httpChooseProxy(getenv("https_proxy"), getenv("HTTPS_PROXY"), true)
420
+ this.noProxy = httpChooseProxy(getenv("no_proxy"), getenv("NO_PROXY"), true)
421
+ }
422
+ }
423
+
424
+ /** Replaces the proxy settings, as the three variables would set them; "" for none. */
425
+ setProxies(httpProxy: string, httpsProxy: string, noProxy: string): void {
426
+ this.httpProxy = httpProxy
427
+ this.httpsProxy = httpsProxy
428
+ this.noProxy = noProxy
429
+ }
430
+
431
+ /**
432
+ * Sends `req` and answers the response once its head is in, following
433
+ * redirects as the options say; null on a failure, with `error` and
434
+ * `errorText` set. The body is read through the response's `body` before
435
+ * the next fetch.
436
+ */
437
+ fetch(req: HttpRequest): HttpResponse | null {
438
+ this.error = HTTP_E_NONE
439
+ this.errorText = ""
440
+ if (this.bodyOpen) {
441
+ // An unread body is still on the wire ahead of the next response.
442
+ this.drop()
443
+ }
444
+ this.generation += 1
445
+ const total: i32 = this.options.totalTimeoutMs
446
+ this.deadline = total > 0 ? monotonicNanos() + toI64(total) * HTTP_CLIENT_NS_PER_MS : HTTP_CLIENT_NEVER
447
+ let url: Url | null = urlParse(req.url)
448
+ if (url === null || (url.scheme !== "http" && url.scheme !== "https")) {
449
+ this.fail(HTTP_E_URL, `not an absolute http or https URL: ${httpClientRedact(url)}`)
450
+ return null
451
+ }
452
+ let method: string = req.method
453
+ let body: u8[] | null = req.body
454
+ const names: string[] = []
455
+ const values: string[] = []
456
+ for (let i: i32 = 0; i < toI32(req.names.length) && i < toI32(req.values.length); i += 1) {
457
+ names.push(req.names[i])
458
+ values.push(req.values[i])
459
+ }
460
+ let hops: i32 = 0
461
+ while (url !== null) {
462
+ const res: HttpResponse | null = this.exchange(url, method, names, values, body)
463
+ if (res === null) {
464
+ return null
465
+ }
466
+ const location: string | null = res.header("location")
467
+ if (!this.options.followRedirects || location === null || !httpClientIsRedirect(res.status)) {
468
+ return res
469
+ }
470
+ const next: Url | null = urlResolve(url, location)
471
+ if (hops >= this.options.maxRedirects) {
472
+ return this.refuse(
473
+ HTTP_E_TOO_MANY_REDIRECTS,
474
+ `more than ${this.options.maxRedirects} redirects, the last to ${httpClientRedact(next)}`
475
+ )
476
+ }
477
+ if (next === null || (next.scheme !== "http" && next.scheme !== "https")) {
478
+ return this.refuse(
479
+ HTTP_E_REDIRECT_REFUSED,
480
+ `a redirect to ${httpClientRedact(next)}, which is not an http or https URL`
481
+ )
482
+ }
483
+ if (url.scheme === "https" && next.scheme === "http") {
484
+ return this.refuse(
485
+ HTTP_E_REDIRECT_REFUSED,
486
+ `a redirect from https to http (${httpClientRedact(next)})`
487
+ )
488
+ }
489
+ const from: string | null = urlOrigin(url)
490
+ const to: string | null = urlOrigin(next)
491
+ if (from === null || to === null || from !== to) {
492
+ if (!this.options.crossOriginRedirects) {
493
+ return this.refuse(
494
+ HTTP_E_REDIRECT_REFUSED,
495
+ `a redirect to another origin (${httpClientRedact(next)})`
496
+ )
497
+ }
498
+ // Credentials were meant for the first origin, not for wherever it points.
499
+ httpClientDropField(names, values, "authorization")
500
+ httpClientDropField(names, values, "proxy-authorization")
501
+ httpClientDropField(names, values, "cookie")
502
+ httpClientDropField(names, values, "host")
503
+ }
504
+ const status: i32 = res.status
505
+ if (
506
+ (status === 303 && method !== "HEAD") ||
507
+ ((status === 301 || status === 302) && method === "POST")
508
+ ) {
509
+ method = "GET"
510
+ body = null
511
+ httpClientDropField(names, values, "content-type")
512
+ }
513
+ this.drain()
514
+ hops += 1
515
+ url = next
516
+ }
517
+ return null
518
+ }
519
+
520
+ /** Closes the connection; the client can still fetch, on a new one. */
521
+ close(): void {
522
+ this.drop()
523
+ if (this.loop >= 0) {
524
+ netClose(this.loop)
525
+ this.loop = -1
526
+ }
527
+ }
528
+
529
+ /**
530
+ * One request and its response's head, on the kept connection when it
531
+ * leads to the same place, once more on a fresh one when a kept connection
532
+ * turns out to be gone (see the module comment).
533
+ */
534
+ exchange(
535
+ url: Url,
536
+ method: string,
537
+ names: string[],
538
+ values: string[],
539
+ body: u8[] | null
540
+ ): HttpResponse | null {
541
+ for (let attempt: i32 = 0; attempt < 2; attempt += 1) {
542
+ if (!this.open(url)) {
543
+ return null
544
+ }
545
+ const res: HttpResponse | null = this.send(url, method, names, values, body)
546
+ if (res !== null) {
547
+ return res
548
+ }
549
+ const retry: boolean = this.reused && this.lost && !this.gotByte && httpClientIdempotent(method)
550
+ this.drop()
551
+ if (!retry) {
552
+ return null
553
+ }
554
+ this.error = HTTP_E_NONE
555
+ this.errorText = ""
556
+ }
557
+ return null
558
+ }
559
+
560
+ /**
561
+ * Makes `fd` a connection to where `url` goes: the kept one when it leads
562
+ * there, else a new one, through a proxy when the settings say so. False on
563
+ * a failure, with the error set.
564
+ */
565
+ open(url: Url): boolean {
566
+ const port: i32 = url.port >= 0 ? url.port : urlDefaultPort(url.scheme)
567
+ const proxyText: string = httpProxyFor(
568
+ url.scheme,
569
+ url.host,
570
+ port,
571
+ this.httpProxy,
572
+ this.httpsProxy,
573
+ this.noProxy
574
+ )
575
+ let proxy: Url | null = null
576
+ if (proxyText !== "") {
577
+ proxy = httpClientProxyUrl(proxyText)
578
+ if (proxy === null) {
579
+ this.fail(
580
+ HTTP_E_PROXY,
581
+ `the proxy setting is not an http:// URL with a host: ${httpClientRedact(
582
+ urlParse(proxyText.indexOf("://") >= 0 ? proxyText : "http://" + proxyText)
583
+ )}`
584
+ )
585
+ return false
586
+ }
587
+ }
588
+ const origin: string = `${url.scheme}://${httpClientHostText(url.host)}:${port}`
589
+ let route: string = origin
590
+ if (proxy !== null && url.scheme === "http") {
591
+ route = `proxy ${httpClientHostText(proxy.host)}:${proxy.port}`
592
+ } else if (proxy !== null) {
593
+ route = `${origin} via ${httpClientHostText(proxy.host)}:${proxy.port}`
594
+ }
595
+ if (this.fd >= 0 && this.route === route && this.quiet()) {
596
+ this.reused = true
597
+ return true
598
+ }
599
+ this.drop()
600
+ this.reused = false
601
+ // The roots come first, so a client with none never reaches the network.
602
+ if (url.scheme === "https" && !this.loadTrust()) {
603
+ return false
604
+ }
605
+ const dialHost: string = proxy !== null ? proxy.host : url.host
606
+ const dialPort: i32 = proxy !== null ? proxy.port : port
607
+ const fd: i32 = this.dial(dialHost, dialPort)
608
+ if (fd < 0) {
609
+ return false
610
+ }
611
+ this.fd = fd
612
+ this.route = route
613
+ this.parser = httpClientParser(this.options)
614
+ if (url.scheme === "https") {
615
+ let early: u8[] = []
616
+ if (proxy !== null) {
617
+ if (!this.tunnel(url.host, port, proxy)) {
618
+ this.drop()
619
+ return false
620
+ }
621
+ early = this.parser.tunnelBytes()
622
+ this.parser = httpClientParser(this.options)
623
+ }
624
+ if (!this.secure(url.host, early)) {
625
+ this.drop()
626
+ return false
627
+ }
628
+ }
629
+ return true
630
+ }
631
+
632
+ /**
633
+ * Reads the roots an https server's chain must end at, once per client:
634
+ * the PEM file `rootsPemPath` names, else the system's. False, with the
635
+ * error set, when none could be read, since no server could then be trusted.
636
+ */
637
+ loadTrust(): boolean {
638
+ if (this.trust !== null) {
639
+ return true
640
+ }
641
+ const path: string | null = this.options.rootsPemPath
642
+ const roots: PkiRoots = path !== null ? pkiLoadRoots(path, [], []) : pkiLoadSystemRoots()
643
+ if (toI32(roots.roots.length) === 0) {
644
+ const where: string = path !== null ? path : "SSL_CERT_FILE, SSL_CERT_DIR or the system's bundle"
645
+ this.fail(HTTP_E_CERTIFICATE, `no trusted root certificate could be read from ${where}`)
646
+ return false
647
+ }
648
+ this.trust = tlsClientTrust(roots)
649
+ return true
650
+ }
651
+
652
+ /**
653
+ * Runs a TLS 1.3 handshake for `host` over the fresh `fd`, `early` being
654
+ * what the proxy sent past its CONNECT answer. The server's chain and
655
+ * CertificateVerify are checked under the client's roots at the wall
656
+ * clock's time before its Finished is read. True once the connection is
657
+ * open and the client's last flight sent; false with the error set.
658
+ */
659
+ secure(host: string, early: u8[]): boolean {
660
+ const trust: TlsClientTrust | null = this.trust
661
+ if (trust === null) {
662
+ this.fail(HTTP_E_CERTIFICATE, "no trusted root certificates are loaded")
663
+ return false
664
+ }
665
+ const random: u8[] = new Array<u8>(HTTP_CLIENT_TLS_KEY_BYTES)
666
+ const x25519: u8[] = new Array<u8>(HTTP_CLIENT_TLS_KEY_BYTES)
667
+ const p256: u8[] = new Array<u8>(HTTP_CLIENT_TLS_KEY_BYTES)
668
+ crypto.getRandomValues(random)
669
+ crypto.getRandomValues(x25519)
670
+ crypto.getRandomValues(p256)
671
+ // `TlsClient` copies both keys and zeroes these arrays.
672
+ const conn: TlsRecordClient = new TlsRecordClient(
673
+ new TlsClient(new TlsClientConfig(host, ["http/1.1"]), random, x25519, p256)
674
+ )
675
+ this.tls = conn
676
+ if (toI32(this.plain.length) === 0) {
677
+ this.plain = new Array<u8>(HTTP_CLIENT_READ_BYTES)
678
+ }
679
+ if (toI32(early.length) > 0) {
680
+ conn.receive(early, HTTP_CLIENT_ZERO, toI32(early.length))
681
+ }
682
+ // The connection opens on the server's Finished, with the client's own
683
+ // perhaps still queued: it is sent before the request is.
684
+ while (conn.state !== TLS_RECORD_STATE_OPEN || conn.outputEnd > conn.outputStart) {
685
+ if ((conn.interest() & TLS_RECORD_AUTH) !== 0) {
686
+ const verdict: i32 = tlsClientVerdict(conn.tls, trust, wallClockNanos() / HTTP_CLIENT_NS_PER_MS)
687
+ if (verdict !== 0) {
688
+ conn.reject(verdict)
689
+ this.fail(
690
+ HTTP_E_CERTIFICATE,
691
+ `the certificate ${host} presented was refused (TLS alert ${verdict})`
692
+ )
693
+ return false
694
+ }
695
+ conn.authenticate(true)
696
+ }
697
+ if (conn.state === TLS_RECORD_STATE_FAILED) {
698
+ this.failTls(conn)
699
+ return false
700
+ }
701
+ const moved: boolean = conn.outputEnd > conn.outputStart ? this.flushTls(conn) : this.receiveTls(conn)
702
+ if (!moved) {
703
+ return false
704
+ }
705
+ }
706
+ return true
707
+ }
708
+
709
+ /**
710
+ * Asks `proxy`, on the fresh `fd`, for a tunnel to `host:port`. True once
711
+ * the proxy answers 2xx, every byte after its head belonging to the tunnel.
712
+ */
713
+ tunnel(host: string, port: i32, proxy: Url): boolean {
714
+ const authority: string = `${httpClientHostText(host)}:${port}`
715
+ const names: string[] = ["Host"]
716
+ const values: string[] = [authority]
717
+ const auth: string | null = httpClientProxyAuth(proxy)
718
+ if (auth === null) {
719
+ this.fail(HTTP_E_PROXY, "the proxy's user name or password is not valid percent-encoding")
720
+ return false
721
+ }
722
+ if (auth !== "") {
723
+ names.push("Proxy-Authorization")
724
+ values.push(auth)
725
+ }
726
+ const head: u8[] | null = http1RequestHead("CONNECT", authority, names, values, toI64(-2))
727
+ if (head === null) {
728
+ this.fail(HTTP_E_REQUEST, `a CONNECT to ${authority} could not be written`)
729
+ return false
730
+ }
731
+ this.gotByte = false
732
+ this.parser.expect(false, true)
733
+ if (!this.writeAll(head, HTTP_CLIENT_ZERO, toI32(head.length))) {
734
+ return false
735
+ }
736
+ let event: i32 = this.head()
737
+ if (event !== HTTP1_HEAD) {
738
+ return false
739
+ }
740
+ const status: i32 = this.parser.status
741
+ if (status < 200 || status > 299) {
742
+ this.fail(HTTP_E_PROXY_REFUSED, `the proxy answered CONNECT ${authority} with ${status}`)
743
+ return false
744
+ }
745
+ // A 2xx has no body: the parser ends it, then reports the tunnel.
746
+ event = this.pump()
747
+ while (event >= 0 && event !== HTTP1_UPGRADE) {
748
+ event = this.pump()
749
+ }
750
+ return event === HTTP1_UPGRADE
751
+ }
752
+
753
+ /** Writes the request on `fd` and reads its response's head. */
754
+ send(url: Url, method: string, names: string[], values: string[], body: u8[] | null): HttpResponse | null {
755
+ const viaProxy: boolean = this.route.startsWith("proxy ")
756
+ const sendNames: string[] = []
757
+ const sendValues: string[] = []
758
+ let hasHost: boolean = false
759
+ let hasProxyAuth: boolean = false
760
+ for (let i: i32 = 0; i < toI32(names.length) && i < toI32(values.length); i += 1) {
761
+ // Read before any call, which is where the bounds proof forgets a length.
762
+ const name: string = names[i]
763
+ const value: string = values[i]
764
+ hasHost = hasHost || httpClientNameIs(name, "host")
765
+ const proxyAuth: boolean = httpClientNameIs(name, "proxy-authorization")
766
+ if (proxyAuth && !viaProxy) {
767
+ // Only an http proxy reads it: sent direct or through a tunnel, it
768
+ // would hand the proxy's credentials to the origin.
769
+ continue
770
+ }
771
+ hasProxyAuth = hasProxyAuth || proxyAuth
772
+ sendNames.push(name)
773
+ sendValues.push(value)
774
+ }
775
+ const hostText: string = httpClientHostText(url.host)
776
+ const authority: string = url.port >= 0 ? `${hostText}:${url.port}` : hostText
777
+ if (!hasHost) {
778
+ sendNames.push("Host")
779
+ sendValues.push(authority)
780
+ }
781
+ let target: string = url.path === "" ? "/" : url.path
782
+ const query: string | null = url.query
783
+ if (query !== null) {
784
+ target = target + "?" + query
785
+ }
786
+ if (viaProxy) {
787
+ target = `${url.scheme}://${authority}${target}`
788
+ const proxy: Url | null = httpClientProxyUrl(this.httpProxy)
789
+ const auth: string | null = proxy !== null ? httpClientProxyAuth(proxy) : ""
790
+ if (auth === null) {
791
+ this.fail(HTTP_E_PROXY, "the proxy's user name or password is not valid percent-encoding")
792
+ return null
793
+ }
794
+ if (auth !== "" && !hasProxyAuth) {
795
+ sendNames.push("Proxy-Authorization")
796
+ sendValues.push(auth)
797
+ }
798
+ }
799
+ const bodyLength: i64 = body !== null ? toI64(body.length) : toI64(-2)
800
+ const head: u8[] | null = http1RequestHead(method, target, sendNames, sendValues, bodyLength)
801
+ if (head === null) {
802
+ this.fail(
803
+ HTTP_E_REQUEST,
804
+ `the request ${method} ${httpClientRedact(url)} was refused: a bad method, target or header field`
805
+ )
806
+ return null
807
+ }
808
+ this.gotByte = false
809
+ this.lost = false
810
+ this.parser.expect(method === "HEAD", false)
811
+ if (!this.writeAll(head, HTTP_CLIENT_ZERO, toI32(head.length))) {
812
+ return null
813
+ }
814
+ if (body !== null && !this.writeAll(body, HTTP_CLIENT_ZERO, toI32(body.length))) {
815
+ return null
816
+ }
817
+ const event: i32 = this.head()
818
+ if (event !== HTTP1_HEAD) {
819
+ return null
820
+ }
821
+ const res: HttpResponse = new HttpResponse(new HttpBodyReader(this, this.generation))
822
+ res.status = this.parser.status
823
+ res.reason = this.parser.reason
824
+ res.url = httpClientUrlText(url)
825
+ for (
826
+ let i: i32 = 0;
827
+ i < toI32(this.parser.names.length) && i < toI32(this.parser.values.length);
828
+ i += 1
829
+ ) {
830
+ res.names.push(this.parser.names[i])
831
+ res.values.push(this.parser.values[i])
832
+ }
833
+ this.bodyOpen = true
834
+ this.bodyError = HTTP_E_NONE
835
+ this.pendLen = 0
836
+ return res
837
+ }
838
+
839
+ /** Reads up to the final response's head: `HTTP1_HEAD`, `HTTP1_UPGRADE`, or -1 with the error set. */
840
+ head(): i32 {
841
+ let event: i32 = this.pump()
842
+ while (event === HTTP1_INFORMATIONAL) {
843
+ event = this.pump()
844
+ }
845
+ if (event !== HTTP1_HEAD && event !== HTTP1_UPGRADE && event >= 0) {
846
+ this.fail(HTTP_E_PROTOCOL, `the response ended before its head (event ${event})`)
847
+ return -1
848
+ }
849
+ return event
850
+ }
851
+
852
+ /** The parser's next event, reading from the socket as it needs; -1 with the error set. */
853
+ pump(): i32 {
854
+ let event: i32 = this.parser.next()
855
+ while (event === HTTP1_NEED_MORE) {
856
+ if (!this.fill()) {
857
+ return -1
858
+ }
859
+ event = this.parser.next()
860
+ }
861
+ if (event === HTTP1_ERROR) {
862
+ const code: i32 = this.parser.error
863
+ if (code === HTTP1_E_BODY_LIMIT) {
864
+ this.fail(
865
+ HTTP_E_BODY_TOO_LARGE,
866
+ `the body is larger than maxBodyBytes (${this.options.maxBodyBytes})`
867
+ )
868
+ } else {
869
+ // A response the server closed part way through is a connection lost,
870
+ // as one closed before any byte is; it is never sent again, because
871
+ // a byte of it came.
872
+ this.lost = code === HTTP1_E_TRUNCATED
873
+ this.fail(
874
+ HTTP_E_PROTOCOL,
875
+ `the response broke HTTP/1.1: ${this.parser.errorText} (http1 error ${code})`
876
+ )
877
+ }
878
+ return -1
879
+ }
880
+ if (event === HTTP1_CLOSED) {
881
+ this.lost = true
882
+ this.fail(HTTP_E_IO, "the server closed the connection before it answered")
883
+ return -1
884
+ }
885
+ return event
886
+ }
887
+
888
+ /** One read of the response into the parser, waiting up to `readTimeoutMs`; false with the error set. */
889
+ fill(): boolean {
890
+ if (this.overdue()) {
891
+ return false
892
+ }
893
+ const conn: TlsRecordClient | null = this.tls
894
+ if (conn === null) {
895
+ const n: i32 = this.readRaw(HTTP_CLIENT_READ_BYTES)
896
+ if (n > 0) {
897
+ this.gotByte = true
898
+ this.parser.feed(this.rbuf, HTTP_CLIENT_ZERO, n)
899
+ } else if (n === 0) {
900
+ this.parser.finish()
901
+ }
902
+ return n >= 0
903
+ }
904
+ let got: i32 = conn.read(this.plain, HTTP_CLIENT_ZERO, toI32(this.plain.length))
905
+ while (got === TLS_RECORD_AGAIN) {
906
+ if (!this.flushTls(conn) || !this.receiveTls(conn)) {
907
+ return false
908
+ }
909
+ got = conn.read(this.plain, HTTP_CLIENT_ZERO, toI32(this.plain.length))
910
+ }
911
+ if (got > 0) {
912
+ this.gotByte = true
913
+ this.parser.feed(this.plain, HTTP_CLIENT_ZERO, got)
914
+ return true
915
+ }
916
+ if (got === 0) {
917
+ // The server's close_notify: the orderly end of its stream.
918
+ this.parser.finish()
919
+ return true
920
+ }
921
+ this.lost = true
922
+ this.failTls(conn)
923
+ return false
924
+ }
925
+
926
+ /**
927
+ * One read of up to `want` bytes from `fd` into `rbuf`, waiting up to
928
+ * `readTimeoutMs`: the count, 0 at the end of the stream, or -1 with the
929
+ * error set.
930
+ */
931
+ readRaw(want: i32): i32 {
932
+ const until: i64 = this.readDeadline()
933
+ let n: i32 = netRead(this.fd, this.rbuf, 0, want)
934
+ while (n === HTTP_CLIENT_WOULD_BLOCK) {
935
+ const waited: i32 = this.wait(this.fd, HTTP_CLIENT_READABLE, until, HTTP_E_READ_TIMEOUT)
936
+ if (waited !== HTTP_E_NONE) {
937
+ this.failWait(waited, `the server sent nothing for ${this.options.readTimeoutMs} ms`)
938
+ return -1
939
+ }
940
+ n = netRead(this.fd, this.rbuf, 0, want)
941
+ }
942
+ if (n >= 0) {
943
+ return n
944
+ }
945
+ this.lost = true
946
+ this.fail(HTTP_E_IO, `reading from the server failed (errno ${-n})`)
947
+ return -1
948
+ }
949
+
950
+ /** One read of the socket into the TLS connection `conn`, waiting up to `readTimeoutMs`; false with the error set. */
951
+ receiveTls(conn: TlsRecordClient): boolean {
952
+ const room: i32 = conn.space()
953
+ const want: i32 = room < HTTP_CLIENT_READ_BYTES ? room : HTTP_CLIENT_READ_BYTES
954
+ if (want <= 0) {
955
+ this.fail(HTTP_E_TLS, "the TLS connection has no room for the server's next record")
956
+ return false
957
+ }
958
+ const n: i32 = this.readRaw(want)
959
+ if (n < 0) {
960
+ conn.abort()
961
+ return false
962
+ }
963
+ if (n === 0) {
964
+ // Before the server's close_notify this is truncation, which fails `conn`.
965
+ conn.end()
966
+ } else {
967
+ conn.receive(this.rbuf, HTTP_CLIENT_ZERO, n)
968
+ }
969
+ return true
970
+ }
971
+
972
+ /** Sends everything `conn` has sealed, waiting as `writeRaw` does; false with the error set. */
973
+ flushTls(conn: TlsRecordClient): boolean {
974
+ while (conn.outputEnd > conn.outputStart) {
975
+ const len: i32 = conn.outputEnd - conn.outputStart
976
+ if (!this.writeRaw(conn.output, conn.outputStart, len)) {
977
+ conn.abort()
978
+ return false
979
+ }
980
+ conn.consume(len)
981
+ }
982
+ return true
983
+ }
984
+
985
+ /** Sets the error for the failed TLS connection `conn`, saying how it failed. */
986
+ failTls(conn: TlsRecordClient): void {
987
+ if (conn.ended && !conn.peerClosed) {
988
+ this.fail(HTTP_E_TLS, "the server closed the connection without a TLS close_notify")
989
+ } else if (conn.peerAlert > 0) {
990
+ this.fail(HTTP_E_TLS, `the server sent TLS alert ${conn.peerAlert}`)
991
+ } else {
992
+ this.fail(HTTP_E_TLS, `the TLS connection failed (alert ${conn.failure()})`)
993
+ }
994
+ }
995
+
996
+ /** Writes `bytes[off, off + len)` on the connection, through TLS for an https origin; false with the error set. */
997
+ writeAll(bytes: u8[], off: i32, len: i32): boolean {
998
+ const conn: TlsRecordClient | null = this.tls
999
+ if (conn === null) {
1000
+ return this.writeRaw(bytes, off, len)
1001
+ }
1002
+ let at: i32 = off
1003
+ const end: i32 = off + len
1004
+ while (at < end) {
1005
+ const n: i32 = conn.write(bytes, at, end - at)
1006
+ if (n > 0) {
1007
+ at += n
1008
+ } else if (n !== TLS_RECORD_AGAIN || conn.outputEnd === conn.outputStart) {
1009
+ this.lost = true
1010
+ this.failTls(conn)
1011
+ return false
1012
+ }
1013
+ if (!this.flushTls(conn)) {
1014
+ return false
1015
+ }
1016
+ }
1017
+ return this.flushTls(conn)
1018
+ }
1019
+
1020
+ /** Writes `bytes[off, off + len)` to `fd`, waiting up to `readTimeoutMs` each time it is full. */
1021
+ writeRaw(bytes: u8[], off: i32, len: i32): boolean {
1022
+ let at: i32 = off
1023
+ const end: i32 = off + len
1024
+ while (at < end) {
1025
+ if (this.overdue()) {
1026
+ return false
1027
+ }
1028
+ const n: i32 = netWrite(this.fd, bytes, at, end - at)
1029
+ if (n > 0) {
1030
+ at += n
1031
+ } else if (n === HTTP_CLIENT_WOULD_BLOCK || n === 0) {
1032
+ const waited: i32 = this.wait(this.fd, HTTP_CLIENT_WRITABLE, this.readDeadline(), HTTP_E_READ_TIMEOUT)
1033
+ if (waited !== HTTP_E_NONE) {
1034
+ this.failWait(waited, `the server took nothing for ${this.options.readTimeoutMs} ms`)
1035
+ return false
1036
+ }
1037
+ } else {
1038
+ this.lost = true
1039
+ this.fail(HTTP_E_IO, `writing to the server failed (errno ${-n})`)
1040
+ return false
1041
+ }
1042
+ }
1043
+ return true
1044
+ }
1045
+
1046
+ /**
1047
+ * Looks `host` up and connects to its addresses in turn, every step within
1048
+ * `connectTimeoutMs` together: the connected fd, or -1 with the error set.
1049
+ */
1050
+ dial(host: string, port: i32): i32 {
1051
+ const start: i64 = monotonicNanos()
1052
+ const budget: i32 = this.options.connectTimeoutMs
1053
+ let until: i64 = budget > 0 ? start + toI64(budget) * HTTP_CLIENT_NS_PER_MS : HTTP_CLIENT_NEVER
1054
+ let code: i32 = HTTP_E_CONNECT_TIMEOUT
1055
+ if (this.deadline < HTTP_CLIENT_NEVER && this.deadline <= until) {
1056
+ until = this.deadline
1057
+ code = HTTP_E_TOTAL_TIMEOUT
1058
+ }
1059
+ if (until <= start) {
1060
+ this.fail(code, `no time was left to connect to ${host}`)
1061
+ return -1
1062
+ }
1063
+ let lookupMs: i64 = (until - start) / HTTP_CLIENT_NS_PER_MS
1064
+ if (lookupMs < toI64(1)) {
1065
+ lookupMs = toI64(1)
1066
+ } else if (lookupMs > toI64(2147483647)) {
1067
+ lookupMs = toI64(2147483647)
1068
+ }
1069
+ const addresses: u8[] = new Array<u8>(DNS_RESOLVE_MAX_ADDRESSES * DNS_ADDRESS_BYTES)
1070
+ const ttls: i32[] = new Array<i32>(DNS_RESOLVE_MAX_ADDRESSES)
1071
+ const count: i32 = resolveHost(host, toI32(lookupMs), port, addresses, ttls)
1072
+ if (count === DNS_RESOLVE_TIMEOUT && until < HTTP_CLIENT_NEVER) {
1073
+ this.failWait(code, `${host} was not looked up within ${budget} ms`)
1074
+ return -1
1075
+ }
1076
+ if (count <= 0) {
1077
+ this.fail(
1078
+ HTTP_E_DNS,
1079
+ `${host} could not be looked up (${count === 0 ? "no address" : `resolver code ${count}`})`
1080
+ )
1081
+ return -1
1082
+ }
1083
+ const one: u8[] = new Array<u8>(DNS_ADDRESS_BYTES)
1084
+ let last: i32 = 0
1085
+ for (let i: i32 = 0; i < count && i < DNS_RESOLVE_MAX_ADDRESSES; i += 1) {
1086
+ copyBytes(one, 0, addresses, i * DNS_ADDRESS_BYTES, DNS_ADDRESS_BYTES)
1087
+ const fd: i32 = tcpConnect(one)
1088
+ if (fd < 0) {
1089
+ last = fd
1090
+ continue
1091
+ }
1092
+ const waited: i32 = this.wait(fd, HTTP_CLIENT_WRITABLE, until, code)
1093
+ if (waited !== HTTP_E_NONE) {
1094
+ this.unwatch(fd)
1095
+ netClose(fd)
1096
+ this.failWait(waited, `no address of ${host} answered on port ${port} within ${budget} ms`)
1097
+ return -1
1098
+ }
1099
+ const result: i32 = connectResult(fd)
1100
+ if (result === 0) {
1101
+ return fd
1102
+ }
1103
+ this.unwatch(fd)
1104
+ netClose(fd)
1105
+ last = result
1106
+ }
1107
+ this.fail(HTTP_E_REFUSED, `could not connect to ${host} port ${port} (errno ${-last})`)
1108
+ return -1
1109
+ }
1110
+
1111
+ /**
1112
+ * Waits for `events` on `fd` until `until`, or the fetch's deadline if that
1113
+ * comes first: `HTTP_E_NONE` when it is ready, `code` or
1114
+ * `HTTP_E_TOTAL_TIMEOUT` for whichever deadline passed, `HTTP_E_IO` when
1115
+ * the poll loop cannot be made.
1116
+ */
1117
+ wait(fd: i32, events: i32, until: i64, code: i32): i32 {
1118
+ if (!this.watch(fd, events)) {
1119
+ return HTTP_E_IO
1120
+ }
1121
+ let end: i64 = until
1122
+ let which: i32 = code
1123
+ if (this.deadline <= end) {
1124
+ end = this.deadline
1125
+ which = HTTP_E_TOTAL_TIMEOUT
1126
+ }
1127
+ let now: i64 = monotonicNanos()
1128
+ while (now < end) {
1129
+ let ms: i64 = (end - now + HTTP_CLIENT_NS_PER_MS - toI64(1)) / HTTP_CLIENT_NS_PER_MS
1130
+ if (ms > HTTP_CLIENT_MAX_WAIT_MS) {
1131
+ ms = HTTP_CLIENT_MAX_WAIT_MS
1132
+ }
1133
+ const count: i32 = pollWait(this.loop, this.ready, toI32(ms))
1134
+ if (count > 0) {
1135
+ return HTTP_E_NONE
1136
+ }
1137
+ if (count < 0) {
1138
+ return HTTP_E_IO
1139
+ }
1140
+ now = monotonicNanos()
1141
+ }
1142
+ return which
1143
+ }
1144
+
1145
+ /** Puts `fd` in the poll loop, waiting for `events`: false when the loop cannot be made. */
1146
+ watch(fd: i32, events: i32): boolean {
1147
+ if (this.loop < 0) {
1148
+ this.loop = pollCreate()
1149
+ if (this.loop < 0) {
1150
+ return false
1151
+ }
1152
+ }
1153
+ if (this.watched !== fd) {
1154
+ if (this.watched >= 0) {
1155
+ pollRemove(this.loop, this.watched)
1156
+ }
1157
+ if (pollAdd(this.loop, fd, events, 0) < 0) {
1158
+ this.watched = -1
1159
+ return false
1160
+ }
1161
+ this.watched = fd
1162
+ this.watchedEvents = events
1163
+ } else if (this.watchedEvents !== events) {
1164
+ pollModify(this.loop, fd, events, 0)
1165
+ this.watchedEvents = events
1166
+ }
1167
+ return true
1168
+ }
1169
+
1170
+ /**
1171
+ * Whether the kept connection has said nothing since its last response
1172
+ * ended: no byte and no close waiting on the socket, none held unread. A
1173
+ * server that spoke while idle answered nothing yet asked, and is closing
1174
+ * or out of step, so the connection is not reused.
1175
+ */
1176
+ quiet(): boolean {
1177
+ return (
1178
+ !this.leftover() &&
1179
+ this.watch(this.fd, HTTP_CLIENT_READABLE) &&
1180
+ pollWait(this.loop, this.ready, 0) === 0
1181
+ )
1182
+ }
1183
+
1184
+ /** Whether bytes past the last response are held, in the parser or the TLS layer. */
1185
+ leftover(): boolean {
1186
+ const conn: TlsRecordClient | null = this.tls
1187
+ if (conn !== null && (conn.plainEnd > conn.plainStart || conn.reader.end > conn.reader.start)) {
1188
+ return true
1189
+ }
1190
+ return this.parser.buffered() > 0
1191
+ }
1192
+
1193
+ /** Takes `fd` out of the poll loop before it is closed. */
1194
+ unwatch(fd: i32): void {
1195
+ if (this.watched === fd && this.watched >= 0) {
1196
+ pollRemove(this.loop, fd)
1197
+ this.watched = -1
1198
+ }
1199
+ }
1200
+
1201
+ /** When the wait for the server's next byte ends: `readTimeoutMs` from now. */
1202
+ readDeadline(): i64 {
1203
+ const ms: i32 = this.options.readTimeoutMs
1204
+ return ms > 0 ? monotonicNanos() + toI64(ms) * HTTP_CLIENT_NS_PER_MS : HTTP_CLIENT_NEVER
1205
+ }
1206
+
1207
+ /** Closes the connection, if any, and forgets the open body. */
1208
+ drop(): void {
1209
+ const conn: TlsRecordClient | null = this.tls
1210
+ if (conn !== null) {
1211
+ conn.close()
1212
+ // One try, without waiting, so the server sees the close_notify, or
1213
+ // the alert a refusal sealed, when the socket has room for it.
1214
+ if (this.fd >= 0 && conn.outputEnd > conn.outputStart) {
1215
+ netWrite(this.fd, conn.output, conn.outputStart, conn.outputEnd - conn.outputStart)
1216
+ }
1217
+ conn.wipe()
1218
+ this.tls = null
1219
+ }
1220
+ if (this.fd >= 0) {
1221
+ this.unwatch(this.fd)
1222
+ netClose(this.fd)
1223
+ this.fd = -1
1224
+ }
1225
+ this.route = ""
1226
+ if (this.bodyOpen) {
1227
+ // Closed before its end, the body was cut short: never a whole one.
1228
+ this.bodyError = HTTP_E_IO
1229
+ }
1230
+ this.bodyOpen = false
1231
+ this.pendLen = 0
1232
+ }
1233
+
1234
+ /**
1235
+ * Reads past a redirect's body so the connection can carry the next
1236
+ * request; a body too long to be worth reading, or any failure, closes it.
1237
+ */
1238
+ drain(): void {
1239
+ const sink: u8[] = new Array<u8>(HTTP_CLIENT_DRAIN_CHUNK)
1240
+ let left: i32 = HTTP_CLIENT_DRAIN_BYTES
1241
+ while (this.bodyOpen && left > 0) {
1242
+ const n: i32 = this.readBody(this.generation, sink, HTTP_CLIENT_ZERO, HTTP_CLIENT_DRAIN_CHUNK)
1243
+ if (n <= 0) {
1244
+ break
1245
+ }
1246
+ left -= n
1247
+ }
1248
+ if (this.bodyOpen) {
1249
+ this.drop()
1250
+ }
1251
+ this.error = HTTP_E_NONE
1252
+ this.errorText = ""
1253
+ }
1254
+
1255
+ /** `HttpBodyReader.read` for the response numbered `generation`. */
1256
+ readBody(generation: i32, buf: u8[], off: i32, n: i32): i32 {
1257
+ if (generation !== this.generation) {
1258
+ // The client's error is the later fetch's; a stale reader leaves it be.
1259
+ return -HTTP_E_IO
1260
+ }
1261
+ if (!this.bodyOpen) {
1262
+ return -this.bodyError
1263
+ }
1264
+ while (this.pendLen === 0) {
1265
+ const event: i32 = this.pump()
1266
+ if (event < 0) {
1267
+ this.release()
1268
+ this.bodyError = this.error
1269
+ return -this.error
1270
+ }
1271
+ if (event === HTTP1_END) {
1272
+ this.bodyOpen = false
1273
+ // Bytes past the end are no answer to anything yet asked: a server
1274
+ // that sends them cannot be trusted with the next request.
1275
+ if (this.once || !this.parser.keepAlive() || this.leftover()) {
1276
+ this.release()
1277
+ }
1278
+ return 0
1279
+ }
1280
+ if (event === HTTP1_BODY) {
1281
+ this.pendOff = this.parser.bodyOff
1282
+ this.pendLen = this.parser.bodyLen
1283
+ }
1284
+ }
1285
+ const take: i32 = n < this.pendLen ? n : this.pendLen
1286
+ copyBytes(buf, off, this.parser.body, this.pendOff, take)
1287
+ this.pendOff += take
1288
+ this.pendLen -= take
1289
+ return take
1290
+ }
1291
+
1292
+ /** Closes the connection a body is done with, and `httpFetch`'s whole client with it. */
1293
+ release(): void {
1294
+ if (this.once) {
1295
+ this.close()
1296
+ } else {
1297
+ this.drop()
1298
+ }
1299
+ }
1300
+
1301
+ /** `readAll`'s refusal: the body passed the caller's limit. */
1302
+ refuseBody(limit: i32): void {
1303
+ this.release()
1304
+ this.fail(HTTP_E_BODY_TOO_LARGE, `the body is larger than the ${limit} bytes asked for`)
1305
+ this.bodyError = HTTP_E_BODY_TOO_LARGE
1306
+ }
1307
+
1308
+ /**
1309
+ * Fails with `HTTP_E_TOTAL_TIMEOUT` once the fetch's deadline has passed:
1310
+ * a server that always has the next byte ready never lets a wait see it.
1311
+ */
1312
+ overdue(): boolean {
1313
+ if (this.deadline < HTTP_CLIENT_NEVER && monotonicNanos() >= this.deadline) {
1314
+ this.failWait(HTTP_E_TOTAL_TIMEOUT, "")
1315
+ return true
1316
+ }
1317
+ return false
1318
+ }
1319
+
1320
+ /** Sets the error for a wait that ended with `code`, `text` saying what timed out. */
1321
+ failWait(code: i32, text: string): void {
1322
+ if (code === HTTP_E_TOTAL_TIMEOUT) {
1323
+ this.fail(code, `the fetch ran past ${this.options.totalTimeoutMs} ms`)
1324
+ } else if (code === HTTP_E_IO) {
1325
+ this.fail(code, "the poll loop failed")
1326
+ } else {
1327
+ this.fail(code, text)
1328
+ }
1329
+ }
1330
+
1331
+ /** Refuses a redirect: closes the connection, whose response is left unread, and fails. */
1332
+ refuse(code: i32, text: string): HttpResponse | null {
1333
+ this.drop()
1334
+ this.fail(code, text)
1335
+ return null
1336
+ }
1337
+
1338
+ fail(code: i32, text: string): void {
1339
+ this.error = code
1340
+ this.errorText = `${httpErrorName(code)}: ${text}`
1341
+ }
1342
+ }
1343
+
1344
+ /**
1345
+ * One fetch on a client of its own, closed once the body has been read or
1346
+ * the fetch failed. Null on a failure, with no way to say why: a program
1347
+ * that reports its errors fetches through an `HttpClient`.
1348
+ */
1349
+ export const httpFetch = (req: HttpRequest, options: HttpOptions): HttpResponse | null => {
1350
+ const client: HttpClient = new HttpClient(options)
1351
+ client.once = true
1352
+ const res: HttpResponse | null = client.fetch(req)
1353
+ if (res === null) {
1354
+ client.close()
1355
+ }
1356
+ return res
1357
+ }
1358
+
1359
+ /**
1360
+ * A proxy variable's value: `lower` when it is set and not empty, else
1361
+ * `upper` when `upperAllowed`, else "". `HTTP_PROXY` is not allowed under CGI.
1362
+ */
1363
+ export const httpChooseProxy = (
1364
+ lower: string | null,
1365
+ upper: string | null,
1366
+ upperAllowed: boolean
1367
+ ): string => {
1368
+ if (lower !== null && lower !== "") {
1369
+ return lower
1370
+ }
1371
+ if (upperAllowed && upper !== null) {
1372
+ return upper
1373
+ }
1374
+ return ""
1375
+ }
1376
+
1377
+ /**
1378
+ * The proxy a request to `scheme://host:port` goes through, given the three
1379
+ * settings: `httpsProxy` for https, `httpProxy` for http, and "" for none or
1380
+ * when `noProxy` names the host.
1381
+ */
1382
+ export const httpProxyFor = (
1383
+ scheme: string,
1384
+ host: string,
1385
+ port: i32,
1386
+ httpProxy: string,
1387
+ httpsProxy: string,
1388
+ noProxy: string
1389
+ ): string => {
1390
+ let proxy: string = ""
1391
+ if (scheme === "https") {
1392
+ proxy = httpsProxy
1393
+ } else if (scheme === "http") {
1394
+ proxy = httpProxy
1395
+ }
1396
+ if (proxy === "" || httpNoProxyMatch(noProxy, host, port)) {
1397
+ return ""
1398
+ }
1399
+ return proxy
1400
+ }
1401
+
1402
+ /**
1403
+ * Whether `noProxy`, a comma-separated list, names `host` on `port`: an entry
1404
+ * is `*` (every host), a name that matches the host or any domain under it
1405
+ * (with or without a leading dot), or an IP address that matches only itself;
1406
+ * any of them may end in `:port`, and an IPv6 address with a port is written
1407
+ * in brackets. Case does not matter. Address ranges are not understood.
1408
+ */
1409
+ export const httpNoProxyMatch = (noProxy: string, host: string, port: i32): boolean => {
1410
+ const name: string = httpClientTrimDots(httpClientLower(host))
1411
+ const literal: boolean = tlsClientIsIpLiteral(name)
1412
+ let start: i32 = 0
1413
+ const n: i32 = toI32(noProxy.length)
1414
+ while (start <= n) {
1415
+ let end: i32 = toI32(noProxy.indexOf(",", start))
1416
+ if (end < 0) {
1417
+ end = n
1418
+ }
1419
+ const entry: string = httpClientLower(httpClientTrim(noProxy.slice(start, end)))
1420
+ start = end + 1
1421
+ if (entry === "*") {
1422
+ return true
1423
+ }
1424
+ let entryHost: string = entry
1425
+ let entryPort: i32 = -1
1426
+ const colon: i32 = toI32(entry.indexOf(":"))
1427
+ if (entry.startsWith("[")) {
1428
+ const close: i32 = toI32(entry.indexOf("]"))
1429
+ if (close < 0) {
1430
+ continue
1431
+ }
1432
+ entryHost = entry.slice(1, close)
1433
+ if (close + 1 < toI32(entry.length)) {
1434
+ if (entry.charCodeAt(close + 1) !== 58) {
1435
+ continue
1436
+ }
1437
+ entryPort = httpClientPort(entry.substring(close + 2, toI32(entry.length)))
1438
+ if (entryPort < 0) {
1439
+ continue
1440
+ }
1441
+ }
1442
+ } else if (colon >= 0 && entry.indexOf(":", colon + 1) < 0) {
1443
+ entryHost = entry.slice(0, colon)
1444
+ entryPort = httpClientPort(entry.substring(colon + 1, toI32(entry.length)))
1445
+ if (entryPort < 0) {
1446
+ continue
1447
+ }
1448
+ }
1449
+ entryHost = httpClientTrimDots(entryHost)
1450
+ if (entryHost === "" || (entryPort >= 0 && entryPort !== port)) {
1451
+ continue
1452
+ }
1453
+ if (entryHost === name) {
1454
+ return true
1455
+ }
1456
+ if (!literal && !tlsClientIsIpLiteral(entryHost) && name.endsWith("." + entryHost)) {
1457
+ return true
1458
+ }
1459
+ }
1460
+ return false
1461
+ }
1462
+
1463
+ // --- helpers -----------------------------------------------------------------
1464
+
1465
+ /** A parser with the options' limits. */
1466
+ const httpClientParser = (options: HttpOptions): Http1ResponseParser =>
1467
+ new Http1ResponseParser(
1468
+ options.maxHeaderBytes,
1469
+ options.maxHeaderBytes,
1470
+ HTTP_CLIENT_MAX_HEADERS,
1471
+ options.maxBodyBytes,
1472
+ options.maxHeaderBytes
1473
+ )
1474
+
1475
+ /** The statuses a redirect is followed for: 301, 302, 303, 307 and 308. */
1476
+ const httpClientIsRedirect = (status: i32): boolean =>
1477
+ status === 301 || status === 302 || status === 303 || status === 307 || status === 308
1478
+
1479
+ /** RFC 9110 §9.2.2's idempotent methods, the ones a retry cannot do twice. */
1480
+ const httpClientIdempotent = (method: string): boolean =>
1481
+ method === "GET" ||
1482
+ method === "HEAD" ||
1483
+ method === "OPTIONS" ||
1484
+ method === "TRACE" ||
1485
+ method === "PUT" ||
1486
+ method === "DELETE"
1487
+
1488
+ /** Removes every field named `lower`, in any case. */
1489
+ const httpClientDropField = (names: string[], values: string[], lower: string): void => {
1490
+ let i: i32 = 0
1491
+ while (i < toI32(names.length) && i < toI32(values.length)) {
1492
+ if (httpClientNameIs(names[i], lower)) {
1493
+ for (let k: i32 = i; k + 1 < toI32(names.length) && k + 1 < toI32(values.length); k += 1) {
1494
+ names[k] = names[k + 1]
1495
+ values[k] = values[k + 1]
1496
+ }
1497
+ names.pop()
1498
+ values.pop()
1499
+ } else {
1500
+ i += 1
1501
+ }
1502
+ }
1503
+ }
1504
+
1505
+ /** Whether the field name `name` is `lower`, a lower-case name, in any case; it allocates nothing. */
1506
+ const httpClientNameIs = (name: string, lower: string): boolean => {
1507
+ const n: i32 = toI32(name.length)
1508
+ if (n !== toI32(lower.length)) {
1509
+ return false
1510
+ }
1511
+ for (let i: i32 = 0; i < n && i < toI32(lower.length); i += 1) {
1512
+ let c: i32 = toI32(name.charCodeAt(i))
1513
+ if (c >= 65 && c <= 90) {
1514
+ c += 32
1515
+ }
1516
+ if (c !== toI32(lower.charCodeAt(i))) {
1517
+ return false
1518
+ }
1519
+ }
1520
+ return true
1521
+ }
1522
+
1523
+ /** `s` with A to Z lower-cased. */
1524
+ const httpClientLower = (s: string): string => {
1525
+ const n: i32 = toI32(s.length)
1526
+ let upper: boolean = false
1527
+ for (let i: i32 = 0; i < n; i += 1) {
1528
+ const c: i32 = toI32(s.charCodeAt(i))
1529
+ upper = upper || (c >= 65 && c <= 90)
1530
+ }
1531
+ if (!upper) {
1532
+ return s
1533
+ }
1534
+ const out: u8[] = new Array<u8>(n)
1535
+ for (let i: i32 = 0; i < toI32(out.length) && i < toI32(s.length); i += 1) {
1536
+ const c: i32 = toI32(s.charCodeAt(i))
1537
+ out[i] = c >= 65 && c <= 90 ? toU8(c + 32) : toU8(c)
1538
+ }
1539
+ return String.fromBytes(out, 0, n)
1540
+ }
1541
+
1542
+ /** `s` without spaces and tabs at either end. */
1543
+ const httpClientTrim = (s: string): string => {
1544
+ let a: i32 = 0
1545
+ let b: i32 = toI32(s.length)
1546
+ while (a < b && (s.charCodeAt(a) === 32 || s.charCodeAt(a) === 9)) {
1547
+ a += 1
1548
+ }
1549
+ while (b > a && (s.charCodeAt(b - 1) === 32 || s.charCodeAt(b - 1) === 9)) {
1550
+ b -= 1
1551
+ }
1552
+ return s.substring(a, b)
1553
+ }
1554
+
1555
+ /** `s` without one leading dot and one trailing dot, so `.a.com.` compares as `a.com`. */
1556
+ const httpClientTrimDots = (s: string): string => {
1557
+ let a: i32 = 0
1558
+ let b: i32 = toI32(s.length)
1559
+ if (a < b && s.charCodeAt(a) === 46) {
1560
+ a += 1
1561
+ }
1562
+ if (b > a && s.charCodeAt(b - 1) === 46) {
1563
+ b -= 1
1564
+ }
1565
+ return s.substring(a, b)
1566
+ }
1567
+
1568
+ /** `s` as a port, 0 to 65535, or -1 when it is not one. */
1569
+ const httpClientPort = (s: string): i32 => {
1570
+ const n: i32 = toI32(s.length)
1571
+ if (n === 0 || n > 5) {
1572
+ return -1
1573
+ }
1574
+ let v: i32 = 0
1575
+ for (let i: i32 = 0; i < n; i += 1) {
1576
+ const c: i32 = toI32(s.charCodeAt(i))
1577
+ if (c < 48 || c > 57) {
1578
+ return -1
1579
+ }
1580
+ v = v * 10 + c - 48
1581
+ }
1582
+ return v > 65535 ? -1 : v
1583
+ }
1584
+
1585
+ /** A host as an authority writes it: an IPv6 address in brackets. */
1586
+ const httpClientHostText = (host: string): string => (host.indexOf(":") >= 0 ? "[" + host + "]" : host)
1587
+
1588
+ /** `url` without its userinfo and fragment, as a response names it. */
1589
+ const httpClientUrlText = (url: Url): string => {
1590
+ const host: string = httpClientHostText(url.host)
1591
+ const authority: string = url.port >= 0 ? `${host}:${url.port}` : host
1592
+ const query: string | null = url.query
1593
+ return `${url.scheme}://${authority}${url.path === "" ? "/" : url.path}${query !== null ? "?" + query : ""}`
1594
+ }
1595
+
1596
+ /**
1597
+ * A URL as an error may name it, built from its parsed scheme, host, port and
1598
+ * path alone, so no userinfo, query or fragment reaches `errorText` whatever
1599
+ * the text holds; `?...` marks a query or fragment left out. A URL that does
1600
+ * not parse, or has no host, is not echoed at all: a scan of the raw text
1601
+ * cannot tell a password's `@`, `?` or `/` from a query's.
1602
+ */
1603
+ const httpClientRedact = (u: Url | null): string => {
1604
+ if (u === null) {
1605
+ return "(a URL that does not parse)"
1606
+ }
1607
+ if (!u.hasAuthority || u.host === "") {
1608
+ return "(a URL without a host)"
1609
+ }
1610
+ const port: string = u.port >= 0 ? `:${u.port}` : ""
1611
+ const cut: string = u.query !== null || u.fragment !== null ? "?..." : ""
1612
+ return `${u.scheme}://${httpClientHostText(u.host)}${port}${u.path}${cut}`
1613
+ }
1614
+
1615
+ /**
1616
+ * A proxy setting as a URL: `http://` is assumed when no scheme is written,
1617
+ * the port is 1080 when none is, and anything but `http` with a host is null.
1618
+ */
1619
+ const httpClientProxyUrl = (text: string): Url | null => {
1620
+ const u: Url | null = urlParse(text.indexOf("://") >= 0 ? text : "http://" + text)
1621
+ if (u === null || u.scheme !== "http" || u.host === "") {
1622
+ return null
1623
+ }
1624
+ if (u.port < 0) {
1625
+ u.port = HTTP_CLIENT_PROXY_PORT
1626
+ }
1627
+ return u
1628
+ }
1629
+
1630
+ /**
1631
+ * The `Proxy-Authorization` value for a proxy URL's userinfo, `Basic` and
1632
+ * the decoded `user:password` in base64; "" when it has none; null when
1633
+ * either half is not valid percent-encoding.
1634
+ */
1635
+ const httpClientProxyAuth = (proxy: Url): string | null => {
1636
+ if (proxy.username === "" && proxy.password === "") {
1637
+ return ""
1638
+ }
1639
+ const user: string | null = urlDecode(proxy.username, 0)
1640
+ const password: string | null = urlDecode(proxy.password, 0)
1641
+ if (user === null || password === null) {
1642
+ return null
1643
+ }
1644
+ return "Basic " + base64Encode(httpFieldBytes(user + ":" + password))
1645
+ }