@amritk/nish-x86_64-linux 0.29.0 → 0.30.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 +3 -3
- package/bin/nish +0 -0
- package/docs/AI.md +15 -0
- package/package.json +1 -1
- package/runtime/nish.h +22 -2
- package/runtime/runtime-region.c +231 -0
- package/runtime/runtime.c +12 -2
- package/scripts/build.sh +19 -6
- package/std/README.md +2 -0
- package/std/net/http1-client.ts +270 -41
- package/std/net/http1-server.ts +68 -9
- package/std/net/http1.ts +250 -47
- package/std/net/postgres-carrier.ts +985 -0
- package/std/net/postgres.ts +297 -4
- package/std/net/upstream.ts +759 -0
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nish/net/upstream` — outbound HTTP/1.1 calls from a route handler that
|
|
3
|
+
* never block its worker: a pool of keep-alive connections to one origin,
|
|
4
|
+
* registered on the `HttpHandler`'s own poll loop and read through the
|
|
5
|
+
* `nish/net/http1-client` codec.
|
|
6
|
+
*
|
|
7
|
+
* const h = new HttpHandler(new HandlerLimits(), listener, router)
|
|
8
|
+
* const users = new Upstream(h, "http://127.0.0.1:9000", new UpstreamLimits())
|
|
9
|
+
* while (true) {
|
|
10
|
+
* h.wait(100) // one turn: the handler's sockets and the upstream's
|
|
11
|
+
* while (h.ready()) {
|
|
12
|
+
* const req = h.take()
|
|
13
|
+
* switch (req.route) {
|
|
14
|
+
* case USER: if (users.get(req, "/v1/users/7") !== UPSTREAM_E_NONE) { h.problem(req, 503, "Busy", "") } break
|
|
15
|
+
* default: h.refuse(req)
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
* while (users.ready()) { // the answers that arrived this turn
|
|
19
|
+
* const r = users.take() // r.req is the request that asked
|
|
20
|
+
* if (r.ok) { h.send(r.req, r.status, JSON, r.body, 0, r.length) } else { h.problem(r.req, 502, "Bad Gateway", "") }
|
|
21
|
+
* users.release(r)
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* **`get` returns at once.** It writes the request head into a call slot
|
|
26
|
+
* taken from a pool, then onto a free connection, or queues it until one is
|
|
27
|
+
* free, and the program goes on serving. The asking request stays held, as
|
|
28
|
+
* `nish/net/handler` allows. Each connection is watched on the handler's loop
|
|
29
|
+
* under a watch id (`UpstreamLimits.firstId` and up), so the program's next
|
|
30
|
+
* `h.wait` wakes for the upstream as well as for its clients; `ready` then
|
|
31
|
+
* reads what arrived, once per turn, and hands back every finished call.
|
|
32
|
+
*
|
|
33
|
+
* **Every call ends exactly once, as a call.** A response the codec reads to
|
|
34
|
+
* its end is `ok` with its `status` and body, whatever the status. Anything
|
|
35
|
+
* else is `ok === false` with an `error`: one of the codec's `HTTP1_E_`
|
|
36
|
+
* refusals (a malformed status line, a bare LF, conflicting framing, a body
|
|
37
|
+
* past `maxBody`, a response cut short), or one of the `UPSTREAM_E_` codes
|
|
38
|
+
* below (the origin was unusable, the connection failed or closed, the
|
|
39
|
+
* deadline passed, the path could not be written). The program answers its
|
|
40
|
+
* own client either way and gives the call back with `release`.
|
|
41
|
+
*
|
|
42
|
+
* **A kept-alive connection is checked before it is reused.** One the origin
|
|
43
|
+
* closed or sent bytes on while it was idle is closed instead, so no call is
|
|
44
|
+
* written to a dead socket or handed bytes it did not ask for. A call the
|
|
45
|
+
* origin drops unanswered on a reused connection anyway (it closed as the
|
|
46
|
+
* request crossed) is made once more, on a new connection; a `GET` is safe
|
|
47
|
+
* to repeat. Two `Upstream`s on one handler must be given watch ids that do
|
|
48
|
+
* not overlap: nothing checks it, and two with the default limits would both
|
|
49
|
+
* take ids 8 to 11, each taking the other's wake-ups for its own.
|
|
50
|
+
*
|
|
51
|
+
* **The origin is fixed at start-up.** The constructor parses it, looks its
|
|
52
|
+
* host up (blocking, once, as a program's start-up may), and keeps one
|
|
53
|
+
* address; nothing is looked up again, so no later answer can move the pool
|
|
54
|
+
* somewhere else. When the origin comes from input — configuration, an
|
|
55
|
+
* environment variable, a request — set `UpstreamLimits.outbound`: the
|
|
56
|
+
* `nish/net/outbound` policy then checks the host name before the lookup and
|
|
57
|
+
* the address after it, and a refused origin fails every call with
|
|
58
|
+
* `UPSTREAM_E_BLOCKED`. Only `http:` is spoken; TLS is not, so `https:` is
|
|
59
|
+
* `UPSTREAM_E_ORIGIN`.
|
|
60
|
+
*
|
|
61
|
+
* **Every cap is set at start-up.** The connection count, the call slots
|
|
62
|
+
* (calls queued, in flight and finished but not yet released), the request
|
|
63
|
+
* head's room, the response limits the codec enforces, and a deadline per
|
|
64
|
+
* call. The slots, their head buffers and the connections' read buffers are
|
|
65
|
+
* made by the constructor; a call's body buffer grows to the largest body it
|
|
66
|
+
* has held and is kept. Each connection keeps one codec for its life and
|
|
67
|
+
* the next connection's, reset between them, in its spans mode: a response's
|
|
68
|
+
* fields are read as spans of a buffer the codec reuses, so a warm pool
|
|
69
|
+
* allocates nothing per call.
|
|
70
|
+
*
|
|
71
|
+
* `docs/security/upstream.md` is the record. Private helpers share the
|
|
72
|
+
* importing program's flat symbol namespace (`docs/wp26-stdlib.md` §3e),
|
|
73
|
+
* which is why each one carries the module's name.
|
|
74
|
+
*
|
|
75
|
+
* Written from RFC 9110 and RFC 9112, not ported from another implementation.
|
|
76
|
+
*/
|
|
77
|
+
import { copyBytes } from "nish/bytes"
|
|
78
|
+
import { DNS_ADDRESS_BYTES } from "nish/net/dns-message"
|
|
79
|
+
import { DNS_RESOLVE_MAX_ADDRESSES } from "nish/net/dns-resolver"
|
|
80
|
+
import { resolveHost } from "nish/net/dns"
|
|
81
|
+
import { HANDLER_MAX_WATCHED, HttpHandler, HttpRequest } from "nish/net/handler"
|
|
82
|
+
import {
|
|
83
|
+
HTTP1_BODY,
|
|
84
|
+
HTTP1_CLOSED,
|
|
85
|
+
HTTP1_END,
|
|
86
|
+
HTTP1_ERROR,
|
|
87
|
+
HTTP1_HEAD,
|
|
88
|
+
HTTP1_NEED_MORE,
|
|
89
|
+
HTTP1_NO_BODY,
|
|
90
|
+
} from "nish/net/http1"
|
|
91
|
+
import { HTTP1_INFORMATIONAL, Http1ResponseParser, http1WriteRequestHead } from "nish/net/http1-client"
|
|
92
|
+
import { OutboundPolicy, outboundAddressText, outboundFilter, outboundHostAllowed } from "nish/net/outbound"
|
|
93
|
+
import { Url, urlParse } from "nish/net/url"
|
|
94
|
+
import {
|
|
95
|
+
connectResult,
|
|
96
|
+
netClose,
|
|
97
|
+
netRead,
|
|
98
|
+
netWrite,
|
|
99
|
+
pollAdd,
|
|
100
|
+
pollModify,
|
|
101
|
+
pollRemove,
|
|
102
|
+
tcpConnect,
|
|
103
|
+
} from "nish:net"
|
|
104
|
+
import { monotonicNanos } from "nish:process"
|
|
105
|
+
|
|
106
|
+
/** The call was read to its end: `ok`. */
|
|
107
|
+
export const UPSTREAM_E_NONE: i32 = 0
|
|
108
|
+
// 1 to 17 are the codec's own `HTTP1_E_` refusals, passed through as they are.
|
|
109
|
+
/** The origin is not an `http:` URL with a host and nothing past its authority but `/`. */
|
|
110
|
+
export const UPSTREAM_E_ORIGIN: i32 = 32
|
|
111
|
+
/** The origin's host could not be looked up at start-up. */
|
|
112
|
+
export const UPSTREAM_E_LOOKUP: i32 = 33
|
|
113
|
+
/** The outbound policy refused the origin's host name or every address it has. */
|
|
114
|
+
export const UPSTREAM_E_BLOCKED: i32 = 34
|
|
115
|
+
/** The connection to the origin could not be made. */
|
|
116
|
+
export const UPSTREAM_E_CONNECT: i32 = 35
|
|
117
|
+
/** The call's deadline passed before its response ended. */
|
|
118
|
+
export const UPSTREAM_E_TIMEOUT: i32 = 36
|
|
119
|
+
/** The connection failed or closed before a response began. */
|
|
120
|
+
export const UPSTREAM_E_CLOSED: i32 = 37
|
|
121
|
+
/**
|
|
122
|
+
* The path does not start with `/` (an absolute-form or asterisk target is
|
|
123
|
+
* not this pool's to send), the codec refused to write it, or the head did
|
|
124
|
+
* not fit `maxHead`.
|
|
125
|
+
*/
|
|
126
|
+
export const UPSTREAM_E_REQUEST: i32 = 38
|
|
127
|
+
/** Every call slot is taken: `get` answers this and holds nothing. */
|
|
128
|
+
export const UPSTREAM_E_FULL: i32 = 39
|
|
129
|
+
/** The connection failed (it was reset, say) while the response was being read: what came is not the whole of it. */
|
|
130
|
+
export const UPSTREAM_E_RESET: i32 = 40
|
|
131
|
+
|
|
132
|
+
const UPSTREAM_READ: i32 = 1
|
|
133
|
+
const UPSTREAM_WRITE: i32 = 2
|
|
134
|
+
const UPSTREAM_WOULD_BLOCK: i32 = -11
|
|
135
|
+
const UPSTREAM_NS_PER_MS: i64 = 1000000
|
|
136
|
+
const UPSTREAM_READ_CHUNK: i32 = 16384
|
|
137
|
+
/** The largest body a call takes, whatever `maxBody` says: doubling the buffer up to it stays in i32's range. */
|
|
138
|
+
const UPSTREAM_MAX_BODY: i64 = 1073741824
|
|
139
|
+
|
|
140
|
+
/** A connection with no socket. */
|
|
141
|
+
const UPSTREAM_C_CLOSED: i32 = 0
|
|
142
|
+
/** Connecting, with a call waiting on it. */
|
|
143
|
+
const UPSTREAM_C_CONNECTING: i32 = 1
|
|
144
|
+
/** Carrying a call: its head being written, or its response being read. */
|
|
145
|
+
const UPSTREAM_C_BUSY: i32 = 2
|
|
146
|
+
/** Open and kept alive, with no call on it. */
|
|
147
|
+
const UPSTREAM_C_IDLE: i32 = 3
|
|
148
|
+
|
|
149
|
+
/** The caps of one `Upstream`, all read once by its constructor. */
|
|
150
|
+
export class UpstreamLimits {
|
|
151
|
+
/** Checks the origin's host and address when set; set it whenever the origin comes from input. */
|
|
152
|
+
outbound: OutboundPolicy | null = null
|
|
153
|
+
/** The largest response body the codec accepts, at most 1 GiB; past it the call fails `HTTP1_E_BODY_LIMIT`. */
|
|
154
|
+
maxBody: i64 = 1048576
|
|
155
|
+
/** Connections to the origin, at most; each takes one handler watch id. */
|
|
156
|
+
connections: i32 = 4
|
|
157
|
+
/** Call slots: calls queued, in flight, and finished but not yet released, at most. */
|
|
158
|
+
calls: i32 = 256
|
|
159
|
+
/** The first handler watch id the connections take: they use `firstId` to `firstId + connections - 1`. */
|
|
160
|
+
firstId: i32 = 8
|
|
161
|
+
/** A call fails `UPSTREAM_E_TIMEOUT` this long after `get` if its response has not ended. */
|
|
162
|
+
timeoutMs: i32 = 10000
|
|
163
|
+
/** How long the constructor's one lookup of the origin's host may take. */
|
|
164
|
+
lookupMs: i32 = 5000
|
|
165
|
+
/** Room for one request head: the request line, `Host` and the empty line. */
|
|
166
|
+
maxHead: i32 = 8192
|
|
167
|
+
maxStatusLine: i32 = 8192
|
|
168
|
+
maxHeaderBytes: i32 = 65536
|
|
169
|
+
maxHeaders: i32 = 100
|
|
170
|
+
maxTrailerBytes: i32 = 8192
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* One call: the request that asked, then, once `ready` hands it back, how it
|
|
175
|
+
* ended. The object is the pool's again after `release`.
|
|
176
|
+
*/
|
|
177
|
+
export class UpstreamResponse {
|
|
178
|
+
/** The handler's request this call answers. */
|
|
179
|
+
req: HttpRequest
|
|
180
|
+
/** The body, in `body[0 .. length)`; the buffer is reused, so read `length`, not `body.length`. */
|
|
181
|
+
body: u8[]
|
|
182
|
+
/** The request head, written by `get`. */
|
|
183
|
+
head: u8[]
|
|
184
|
+
/** When the call fails `UPSTREAM_E_TIMEOUT`, on `monotonicNanos`'s clock. */
|
|
185
|
+
deadline: i64 = 0
|
|
186
|
+
/** The final status code, once the response's head has arrived. */
|
|
187
|
+
status: i32 = 0
|
|
188
|
+
/** `UPSTREAM_E_NONE`, an `HTTP1_E_` refusal of the codec, or an `UPSTREAM_E_` code. */
|
|
189
|
+
error: i32 = 0
|
|
190
|
+
length: i32 = 0
|
|
191
|
+
headLength: i32 = 0
|
|
192
|
+
index: i32 = 0
|
|
193
|
+
/** Whether the response was read to its end; `error` says why not. */
|
|
194
|
+
ok: boolean = false
|
|
195
|
+
/** Taken by the program and not yet released. */
|
|
196
|
+
taken: boolean = false
|
|
197
|
+
/** Made once more already, after a reused connection dropped it unanswered. */
|
|
198
|
+
retried: boolean = false
|
|
199
|
+
|
|
200
|
+
constructor(index: i32, headRoom: i32, placeholder: HttpRequest) {
|
|
201
|
+
this.index = index
|
|
202
|
+
this.req = placeholder
|
|
203
|
+
this.head = new Array<u8>(headRoom)
|
|
204
|
+
this.body = new Array<u8>(0)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** One connection of the pool. */
|
|
209
|
+
class UpstreamConnection {
|
|
210
|
+
parser: Http1ResponseParser
|
|
211
|
+
input: u8[]
|
|
212
|
+
fd: i32 = -1
|
|
213
|
+
/** Its handler watch id. */
|
|
214
|
+
id: i32 = 0
|
|
215
|
+
state: i32 = 0
|
|
216
|
+
/** The slot of the call it carries, or -1. */
|
|
217
|
+
call: i32 = -1
|
|
218
|
+
/** How much of the call's head has been written. */
|
|
219
|
+
sent: i32 = 0
|
|
220
|
+
/** The poll interest it is registered with, 0 for none. */
|
|
221
|
+
events: i32 = 0
|
|
222
|
+
/** Whether the call it carries went out on a connection kept alive from an earlier call. */
|
|
223
|
+
reused: boolean = false
|
|
224
|
+
/** Whether any byte of the call's response has arrived. */
|
|
225
|
+
answered: boolean = false
|
|
226
|
+
|
|
227
|
+
constructor(id: i32, parser: Http1ResponseParser) {
|
|
228
|
+
this.id = id
|
|
229
|
+
this.parser = parser
|
|
230
|
+
this.input = new Array<u8>(UPSTREAM_READ_CHUNK)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** A keep-alive pool to one origin, on an `HttpHandler`'s loop. */
|
|
235
|
+
export class Upstream {
|
|
236
|
+
h: HttpHandler
|
|
237
|
+
limits: UpstreamLimits
|
|
238
|
+
/** The one address the pool connects to, checked against the policy if there is one. */
|
|
239
|
+
address: u8[]
|
|
240
|
+
hostNames: string[]
|
|
241
|
+
hostValues: string[]
|
|
242
|
+
connections: UpstreamConnection[]
|
|
243
|
+
calls: UpstreamResponse[]
|
|
244
|
+
free: i32[]
|
|
245
|
+
/** Calls written into their slot and waiting for a connection, oldest first. */
|
|
246
|
+
waiting: i32[]
|
|
247
|
+
/** Calls ended and not yet taken, in the order they ended. */
|
|
248
|
+
done: i32[]
|
|
249
|
+
/** Why the origin is unusable; empty when it is not. For a log. */
|
|
250
|
+
errorText: string = ""
|
|
251
|
+
/** The handler turn `pump` last read. */
|
|
252
|
+
turn: i64 = -1
|
|
253
|
+
/** Not `UPSTREAM_E_NONE` when the origin is unusable: every call then fails with it. */
|
|
254
|
+
error: i32 = 0
|
|
255
|
+
freeCount: i32 = 0
|
|
256
|
+
waitingStart: i32 = 0
|
|
257
|
+
waitingCount: i32 = 0
|
|
258
|
+
doneStart: i32 = 0
|
|
259
|
+
doneCount: i32 = 0
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Parses `origin`, looks its host up once (this blocks for at most
|
|
263
|
+
* `limits.lookupMs`), and makes every slot and connection buffer. Panics
|
|
264
|
+
* when a cap is below 1 or the watch ids run past the handler's. An
|
|
265
|
+
* unusable origin is not a panic: `error` says why, and every call fails
|
|
266
|
+
* with it.
|
|
267
|
+
*/
|
|
268
|
+
constructor(h: HttpHandler, origin: string, limits: UpstreamLimits) {
|
|
269
|
+
if (limits.connections < 1 || limits.calls < 1 || limits.maxHead < 1 || limits.timeoutMs < 1) {
|
|
270
|
+
panic("Upstream: connections, calls, maxHead and timeoutMs must each be at least 1")
|
|
271
|
+
}
|
|
272
|
+
// Each operand on its own: their sum could pass i32's range.
|
|
273
|
+
if (
|
|
274
|
+
limits.firstId < 0 ||
|
|
275
|
+
limits.connections > HANDLER_MAX_WATCHED ||
|
|
276
|
+
limits.firstId > HANDLER_MAX_WATCHED - limits.connections
|
|
277
|
+
) {
|
|
278
|
+
panic(
|
|
279
|
+
`Upstream: watch ids ${limits.firstId} to ${limits.firstId + limits.connections - 1} are outside 0 to ${HANDLER_MAX_WATCHED - 1}`
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
this.h = h
|
|
283
|
+
this.limits = limits
|
|
284
|
+
this.address = new Array<u8>(DNS_ADDRESS_BYTES)
|
|
285
|
+
this.hostNames = ["host"]
|
|
286
|
+
this.hostValues = [""]
|
|
287
|
+
this.connections = []
|
|
288
|
+
for (let k: i32 = 0; k < limits.connections; k += 1) {
|
|
289
|
+
this.connections.push(new UpstreamConnection(limits.firstId + k, upstreamParser(limits)))
|
|
290
|
+
}
|
|
291
|
+
const placeholder: HttpRequest = new HttpRequest(-1)
|
|
292
|
+
this.calls = []
|
|
293
|
+
this.free = new Array<i32>(limits.calls)
|
|
294
|
+
this.waiting = new Array<i32>(limits.calls)
|
|
295
|
+
this.done = new Array<i32>(limits.calls)
|
|
296
|
+
for (let k: i32 = 0; k < limits.calls; k += 1) {
|
|
297
|
+
this.calls.push(new UpstreamResponse(k, limits.maxHead, placeholder))
|
|
298
|
+
this.free[k] = limits.calls - 1 - k
|
|
299
|
+
}
|
|
300
|
+
this.freeCount = limits.calls
|
|
301
|
+
this.settle(origin)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Settles the origin: its `Host` value and the one address, or `error`. */
|
|
305
|
+
settle(origin: string): void {
|
|
306
|
+
const url: Url | null = urlParse(origin)
|
|
307
|
+
if (
|
|
308
|
+
url === null ||
|
|
309
|
+
url.scheme !== "http" ||
|
|
310
|
+
url.host === "" ||
|
|
311
|
+
url.username !== "" ||
|
|
312
|
+
url.password !== "" ||
|
|
313
|
+
url.query !== null ||
|
|
314
|
+
url.fragment !== null ||
|
|
315
|
+
(url.path !== "" && url.path !== "/")
|
|
316
|
+
) {
|
|
317
|
+
this.fail(UPSTREAM_E_ORIGIN, `${origin} is not an http: origin`)
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
const host: string = url.host
|
|
321
|
+
const port: i32 = url.port >= 0 ? url.port : 80
|
|
322
|
+
const named: string = host.indexOf(":") >= 0 ? `[${host}]` : host
|
|
323
|
+
this.hostValues[0] = url.port >= 0 ? `${named}:${port}` : named
|
|
324
|
+
const policy: OutboundPolicy | null = this.limits.outbound
|
|
325
|
+
if (policy !== null && !outboundHostAllowed(policy, host)) {
|
|
326
|
+
this.fail(UPSTREAM_E_BLOCKED, `${host} is not a host the outbound policy allows`)
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
const addresses: u8[] = new Array<u8>(DNS_RESOLVE_MAX_ADDRESSES * DNS_ADDRESS_BYTES)
|
|
330
|
+
const ttls: i32[] = new Array<i32>(DNS_RESOLVE_MAX_ADDRESSES)
|
|
331
|
+
const count: i32 = resolveHost(host, this.limits.lookupMs, port, addresses, ttls)
|
|
332
|
+
if (count <= 0) {
|
|
333
|
+
this.fail(
|
|
334
|
+
UPSTREAM_E_LOOKUP,
|
|
335
|
+
`${host} could not be looked up (${count === 0 ? "no address" : `code ${count}`})`
|
|
336
|
+
)
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
let usable: i32 = count < DNS_RESOLVE_MAX_ADDRESSES ? count : DNS_RESOLVE_MAX_ADDRESSES
|
|
340
|
+
if (policy !== null) {
|
|
341
|
+
// Only the bytes checked here are connected to: there is no second lookup.
|
|
342
|
+
usable = outboundFilter(policy, addresses, usable)
|
|
343
|
+
if (usable === 0) {
|
|
344
|
+
this.fail(
|
|
345
|
+
UPSTREAM_E_BLOCKED,
|
|
346
|
+
`${host} resolves to ${outboundAddressText(addresses, 0)}, which the outbound policy does not allow`
|
|
347
|
+
)
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
copyBytes(this.address, 0, addresses, 0, DNS_ADDRESS_BYTES)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
fail(code: i32, text: string): void {
|
|
355
|
+
this.error = code
|
|
356
|
+
this.errorText = text
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ---- Calls ------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Starts `GET path` for `req` and returns at once: `UPSTREAM_E_NONE` when
|
|
363
|
+
* the call is the pool's, which hands it back through `ready` and `take`
|
|
364
|
+
* however it ends; `UPSTREAM_E_FULL` when every slot is taken, and then
|
|
365
|
+
* nothing is held and `req` is still the program's to answer. `path` is
|
|
366
|
+
* copied out before this returns.
|
|
367
|
+
*/
|
|
368
|
+
get(req: HttpRequest, path: string): i32 {
|
|
369
|
+
if (this.freeCount === 0) {
|
|
370
|
+
return UPSTREAM_E_FULL
|
|
371
|
+
}
|
|
372
|
+
this.freeCount -= 1
|
|
373
|
+
const r: UpstreamResponse = this.calls[this.free[this.freeCount]]
|
|
374
|
+
r.req = req
|
|
375
|
+
r.ok = false
|
|
376
|
+
r.status = 0
|
|
377
|
+
r.error = UPSTREAM_E_NONE
|
|
378
|
+
r.length = 0
|
|
379
|
+
r.retried = false
|
|
380
|
+
r.deadline = monotonicNanos() + toI64(this.limits.timeoutMs) * UPSTREAM_NS_PER_MS
|
|
381
|
+
if (this.error !== UPSTREAM_E_NONE) {
|
|
382
|
+
this.end(r, this.error)
|
|
383
|
+
return UPSTREAM_E_NONE
|
|
384
|
+
}
|
|
385
|
+
// An origin-form target only: an absolute-form one would name another host on this pool's connection.
|
|
386
|
+
if (path.length === 0 || path.charCodeAt(0) !== 47) {
|
|
387
|
+
this.end(r, UPSTREAM_E_REQUEST)
|
|
388
|
+
return UPSTREAM_E_NONE
|
|
389
|
+
}
|
|
390
|
+
const n: i32 = http1WriteRequestHead(
|
|
391
|
+
r.head,
|
|
392
|
+
0,
|
|
393
|
+
"GET",
|
|
394
|
+
path,
|
|
395
|
+
this.hostNames,
|
|
396
|
+
this.hostValues,
|
|
397
|
+
toI64(HTTP1_NO_BODY)
|
|
398
|
+
)
|
|
399
|
+
if (n < 0) {
|
|
400
|
+
this.end(r, UPSTREAM_E_REQUEST)
|
|
401
|
+
return UPSTREAM_E_NONE
|
|
402
|
+
}
|
|
403
|
+
r.headLength = n
|
|
404
|
+
const at: i32 = (this.waitingStart + this.waitingCount) % toI32(this.waiting.length)
|
|
405
|
+
this.waiting[at] = r.index
|
|
406
|
+
this.waitingCount += 1
|
|
407
|
+
this.dispatch()
|
|
408
|
+
return UPSTREAM_E_NONE
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Whether a finished call is waiting for `take`; reads the upstream's sockets once per handler turn. */
|
|
412
|
+
ready(): boolean {
|
|
413
|
+
this.pump()
|
|
414
|
+
return this.doneCount > 0
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** The next finished call, which the program gives back with `release`. Panics when none is `ready`. */
|
|
418
|
+
take(): UpstreamResponse {
|
|
419
|
+
if (!this.ready()) {
|
|
420
|
+
panic("Upstream.take: no call is ready")
|
|
421
|
+
}
|
|
422
|
+
const r: UpstreamResponse = this.calls[this.done[this.doneStart]]
|
|
423
|
+
this.doneStart = (this.doneStart + 1) % toI32(this.done.length)
|
|
424
|
+
this.doneCount -= 1
|
|
425
|
+
r.taken = true
|
|
426
|
+
return r
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Gives a taken call's slot back to the pool. Panics for a call not taken
|
|
431
|
+
* or already released: two calls sharing a slot would hand one request's
|
|
432
|
+
* answer to another.
|
|
433
|
+
*/
|
|
434
|
+
release(r: UpstreamResponse): void {
|
|
435
|
+
if (!r.taken) {
|
|
436
|
+
panic("Upstream.release: the call is not taken, or was released already")
|
|
437
|
+
}
|
|
438
|
+
r.taken = false
|
|
439
|
+
this.free[this.freeCount] = r.index
|
|
440
|
+
this.freeCount += 1
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** How many calls hold a slot: queued, in flight, or finished and not yet released. */
|
|
444
|
+
pending(): i32 {
|
|
445
|
+
return toI32(this.free.length) - this.freeCount
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** How many connections to the origin are open. */
|
|
449
|
+
openConnections(): i32 {
|
|
450
|
+
let n: i32 = 0
|
|
451
|
+
for (let k: i32 = 0; k < toI32(this.connections.length); k += 1) {
|
|
452
|
+
if (this.connections[k].fd >= 0) {
|
|
453
|
+
n += 1
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return n
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ---- The loop ---------------------------------------------------------------
|
|
460
|
+
|
|
461
|
+
/** Once per handler turn: reads and writes the connections that fired, then fails what is past its deadline. */
|
|
462
|
+
pump(): void {
|
|
463
|
+
if (this.h.turnAt === this.turn) {
|
|
464
|
+
return
|
|
465
|
+
}
|
|
466
|
+
this.turn = this.h.turnAt
|
|
467
|
+
for (let k: i32 = 0; k < toI32(this.connections.length); k += 1) {
|
|
468
|
+
const c: UpstreamConnection = this.connections[k]
|
|
469
|
+
if (c.fd >= 0 && this.h.fired(c.id)) {
|
|
470
|
+
this.service(c)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const now: i64 = monotonicNanos()
|
|
474
|
+
for (let k: i32 = 0; k < toI32(this.connections.length); k += 1) {
|
|
475
|
+
const c: UpstreamConnection = this.connections[k]
|
|
476
|
+
if (c.call >= 0 && this.calls[c.call].deadline <= now) {
|
|
477
|
+
this.finish(c, UPSTREAM_E_TIMEOUT)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// Every call has the same timeout, so the oldest waiting call is the first to expire.
|
|
481
|
+
while (this.waitingCount > 0 && this.calls[this.waiting[this.waitingStart]].deadline <= now) {
|
|
482
|
+
const r: UpstreamResponse = this.calls[this.waiting[this.waitingStart]]
|
|
483
|
+
this.waitingStart = (this.waitingStart + 1) % toI32(this.waiting.length)
|
|
484
|
+
this.waitingCount -= 1
|
|
485
|
+
this.end(r, UPSTREAM_E_TIMEOUT)
|
|
486
|
+
}
|
|
487
|
+
this.dispatch()
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Puts waiting calls on connections: a kept-alive one first, then a new one. */
|
|
491
|
+
dispatch(): void {
|
|
492
|
+
while (this.waitingCount > 0) {
|
|
493
|
+
const c: UpstreamConnection | null = this.idle()
|
|
494
|
+
if (c === null) {
|
|
495
|
+
return
|
|
496
|
+
}
|
|
497
|
+
const index: i32 = this.waiting[this.waitingStart]
|
|
498
|
+
this.waitingStart = (this.waitingStart + 1) % toI32(this.waiting.length)
|
|
499
|
+
this.waitingCount -= 1
|
|
500
|
+
c.call = index
|
|
501
|
+
c.sent = 0
|
|
502
|
+
c.answered = false
|
|
503
|
+
c.reused = c.state === UPSTREAM_C_IDLE
|
|
504
|
+
if (c.reused) {
|
|
505
|
+
c.state = UPSTREAM_C_BUSY
|
|
506
|
+
c.parser.expect(false, false)
|
|
507
|
+
this.send(c)
|
|
508
|
+
} else {
|
|
509
|
+
this.connect(c)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* A connection free for the next call: open, idle and still usable if one
|
|
516
|
+
* is, else closed, else null. An idle connection the origin has closed or
|
|
517
|
+
* sent bytes on is closed here, before a call is put on it.
|
|
518
|
+
*/
|
|
519
|
+
idle(): UpstreamConnection | null {
|
|
520
|
+
let closed: UpstreamConnection | null = null
|
|
521
|
+
for (let k: i32 = 0; k < toI32(this.connections.length); k += 1) {
|
|
522
|
+
const c: UpstreamConnection = this.connections[k]
|
|
523
|
+
if (c.state === UPSTREAM_C_IDLE && this.usable(c)) {
|
|
524
|
+
return c
|
|
525
|
+
}
|
|
526
|
+
if (c.state === UPSTREAM_C_CLOSED && closed === null) {
|
|
527
|
+
closed = c
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return closed
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Whether the idle `c` has nothing to read. Anything else closes it: an end
|
|
535
|
+
* of stream or an error means the origin has given it up, and bytes no call
|
|
536
|
+
* asked for would be read as the next call's response.
|
|
537
|
+
*/
|
|
538
|
+
usable(c: UpstreamConnection): boolean {
|
|
539
|
+
const start: i32 = 0
|
|
540
|
+
if (netRead(c.fd, c.input, start, toI32(c.input.length)) === UPSTREAM_WOULD_BLOCK) {
|
|
541
|
+
return true
|
|
542
|
+
}
|
|
543
|
+
this.close(c)
|
|
544
|
+
return false
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Starts a connection for the call `c` holds; a failure ends the call. */
|
|
548
|
+
connect(c: UpstreamConnection): void {
|
|
549
|
+
const fd: i32 = tcpConnect(this.address)
|
|
550
|
+
if (fd < 0) {
|
|
551
|
+
this.finish(c, UPSTREAM_E_CONNECT)
|
|
552
|
+
return
|
|
553
|
+
}
|
|
554
|
+
if (pollAdd(this.h.loop, fd, UPSTREAM_WRITE, -2 - c.id) < 0) {
|
|
555
|
+
netClose(fd)
|
|
556
|
+
this.finish(c, UPSTREAM_E_CONNECT)
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
c.fd = fd
|
|
560
|
+
c.events = UPSTREAM_WRITE
|
|
561
|
+
c.state = UPSTREAM_C_CONNECTING
|
|
562
|
+
// The slot's parser is kept and starts over with the new connection.
|
|
563
|
+
c.parser.reset()
|
|
564
|
+
c.parser.expect(false, false)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** What a connection's turn found ready. */
|
|
568
|
+
service(c: UpstreamConnection): void {
|
|
569
|
+
if (c.state === UPSTREAM_C_CONNECTING) {
|
|
570
|
+
if (connectResult(c.fd) !== 0) {
|
|
571
|
+
this.finish(c, UPSTREAM_E_CONNECT)
|
|
572
|
+
return
|
|
573
|
+
}
|
|
574
|
+
c.state = UPSTREAM_C_BUSY
|
|
575
|
+
}
|
|
576
|
+
if (c.state === UPSTREAM_C_BUSY && c.call >= 0 && c.sent < this.calls[c.call].headLength) {
|
|
577
|
+
this.send(c)
|
|
578
|
+
return
|
|
579
|
+
}
|
|
580
|
+
this.receive(c)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Writes what the socket takes of the call's head, then waits to read. */
|
|
584
|
+
send(c: UpstreamConnection): void {
|
|
585
|
+
const r: UpstreamResponse = this.calls[c.call]
|
|
586
|
+
while (c.sent < r.headLength) {
|
|
587
|
+
const n: i32 = netWrite(c.fd, r.head, c.sent, r.headLength - c.sent)
|
|
588
|
+
if (n === UPSTREAM_WOULD_BLOCK) {
|
|
589
|
+
this.interest(c, UPSTREAM_WRITE)
|
|
590
|
+
return
|
|
591
|
+
}
|
|
592
|
+
if (n <= 0) {
|
|
593
|
+
this.finish(c, UPSTREAM_E_CLOSED)
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
c.sent += n
|
|
597
|
+
}
|
|
598
|
+
this.interest(c, UPSTREAM_READ)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** Reads what arrived and runs the codec over it. */
|
|
602
|
+
receive(c: UpstreamConnection): void {
|
|
603
|
+
while (c.fd >= 0) {
|
|
604
|
+
const start: i32 = 0
|
|
605
|
+
const n: i32 = netRead(c.fd, c.input, start, toI32(c.input.length))
|
|
606
|
+
if (n === UPSTREAM_WOULD_BLOCK) {
|
|
607
|
+
return
|
|
608
|
+
}
|
|
609
|
+
if (c.call < 0) {
|
|
610
|
+
// Nothing was asked: the server closed a kept-alive connection, or spoke out of turn.
|
|
611
|
+
this.close(c)
|
|
612
|
+
return
|
|
613
|
+
}
|
|
614
|
+
if (n < 0) {
|
|
615
|
+
// Not the origin's close: a close-delimited body cut off here must not end as whole.
|
|
616
|
+
this.finish(c, UPSTREAM_E_RESET)
|
|
617
|
+
return
|
|
618
|
+
}
|
|
619
|
+
if (n > 0) {
|
|
620
|
+
c.answered = true
|
|
621
|
+
c.parser.feed(c.input, start, n)
|
|
622
|
+
} else {
|
|
623
|
+
c.parser.finish()
|
|
624
|
+
}
|
|
625
|
+
this.parse(c)
|
|
626
|
+
if (n === 0 && c.fd >= 0) {
|
|
627
|
+
this.close(c)
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** Takes every event the codec has for the call `c` carries. */
|
|
633
|
+
parse(c: UpstreamConnection): void {
|
|
634
|
+
const p: Http1ResponseParser = c.parser
|
|
635
|
+
while (c.call >= 0) {
|
|
636
|
+
const event: i32 = p.next()
|
|
637
|
+
const r: UpstreamResponse = this.calls[c.call]
|
|
638
|
+
if (event === HTTP1_NEED_MORE) {
|
|
639
|
+
return
|
|
640
|
+
}
|
|
641
|
+
if (event === HTTP1_HEAD) {
|
|
642
|
+
r.status = p.status
|
|
643
|
+
} else if (event === HTTP1_BODY) {
|
|
644
|
+
upstreamAppend(r, p.body, p.bodyOff, p.bodyLen)
|
|
645
|
+
} else if (event === HTTP1_END) {
|
|
646
|
+
const again: boolean = p.keepAlive() && p.buffered() === 0
|
|
647
|
+
this.finish(c, UPSTREAM_E_NONE)
|
|
648
|
+
if (again) {
|
|
649
|
+
c.state = UPSTREAM_C_IDLE
|
|
650
|
+
} else {
|
|
651
|
+
this.close(c)
|
|
652
|
+
}
|
|
653
|
+
} else if (event === HTTP1_ERROR) {
|
|
654
|
+
this.finish(c, p.error)
|
|
655
|
+
} else if (event === HTTP1_CLOSED) {
|
|
656
|
+
this.finish(c, UPSTREAM_E_CLOSED)
|
|
657
|
+
} else if (event !== HTTP1_INFORMATIONAL) {
|
|
658
|
+
// Nothing here asks to switch protocols, so the codec has no other event for it.
|
|
659
|
+
this.finish(c, UPSTREAM_E_CLOSED)
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Ends the call `c` carries with `code`. A connection whose call ended any
|
|
666
|
+
* way but `UPSTREAM_E_NONE` is closed: what is left on it cannot be trusted
|
|
667
|
+
* to start the next response.
|
|
668
|
+
*
|
|
669
|
+
* A call that failed on a reused connection before any byte of its response
|
|
670
|
+
* came is not ended but put back at the front of the queue, once: the
|
|
671
|
+
* origin closed the connection as the request crossed it, and a `GET` is
|
|
672
|
+
* safe to send again. Past its deadline it is not.
|
|
673
|
+
*/
|
|
674
|
+
finish(c: UpstreamConnection, code: i32): void {
|
|
675
|
+
if (c.call >= 0) {
|
|
676
|
+
const r: UpstreamResponse = this.calls[c.call]
|
|
677
|
+
c.call = -1
|
|
678
|
+
if (code !== UPSTREAM_E_NONE && code !== UPSTREAM_E_TIMEOUT && c.reused && !c.answered && !r.retried) {
|
|
679
|
+
r.retried = true
|
|
680
|
+
const length: i32 = toI32(this.waiting.length)
|
|
681
|
+
this.waitingStart = (this.waitingStart + length - 1) % length
|
|
682
|
+
this.waiting[this.waitingStart] = r.index
|
|
683
|
+
this.waitingCount += 1
|
|
684
|
+
} else {
|
|
685
|
+
this.end(r, code)
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (code !== UPSTREAM_E_NONE) {
|
|
689
|
+
this.close(c)
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** Hands `r` to `ready` with `code`. */
|
|
694
|
+
end(r: UpstreamResponse, code: i32): void {
|
|
695
|
+
r.error = code
|
|
696
|
+
r.ok = code === UPSTREAM_E_NONE
|
|
697
|
+
if (!r.ok) {
|
|
698
|
+
r.length = 0
|
|
699
|
+
}
|
|
700
|
+
const at: i32 = (this.doneStart + this.doneCount) % toI32(this.done.length)
|
|
701
|
+
this.done[at] = r.index
|
|
702
|
+
this.doneCount += 1
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** Watches `c` for `events` alone. */
|
|
706
|
+
interest(c: UpstreamConnection, events: i32): void {
|
|
707
|
+
if (c.events !== events) {
|
|
708
|
+
pollModify(this.h.loop, c.fd, events, -2 - c.id)
|
|
709
|
+
c.events = events
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
close(c: UpstreamConnection): void {
|
|
714
|
+
if (c.fd >= 0) {
|
|
715
|
+
pollRemove(this.h.loop, c.fd)
|
|
716
|
+
netClose(c.fd)
|
|
717
|
+
}
|
|
718
|
+
c.fd = -1
|
|
719
|
+
c.events = 0
|
|
720
|
+
c.state = UPSTREAM_C_CLOSED
|
|
721
|
+
if (c.call >= 0) {
|
|
722
|
+
this.finish(c, UPSTREAM_E_CLOSED)
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** A connection slot's codec, made once, reading fields as spans so that a response allocates no strings. */
|
|
728
|
+
const upstreamParser = (limits: UpstreamLimits): Http1ResponseParser => {
|
|
729
|
+
const p: Http1ResponseParser = new Http1ResponseParser(
|
|
730
|
+
limits.maxStatusLine,
|
|
731
|
+
limits.maxHeaderBytes,
|
|
732
|
+
limits.maxHeaders,
|
|
733
|
+
// A body buffer is an i32-indexed array, so past this the codec refuses the body rather than the buffer overflowing.
|
|
734
|
+
limits.maxBody < UPSTREAM_MAX_BODY ? limits.maxBody : UPSTREAM_MAX_BODY,
|
|
735
|
+
limits.maxTrailerBytes
|
|
736
|
+
)
|
|
737
|
+
p.fieldsAsSpans()
|
|
738
|
+
return p
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Appends `data[off .. off + len)` to `r`'s body, growing its buffer by
|
|
743
|
+
* doubling when it is full: to under twice the body, and at least 1 KiB.
|
|
744
|
+
*/
|
|
745
|
+
const upstreamAppend = (r: UpstreamResponse, data: u8[], off: i32, len: i32): void => {
|
|
746
|
+
const need: i32 = r.length + len
|
|
747
|
+
if (need > toI32(r.body.length)) {
|
|
748
|
+
let size: i32 = toI32(r.body.length) < 1024 ? 1024 : toI32(r.body.length)
|
|
749
|
+
// `need` is at most UPSTREAM_MAX_BODY, 2^30, so `size` doubles to 2^30 at most.
|
|
750
|
+
while (size < need) {
|
|
751
|
+
size = size * 2
|
|
752
|
+
}
|
|
753
|
+
const grown: u8[] = new Array<u8>(size)
|
|
754
|
+
copyBytes(grown, 0, r.body, 0, r.length)
|
|
755
|
+
r.body = grown
|
|
756
|
+
}
|
|
757
|
+
copyBytes(r.body, r.length, data, off, len)
|
|
758
|
+
r.length = need
|
|
759
|
+
}
|