@amritk/nish-x86_64-linux 0.21.0 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +30 -0
- package/bin/nish +0 -0
- package/docs/AI.md +18 -0
- package/package.json +1 -1
- package/runtime/browser.d.ts +1 -9
- package/runtime/browser.mjs +2 -13
- package/runtime/core/arrays.d.ts +26 -8
- package/runtime/core/arrays.mjs +31 -3
- package/runtime/core/host.d.ts +10 -0
- package/runtime/core/host.mjs +14 -0
- package/runtime/core/numbers.d.ts +14 -1
- package/runtime/core/numbers.mjs +30 -3
- package/runtime/core/result.d.ts +4 -0
- package/runtime/core/result.mjs +13 -0
- package/runtime/core/strings.d.ts +2 -2
- package/runtime/nish.d.ts +2 -2
- package/runtime/node.mjs +3 -0
- package/runtime/runtime-net.c +12 -1
- package/scripts/build.sh +58 -14
- package/std/README.md +2 -2
- package/std/cli.ts +17 -1
- package/std/crypto/bcrypt.ts +869 -72
- package/std/json.ts +4 -7
- package/std/net/http1-server.ts +199 -34
- package/std/net/http1.ts +40 -21
- package/std/net/postgres.ts +471 -52
- package/std/net/prefork.ts +23 -3
- package/std/random.ts +66 -4
package/std/json.ts
CHANGED
|
@@ -1220,8 +1220,9 @@ export class JsonWriter {
|
|
|
1220
1220
|
* `bytes[off .. off + len)`, copied as it is and counted as one value: a
|
|
1221
1221
|
* fragment the caller encoded already, such as a cached object. Nothing in
|
|
1222
1222
|
* it is checked, so a fragment that is not one whole JSON value makes a
|
|
1223
|
-
* text that is not JSON either. The bytes are copied
|
|
1224
|
-
* `
|
|
1223
|
+
* text that is not JSON either. The bytes are copied in one move
|
|
1224
|
+
* (`copyBytes`), as if all were read before any was written, so a window
|
|
1225
|
+
* of the writer's own buffer comes out as it was.
|
|
1225
1226
|
* In an indented text the fragment goes in where a value would, after its
|
|
1226
1227
|
* line break and indent, but its own bytes are not re-indented: an object
|
|
1227
1228
|
* fragment stays on one line, where `JSON.stringify` would spread it out.
|
|
@@ -1243,11 +1244,7 @@ export class JsonWriter {
|
|
|
1243
1244
|
this.failed = JSON_WRITE_FULL
|
|
1244
1245
|
return
|
|
1245
1246
|
}
|
|
1246
|
-
|
|
1247
|
-
if (off + k >= 0 && at + k >= 0) {
|
|
1248
|
-
buf[at + k] = bytes[off + k]
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1247
|
+
copyBytes(buf, at, bytes, off, len)
|
|
1251
1248
|
this.end = at + len
|
|
1252
1249
|
jsonWriterValueDone(this)
|
|
1253
1250
|
}
|
package/std/net/http1-server.ts
CHANGED
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
* implementation.
|
|
114
114
|
*/
|
|
115
115
|
import { Secret } from "nish:secret"
|
|
116
|
+
import { copyBytes } from "nish/bytes"
|
|
116
117
|
import { netClose, netRead, netWrite, tcpAccept } from "nish:net"
|
|
117
118
|
import {
|
|
118
119
|
HTTP1_BODY,
|
|
@@ -120,6 +121,8 @@ import {
|
|
|
120
121
|
HTTP1_CLOSED,
|
|
121
122
|
HTTP1_END,
|
|
122
123
|
HTTP1_ERROR,
|
|
124
|
+
HTTP1_FIRST_HEAD,
|
|
125
|
+
HTTP1_FIRST_INPUT,
|
|
123
126
|
HTTP1_HEAD,
|
|
124
127
|
HTTP1_NO_BODY,
|
|
125
128
|
HTTP1_REFUSED,
|
|
@@ -240,6 +243,11 @@ const http1ServerCheckWindow = (what: string, buf: u8[], off: i32, len: i32): vo
|
|
|
240
243
|
|
|
241
244
|
/** Copies `src[off .. off + n)` to `out` at `at`, which the caller has made sure holds it, and answers where it ends. */
|
|
242
245
|
const http1ServerCopy = (out: u8[], at: i32, src: u8[], off: i32, n: i32): i32 => {
|
|
246
|
+
if (n > 0 && at >= 0 && off >= 0 && at <= toI32(out.length) - n && off <= toI32(src.length) - n) {
|
|
247
|
+
// Both windows fit, as they do for every caller: one move, not a byte loop.
|
|
248
|
+
copyBytes(out, at, src, off, n)
|
|
249
|
+
return at + n
|
|
250
|
+
}
|
|
243
251
|
for (let k: i32 = 0; k < n && at + k < toI32(out.length) && off + k < toI32(src.length); k++) {
|
|
244
252
|
if (at + k >= 0 && off + k >= 0) {
|
|
245
253
|
out[at + k] = src[off + k]
|
|
@@ -352,6 +360,15 @@ export class Http1Config {
|
|
|
352
360
|
* of its head each `idleTimeout` would hold its slot for good (slowloris).
|
|
353
361
|
*/
|
|
354
362
|
headTimeout: i32 = 30000
|
|
363
|
+
/**
|
|
364
|
+
* Lend a slot's input and head buffers back to the server between
|
|
365
|
+
* exchanges: once a response is sent and no byte of the next request has
|
|
366
|
+
* come, the slot gives both to pools the server's slots share, and takes
|
|
367
|
+
* them again with the next byte. The memory they hold then follows the
|
|
368
|
+
* connections mid-request rather than every keep-alive connection open,
|
|
369
|
+
* for two pool operations a request. Off by default.
|
|
370
|
+
*/
|
|
371
|
+
idleInput: boolean = false
|
|
355
372
|
}
|
|
356
373
|
|
|
357
374
|
/** The reason phrase a status is sent with; RFC 9112 §4 lets a client ignore it. */
|
|
@@ -470,8 +487,12 @@ export class Http1Connection {
|
|
|
470
487
|
date: HttpDate
|
|
471
488
|
/** The request parser: its head (`methodIs`, `targetIs`, `header`, spans) is the last `H1_REQUEST`'s. */
|
|
472
489
|
parser: Http1Parser
|
|
473
|
-
/**
|
|
474
|
-
|
|
490
|
+
/**
|
|
491
|
+
* The frame decoder: made when the slot first switches to WebSocket, so a
|
|
492
|
+
* slot that only ever speaks HTTP never holds its buffers, then kept for
|
|
493
|
+
* the slot's later peers.
|
|
494
|
+
*/
|
|
495
|
+
ws: WsDecoder | null
|
|
475
496
|
/**
|
|
476
497
|
* Bytes to send are `output[outputStart .. outputEnd)`. Empty until the
|
|
477
498
|
* connection first needs room to write: then its own buffer, made once, or
|
|
@@ -482,9 +503,12 @@ export class Http1Connection {
|
|
|
482
503
|
noOutput: u8[]
|
|
483
504
|
/** Where `output` is borrowed from, for a connection of a pool; null for one that keeps its own. */
|
|
484
505
|
pool: Http1OutputPool | null
|
|
506
|
+
/** Where the parser's input and head buffers go while the slot rests, with `Http1Config.idleInput`; null otherwise. */
|
|
507
|
+
inputs: Http1OutputPool | null
|
|
508
|
+
heads: Http1OutputPool | null
|
|
485
509
|
/** `H1_BODY`'s and `H1_WS_MESSAGE`'s bytes. */
|
|
486
510
|
data: u8[]
|
|
487
|
-
/** A ping's payload, kept until its pong fits. */
|
|
511
|
+
/** A ping's payload, kept until its pong fits: empty until the first ping. */
|
|
488
512
|
pong: u8[]
|
|
489
513
|
/** No fields, for the responses the connection writes itself: made once. */
|
|
490
514
|
noFields: u8[][]
|
|
@@ -561,12 +585,14 @@ export class Http1Connection {
|
|
|
561
585
|
this.parser = new Http1Parser(config.maxTarget, config.maxHeaderBytes, config.maxHeaders, config.maxBody)
|
|
562
586
|
this.parser.keepText = false
|
|
563
587
|
this.parser.maxChunk = config.chunkSize
|
|
564
|
-
this.ws =
|
|
588
|
+
this.ws = null
|
|
565
589
|
this.noOutput = []
|
|
566
590
|
this.output = this.noOutput
|
|
567
591
|
this.pool = null
|
|
592
|
+
this.inputs = null
|
|
593
|
+
this.heads = null
|
|
568
594
|
this.data = this.noOutput
|
|
569
|
-
this.pong =
|
|
595
|
+
this.pong = this.noOutput
|
|
570
596
|
this.noFields = []
|
|
571
597
|
// A line must fit whole before the parser can read it, so the buffer
|
|
572
598
|
// holds the longest request line or field line however small a chunk is.
|
|
@@ -584,7 +610,10 @@ export class Http1Connection {
|
|
|
584
610
|
/** Puts the connection back as the constructor left it, for the next peer. It allocates nothing. */
|
|
585
611
|
restart(): void {
|
|
586
612
|
this.parser.restart()
|
|
587
|
-
this.ws
|
|
613
|
+
const ws: WsDecoder | null = this.ws
|
|
614
|
+
if (ws !== null) {
|
|
615
|
+
ws.reset()
|
|
616
|
+
}
|
|
588
617
|
this.outputStart = 0
|
|
589
618
|
this.outputEnd = 0
|
|
590
619
|
this.data = this.noOutput
|
|
@@ -608,6 +637,49 @@ export class Http1Connection {
|
|
|
608
637
|
this.headTimed = false
|
|
609
638
|
this.headSince = 0
|
|
610
639
|
this.release()
|
|
640
|
+
this.park()
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Gives the parser's input and head buffers to `inputs` and `heads` while
|
|
645
|
+
* the connection rests between exchanges: speaking HTTP, no request open,
|
|
646
|
+
* nothing fed and unread. Does nothing without the pools or otherwise.
|
|
647
|
+
*/
|
|
648
|
+
park(): void {
|
|
649
|
+
const inputs: Http1OutputPool | null = this.inputs
|
|
650
|
+
const heads: Http1OutputPool | null = this.heads
|
|
651
|
+
const p: Http1Parser = this.parser
|
|
652
|
+
if (inputs === null || heads === null || this.state !== H1_S_HTTP || this.requestOpen || !p.resting()) {
|
|
653
|
+
return
|
|
654
|
+
}
|
|
655
|
+
if (toI32(p.buf.length) > 0) {
|
|
656
|
+
inputs.give(p.buf)
|
|
657
|
+
p.buf = this.noOutput
|
|
658
|
+
p.body = this.noOutput
|
|
659
|
+
p.start = 0
|
|
660
|
+
p.end = 0
|
|
661
|
+
p.scan = 0
|
|
662
|
+
p.lineStart = 0
|
|
663
|
+
p.lineEnd = 0
|
|
664
|
+
}
|
|
665
|
+
if (toI32(p.head.length) > 0) {
|
|
666
|
+
heads.give(p.head)
|
|
667
|
+
p.head = this.noOutput
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** Takes back the buffers `park` gave away, before bytes are fed. */
|
|
672
|
+
unpark(): void {
|
|
673
|
+
const inputs: Http1OutputPool | null = this.inputs
|
|
674
|
+
const heads: Http1OutputPool | null = this.heads
|
|
675
|
+
const p: Http1Parser = this.parser
|
|
676
|
+
if (inputs !== null && toI32(p.buf.length) === 0) {
|
|
677
|
+
p.buf = inputs.take()
|
|
678
|
+
p.body = p.buf
|
|
679
|
+
}
|
|
680
|
+
if (heads !== null && toI32(p.head.length) === 0) {
|
|
681
|
+
p.head = heads.take()
|
|
682
|
+
}
|
|
611
683
|
}
|
|
612
684
|
|
|
613
685
|
// ---- Bytes in and out ---------------------------------------------------------
|
|
@@ -622,7 +694,9 @@ export class Http1Connection {
|
|
|
622
694
|
if (this.writeHeld || this.inputEnded || this.state === H1_S_CLOSING || this.state === H1_S_FAILED) {
|
|
623
695
|
return 0
|
|
624
696
|
}
|
|
625
|
-
const
|
|
697
|
+
const ws: WsDecoder | null = this.ws
|
|
698
|
+
const held: i32 =
|
|
699
|
+
this.state === H1_S_WEBSOCKET && ws !== null ? ws.end - ws.start : this.parser.buffered()
|
|
626
700
|
const room: i32 = this.inputLimit() - held
|
|
627
701
|
return room > 0 ? room : 0
|
|
628
702
|
}
|
|
@@ -640,8 +714,9 @@ export class Http1Connection {
|
|
|
640
714
|
return 0
|
|
641
715
|
}
|
|
642
716
|
if (this.state === H1_S_WEBSOCKET) {
|
|
643
|
-
this.
|
|
717
|
+
this.decoder().feed(buf, off, n)
|
|
644
718
|
} else {
|
|
719
|
+
this.unpark()
|
|
645
720
|
this.parser.feed(buf, off, n)
|
|
646
721
|
}
|
|
647
722
|
return n
|
|
@@ -727,12 +802,8 @@ export class Http1Connection {
|
|
|
727
802
|
const output: u8[] = this.output
|
|
728
803
|
const live: i32 = this.outputEnd - this.outputStart
|
|
729
804
|
if (this.outputStart > 0) {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
k < live && k < toI32(output.length) && this.outputStart + k < toI32(output.length);
|
|
733
|
-
k++
|
|
734
|
-
) {
|
|
735
|
-
output[k] = output[this.outputStart + k]
|
|
805
|
+
if (live > 0 && this.outputStart <= toI32(output.length) - live) {
|
|
806
|
+
copyBytes(output, 0, output, this.outputStart, live)
|
|
736
807
|
}
|
|
737
808
|
this.outputStart = 0
|
|
738
809
|
this.outputEnd = live
|
|
@@ -784,8 +855,9 @@ export class Http1Connection {
|
|
|
784
855
|
this.response = H1_R_NONE
|
|
785
856
|
if (this.upgraded) {
|
|
786
857
|
const p: Http1Parser = this.parser
|
|
787
|
-
this.
|
|
788
|
-
|
|
858
|
+
const ws: WsDecoder = this.decoder()
|
|
859
|
+
ws.reset()
|
|
860
|
+
ws.feed(p.buf, p.start, p.buffered())
|
|
789
861
|
this.state = H1_S_WEBSOCKET
|
|
790
862
|
return this.nextFrame()
|
|
791
863
|
}
|
|
@@ -867,35 +939,50 @@ export class Http1Connection {
|
|
|
867
939
|
return H1_ERROR
|
|
868
940
|
}
|
|
869
941
|
|
|
942
|
+
/** The frame decoder, made on the slot's first switch to WebSocket. */
|
|
943
|
+
decoder(): WsDecoder {
|
|
944
|
+
const ws: WsDecoder | null = this.ws
|
|
945
|
+
if (ws !== null) {
|
|
946
|
+
return ws
|
|
947
|
+
}
|
|
948
|
+
const made: WsDecoder = new WsDecoder(true, this.config.maxMessage)
|
|
949
|
+
this.ws = made
|
|
950
|
+
return made
|
|
951
|
+
}
|
|
952
|
+
|
|
870
953
|
/** `next` once the slot has switched to WebSocket. */
|
|
871
954
|
nextFrame(): i32 {
|
|
872
955
|
while (true) {
|
|
873
956
|
if (this.pongPending && !this.sendPong()) {
|
|
874
957
|
return H1_NEED_MORE
|
|
875
958
|
}
|
|
876
|
-
const
|
|
959
|
+
const ws: WsDecoder = this.decoder()
|
|
960
|
+
const event: i32 = ws.next()
|
|
877
961
|
if (event === WS_MESSAGE) {
|
|
878
|
-
this.opcode =
|
|
879
|
-
this.data =
|
|
962
|
+
this.opcode = ws.opcode
|
|
963
|
+
this.data = ws.data
|
|
880
964
|
this.dataStart = 0
|
|
881
|
-
this.dataLength =
|
|
965
|
+
this.dataLength = ws.dataLen
|
|
882
966
|
return H1_WS_MESSAGE
|
|
883
967
|
}
|
|
884
968
|
if (event === WS_PING) {
|
|
885
|
-
const n: i32 =
|
|
886
|
-
|
|
887
|
-
this.pong
|
|
969
|
+
const n: i32 = ws.dataLen
|
|
970
|
+
if (this.pong.length === 0) {
|
|
971
|
+
this.pong = new Array<u8>(WS_MAX_CONTROL)
|
|
972
|
+
}
|
|
973
|
+
for (let k: i32 = 0; k < n && k < toI32(this.pong.length) && k < toI32(ws.data.length); k++) {
|
|
974
|
+
this.pong[k] = ws.data[k]
|
|
888
975
|
}
|
|
889
976
|
this.pongLength = n
|
|
890
977
|
this.pongPending = true
|
|
891
978
|
} else if (event === WS_CLOSE) {
|
|
892
|
-
this.status =
|
|
893
|
-
this.sendClose(
|
|
979
|
+
this.status = ws.closeCode
|
|
980
|
+
this.sendClose(ws.closeCode)
|
|
894
981
|
this.state = H1_S_CLOSING
|
|
895
982
|
return H1_WS_CLOSE
|
|
896
983
|
} else if (event === WS_ERROR) {
|
|
897
|
-
this.status =
|
|
898
|
-
this.sendClose(
|
|
984
|
+
this.status = ws.closeCode
|
|
985
|
+
this.sendClose(ws.closeCode)
|
|
899
986
|
this.state = H1_S_FAILED
|
|
900
987
|
return H1_ERROR
|
|
901
988
|
} else if (event === WS_NEED_MORE) {
|
|
@@ -935,7 +1022,7 @@ export class Http1Connection {
|
|
|
935
1022
|
const payload: u8[] | null = websocketClosePayload(code, "")
|
|
936
1023
|
// A code that may not be sent (1005, for a close that had none) is
|
|
937
1024
|
// answered with an empty close.
|
|
938
|
-
const body: u8[] = payload === null ? this.
|
|
1025
|
+
const body: u8[] = payload === null ? this.noOutput : payload
|
|
939
1026
|
const n: i32 = payload === null ? H1_ZERO : toI32(body.length)
|
|
940
1027
|
const end: i32 = websocketWriteFrame(
|
|
941
1028
|
this.output,
|
|
@@ -1245,6 +1332,30 @@ export class Http1Connection {
|
|
|
1245
1332
|
expired(now: i64): boolean {
|
|
1246
1333
|
return this.idle(now) || this.headLate(now)
|
|
1247
1334
|
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* The soonest `expired` can turn true, however the connection moves from
|
|
1338
|
+
* here: `lastActive` only grows, and a head clock not yet started starts
|
|
1339
|
+
* no earlier than it.
|
|
1340
|
+
*/
|
|
1341
|
+
expiresNoSooner(): i64 {
|
|
1342
|
+
const idle: i64 = toI64(this.config.idleTimeout) * toI64(1000000)
|
|
1343
|
+
const head: i64 = toI64(this.config.headTimeout) * toI64(1000000)
|
|
1344
|
+
const soonest: i64 = this.lastActive + (head < idle ? head : idle)
|
|
1345
|
+
if (this.headTimed && this.headSince + head < soonest) {
|
|
1346
|
+
return this.headSince + head
|
|
1347
|
+
}
|
|
1348
|
+
return soonest
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
/**
|
|
1353
|
+
* The soonest any slot accepted after now can expire: no sooner than the
|
|
1354
|
+
* shorter of `idleTimeout` and `headTimeout` from now.
|
|
1355
|
+
*/
|
|
1356
|
+
const http1ExpireFloor = (config: Http1Config): i64 => {
|
|
1357
|
+
const shorter: i32 = config.headTimeout < config.idleTimeout ? config.headTimeout : config.idleTimeout
|
|
1358
|
+
return monotonicNanos() + toI64(shorter) * toI64(1000000)
|
|
1248
1359
|
}
|
|
1249
1360
|
|
|
1250
1361
|
// ---- Carriers -------------------------------------------------------------------
|
|
@@ -1273,6 +1384,9 @@ export class Http1Server {
|
|
|
1273
1384
|
armed: i32[]
|
|
1274
1385
|
/** The output buffers the slots borrow while they write. */
|
|
1275
1386
|
outputs: Http1OutputPool
|
|
1387
|
+
/** The input and head buffers resting slots lend, with `Http1Config.idleInput`. */
|
|
1388
|
+
inputs: Http1OutputPool
|
|
1389
|
+
heads: Http1OutputPool
|
|
1276
1390
|
/**
|
|
1277
1391
|
* The slots closed since they were used, a stack whose top `accept` takes
|
|
1278
1392
|
* before it builds a slot past the high-water mark: finding one costs the
|
|
@@ -1288,6 +1402,14 @@ export class Http1Server {
|
|
|
1288
1402
|
slots: i32 = 1
|
|
1289
1403
|
/** The listening socket, which the program made with `tcpListen` and still owns. */
|
|
1290
1404
|
listener: i32 = -1
|
|
1405
|
+
/**
|
|
1406
|
+
* No slot can expire before this (`monotonicNanos`), so `expire` called
|
|
1407
|
+
* sooner walks none: it walks every slot only about once per the shorter
|
|
1408
|
+
* timeout, however many there are.
|
|
1409
|
+
*/
|
|
1410
|
+
expireAfter: i64 = 0
|
|
1411
|
+
/** How many calls of `expire` walked every slot, for a program measuring its cost. */
|
|
1412
|
+
expireScans: i32 = 0
|
|
1291
1413
|
|
|
1292
1414
|
/**
|
|
1293
1415
|
* A pool of `size` slots, at least one, serving `config` on `listener`.
|
|
@@ -1305,6 +1427,8 @@ export class Http1Server {
|
|
|
1305
1427
|
this.free = []
|
|
1306
1428
|
this.slots = size < 1 ? 1 : size
|
|
1307
1429
|
this.outputs = new Http1OutputPool(config.outputSize)
|
|
1430
|
+
this.inputs = new Http1OutputPool(HTTP1_FIRST_INPUT)
|
|
1431
|
+
this.heads = new Http1OutputPool(HTTP1_FIRST_HEAD)
|
|
1308
1432
|
this.date = new HttpDate()
|
|
1309
1433
|
this.scratch = new Array<u8>(config.chunkSize)
|
|
1310
1434
|
this.peer = new Array<u8>(H1_ADDRESS_SIZE)
|
|
@@ -1315,6 +1439,10 @@ export class Http1Server {
|
|
|
1315
1439
|
const conn: Http1Connection = new Http1Connection(this.config)
|
|
1316
1440
|
conn.pool = this.outputs
|
|
1317
1441
|
conn.date = this.date
|
|
1442
|
+
if (this.config.idleInput) {
|
|
1443
|
+
conn.inputs = this.inputs
|
|
1444
|
+
conn.heads = this.heads
|
|
1445
|
+
}
|
|
1318
1446
|
this.connections.push(conn)
|
|
1319
1447
|
return conn
|
|
1320
1448
|
}
|
|
@@ -1473,6 +1601,7 @@ export class Http1Server {
|
|
|
1473
1601
|
if (conn.isDone()) {
|
|
1474
1602
|
return H1_DONE
|
|
1475
1603
|
}
|
|
1604
|
+
conn.park()
|
|
1476
1605
|
let wants: i32 = 0
|
|
1477
1606
|
if (conn.inputRoom() > 0) {
|
|
1478
1607
|
wants = wants | H1_WANT_READ
|
|
@@ -1538,13 +1667,24 @@ export class Http1Server {
|
|
|
1538
1667
|
* head for longer than `headTimeout`, at `now` (`monotonicNanos`); answers how many.
|
|
1539
1668
|
*/
|
|
1540
1669
|
expire(now: i64): i32 {
|
|
1670
|
+
if (now < this.expireAfter) {
|
|
1671
|
+
return 0
|
|
1672
|
+
}
|
|
1673
|
+
this.expireScans = this.expireScans + 1
|
|
1674
|
+
let soonest: i64 = http1ExpireFloor(this.config)
|
|
1541
1675
|
let n: i32 = 0
|
|
1542
1676
|
for (let k: i32 = 0; k < toI32(this.fds.length) && k < toI32(this.connections.length); k++) {
|
|
1543
|
-
if (this.fds[k] >= 0
|
|
1544
|
-
this.
|
|
1545
|
-
|
|
1677
|
+
if (this.fds[k] >= 0) {
|
|
1678
|
+
const conn = this.connections[k]
|
|
1679
|
+
if (conn.expired(now)) {
|
|
1680
|
+
this.close(k)
|
|
1681
|
+
n = n + 1
|
|
1682
|
+
} else if (conn.expiresNoSooner() < soonest) {
|
|
1683
|
+
soonest = conn.expiresNoSooner()
|
|
1684
|
+
}
|
|
1546
1685
|
}
|
|
1547
1686
|
}
|
|
1687
|
+
this.expireAfter = soonest
|
|
1548
1688
|
return n
|
|
1549
1689
|
}
|
|
1550
1690
|
}
|
|
@@ -1580,6 +1720,13 @@ export class Http1TlsServer {
|
|
|
1580
1720
|
scratch: u8[]
|
|
1581
1721
|
/** The output buffers the slots borrow while they write. */
|
|
1582
1722
|
outputs: Http1OutputPool
|
|
1723
|
+
/** The input and head buffers resting slots lend, with `Http1Config.idleInput`. */
|
|
1724
|
+
inputs: Http1OutputPool
|
|
1725
|
+
heads: Http1OutputPool
|
|
1726
|
+
/** No slot can expire before this (`monotonicNanos`): `Http1Server.expireAfter`. */
|
|
1727
|
+
expireAfter: i64 = 0
|
|
1728
|
+
/** How many calls of `expire` walked every slot. */
|
|
1729
|
+
expireScans: i32 = 0
|
|
1583
1730
|
|
|
1584
1731
|
/**
|
|
1585
1732
|
* A pool of `size` slots, at least one, serving `config` over TLS under
|
|
@@ -1593,6 +1740,8 @@ export class Http1TlsServer {
|
|
|
1593
1740
|
this.tls = new TlsTcpServer(tlsConfig, listener, size)
|
|
1594
1741
|
this.connections = []
|
|
1595
1742
|
this.outputs = new Http1OutputPool(config.outputSize)
|
|
1743
|
+
this.inputs = new Http1OutputPool(HTTP1_FIRST_INPUT)
|
|
1744
|
+
this.heads = new Http1OutputPool(HTTP1_FIRST_HEAD)
|
|
1596
1745
|
this.date = new HttpDate()
|
|
1597
1746
|
this.scratch = new Array<u8>(config.chunkSize)
|
|
1598
1747
|
}
|
|
@@ -1613,6 +1762,10 @@ export class Http1TlsServer {
|
|
|
1613
1762
|
const conn: Http1Connection = new Http1Connection(this.config)
|
|
1614
1763
|
conn.pool = this.outputs
|
|
1615
1764
|
conn.date = this.date
|
|
1765
|
+
if (this.config.idleInput) {
|
|
1766
|
+
conn.inputs = this.inputs
|
|
1767
|
+
conn.heads = this.heads
|
|
1768
|
+
}
|
|
1616
1769
|
return conn
|
|
1617
1770
|
}
|
|
1618
1771
|
|
|
@@ -1776,6 +1929,7 @@ export class Http1TlsServer {
|
|
|
1776
1929
|
if (conn.isDone()) {
|
|
1777
1930
|
return this.tls.shutdown(slot)
|
|
1778
1931
|
}
|
|
1932
|
+
conn.park()
|
|
1779
1933
|
const wants: i32 = this.tls.interest(slot)
|
|
1780
1934
|
return conn.wantsWrite() && (wants & TLS_RECORD_DONE) === 0 ? wants | TLS_RECORD_WANT_WRITE : wants
|
|
1781
1935
|
}
|
|
@@ -1816,13 +1970,24 @@ export class Http1TlsServer {
|
|
|
1816
1970
|
* head for longer than `headTimeout`, at `now` (`monotonicNanos`); answers how many.
|
|
1817
1971
|
*/
|
|
1818
1972
|
expire(now: i64): i32 {
|
|
1973
|
+
if (now < this.expireAfter) {
|
|
1974
|
+
return 0
|
|
1975
|
+
}
|
|
1976
|
+
this.expireScans = this.expireScans + 1
|
|
1977
|
+
let soonest: i64 = http1ExpireFloor(this.config)
|
|
1819
1978
|
let n: i32 = 0
|
|
1820
1979
|
for (let k: i32 = 0; k < this.tls.size() && k < toI32(this.connections.length); k++) {
|
|
1821
|
-
if (this.tls.holds(k)
|
|
1822
|
-
this.
|
|
1823
|
-
|
|
1980
|
+
if (this.tls.holds(k)) {
|
|
1981
|
+
const conn = this.connections[k]
|
|
1982
|
+
if (conn.expired(now)) {
|
|
1983
|
+
this.tls.close(k)
|
|
1984
|
+
n = n + 1
|
|
1985
|
+
} else if (conn.expiresNoSooner() < soonest) {
|
|
1986
|
+
soonest = conn.expiresNoSooner()
|
|
1987
|
+
}
|
|
1824
1988
|
}
|
|
1825
1989
|
}
|
|
1990
|
+
this.expireAfter = soonest
|
|
1826
1991
|
return n
|
|
1827
1992
|
}
|
|
1828
1993
|
}
|
package/std/net/http1.ts
CHANGED
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
*
|
|
81
81
|
* Written from RFC 9112 and RFC 9110, not ported from another implementation.
|
|
82
82
|
*/
|
|
83
|
+
import { copyBytes } from "nish/bytes"
|
|
83
84
|
|
|
84
85
|
/** `next()`: the input so far holds no further event; feed more. */
|
|
85
86
|
export const HTTP1_NEED_MORE: i32 = 0
|
|
@@ -234,11 +235,15 @@ export class Http1Parser {
|
|
|
234
235
|
this.maxHeaderBytes = http1Clamp(maxHeaderBytes)
|
|
235
236
|
this.maxHeaders = http1Clamp(maxHeaders)
|
|
236
237
|
this.maxBody = http1Clamp(maxBody)
|
|
237
|
-
|
|
238
|
+
// The input and the head are made by the first byte fed and the first
|
|
239
|
+
// head kept, at HTTP1_FIRST_INPUT and HTTP1_FIRST_HEAD bytes: a parser
|
|
240
|
+
// that is built and never fed (a pool's spare slot) costs neither, and a
|
|
241
|
+
// caller that lends them out between requests puts empty ones here.
|
|
242
|
+
this.buf = []
|
|
238
243
|
this.body = this.buf
|
|
239
244
|
this.names = []
|
|
240
245
|
this.values = []
|
|
241
|
-
this.head =
|
|
246
|
+
this.head = []
|
|
242
247
|
this.spans = new Array<i32>(64)
|
|
243
248
|
this.tally = new Http1ListTally()
|
|
244
249
|
this.restart()
|
|
@@ -261,19 +266,9 @@ export class Http1Parser {
|
|
|
261
266
|
http1Reserve(this, len)
|
|
262
267
|
const buf: u8[] = this.buf
|
|
263
268
|
const at: i32 = this.end
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
buf.set(data, at)
|
|
268
|
-
this.end = at + len
|
|
269
|
-
return
|
|
270
|
-
}
|
|
271
|
-
for (let k: i32 = 0; k < len; k += 1) {
|
|
272
|
-
const to: i32 = at + k
|
|
273
|
-
if (to >= 0 && to < toI32(buf.length)) {
|
|
274
|
-
buf[to] = data[off + k]
|
|
275
|
-
}
|
|
276
|
-
}
|
|
269
|
+
// One move: http1Reserve made room for `len` bytes at `at`, and the
|
|
270
|
+
// window was checked above.
|
|
271
|
+
copyBytes(buf, at, data, off, len)
|
|
277
272
|
this.end = at + len
|
|
278
273
|
}
|
|
279
274
|
|
|
@@ -359,6 +354,16 @@ export class Http1Parser {
|
|
|
359
354
|
return http1HeadIs(this.head, spans[4 * found + 2], spans[4 * found + 3], protocol, true)
|
|
360
355
|
}
|
|
361
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Whether the parser waits for a request line with nothing fed and unread,
|
|
359
|
+
* so its input and head buffers hold nothing it needs: a caller may lend
|
|
360
|
+
* them out and leave empty arrays in `buf`, `body` and `head`, which the
|
|
361
|
+
* next feed and head make or the caller puts back first.
|
|
362
|
+
*/
|
|
363
|
+
resting(): boolean {
|
|
364
|
+
return this.state === HTTP1_S_REQUEST_LINE && this.end === this.start
|
|
365
|
+
}
|
|
366
|
+
|
|
362
367
|
/** How many bytes were fed and not consumed yet: what a caller bounds its reads by. */
|
|
363
368
|
buffered(): i32 {
|
|
364
369
|
return this.end - this.start
|
|
@@ -410,6 +415,11 @@ export class Http1Parser {
|
|
|
410
415
|
}
|
|
411
416
|
}
|
|
412
417
|
|
|
418
|
+
/** The input buffer a parser makes on its first feed, the smallest it grows from. */
|
|
419
|
+
export const HTTP1_FIRST_INPUT: i32 = 1024
|
|
420
|
+
/** The head buffer a parser makes for its first head. */
|
|
421
|
+
export const HTTP1_FIRST_HEAD: i32 = 256
|
|
422
|
+
|
|
413
423
|
/** Makes room for `len` more bytes at `p.end`, moving the live bytes down first. */
|
|
414
424
|
const http1Reserve = (p: Http1Parser, len: i32): void => {
|
|
415
425
|
const old: u8[] = p.buf
|
|
@@ -418,7 +428,7 @@ const http1Reserve = (p: Http1Parser, len: i32): void => {
|
|
|
418
428
|
return
|
|
419
429
|
}
|
|
420
430
|
const live: i32 = p.end - p.start
|
|
421
|
-
let size: i32 = capacity
|
|
431
|
+
let size: i32 = capacity < HTTP1_FIRST_INPUT ? HTTP1_FIRST_INPUT : capacity
|
|
422
432
|
while (size - live < len) {
|
|
423
433
|
if (size > 1073741823) {
|
|
424
434
|
panic("Http1Parser: more than 2^31 - 1 bytes buffered")
|
|
@@ -429,8 +439,9 @@ const http1Reserve = (p: Http1Parser, len: i32): void => {
|
|
|
429
439
|
// the live bytes move to the front, and every index into them moves too.
|
|
430
440
|
const buf: u8[] = size === capacity ? old : new Array<u8>(size)
|
|
431
441
|
const from: i32 = p.start
|
|
432
|
-
|
|
433
|
-
|
|
442
|
+
if (live > 0) {
|
|
443
|
+
// One move, overlapping when the buffer is the same one.
|
|
444
|
+
copyBytes(buf, 0, old, from, live)
|
|
434
445
|
}
|
|
435
446
|
p.buf = buf
|
|
436
447
|
p.lineStart -= from
|
|
@@ -601,6 +612,9 @@ const http1HeadAppend = (p: Http1Parser, from: i32, to: i32): i32 => {
|
|
|
601
612
|
const at: i32 = p.headLength
|
|
602
613
|
let size: i32 = toI32(p.head.length)
|
|
603
614
|
if (n > size - at) {
|
|
615
|
+
if (size < HTTP1_FIRST_HEAD) {
|
|
616
|
+
size = HTTP1_FIRST_HEAD
|
|
617
|
+
}
|
|
604
618
|
while (size - at < n) {
|
|
605
619
|
if (size > 1073741823) {
|
|
606
620
|
panic("Http1Parser: a head of more than 2^31 - 1 bytes")
|
|
@@ -616,9 +630,14 @@ const http1HeadAppend = (p: Http1Parser, from: i32, to: i32): i32 => {
|
|
|
616
630
|
}
|
|
617
631
|
const head: u8[] = p.head
|
|
618
632
|
const buf: u8[] = p.buf
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
633
|
+
if (n > 0 && from >= 0 && from <= toI32(buf.length) - n && at <= toI32(head.length) - n) {
|
|
634
|
+
// The line lies in `buf` and the head was grown to hold it: one move.
|
|
635
|
+
copyBytes(head, at, buf, from, n)
|
|
636
|
+
} else {
|
|
637
|
+
for (let k: i32 = 0; k < n && at + k < toI32(head.length) && from + k < toI32(buf.length); k += 1) {
|
|
638
|
+
if (from + k >= 0) {
|
|
639
|
+
head[at + k] = buf[from + k]
|
|
640
|
+
}
|
|
622
641
|
}
|
|
623
642
|
}
|
|
624
643
|
p.headLength = at + n
|