@amritk/nish-x86_64-linux 0.24.0 → 0.25.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/bin/nish +0 -0
- package/docs/AI.md +24 -4
- package/package.json +1 -1
- package/runtime/browser.mjs +4 -3
- package/runtime/core/host.d.ts +8 -0
- package/runtime/core/host.mjs +50 -2
- package/runtime/nish.d.ts +100 -0
- package/runtime/nish.h +34 -0
- package/runtime/nish.mjs +21 -0
- package/runtime/node.mjs +57 -6
- package/runtime/runtime-net.c +44 -3
- package/runtime/runtime-os.c +115 -12
- package/scripts/build.sh +13 -3
- package/std/README.md +25 -8
- package/std/cli-args.ts +1216 -0
- package/std/cli-flags.ts +585 -0
- package/std/cli.ts +308 -19
- package/std/compress/checksum.ts +132 -20
- package/std/compress/deflate.ts +638 -248
- package/std/compress/gzip.ts +20 -6
- package/std/crypto/LICENSE-fiat-crypto-p384 +21 -0
- package/std/crypto/p256.ts +63 -1
- package/std/crypto/p384.ts +6044 -0
- package/std/crypto/pki-roots.ts +322 -0
- package/std/crypto/pki.ts +1207 -0
- package/std/crypto/rsa.ts +663 -0
- package/std/crypto/x509.ts +1054 -58
- package/std/net/dns-config.ts +734 -0
- package/std/net/dns-message.ts +517 -0
- package/std/net/dns-resolver.ts +1015 -0
- package/std/net/dns.ts +478 -0
- package/std/net/http1-client.ts +1227 -0
- package/std/net/http1-server.ts +7 -2
- package/std/net/http1.ts +9 -9
- package/std/net/tls/client.ts +1094 -0
- package/std/net/tls/codec.ts +585 -3
- package/std/net/tls/record-client.ts +1192 -0
- package/std/net/tls/verify.ts +221 -0
- package/std/net/tls-tcp.ts +642 -11
- package/std/net/tls.ts +2 -2
- package/std/net/url.ts +559 -4
- package/std/yaml-write.ts +415 -0
package/std/net/tls-tcp.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `nish/net/tls-tcp` — TLS 1.3 over TCP (WP34 T2): a pool of server
|
|
3
3
|
* connections on `nish:net`, each a `TlsRecordServer` driving a `TlsServer`,
|
|
4
|
-
* for a program that owns its readiness loop
|
|
4
|
+
* for a program that owns its readiness loop; and `TlsTcpClient`, one
|
|
5
|
+
* client connection that authenticates the server (`nish/net/tls/verify`)
|
|
6
|
+
* and is driven by the same kind of loop or, through `tlsConnect`,
|
|
7
|
+
* `readWait` and `writeAll`, by none (its own comment, below, says how).
|
|
5
8
|
*
|
|
6
9
|
* const listener = tcpListen("0.0.0.0", 8443, 128);
|
|
7
10
|
* const tls = new TlsTcpServer(config, listener, 64);
|
|
@@ -72,20 +75,61 @@
|
|
|
72
75
|
* `close` frees the slot at once; `shutdown` sends the `close_notify` as the
|
|
73
76
|
* socket takes it, and the slot is done when its interest says so.
|
|
74
77
|
*
|
|
78
|
+
* **A closed connection lingers until the client hangs up.** Once a
|
|
79
|
+
* connection the server closed has sent everything, the slot shuts the
|
|
80
|
+
* socket's write side — the client reads the response, the `close_notify`
|
|
81
|
+
* and then the end of the stream — and reads and throws away what the
|
|
82
|
+
* client still sends, until its own end of stream, before its interest says
|
|
83
|
+
* `TLS_RECORD_DONE` (RFC 9112 §9.6's staged close). A socket closed with
|
|
84
|
+
* the client's bytes unread, or that the client writes to after the close,
|
|
85
|
+
* answers with a TCP reset, and a client may report that reset instead of
|
|
86
|
+
* the response it was sent: Chromium's `ERR_CONNECTION_RESET` after an HTTP/2
|
|
87
|
+
* `GOAWAY`, when its SETTINGS acknowledgement crossed the server's close. A
|
|
88
|
+
* lingering slot holds no key and no record buffer, asks only to be read,
|
|
89
|
+
* and gives up past `TLS_TCP_LINGER_LIMIT` bytes. A connection that failed,
|
|
90
|
+
* whose client sent its own `close_notify` (it writes nothing after it,
|
|
91
|
+
* RFC 8446 §6.1) or whose client's stream had already ended, is done at
|
|
92
|
+
* once. A client that never hangs up keeps its slot, as one that never sends
|
|
93
|
+
* does: how long a slot may wait is the program's (`docs/security/tls.md`,
|
|
94
|
+
* TLS-6 and TLS-4).
|
|
95
|
+
*
|
|
75
96
|
* Written from RFC 8446 and docs/LANGUAGE.md's `nish:net`, not ported from
|
|
76
97
|
* another implementation.
|
|
77
98
|
*/
|
|
78
99
|
import { Secret } from "nish:secret"
|
|
79
|
-
import {
|
|
100
|
+
import {
|
|
101
|
+
connectResult,
|
|
102
|
+
netClose,
|
|
103
|
+
netRead,
|
|
104
|
+
netShutdown,
|
|
105
|
+
netWrite,
|
|
106
|
+
pollAdd,
|
|
107
|
+
pollCreate,
|
|
108
|
+
pollModify,
|
|
109
|
+
pollWait,
|
|
110
|
+
tcpAccept,
|
|
111
|
+
tcpConnect,
|
|
112
|
+
} from "nish:net"
|
|
113
|
+
import { monotonicNanos, wallClockNanos } from "nish:process"
|
|
80
114
|
import { TlsP256Signer, TlsServer, TlsServerConfig } from "nish/net/tls"
|
|
115
|
+
import { TlsClient, TlsClientConfig } from "nish/net/tls/client"
|
|
116
|
+
import { TLS_RECORD_AUTH, TlsRecordClient } from "nish/net/tls/record-client"
|
|
117
|
+
import { TlsClientTrust, tlsClientAuthenticate, tlsClientSystemTrust } from "nish/net/tls/verify"
|
|
81
118
|
import { TLS_ALERT_INTERNAL_ERROR } from "nish/net/tls/codec"
|
|
82
119
|
import { TLS_MAX_RECORD } from "nish/net/tls/record"
|
|
83
120
|
import { TlsTicketKeyInfo, TlsTicketStrikes } from "nish/net/tls/ticket"
|
|
84
121
|
import {
|
|
85
122
|
TLS_RECORD_AGAIN,
|
|
123
|
+
TLS_RECORD_DATA,
|
|
86
124
|
TLS_RECORD_DONE,
|
|
87
125
|
TLS_RECORD_INVALID,
|
|
126
|
+
TLS_RECORD_PIPE,
|
|
127
|
+
TLS_RECORD_RESET,
|
|
128
|
+
TLS_RECORD_STATE_CLOSED,
|
|
88
129
|
TLS_RECORD_STATE_FAILED,
|
|
130
|
+
TLS_RECORD_STATE_OPEN,
|
|
131
|
+
TLS_RECORD_WANT_READ,
|
|
132
|
+
TLS_RECORD_WANT_WRITE,
|
|
89
133
|
TlsRecordPools,
|
|
90
134
|
TlsRecordServer,
|
|
91
135
|
} from "nish/net/tls/record-server"
|
|
@@ -102,9 +146,36 @@ const TLS_TCP_FROM: i32 = 0
|
|
|
102
146
|
/** The length of an x25519 private key and of a server random: what `TlsServer` takes for each. */
|
|
103
147
|
const TLS_TCP_KEY_SIZE: i32 = 32
|
|
104
148
|
|
|
149
|
+
/** The most a lingering slot reads and throws away before it is done anyway: 64 KiB. */
|
|
150
|
+
const TLS_TCP_LINGER_LIMIT: i32 = 65536
|
|
151
|
+
|
|
152
|
+
/** In `lingered`: a slot that is not lingering. */
|
|
153
|
+
const TLS_TCP_LIVE: i32 = -1
|
|
154
|
+
|
|
155
|
+
/** In `lingered`: a slot whose linger is over, which is done. */
|
|
156
|
+
const TLS_TCP_LINGERED: i32 = -2
|
|
157
|
+
|
|
158
|
+
/** `netShutdown`'s write side. */
|
|
159
|
+
const TLS_TCP_SHUT_WRITE: i32 = 1
|
|
160
|
+
|
|
105
161
|
/** The address form's length, from `nish:net`. */
|
|
106
162
|
const TLS_TCP_ADDRESS_SIZE: i32 = 18
|
|
107
163
|
|
|
164
|
+
/** What `tlsConnect` answers when the connection did not open within its timeout. Linux's ETIMEDOUT. */
|
|
165
|
+
export const TLS_TCP_TIMED_OUT: i32 = -110
|
|
166
|
+
|
|
167
|
+
/** `pollWait`'s readable bit and its hang-up-or-error bit: the events `readable` takes. */
|
|
168
|
+
const TLS_TCP_READ_EVENTS: i32 = 5
|
|
169
|
+
|
|
170
|
+
/** The events `pollModify` takes, readable and writable: the low two `TLS_RECORD_*` bits. */
|
|
171
|
+
const TLS_TCP_POLL_EVENTS: i32 = 3
|
|
172
|
+
|
|
173
|
+
/** The token `tlsConnect`'s own loop watches its one socket under. */
|
|
174
|
+
const TLS_TCP_TOKEN: i32 = 1
|
|
175
|
+
|
|
176
|
+
/** Nanoseconds in a millisecond, for the clocks of `nish:process`. */
|
|
177
|
+
const TLS_TCP_NANOS_PER_MS: i64 = 1000000
|
|
178
|
+
|
|
108
179
|
/**
|
|
109
180
|
* A pool of TLS 1.3 server connections on one listening socket. `slot`s are
|
|
110
181
|
* the indices `accept` answers, from 0 to the pool's size; a slot is the
|
|
@@ -125,6 +196,12 @@ export class TlsTcpServer {
|
|
|
125
196
|
* it has never been used.
|
|
126
197
|
*/
|
|
127
198
|
fds: i32[]
|
|
199
|
+
/**
|
|
200
|
+
* Each slot's linger, beside `fds`: `TLS_TCP_LIVE` until its connection is
|
|
201
|
+
* over, then how many bytes it has thrown away while it waits for the
|
|
202
|
+
* client's end of stream, then `TLS_TCP_LINGERED` once it is done.
|
|
203
|
+
*/
|
|
204
|
+
lingered: i32[]
|
|
128
205
|
/**
|
|
129
206
|
* The slots closed since they were used, a stack whose top `accept` takes
|
|
130
207
|
* before it builds a slot past the high-water mark, so finding one costs
|
|
@@ -180,6 +257,7 @@ export class TlsTcpServer {
|
|
|
180
257
|
this.listener = listener
|
|
181
258
|
this.connections = []
|
|
182
259
|
this.fds = []
|
|
260
|
+
this.lingered = []
|
|
183
261
|
this.free = []
|
|
184
262
|
this.randoms = []
|
|
185
263
|
this.slots = size < 1 ? 1 : size
|
|
@@ -301,6 +379,7 @@ export class TlsTcpServer {
|
|
|
301
379
|
} else if (toI32(this.fds.length) < this.slots) {
|
|
302
380
|
slot = toI32(this.fds.length)
|
|
303
381
|
this.fds.push(TLS_TCP_FREE)
|
|
382
|
+
this.lingered.push(TLS_TCP_LIVE)
|
|
304
383
|
}
|
|
305
384
|
if (slot < 0 || slot >= toI32(this.fds.length)) {
|
|
306
385
|
netClose(fd)
|
|
@@ -309,6 +388,7 @@ export class TlsTcpServer {
|
|
|
309
388
|
const conn: TlsRecordServer =
|
|
310
389
|
slot < toI32(this.connections.length) ? this.connections[slot] : this.build()
|
|
311
390
|
this.fds[slot] = fd
|
|
391
|
+
this.lingered[slot] = TLS_TCP_LIVE
|
|
312
392
|
const random: u8[] = this.randoms[slot]
|
|
313
393
|
for (
|
|
314
394
|
let k: i32 = 0;
|
|
@@ -326,13 +406,16 @@ export class TlsTcpServer {
|
|
|
326
406
|
/**
|
|
327
407
|
* The socket of `slot` is readable: reads it until it would block or the
|
|
328
408
|
* connection has no room, handing each read to the connection, then sends
|
|
329
|
-
* what that produced. Answers the connection's `interest
|
|
330
|
-
* answers `TLS_RECORD_DONE`.
|
|
409
|
+
* what that produced. Answers the connection's `interest`, or, while the
|
|
410
|
+
* slot lingers, what `drain` answers; a free slot answers `TLS_RECORD_DONE`.
|
|
331
411
|
*/
|
|
332
412
|
readable(slot: i32): i32 {
|
|
333
413
|
if (!this.holds(slot)) {
|
|
334
414
|
return TLS_RECORD_DONE
|
|
335
415
|
}
|
|
416
|
+
if (this.lingered[slot] !== TLS_TCP_LIVE) {
|
|
417
|
+
return this.drain(slot)
|
|
418
|
+
}
|
|
336
419
|
const fd: i32 = this.fds[slot]
|
|
337
420
|
const conn: TlsRecordServer = this.connections[slot]
|
|
338
421
|
while (true) {
|
|
@@ -365,13 +448,25 @@ export class TlsTcpServer {
|
|
|
365
448
|
|
|
366
449
|
/**
|
|
367
450
|
* Writes the connection's output until the socket would block or it is all
|
|
368
|
-
* sent, and answers its `interest`;
|
|
369
|
-
* `
|
|
451
|
+
* sent, and answers its `interest`; once the connection is over and the
|
|
452
|
+
* program has read what the client sent, the slot lingers (`linger`). A
|
|
453
|
+
* free slot answers `TLS_RECORD_DONE`.
|
|
370
454
|
*/
|
|
371
455
|
flush(slot: i32): i32 {
|
|
372
|
-
if (
|
|
456
|
+
if (!this.holds(slot)) {
|
|
373
457
|
return TLS_RECORD_DONE
|
|
374
458
|
}
|
|
459
|
+
if (this.lingered[slot] !== TLS_TCP_LIVE) {
|
|
460
|
+
return this.lingerInterest(slot)
|
|
461
|
+
}
|
|
462
|
+
const wants: i32 = this.send(slot)
|
|
463
|
+
// Application data still unread keeps the connection, and its keys, until
|
|
464
|
+
// the program reads it: lingering gives back the buffer that holds it.
|
|
465
|
+
return (wants & TLS_RECORD_DONE) !== 0 && (wants & TLS_RECORD_DATA) === 0 ? this.linger(slot) : wants
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Writes `slot`'s output until the socket would block or it is all sent; answers the connection's `interest`. */
|
|
469
|
+
send(slot: i32): i32 {
|
|
375
470
|
const fd: i32 = this.fds[slot]
|
|
376
471
|
const conn: TlsRecordServer = this.connections[slot]
|
|
377
472
|
while (conn.outputEnd > conn.outputStart) {
|
|
@@ -387,9 +482,70 @@ export class TlsTcpServer {
|
|
|
387
482
|
return conn.interest()
|
|
388
483
|
}
|
|
389
484
|
|
|
485
|
+
/**
|
|
486
|
+
* `slot`'s connection is over and everything is sent. One the server
|
|
487
|
+
* closed, whose client has neither sent its `close_notify` nor ended its
|
|
488
|
+
* stream, lingers: the socket's write side is shut, every key wiped and
|
|
489
|
+
* every record buffer given back, and what the client sends is drained
|
|
490
|
+
* (`drain`). Any other is done.
|
|
491
|
+
*/
|
|
492
|
+
linger(slot: i32): i32 {
|
|
493
|
+
const conn: TlsRecordServer = this.connections[slot]
|
|
494
|
+
if (
|
|
495
|
+
conn.state !== TLS_RECORD_STATE_CLOSED ||
|
|
496
|
+
conn.peerClosed ||
|
|
497
|
+
conn.ended ||
|
|
498
|
+
netShutdown(this.fds[slot], TLS_TCP_SHUT_WRITE) < 0
|
|
499
|
+
) {
|
|
500
|
+
this.lingered[slot] = TLS_TCP_LINGERED
|
|
501
|
+
return TLS_RECORD_DONE
|
|
502
|
+
}
|
|
503
|
+
this.lingered[slot] = 0
|
|
504
|
+
conn.wipeKeys()
|
|
505
|
+
conn.release()
|
|
506
|
+
return this.drain(slot)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Reads what a lingering `slot`'s client sends and throws it away, until
|
|
511
|
+
* the socket would block (`TLS_RECORD_WANT_READ`), or its stream ends, the
|
|
512
|
+
* socket fails or more than `TLS_TCP_LINGER_LIMIT` bytes came
|
|
513
|
+
* (`TLS_RECORD_DONE`).
|
|
514
|
+
*/
|
|
515
|
+
drain(slot: i32): i32 {
|
|
516
|
+
while (this.lingered[slot] >= 0) {
|
|
517
|
+
const n: i32 = netRead(this.fds[slot], this.scratch, TLS_TCP_FROM, toI32(this.scratch.length))
|
|
518
|
+
if (n === TLS_RECORD_AGAIN) {
|
|
519
|
+
return TLS_RECORD_WANT_READ
|
|
520
|
+
}
|
|
521
|
+
this.lingered[slot] =
|
|
522
|
+
n > 0 && this.lingered[slot] + n <= TLS_TCP_LINGER_LIMIT ? this.lingered[slot] + n : TLS_TCP_LINGERED
|
|
523
|
+
}
|
|
524
|
+
return TLS_RECORD_DONE
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Whether `slot` lingers: its connection is over and it waits for the
|
|
529
|
+
* client to hang up. A carrier's idle clock does not move for what a
|
|
530
|
+
* lingering slot drains, so its idle timeout bounds the wait.
|
|
531
|
+
*/
|
|
532
|
+
lingering(slot: i32): boolean {
|
|
533
|
+
return this.holds(slot) && this.lingered[slot] >= 0
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** A lingering `slot`'s interest: to be read, or `TLS_RECORD_DONE` once its linger is over. */
|
|
537
|
+
lingerInterest(slot: i32): i32 {
|
|
538
|
+
return this.lingered[slot] >= 0 ? TLS_RECORD_WANT_READ : TLS_RECORD_DONE
|
|
539
|
+
}
|
|
540
|
+
|
|
390
541
|
/** What `slot`'s connection wants now, without touching the socket; `TLS_RECORD_DONE` for a free slot. */
|
|
391
542
|
interest(slot: i32): i32 {
|
|
392
|
-
|
|
543
|
+
if (!this.holds(slot)) {
|
|
544
|
+
return TLS_RECORD_DONE
|
|
545
|
+
}
|
|
546
|
+
return this.lingered[slot] !== TLS_TCP_LIVE
|
|
547
|
+
? this.lingerInterest(slot)
|
|
548
|
+
: this.connections[slot].interest()
|
|
393
549
|
}
|
|
394
550
|
|
|
395
551
|
/** What `slot`'s CertificateVerify signs, while its `interest` says `TLS_RECORD_SIGN`; `null` otherwise. */
|
|
@@ -522,7 +678,9 @@ export class TlsTcpServer {
|
|
|
522
678
|
* Closes the server's side of `slot`'s connection (§6.1): a `close_notify`
|
|
523
679
|
* after whatever is already written, sent as the socket takes it. The slot
|
|
524
680
|
* stays the program's until its interest says `TLS_RECORD_DONE` — every
|
|
525
|
-
* byte is sent
|
|
681
|
+
* byte is sent and, once the slot lingers (`linger`), the client has hung
|
|
682
|
+
* up — and the program calls `close`. Meanwhile the interest asks for the
|
|
683
|
+
* socket to be read. Answers the interest.
|
|
526
684
|
*/
|
|
527
685
|
shutdown(slot: i32): i32 {
|
|
528
686
|
if (!this.holds(slot)) {
|
|
@@ -545,13 +703,486 @@ export class TlsTcpServer {
|
|
|
545
703
|
return
|
|
546
704
|
}
|
|
547
705
|
const conn: TlsRecordServer = this.connections[slot]
|
|
548
|
-
|
|
549
|
-
|
|
706
|
+
if (this.lingered[slot] === TLS_TCP_LIVE) {
|
|
707
|
+
conn.close()
|
|
708
|
+
this.send(slot)
|
|
709
|
+
}
|
|
550
710
|
netClose(this.fds[slot])
|
|
551
711
|
this.fds[slot] = TLS_TCP_FREE
|
|
712
|
+
this.lingered[slot] = TLS_TCP_LIVE
|
|
552
713
|
this.free.push(slot)
|
|
553
714
|
conn.wipeKeys()
|
|
554
715
|
// Whatever the last flush could not send is dropped with its buffer.
|
|
555
716
|
conn.release()
|
|
556
717
|
}
|
|
557
718
|
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* One TLS 1.3 client connection over TCP: a `TlsRecordClient` driving a
|
|
722
|
+
* `TlsClient` on a socket `tcpConnect` opened, for a program that owns its
|
|
723
|
+
* readiness loop, as `TlsTcpServer` is for a server.
|
|
724
|
+
*
|
|
725
|
+
* const client = new TlsTcpClient(addr, new TlsClientConfig("example.com", ["h2"]), trust);
|
|
726
|
+
* pollAdd(loop, client.fd(), client.interest() & 3, TOKEN);
|
|
727
|
+
* // when the token comes back:
|
|
728
|
+
* let wants: i32 = (events & 5) !== 0 ? client.readable() : client.writable();
|
|
729
|
+
* if ((wants & TLS_RECORD_DATA) !== 0) { client.read(buf, 0, buf.length); ... }
|
|
730
|
+
* if ((wants & TLS_RECORD_DONE) !== 0) { client.close(); }
|
|
731
|
+
* else { pollModify(loop, client.fd(), wants & 3, TOKEN); }
|
|
732
|
+
*
|
|
733
|
+
* **The server is always authenticated.** When the handshake reaches the
|
|
734
|
+
* server's certificate chain and CertificateVerify, the client gives its own
|
|
735
|
+
* verdict at once (`tlsClientAuthenticate` in `nish/net/tls/verify`) under
|
|
736
|
+
* `trust` and the wall clock, before it reads the server's Finished, so
|
|
737
|
+
* `interest` never says `TLS_RECORD_AUTH` and there is no way to open a
|
|
738
|
+
* connection to a server the trust does not vouch for. A refusal fails the
|
|
739
|
+
* connection with its alert sent: `failure()` names it.
|
|
740
|
+
*
|
|
741
|
+
* **Randomness is the CSPRNG's.** The client random and both ephemeral keys
|
|
742
|
+
* (x25519, and P-256 for a server that asks for that group) are drawn from
|
|
743
|
+
* `crypto.getRandomValues` in the constructor; `TlsClient` copies the keys
|
|
744
|
+
* into its own `Secret`s and the arrays they were drawn into are zeroed.
|
|
745
|
+
*
|
|
746
|
+
* The socket is still connecting when the constructor returns, so the first
|
|
747
|
+
* interest is writable alone; the first event on it, writable or a hang-up,
|
|
748
|
+
* says how the connection went (`connectResult`), and a socket that did not
|
|
749
|
+
* connect leaves the client done, its failure in `connectError`.
|
|
750
|
+
*/
|
|
751
|
+
export class TlsTcpClient {
|
|
752
|
+
conn: TlsRecordClient
|
|
753
|
+
trust: TlsClientTrust
|
|
754
|
+
/** The socket, -1 once `close` has run or when `tcpConnect` refused at once. */
|
|
755
|
+
socket: i32 = -1
|
|
756
|
+
/** Whether the socket has yet to say how its connection went. */
|
|
757
|
+
connecting: boolean = true
|
|
758
|
+
/**
|
|
759
|
+
* Why the connection failed below TLS, or 0: `tcpConnect`'s or
|
|
760
|
+
* `connectResult`'s failure, or what ended `tlsConnect`'s wait — its
|
|
761
|
+
* timeout (`TLS_TCP_TIMED_OUT`) or its loop's failure.
|
|
762
|
+
*/
|
|
763
|
+
connectError: i32 = 0
|
|
764
|
+
/** Where the socket is read into before the connection takes the bytes: the largest record. */
|
|
765
|
+
scratch: u8[]
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* A connection to the server at `addr` (`nish:net`'s 18-byte form, which
|
|
769
|
+
* `netAddress` writes) under `config`, authenticated against `trust`. The
|
|
770
|
+
* name the certificate must carry is `config.serverName`, never one taken
|
|
771
|
+
* from `addr`. The ClientHello is sealed here and waits for the socket.
|
|
772
|
+
*/
|
|
773
|
+
constructor(addr: u8[], config: TlsClientConfig, trust: TlsClientTrust) {
|
|
774
|
+
const random: u8[] = new Array<u8>(TLS_TCP_KEY_SIZE)
|
|
775
|
+
const x25519: u8[] = new Array<u8>(TLS_TCP_KEY_SIZE)
|
|
776
|
+
const p256: u8[] = new Array<u8>(TLS_TCP_KEY_SIZE)
|
|
777
|
+
crypto.getRandomValues(random)
|
|
778
|
+
crypto.getRandomValues(x25519)
|
|
779
|
+
crypto.getRandomValues(p256)
|
|
780
|
+
// `TlsClient` copies both keys and zeroes these arrays.
|
|
781
|
+
this.conn = new TlsRecordClient(new TlsClient(config, random, x25519, p256))
|
|
782
|
+
this.trust = trust
|
|
783
|
+
this.scratch = new Array<u8>(TLS_MAX_RECORD)
|
|
784
|
+
// A config `TlsClient` refused (a bad name or protocol) is never dialled:
|
|
785
|
+
// the client is failed already, its alert in `failure()`.
|
|
786
|
+
if (this.conn.state === TLS_RECORD_STATE_FAILED) {
|
|
787
|
+
this.connecting = false
|
|
788
|
+
return
|
|
789
|
+
}
|
|
790
|
+
const fd: i32 = toI32(addr.length) < TLS_TCP_ADDRESS_SIZE ? TLS_RECORD_INVALID : tcpConnect(addr)
|
|
791
|
+
if (fd < 0) {
|
|
792
|
+
this.connecting = false
|
|
793
|
+
this.connectError = fd
|
|
794
|
+
this.conn.abort()
|
|
795
|
+
return
|
|
796
|
+
}
|
|
797
|
+
this.socket = fd
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** The socket, for `pollAdd`; -1 when there is none. */
|
|
801
|
+
fd(): i32 {
|
|
802
|
+
return this.socket
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/** Whether the handshake is done and the server authenticated, so application data flows. */
|
|
806
|
+
open(): boolean {
|
|
807
|
+
return this.conn.state === TLS_RECORD_STATE_OPEN
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/** Why a failed connection failed: `connectError` when TCP did not connect, else the alert (`TlsRecordClient.failure`). */
|
|
811
|
+
failure(): i32 {
|
|
812
|
+
return this.connectError !== 0 ? this.connectError : this.conn.failure()
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** What the connection wants now, as `TLS_RECORD_*` bits, without touching the socket. */
|
|
816
|
+
interest(): i32 {
|
|
817
|
+
if (this.socket < 0) {
|
|
818
|
+
return TLS_RECORD_DONE
|
|
819
|
+
}
|
|
820
|
+
return this.connecting ? TLS_RECORD_WANT_WRITE : this.conn.interest()
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* The first event on a connecting socket: whether it connected. One that
|
|
825
|
+
* did not leaves the client failed with nothing sent, its reason in
|
|
826
|
+
* `connectError`.
|
|
827
|
+
*/
|
|
828
|
+
connected(): boolean {
|
|
829
|
+
this.connecting = false
|
|
830
|
+
const result: i32 = connectResult(this.socket)
|
|
831
|
+
if (result !== 0) {
|
|
832
|
+
this.connectError = result
|
|
833
|
+
this.conn.abort()
|
|
834
|
+
return false
|
|
835
|
+
}
|
|
836
|
+
return true
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* The socket is readable, or hung up: reads it until it would block or the
|
|
841
|
+
* connection has no room, handing each read to the connection, then sends
|
|
842
|
+
* what that produced. Answers the `interest`.
|
|
843
|
+
*/
|
|
844
|
+
readable(): i32 {
|
|
845
|
+
if (this.socket < 0) {
|
|
846
|
+
return TLS_RECORD_DONE
|
|
847
|
+
}
|
|
848
|
+
if (this.connecting && !this.connected()) {
|
|
849
|
+
return TLS_RECORD_DONE
|
|
850
|
+
}
|
|
851
|
+
const conn: TlsRecordClient = this.conn
|
|
852
|
+
while (true) {
|
|
853
|
+
const room: i32 = conn.space()
|
|
854
|
+
const want: i32 = room < toI32(this.scratch.length) ? room : toI32(this.scratch.length)
|
|
855
|
+
if (want <= 0 || conn.state === TLS_RECORD_STATE_FAILED || conn.peerClosed) {
|
|
856
|
+
break
|
|
857
|
+
}
|
|
858
|
+
const n: i32 = netRead(this.socket, this.scratch, TLS_TCP_FROM, want)
|
|
859
|
+
if (n <= 0) {
|
|
860
|
+
if (n === 0) {
|
|
861
|
+
conn.end()
|
|
862
|
+
} else if (n !== TLS_RECORD_AGAIN) {
|
|
863
|
+
conn.abort()
|
|
864
|
+
}
|
|
865
|
+
break
|
|
866
|
+
}
|
|
867
|
+
conn.receive(this.scratch, TLS_TCP_FROM, n)
|
|
868
|
+
}
|
|
869
|
+
return this.flush()
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/** The socket is writable: on the first event, how the connection went; then sends what the connection holds. Answers the `interest`. */
|
|
873
|
+
writable(): i32 {
|
|
874
|
+
if (this.socket < 0) {
|
|
875
|
+
return TLS_RECORD_DONE
|
|
876
|
+
}
|
|
877
|
+
if (this.connecting && !this.connected()) {
|
|
878
|
+
return TLS_RECORD_DONE
|
|
879
|
+
}
|
|
880
|
+
return this.flush()
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Authenticates the server once the handshake asks for it, then writes the
|
|
885
|
+
* connection's output until the socket would block or it is all sent, and
|
|
886
|
+
* answers the `interest`.
|
|
887
|
+
*/
|
|
888
|
+
flush(): i32 {
|
|
889
|
+
if (this.socket < 0) {
|
|
890
|
+
return TLS_RECORD_DONE
|
|
891
|
+
}
|
|
892
|
+
if (this.connecting) {
|
|
893
|
+
return TLS_RECORD_WANT_WRITE
|
|
894
|
+
}
|
|
895
|
+
if ((this.conn.interest() & TLS_RECORD_AUTH) !== 0) {
|
|
896
|
+
tlsClientAuthenticate(this.conn, this.trust, wallClockNanos() / TLS_TCP_NANOS_PER_MS)
|
|
897
|
+
}
|
|
898
|
+
const conn: TlsRecordClient = this.conn
|
|
899
|
+
while (conn.outputEnd > conn.outputStart) {
|
|
900
|
+
const n: i32 = netWrite(this.socket, conn.output, conn.outputStart, conn.outputEnd - conn.outputStart)
|
|
901
|
+
if (n <= 0) {
|
|
902
|
+
if (n !== TLS_RECORD_AGAIN) {
|
|
903
|
+
conn.abort()
|
|
904
|
+
}
|
|
905
|
+
break
|
|
906
|
+
}
|
|
907
|
+
conn.consume(n)
|
|
908
|
+
}
|
|
909
|
+
return conn.interest()
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Application data from the server into `buf[off .. off + len)`: the count,
|
|
914
|
+
* 0 at the end of its stream, -11 when nothing is waiting, -104 once the
|
|
915
|
+
* connection has failed, as `TlsRecordClient.read` says, and -32 once
|
|
916
|
+
* `close` has run. What reading makes room for is answered and sent here;
|
|
917
|
+
* re-arm with `interest()`.
|
|
918
|
+
*/
|
|
919
|
+
read(buf: u8[], off: i32, len: i32): i32 {
|
|
920
|
+
if (this.socket < 0) {
|
|
921
|
+
return this.ended()
|
|
922
|
+
}
|
|
923
|
+
const n: i32 = this.conn.read(buf, off, len)
|
|
924
|
+
this.flush()
|
|
925
|
+
return n
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/** What a call on a client that is done answers: -104 when it failed, else -32. */
|
|
929
|
+
ended(): i32 {
|
|
930
|
+
return this.conn.state === TLS_RECORD_STATE_FAILED ? TLS_RECORD_RESET : TLS_RECORD_PIPE
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Application data from the server, waiting for it up to `timeoutMs`
|
|
935
|
+
* milliseconds in a loop of the client's own, for a program with no loop:
|
|
936
|
+
* the count, 0 at the end of the server's stream, `TLS_TCP_TIMED_OUT` when
|
|
937
|
+
* nothing came in time (the connection stays usable), -104 once it has
|
|
938
|
+
* failed, -32 once it is done otherwise, or the loop's failure. It drives
|
|
939
|
+
* the handshake too, so it may follow `new TlsTcpClient` directly.
|
|
940
|
+
*/
|
|
941
|
+
readWait(buf: u8[], off: i32, len: i32, timeoutMs: i32): i32 {
|
|
942
|
+
const end: i64 = tlsTcpDeadline(timeoutMs)
|
|
943
|
+
let n: i32 = this.read(buf, off, len)
|
|
944
|
+
if (n !== TLS_RECORD_AGAIN) {
|
|
945
|
+
return n
|
|
946
|
+
}
|
|
947
|
+
const loop: i32 = this.ownLoop()
|
|
948
|
+
if (loop < 0) {
|
|
949
|
+
return loop
|
|
950
|
+
}
|
|
951
|
+
while (n === TLS_RECORD_AGAIN) {
|
|
952
|
+
const turned: i32 = this.turn(loop, end)
|
|
953
|
+
if (turned === TLS_RECORD_DONE) {
|
|
954
|
+
n = this.read(buf, off, len)
|
|
955
|
+
if (n === TLS_RECORD_AGAIN) {
|
|
956
|
+
n = this.ended()
|
|
957
|
+
}
|
|
958
|
+
break
|
|
959
|
+
}
|
|
960
|
+
if (turned < 0) {
|
|
961
|
+
n = turned
|
|
962
|
+
break
|
|
963
|
+
}
|
|
964
|
+
n = this.read(buf, off, len)
|
|
965
|
+
}
|
|
966
|
+
netClose(loop)
|
|
967
|
+
return n
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Writes all of `data[off .. off + len)` and waits up to `timeoutMs`
|
|
972
|
+
* milliseconds, in a loop of the client's own, until the socket has taken
|
|
973
|
+
* every byte of it: `len`, or — some of it perhaps sent — `TLS_TCP_TIMED_OUT`,
|
|
974
|
+
* -32 when the client had closed or failed before the call, -104 when the
|
|
975
|
+
* connection fails during it, -22 for a window outside `data`, or the
|
|
976
|
+
* loop's failure. It drives the handshake too, so it may follow
|
|
977
|
+
* `new TlsTcpClient` directly. A timed-out call does not say how much was
|
|
978
|
+
* sent, and the same bytes written again would reach the server twice:
|
|
979
|
+
* once the handshake is done, a timeout leaves the stream unusable, and
|
|
980
|
+
* the client is for `close`.
|
|
981
|
+
*/
|
|
982
|
+
writeAll(data: u8[], off: i32, len: i32, timeoutMs: i32): i32 {
|
|
983
|
+
const end: i64 = tlsTcpDeadline(timeoutMs)
|
|
984
|
+
let done: i32 = 0
|
|
985
|
+
let loop: i32 = -1
|
|
986
|
+
let result: i32 = 0
|
|
987
|
+
while (true) {
|
|
988
|
+
if (done < len) {
|
|
989
|
+
const n: i32 = this.write(data, off + done, len - done)
|
|
990
|
+
if (n >= 0) {
|
|
991
|
+
done = done + n
|
|
992
|
+
} else if (n !== TLS_RECORD_AGAIN) {
|
|
993
|
+
result = n
|
|
994
|
+
break
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (done >= len && !this.sending()) {
|
|
998
|
+
break
|
|
999
|
+
}
|
|
1000
|
+
if (loop < 0) {
|
|
1001
|
+
loop = this.ownLoop()
|
|
1002
|
+
if (loop < 0) {
|
|
1003
|
+
result = loop
|
|
1004
|
+
break
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
const turned: i32 = this.turn(loop, end)
|
|
1008
|
+
if (turned === TLS_RECORD_DONE) {
|
|
1009
|
+
result = this.ended()
|
|
1010
|
+
break
|
|
1011
|
+
}
|
|
1012
|
+
if (turned < 0) {
|
|
1013
|
+
result = turned
|
|
1014
|
+
break
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
if (loop >= 0) {
|
|
1018
|
+
netClose(loop)
|
|
1019
|
+
}
|
|
1020
|
+
return result < 0 ? result : done
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/** Whether sealed bytes are still waiting for the socket. */
|
|
1024
|
+
sending(): boolean {
|
|
1025
|
+
return this.conn.outputEnd > this.conn.outputStart
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/** A readiness loop of the client's own holding its socket, for the waiting calls, or the loop's failure. */
|
|
1029
|
+
ownLoop(): i32 {
|
|
1030
|
+
const loop: i32 = pollCreate()
|
|
1031
|
+
if (loop >= 0) {
|
|
1032
|
+
pollAdd(loop, this.socket, this.interest() & TLS_TCP_POLL_EVENTS, TLS_TCP_TOKEN)
|
|
1033
|
+
}
|
|
1034
|
+
return loop
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* One turn of `loop` (from `ownLoop`): waits until the socket is ready, or
|
|
1039
|
+
* until `end` on the monotonic clock, and handles what came. Answers 0,
|
|
1040
|
+
* `TLS_RECORD_DONE` once the connection wants nothing more,
|
|
1041
|
+
* `TLS_TCP_TIMED_OUT`, or the loop's failure.
|
|
1042
|
+
*/
|
|
1043
|
+
turn(loop: i32, end: i64): i32 {
|
|
1044
|
+
const wants: i32 = this.interest()
|
|
1045
|
+
if ((wants & TLS_RECORD_DONE) !== 0) {
|
|
1046
|
+
return TLS_RECORD_DONE
|
|
1047
|
+
}
|
|
1048
|
+
const now: i64 = monotonicNanos()
|
|
1049
|
+
if (now >= end) {
|
|
1050
|
+
return TLS_TCP_TIMED_OUT
|
|
1051
|
+
}
|
|
1052
|
+
pollModify(loop, this.socket, wants & TLS_TCP_POLL_EVENTS, TLS_TCP_TOKEN)
|
|
1053
|
+
const ready: i32[] = new Array<i32>(2)
|
|
1054
|
+
const count: i32 = pollWait(
|
|
1055
|
+
loop,
|
|
1056
|
+
ready,
|
|
1057
|
+
toI32((end - now + TLS_TCP_NANOS_PER_MS - 1) / TLS_TCP_NANOS_PER_MS)
|
|
1058
|
+
)
|
|
1059
|
+
if (count < 0) {
|
|
1060
|
+
return count
|
|
1061
|
+
}
|
|
1062
|
+
if (count > 0) {
|
|
1063
|
+
if ((ready[1] & TLS_TCP_READ_EVENTS) !== 0) {
|
|
1064
|
+
this.readable()
|
|
1065
|
+
} else {
|
|
1066
|
+
this.writable()
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return 0
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Application data for the server, `data[off .. off + len)`: seals as much
|
|
1074
|
+
* as fits and sends what the socket takes, and answers how much was taken,
|
|
1075
|
+
* -11 when nothing fits yet or the handshake is under way, -32 once the
|
|
1076
|
+
* client has closed, as `TlsRecordClient.write` says.
|
|
1077
|
+
*/
|
|
1078
|
+
write(data: u8[], off: i32, len: i32): i32 {
|
|
1079
|
+
const n: i32 = this.conn.write(data, off, len)
|
|
1080
|
+
this.flush()
|
|
1081
|
+
return n
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/** Sends a KeyUpdate, asking the server for one too when `requestPeer`; answers as `TlsRecordClient.keyUpdate`. */
|
|
1085
|
+
keyUpdate(requestPeer: boolean): i32 {
|
|
1086
|
+
const r: i32 = this.conn.keyUpdate(requestPeer)
|
|
1087
|
+
this.flush()
|
|
1088
|
+
return r
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Closes the client's side (§6.1): a `close_notify` after whatever is
|
|
1093
|
+
* already written, sent as the socket takes it. The client does not wait
|
|
1094
|
+
* for the server's own `close_notify`: data already read off the socket
|
|
1095
|
+
* still reads, and once the `close_notify` is sent the interest says
|
|
1096
|
+
* `TLS_RECORD_DONE` and `readWait` answers -32; call `close` then.
|
|
1097
|
+
* Answers the interest.
|
|
1098
|
+
*/
|
|
1099
|
+
shutdown(): i32 {
|
|
1100
|
+
this.conn.close()
|
|
1101
|
+
return this.flush()
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Ends the connection at once: a `close_notify` unless it already failed
|
|
1106
|
+
* or closed, one attempt to send what is left, every key wiped and the
|
|
1107
|
+
* socket closed, which also takes it out of every loop.
|
|
1108
|
+
*/
|
|
1109
|
+
close(): void {
|
|
1110
|
+
if (this.socket < 0) {
|
|
1111
|
+
this.conn.wipe()
|
|
1112
|
+
return
|
|
1113
|
+
}
|
|
1114
|
+
if (!this.connecting) {
|
|
1115
|
+
this.conn.close()
|
|
1116
|
+
this.flush()
|
|
1117
|
+
}
|
|
1118
|
+
netClose(this.socket)
|
|
1119
|
+
this.socket = -1
|
|
1120
|
+
this.conn.wipe()
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/** The monotonic clock `timeoutMs` milliseconds from now. */
|
|
1125
|
+
const tlsTcpDeadline = (timeoutMs: i32): i64 => monotonicNanos() + toI64(timeoutMs) * TLS_TCP_NANOS_PER_MS
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Opens a TLS 1.3 connection to the server at `addr` (`nish:net`'s 18-byte
|
|
1129
|
+
* form) for `serverName`, offering `alpn`, authenticated against the
|
|
1130
|
+
* system's roots, and waits in a loop of its own until the handshake is done
|
|
1131
|
+
* and the client's last flight sent, or it fails, or `timeoutMs` milliseconds
|
|
1132
|
+
* pass, reading the roots included. Answers the client either way:
|
|
1133
|
+
* `open()` says whether it opened; otherwise `failure()` says why — the TCP
|
|
1134
|
+
* failure, the alert, or `TLS_TCP_TIMED_OUT`, the client closed. Nothing is
|
|
1135
|
+
* looked up: to reach a name, resolve it first with `nish/net/dns`'s
|
|
1136
|
+
* `resolveHost`; the name the certificate must carry is always `serverName`,
|
|
1137
|
+
* never one taken from `addr`. The open client's socket is in no loop: the
|
|
1138
|
+
* program adds it to its own, or uses `writeAll` and `readWait`. The roots
|
|
1139
|
+
* are read on every call; a program that connects often reads them once with
|
|
1140
|
+
* `tlsClientSystemTrust` and passes them to `tlsConnectWith`.
|
|
1141
|
+
*/
|
|
1142
|
+
export const tlsConnect = (addr: u8[], serverName: string, alpn: string[], timeoutMs: i32): TlsTcpClient => {
|
|
1143
|
+
const end: i64 = tlsTcpDeadline(timeoutMs)
|
|
1144
|
+
return tlsConnectBy(addr, new TlsClientConfig(serverName, alpn), tlsClientSystemTrust(), end)
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/** `tlsConnect` under a `config` and `trust` of the caller's. */
|
|
1148
|
+
export const tlsConnectWith = (
|
|
1149
|
+
addr: u8[],
|
|
1150
|
+
config: TlsClientConfig,
|
|
1151
|
+
trust: TlsClientTrust,
|
|
1152
|
+
timeoutMs: i32
|
|
1153
|
+
): TlsTcpClient => tlsConnectBy(addr, config, trust, tlsTcpDeadline(timeoutMs))
|
|
1154
|
+
|
|
1155
|
+
/** `tlsConnectWith`, waiting until `end` on the monotonic clock. */
|
|
1156
|
+
const tlsConnectBy = (addr: u8[], config: TlsClientConfig, trust: TlsClientTrust, end: i64): TlsTcpClient => {
|
|
1157
|
+
const client: TlsTcpClient = new TlsTcpClient(addr, config, trust)
|
|
1158
|
+
if (client.fd() < 0) {
|
|
1159
|
+
return client
|
|
1160
|
+
}
|
|
1161
|
+
const loop: i32 = client.ownLoop()
|
|
1162
|
+
if (loop < 0) {
|
|
1163
|
+
client.connectError = loop
|
|
1164
|
+
client.close()
|
|
1165
|
+
return client
|
|
1166
|
+
}
|
|
1167
|
+
// The connection opens on the server's Finished, with the client's own
|
|
1168
|
+
// perhaps still queued: it is sent before the client is handed back.
|
|
1169
|
+
while (!client.open() || client.sending()) {
|
|
1170
|
+
const turned: i32 = client.turn(loop, end)
|
|
1171
|
+
if (turned === TLS_RECORD_DONE) {
|
|
1172
|
+
break
|
|
1173
|
+
}
|
|
1174
|
+
// A refusal whose alert could not be flushed in time is still the
|
|
1175
|
+
// refusal: the timeout is the reason only while the handshake lives.
|
|
1176
|
+
if (turned < 0) {
|
|
1177
|
+
if (client.conn.state !== TLS_RECORD_STATE_FAILED) {
|
|
1178
|
+
client.connectError = turned
|
|
1179
|
+
}
|
|
1180
|
+
break
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
netClose(loop)
|
|
1184
|
+
if (!client.open() || client.connectError !== 0) {
|
|
1185
|
+
client.close()
|
|
1186
|
+
}
|
|
1187
|
+
return client
|
|
1188
|
+
}
|