@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.
- package/INSTALL.md +13 -8
- package/bin/nish +0 -0
- package/docs/AI.md +27 -5
- package/package.json +1 -1
- package/runtime/core.d.ts +47 -0
- package/runtime/core.mjs +67 -0
- package/runtime/nish.d.ts +54 -5
- package/runtime/nish.h +67 -8
- package/runtime/nish.mjs +6 -0
- package/runtime/runtime-checksum.c +270 -0
- package/runtime/runtime-os.c +9 -4
- package/runtime/runtime-parallel.c +21 -0
- package/runtime/runtime-sandbox.c +470 -0
- package/runtime/runtime-sha256.c +239 -0
- package/runtime/runtime-simd.c +63 -2
- package/runtime/runtime-wasm.c +18 -0
- package/scripts/build.sh +60 -24
- package/std/README.md +16 -10
- package/std/compress/checksum.ts +88 -75
- package/std/compress/deflate.ts +224 -0
- package/std/compress/gzip.ts +52 -0
- package/std/crypto/bcrypt.ts +816 -280
- package/std/crypto/sha256.ts +335 -188
- package/std/json-equal.ts +335 -0
- package/std/json-format.ts +1070 -0
- package/std/json-number.ts +428 -0
- package/std/json-pointer.ts +245 -0
- package/std/json-value.ts +510 -17
- package/std/net/http-client.ts +1645 -0
- package/std/net/http-compress.ts +259 -16
- package/std/net/http1-client.ts +104 -19
- package/std/net/http1-server.ts +61 -12
- package/std/net/postgres.ts +576 -8
- package/std/net/websocket.ts +295 -14
- package/std/regex.ts +613 -52
- package/std/term-io.ts +35 -8
- package/std/yaml-json.ts +841 -0
|
@@ -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
|
+
}
|