@amritk/nish-x86_64-linux 0.27.2 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,26 @@
1
1
  /**
2
- * `nish/net/websocket` — RFC 6455 WebSocket framing and the opening
3
- * handshake's accept key, sans-IO: bytes in, events and bytes out.
2
+ * `nish/net/websocket` — RFC 6455 WebSocket framing and both sides of the
3
+ * opening handshake, sans-IO: bytes in, events and bytes out.
4
4
  *
5
- * **The handshake.** `nish/net/http1` reads the client's request;
5
+ * **The server's handshake.** `nish/net/http1` reads the client's request;
6
6
  * `websocketRequestKey(parser)` answers its `Sec-WebSocket-Key` when the head
7
- * is a valid opening handshake (§4.2.1), and `websocketUpgradeResponse(key,
8
- * protocol)` writes the 101 that accepts it, with `Sec-WebSocket-Accept` from
7
+ * is a valid opening handshake (§4.2.1), `websocketOfferedProtocols(parser)`
8
+ * the subprotocols it offers, and `websocketUpgradeResponse(key, protocol)`
9
+ * writes the 101 that accepts it, with `Sec-WebSocket-Accept` from
9
10
  * `websocketAcceptKey` (§4.2.2): the base64 of the SHA-1 of the key and a
10
11
  * fixed GUID. What `HTTP1_UPGRADE`'s `upgradeBytes()` answers is the first
11
12
  * input for the decoder.
12
13
  *
14
+ * **The client's handshake** (§4.1). `websocketClientKey(nonce)` spells 16
15
+ * random bytes as the key, `websocketUpgradeRequest(host, target, key,
16
+ * protocols)` writes the GET that asks to upgrade, and
17
+ * `websocketCheckResponse(parser, key, offered)` reads the server's answer,
18
+ * parsed by `nish/net/http1-client` after `expectUpgrade()`: it answers the
19
+ * negotiated subprotocol, or which rule of §4.1 the response broke. What
20
+ * `HTTP1_UPGRADE`'s `tunnelBytes()` answers is the first input for
21
+ * `WsDecoder(false, …)`. This module draws no randomness: the nonce and every
22
+ * frame's mask key are the caller's, from `crypto.getRandomValues`.
23
+ *
13
24
  * **The decoder** is a push-pull machine like `Http1Parser`: `feed` any slice,
14
25
  * then call `next()` until it answers `WS_NEED_MORE`.
15
26
  *
@@ -56,9 +67,18 @@
56
67
  *
57
68
  * Written from RFC 6455, not ported from another implementation.
58
69
  */
59
- import { base64urlDecode, base64urlEncode } from "nish/crypto/base64url"
70
+ import { base64StdPadEncode, base64urlDecode } from "nish/crypto/base64url"
60
71
  import { sha1 } from "nish/crypto/sha1"
61
- import { HTTP1_NO_BODY, Http1Parser, http1ResponseHead } from "nish/net/http1"
72
+ import {
73
+ HTTP1_NO_BODY,
74
+ Http1Parser,
75
+ http1IsOws,
76
+ http1IsToken,
77
+ http1ResponseHead,
78
+ http1SameWord,
79
+ http1Text,
80
+ } from "nish/net/http1"
81
+ import { Http1ResponseParser, http1UpgradeRequestHead } from "nish/net/http1-client"
62
82
 
63
83
  /** The GUID RFC 6455 §1.3 appends to the client's key before hashing it. */
64
84
  export const WS_GUID: string = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
@@ -753,14 +773,9 @@ const websocketSwapBytes = (text: string, from1: i32, to1: i32, from2: i32, to2:
753
773
  /**
754
774
  * `Sec-WebSocket-Accept` for the client's `Sec-WebSocket-Key` (§4.2.2 item
755
775
  * 5.4): the SHA-1 of the key and `WS_GUID`, in padded standard base64.
756
- * `nish/crypto/base64url` writes the base64; the URL alphabet's two last
757
- * characters are swapped for the standard ones, and a 20-byte digest takes one
758
- * `=` of padding.
759
776
  */
760
- export const websocketAcceptKey = (key: string): string => {
761
- const digest: u8[] = sha1(websocketBytesOf(`${key}${WS_GUID}`))
762
- return `${websocketSwapBytes(base64urlEncode(digest), 45, 43, 95, 47)}=`
763
- }
777
+ export const websocketAcceptKey = (key: string): string =>
778
+ base64StdPadEncode(sha1(websocketBytesOf(`${key}${WS_GUID}`)))
764
779
 
765
780
  /**
766
781
  * Whether `key` is a valid `Sec-WebSocket-Key` (§4.2.1 item 5): padded
@@ -821,3 +836,269 @@ export const websocketUpgradeResponse = (key: string, protocol: string): u8[] |
821
836
  const switching: i32 = 101
822
837
  return http1ResponseHead(switching, "Switching Protocols", names, values, HTTP1_NO_BODY)
823
838
  }
839
+
840
+ /**
841
+ * The subprotocols the opening handshake `p` has just read offers, in the
842
+ * order offered (§4.2.1 item 8, §11.3.4): every `Sec-WebSocket-Protocol`
843
+ * field's comma-separated list, joined, each element trimmed of OWS and the
844
+ * empty ones dropped (RFC 9110 §5.6.1). Answers `[]` when there is no such
845
+ * field, and `null` when an element is not a token: a server picks one of
846
+ * these, or none, for `websocketUpgradeResponse`.
847
+ */
848
+ export const websocketOfferedProtocols = (p: Http1Parser): string[] | null => {
849
+ const out: string[] = []
850
+ const head: u8[] = p.head
851
+ const spans: i32[] = p.spans
852
+ for (let i: i32 = 0; i < p.headerCount && 4 * i + 3 < toI32(spans.length); i += 1) {
853
+ if (websocketSpanIs(head, spans[4 * i], spans[4 * i + 1], "sec-websocket-protocol")) {
854
+ websocketListInto(head, spans[4 * i + 2], spans[4 * i + 3], out)
855
+ }
856
+ }
857
+ for (let k: i32 = 0; k < toI32(out.length); k += 1) {
858
+ if (!http1IsToken(out[k])) {
859
+ return null
860
+ }
861
+ }
862
+ return out
863
+ }
864
+
865
+ /** Whether `bytes[a .. b)` is `word`, which is lower case, in any ASCII case. Allocates nothing. */
866
+ const websocketSpanIs = (bytes: u8[], a: i32, b: i32, word: string): boolean => {
867
+ const n: i32 = toI32(word.length)
868
+ if (a < 0 || b - a !== n || b > toI32(bytes.length)) {
869
+ return false
870
+ }
871
+ for (let k: i32 = 0; k < n; k += 1) {
872
+ const at: i32 = a + k
873
+ if (at < 0 || at >= toI32(bytes.length) || k >= toI32(word.length)) {
874
+ return false
875
+ }
876
+ let c: i32 = toI32(bytes[at])
877
+ if (c >= 65 && c <= 90) {
878
+ c += 32
879
+ }
880
+ if (c !== toI32(word.charCodeAt(k))) {
881
+ return false
882
+ }
883
+ }
884
+ return true
885
+ }
886
+
887
+ /**
888
+ * Appends the elements of the comma-separated list in `bytes[from .. to)`
889
+ * (RFC 9110 §5.6.1) to `out`, each trimmed of OWS, the empty ones dropped.
890
+ */
891
+ const websocketListInto = (bytes: u8[], from: i32, to: i32, out: string[]): void => {
892
+ if (from < 0 || to > toI32(bytes.length)) {
893
+ return
894
+ }
895
+ let start: i32 = from
896
+ for (let k: i32 = from; k <= to; k += 1) {
897
+ if (k === to || (k >= 0 && k < toI32(bytes.length) && toI32(bytes[k]) === 44)) {
898
+ let a: i32 = start
899
+ let b: i32 = k
900
+ while (a < b && a >= 0 && a < toI32(bytes.length) && http1IsOws(toI32(bytes[a]))) {
901
+ a += 1
902
+ }
903
+ while (b > a && b - 1 >= 0 && b - 1 < toI32(bytes.length) && http1IsOws(toI32(bytes[b - 1]))) {
904
+ b -= 1
905
+ }
906
+ if (b > a) {
907
+ out.push(http1Text(bytes, a, b, false))
908
+ }
909
+ start = k + 1
910
+ }
911
+ }
912
+ }
913
+
914
+ /** Whether the comma-separated list `value` (RFC 9110 §5.6.1) has an element that is `word`, which is lower case, in any ASCII case. */
915
+ const websocketListHas = (value: string, word: string): boolean => {
916
+ const n: i32 = toI32(value.length)
917
+ let start: i32 = 0
918
+ for (let k: i32 = 0; k <= n; k += 1) {
919
+ if (k === n || (k >= 0 && k < n && toI32(value.charCodeAt(k)) === 44)) {
920
+ let a: i32 = start
921
+ let b: i32 = k
922
+ while (a < b && a >= 0 && a < n && http1IsOws(toI32(value.charCodeAt(a)))) {
923
+ a += 1
924
+ }
925
+ while (b > a && b - 1 >= 0 && b - 1 < n && http1IsOws(toI32(value.charCodeAt(b - 1)))) {
926
+ b -= 1
927
+ }
928
+ if (http1SameWord(value, a, b, word)) {
929
+ return true
930
+ }
931
+ start = k + 1
932
+ }
933
+ }
934
+ return false
935
+ }
936
+
937
+ /**
938
+ * The `Sec-WebSocket-Key` for a client's opening handshake (§4.1 item 7): the
939
+ * 16-byte `nonce` in padded standard base64, 24 characters. Answers `null` for
940
+ * a nonce of any other length. The nonce is the caller's, drawn from
941
+ * `crypto.getRandomValues` and fresh for every connection: a key that repeats
942
+ * or can be guessed lets a cache or an intermediary replay an old handshake's
943
+ * answer. This module draws no randomness itself.
944
+ */
945
+ export const websocketClientKey = (nonce: u8[]): string | null => {
946
+ if (toI32(nonce.length) !== 16) {
947
+ return null
948
+ }
949
+ return base64StdPadEncode(nonce)
950
+ }
951
+
952
+ /**
953
+ * A client's opening handshake (§4.1 items 1 to 10): `GET target HTTP/1.1`
954
+ * with `Host`, `Sec-WebSocket-Key`, `Sec-WebSocket-Version: 13`, the offered
955
+ * `protocols` in one `Sec-WebSocket-Protocol` field when there are any, and
956
+ * `Upgrade: websocket` and `Connection: Upgrade`, written by
957
+ * `nish/net/http1-client`'s `http1UpgradeRequestHead`. Answers `null` for a
958
+ * key `websocketKeyIsValid` refuses, a protocol that is not a token (an empty
959
+ * one, or one holding a comma, a space, CR or LF), a protocol offered twice, an
960
+ * empty host, and everything the request writer refuses (CR, LF or another
961
+ * control byte in `host`, a `target` that is not a request target).
962
+ */
963
+ export const websocketUpgradeRequest = (
964
+ host: string,
965
+ target: string,
966
+ key: string,
967
+ protocols: string[]
968
+ ): u8[] | null => {
969
+ if (toI32(host.length) === 0 || !websocketKeyIsValid(key)) {
970
+ return null
971
+ }
972
+ const count: i32 = toI32(protocols.length)
973
+ for (let i: i32 = 0; i < count; i += 1) {
974
+ if (!http1IsToken(protocols[i])) {
975
+ return null
976
+ }
977
+ for (let j: i32 = 0; j < i; j += 1) {
978
+ if (protocols[j] === protocols[i]) {
979
+ return null
980
+ }
981
+ }
982
+ }
983
+ const names: string[] = ["Host", "Sec-WebSocket-Key", "Sec-WebSocket-Version"]
984
+ const values: string[] = [host, key, "13"]
985
+ if (count > 0) {
986
+ names.push("Sec-WebSocket-Protocol")
987
+ values.push(protocols.join(", "))
988
+ }
989
+ return http1UpgradeRequestHead("GET", target, names, values, "websocket")
990
+ }
991
+
992
+ // What `websocketCheckResponse` found (`WsHandshake.error`): the handshake
993
+ // succeeded, or the first rule of §4.1 the server's response broke.
994
+ /** The server accepted the handshake; `protocol` is the subprotocol it chose. */
995
+ export const WS_HS_OK: i32 = 0
996
+ /** The status is not 101; the parser's `status` says what it is. No redirect is followed. */
997
+ export const WS_HS_STATUS: i32 = 1
998
+ /** The response is not HTTP/1.1. */
999
+ export const WS_HS_VERSION: i32 = 2
1000
+ /** `Upgrade` is absent, repeated, or not `websocket` in any case. */
1001
+ export const WS_HS_UPGRADE: i32 = 3
1002
+ /** No `Connection` field lists `upgrade`. */
1003
+ export const WS_HS_CONNECTION: i32 = 4
1004
+ /** `Sec-WebSocket-Accept` is absent, repeated, or not the key's accept value. */
1005
+ export const WS_HS_ACCEPT: i32 = 5
1006
+ /** A `Sec-WebSocket-Extensions` field, when the client offered none. */
1007
+ export const WS_HS_EXTENSION: i32 = 6
1008
+ /** A `Sec-WebSocket-Protocol` that is repeated, a list, or not one the client offered. */
1009
+ export const WS_HS_PROTOCOL: i32 = 7
1010
+
1011
+ /** `websocketCheckResponse`'s answer: `error` is a `WS_HS_*` code; `protocol` is set when it is `WS_HS_OK`. */
1012
+ export class WsHandshake {
1013
+ /** The subprotocol the server chose, or `""` when it chose none. */
1014
+ protocol: string = ""
1015
+ error: i32 = 0
1016
+
1017
+ constructor(error: i32, protocol: string) {
1018
+ this.error = error
1019
+ this.protocol = protocol
1020
+ }
1021
+ }
1022
+
1023
+ /**
1024
+ * Reads the server's answer to `websocketUpgradeRequest(…, key, offered)`
1025
+ * (§4.1, the client's checks of the response): call it at the parser's
1026
+ * `HTTP1_HEAD`. It fails closed, and the connection is closed on any failure:
1027
+ * a status other than 101 (`WS_HS_STATUS`; a redirect is not followed), a
1028
+ * version other than HTTP/1.1, `Upgrade` absent, repeated or not `websocket`,
1029
+ * no `Connection` listing `upgrade`, `Sec-WebSocket-Accept` absent, repeated
1030
+ * or not `websocketAcceptKey(key)` (a forged or replayed answer), any
1031
+ * `Sec-WebSocket-Extensions` (this client offers none, so none may be in use),
1032
+ * and a `Sec-WebSocket-Protocol` that is present when nothing was offered,
1033
+ * repeated, a list, or not one of `offered`. On `WS_HS_OK` the parser's next
1034
+ * events are `HTTP1_END` and `HTTP1_UPGRADE`; its `tunnelBytes()` go to
1035
+ * `WsDecoder(false, …)`, and every frame the client sends is masked with a
1036
+ * fresh mask key from `crypto.getRandomValues` (§5.3).
1037
+ */
1038
+ export const websocketCheckResponse = (
1039
+ response: Http1ResponseParser,
1040
+ key: string,
1041
+ offered: string[]
1042
+ ): WsHandshake => {
1043
+ if (response.status !== 101) {
1044
+ return new WsHandshake(WS_HS_STATUS, "")
1045
+ }
1046
+ if (response.minor !== 1) {
1047
+ return new WsHandshake(WS_HS_VERSION, "")
1048
+ }
1049
+ const expected: string = websocketKeyIsValid(key) ? websocketAcceptKey(key) : ""
1050
+ let upgrades: i32 = 0
1051
+ let upgradeOk: boolean = false
1052
+ let connection: boolean = false
1053
+ let accepts: i32 = 0
1054
+ let acceptOk: boolean = false
1055
+ let extensions: boolean = false
1056
+ let protocols: i32 = 0
1057
+ let protocol: string = ""
1058
+ const names: string[] = response.names
1059
+ const values: string[] = response.values
1060
+ for (let i: i32 = 0; i < toI32(names.length) && i < toI32(values.length); i += 1) {
1061
+ const name: string = names[i]
1062
+ const value: string = values[i]
1063
+ if (name === "upgrade") {
1064
+ upgrades += 1
1065
+ upgradeOk = http1SameWord(value, 0, toI32(value.length), "websocket")
1066
+ } else if (name === "connection") {
1067
+ if (websocketListHas(value, "upgrade")) {
1068
+ connection = true
1069
+ }
1070
+ } else if (name === "sec-websocket-accept") {
1071
+ accepts += 1
1072
+ acceptOk = toI32(expected.length) > 0 && value === expected
1073
+ } else if (name === "sec-websocket-extensions") {
1074
+ extensions = true
1075
+ } else if (name === "sec-websocket-protocol") {
1076
+ protocols += 1
1077
+ protocol = value
1078
+ }
1079
+ }
1080
+ if (upgrades !== 1 || !upgradeOk) {
1081
+ return new WsHandshake(WS_HS_UPGRADE, "")
1082
+ }
1083
+ if (!connection) {
1084
+ return new WsHandshake(WS_HS_CONNECTION, "")
1085
+ }
1086
+ if (accepts !== 1 || !acceptOk) {
1087
+ return new WsHandshake(WS_HS_ACCEPT, "")
1088
+ }
1089
+ if (extensions) {
1090
+ return new WsHandshake(WS_HS_EXTENSION, "")
1091
+ }
1092
+ if (protocols === 0) {
1093
+ return new WsHandshake(WS_HS_OK, "")
1094
+ }
1095
+ if (protocols > 1 || !http1IsToken(protocol)) {
1096
+ return new WsHandshake(WS_HS_PROTOCOL, "")
1097
+ }
1098
+ for (let i: i32 = 0; i < toI32(offered.length); i += 1) {
1099
+ if (offered[i] === protocol) {
1100
+ return new WsHandshake(WS_HS_OK, protocol)
1101
+ }
1102
+ }
1103
+ return new WsHandshake(WS_HS_PROTOCOL, "")
1104
+ }