@fails-components/webtransport 0.2.2 → 0.3.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.
Files changed (101) hide show
  1. package/CMakeLists.txt +3 -0
  2. package/dist/lib/client.d.ts +7 -3
  3. package/dist/lib/client.d.ts.map +1 -1
  4. package/dist/lib/dom.d.ts +15 -4
  5. package/dist/lib/dom.d.ts.map +1 -1
  6. package/dist/lib/error.d.ts +8 -0
  7. package/dist/lib/error.d.ts.map +1 -0
  8. package/dist/lib/event-loop.d.ts +4 -2
  9. package/dist/lib/event-loop.d.ts.map +1 -1
  10. package/dist/lib/native.d.ts.map +1 -1
  11. package/dist/lib/server.d.ts +5 -2
  12. package/dist/lib/server.d.ts.map +1 -1
  13. package/dist/lib/session.d.ts +36 -23
  14. package/dist/lib/session.d.ts.map +1 -1
  15. package/dist/lib/stream.d.ts +15 -4
  16. package/dist/lib/stream.d.ts.map +1 -1
  17. package/dist/lib/transport.d.ts +12 -3
  18. package/dist/lib/transport.d.ts.map +1 -1
  19. package/dist/lib/types.d.ts +43 -4
  20. package/dist/lib/types.d.ts.map +1 -1
  21. package/dist/lib/utils.d.ts +5 -0
  22. package/dist/lib/utils.d.ts.map +1 -1
  23. package/dist/lib/webtransport.d.ts +11 -21
  24. package/dist/lib/webtransport.d.ts.map +1 -1
  25. package/dist/test/datagrams.spec.d.ts.map +1 -1
  26. package/dist/test/event-loop.node.d.ts +2 -0
  27. package/dist/test/event-loop.node.d.ts.map +1 -0
  28. package/dist/test/fixtures/chai.d.ts.map +1 -1
  29. package/dist/test/fixtures/known-bytes.d.ts +6 -0
  30. package/dist/test/fixtures/known-bytes.d.ts.map +1 -0
  31. package/dist/test/fixtures/read-cert-hash.d.ts +6 -0
  32. package/dist/test/fixtures/read-cert-hash.d.ts.map +1 -0
  33. package/dist/test/fixtures/read-stream.d.ts +10 -1
  34. package/dist/test/fixtures/read-stream.d.ts.map +1 -1
  35. package/dist/test/fixtures/reader-value.d.ts +16 -1
  36. package/dist/test/fixtures/reader-value.d.ts.map +1 -1
  37. package/dist/test/fixtures/server.d.ts +0 -5
  38. package/dist/test/fixtures/server.d.ts.map +1 -1
  39. package/dist/test/fixtures/webtransport.browser.d.ts +3 -0
  40. package/dist/test/fixtures/webtransport.browser.d.ts.map +1 -0
  41. package/dist/test/fixtures/webtransport.d.ts +3 -0
  42. package/dist/test/fixtures/webtransport.d.ts.map +1 -0
  43. package/dist/test/fixtures/write-stream.d.ts.map +1 -1
  44. package/dist/test/index.d.ts +2 -0
  45. package/dist/test/index.d.ts.map +1 -0
  46. package/dist/test/session.spec.d.ts +5 -0
  47. package/dist/test/session.spec.d.ts.map +1 -0
  48. package/dist/test/unidirectional-streams.spec.d.ts +1 -4
  49. package/dist/test/unidirectional-streams.spec.d.ts.map +1 -1
  50. package/lib/client.js +57 -43
  51. package/lib/dom.ts +18 -4
  52. package/lib/error.js +10 -0
  53. package/lib/event-loop.js +8 -5
  54. package/lib/native.js +4 -1
  55. package/lib/server.js +18 -10
  56. package/lib/session.js +145 -32
  57. package/lib/stream.js +51 -22
  58. package/lib/transport.js +48 -10
  59. package/lib/types.ts +53 -5
  60. package/lib/utils.js +13 -0
  61. package/lib/webtransport.js +10 -18
  62. package/{test → old_test}/echoserver.js +1 -2
  63. package/{test → old_test}/test.js +5 -3
  64. package/package.json +16 -5
  65. package/readme.md +64 -0
  66. package/src/http3client.cc +2 -2
  67. package/src/http3dispatcher.cc +3 -2
  68. package/src/http3dispatcher.h +2 -1
  69. package/src/http3eventloop.cc +106 -2
  70. package/src/http3eventloop.h +13 -0
  71. package/src/http3wtsessionvisitor.cc +2 -0
  72. package/src/http3wtsessionvisitor.h +41 -1
  73. package/test/bidirectional-streams.spec.js +41 -97
  74. package/test/datagrams.spec.js +50 -99
  75. package/test/fixtures/chai.js +2 -0
  76. package/test/fixtures/known-bytes.js +15 -0
  77. package/test/fixtures/read-cert-hash.js +7 -0
  78. package/test/fixtures/read-stream.js +5 -0
  79. package/test/fixtures/reader-value.js +32 -0
  80. package/test/fixtures/server.js +237 -21
  81. package/test/fixtures/webtransport.browser.js +2 -0
  82. package/test/fixtures/webtransport.js +3 -0
  83. package/test/fixtures/write-stream.js +8 -1
  84. package/test/index.js +87 -0
  85. package/test/session.spec.js +159 -0
  86. package/test/unidirectional-streams.spec.js +67 -138
  87. package/tsconfig.json +0 -1
  88. package/dist/test/echoclient.d.ts +0 -2
  89. package/dist/test/echoclient.d.ts.map +0 -1
  90. package/dist/test/echoserver.d.ts +0 -2
  91. package/dist/test/echoserver.d.ts.map +0 -1
  92. package/dist/test/event-loop.spec.d.ts +0 -2
  93. package/dist/test/event-loop.spec.d.ts.map +0 -1
  94. package/dist/test/test.d.ts +0 -2
  95. package/dist/test/test.d.ts.map +0 -1
  96. package/dist/test/testsuite.d.ts +0 -25
  97. package/dist/test/testsuite.d.ts.map +0 -1
  98. package/platform/base/strings/utf_ostream_operators.h +0 -5
  99. /package/{test → old_test}/echoclient.js +0 -0
  100. /package/{test → old_test}/testsuite.js +0 -0
  101. /package/test/{event-loop.spec.js → event-loop.node.js} +0 -0
@@ -1,5 +1,11 @@
1
1
  import { generateWebTransportCertificate } from './certificate.js'
2
2
  import { Http3Server } from '../../lib/index.js'
3
+ import { pTimeout } from './p-timeout.js'
4
+ import { getReaderStream, getReaderValue } from './reader-value.js'
5
+ import { writeStream } from './write-stream.js'
6
+ import { readStream } from './read-stream.js'
7
+ import * as ui8 from 'uint8arrays'
8
+ import { KNOWN_BYTES, KNOWN_BYTES_LENGTH } from './known-bytes.js'
3
9
 
4
10
  export async function createServer() {
5
11
  const attrs = [
@@ -19,6 +25,8 @@ export async function createServer() {
19
25
  }
20
26
 
21
27
  const server = new Http3Server({
28
+ /* port: 8080,
29
+ host: '0.0.0.0', */
22
30
  port: 0,
23
31
  host: '127.0.0.1',
24
32
  secret: 'mysecret',
@@ -26,33 +34,241 @@ export async function createServer() {
26
34
  privKey: certificate.private
27
35
  })
28
36
 
37
+ server.ready
38
+ .then(async () => {
39
+ // set up listeners for the different server paths used by the tests
40
+
41
+ await Promise.all(
42
+ [
43
+ // echo server, initiated by remote
44
+ async () => {
45
+ for await (const session of getReaderStream(
46
+ server.sessionStream('/bidirectional_client_initiated_echo')
47
+ )) {
48
+ try {
49
+ const bidiStream = await getReaderValue(
50
+ session.incomingBidirectionalStreams
51
+ )
52
+
53
+ // redirect input to output
54
+ await bidiStream.readable.pipeTo(bidiStream.writable)
55
+ } catch {
56
+ // in some tests the client closes the stream
57
+ }
58
+ }
59
+ },
60
+
61
+ // echo server, initiated by local
62
+ async () => {
63
+ for await (const session of getReaderStream(
64
+ server.sessionStream('/bidirectional_server_initiated_echo')
65
+ )) {
66
+ try {
67
+ const stream = await session.createBidirectionalStream()
68
+
69
+ await writeStream(stream.writable, KNOWN_BYTES)
70
+
71
+ const received = await readStream(
72
+ stream.readable,
73
+ KNOWN_BYTES_LENGTH
74
+ )
75
+ // await stream.readable.cancel() // cancel so that the client can progress
76
+
77
+ // if we did not get the data we sent, close the session with a reason
78
+ if (
79
+ !ui8.equals(ui8.concat(KNOWN_BYTES), ui8.concat(received))
80
+ ) {
81
+ session.close({
82
+ closeCode: 500,
83
+ reason: 'data did not match'
84
+ })
85
+ } else {
86
+ session.close()
87
+ }
88
+ } catch {
89
+ // in some tests the client closes the stream
90
+ }
91
+ }
92
+ },
93
+
94
+ // echo datagrams, initiated by remote
95
+ async () => {
96
+ for await (const session of getReaderStream(
97
+ server.sessionStream('/datagrams_client_send')
98
+ )) {
99
+ // datagram transport is unreliable, at least one message should make it through
100
+ const expected = 1
101
+
102
+ try {
103
+ const received = await pTimeout(
104
+ readStream(session.datagrams.readable, expected),
105
+ 1000
106
+ )
107
+
108
+ // if we did not get the data we sent, close the session with a reason
109
+ if (received.length !== expected) {
110
+ throw new Error('Did not receive enough bytes')
111
+ }
112
+
113
+ session.close()
114
+ } catch (/** @type {any} */ err) {
115
+ session.close({
116
+ closeCode: 500,
117
+ reason: err.message
118
+ })
119
+ }
120
+ }
121
+ },
122
+
123
+ // echo datagrams, initiated by local
124
+ async () => {
125
+ for await (const session of getReaderStream(
126
+ server.sessionStream('/datagrams_server_send')
127
+ )) {
128
+ const writer = session.datagrams.writable.getWriter()
129
+ let closed = false
130
+
131
+ // write datagrams until the client receives one and closes the connection
132
+ // eslint-disable-next-line promise/catch-or-return
133
+ Promise.resolve().then(async () => {
134
+ // eslint-disable-next-line no-unmodified-loop-condition
135
+ while (!closed) {
136
+ try {
137
+ await writer.ready
138
+ await writer.write(Uint8Array.from([0, 1, 2, 3, 4]))
139
+ await new Promise((resolve) => setTimeout(resolve, 1)) // do not flood everything
140
+ } catch {
141
+ // the session can be closed while we are writing
142
+ }
143
+ }
144
+ })
145
+
146
+ await session.closed
147
+ closed = true
148
+ }
149
+ },
150
+
151
+ // cleanly close remote sessions
152
+ async () => {
153
+ for await (const session of getReaderStream(
154
+ server.sessionStream('/session_close')
155
+ )) {
156
+ session.close()
157
+ }
158
+ },
159
+
160
+ // cleanly close remote sessions
161
+ async () => {
162
+ for await (const session of getReaderStream(
163
+ server.sessionStream('/session_close_with_reason')
164
+ )) {
165
+ session.close({
166
+ closeCode: 7,
167
+ reason: 'this is the reason'
168
+ })
169
+ }
170
+ },
171
+
172
+ // send data over unidirectional stream, initiated by remote
173
+ async () => {
174
+ for await (const session of getReaderStream(
175
+ server.sessionStream('/unidirectional_client_send')
176
+ )) {
177
+ try {
178
+ const stream = await getReaderValue(
179
+ session.incomingUnidirectionalStreams
180
+ )
181
+ const received = await readStream(stream, KNOWN_BYTES_LENGTH)
182
+ // await stream.cancel() // cancel so that the client can progress
183
+ // if we did not get the data we sent, close the session with a reason
184
+ if (
185
+ !ui8.equals(ui8.concat(KNOWN_BYTES), ui8.concat(received))
186
+ ) {
187
+ session.close({
188
+ closeCode: 500,
189
+ reason: 'data did not match'
190
+ })
191
+ } else {
192
+ session.close()
193
+ }
194
+ } catch (error) {
195
+ // in some tests the client closes the stream
196
+ }
197
+ }
198
+ },
199
+
200
+ // send data over unidirectional stream, initiated by local
201
+ async () => {
202
+ for await (const session of getReaderStream(
203
+ server.sessionStream('/unidirectional_server_send')
204
+ )) {
205
+ const stream = await session.createUnidirectionalStream()
206
+
207
+ await writeStream(stream, KNOWN_BYTES)
208
+ }
209
+ },
210
+
211
+ // delays reading from stream after client writes
212
+ async () => {
213
+ for await (const session of getReaderStream(
214
+ server.sessionStream('/unidirectional_server_delay_before_read')
215
+ )) {
216
+ const stream = await getReaderValue(
217
+ session.incomingUnidirectionalStreams
218
+ )
219
+
220
+ // wait before we read from the stream, should trigger backpressure
221
+ // on the client
222
+ await new Promise((resolve) => setTimeout(resolve, 1000))
223
+
224
+ const received = await readStream(stream, KNOWN_BYTES_LENGTH)
225
+ // await stream.cancel() // cancel so that the client can progress
226
+
227
+ // if we did not get expected data, close the session with a reason
228
+ if (!ui8.equals(ui8.concat(KNOWN_BYTES), ui8.concat(received))) {
229
+ console.log('ERROR', KNOWN_BYTES, received)
230
+ session.close({
231
+ closeCode: 500,
232
+ reason: 'data did not match'
233
+ })
234
+ } else {
235
+ await new Promise((resolve) => setTimeout(resolve, 2000)) // time out is needed, since it can be received before the read is complete
236
+ // and we want the client to close the session after it has processeed the read
237
+ session.close()
238
+ }
239
+ }
240
+ }
241
+ ].map((fn) => fn())
242
+ )
243
+ })
244
+ .catch((/** @type {any} */ err) => {
245
+ console.error('server crashed', err)
246
+ })
247
+
29
248
  return {
30
249
  server,
31
250
  certificate
32
251
  }
33
252
  }
34
253
 
35
- /**
36
- * @param {import('../../lib/server.js').Http3Server} server
37
- * @param {string} path
38
- */
39
- export async function getServerSession(server, path) {
40
- const sessionStream = await server.sessionStream(path)
41
- const sessionReader = sessionStream.getReader()
42
-
43
- try {
44
- const { done, value } = await sessionReader.read()
45
-
46
- if (done) {
47
- throw new Error('Server is gone')
48
- }
254
+ const { server, certificate } = await createServer()
255
+ server.startServer()
256
+ await server.ready
49
257
 
50
- if (!value) {
51
- throw new Error('Session was undefined')
52
- }
258
+ const address = server.address()
53
259
 
54
- return value
55
- } finally {
56
- sessionReader.releaseLock()
57
- }
260
+ if (address == null) {
261
+ throw new Error('Could not determine server address')
58
262
  }
263
+
264
+ // tell the calling process how to contact us
265
+ if (process.send)
266
+ process.send({
267
+ address: `https://${address.host}:${address.port}`,
268
+ certificate: certificate.fingerprint
269
+ })
270
+ else console.error('No IPC channel')
271
+ /* console.log({
272
+ address: `https://${address.host}:${address.port}`,
273
+ certificate: certificate.fingerprint
274
+ }) */
@@ -0,0 +1,2 @@
1
+ // @ts-expect-error not TypeScript's dom.d.ts yet
2
+ export default WebTransport // eslint-disable-line no-undef
@@ -0,0 +1,3 @@
1
+ import { WebTransport } from '../../lib/index.js'
2
+
3
+ export default WebTransport
@@ -10,8 +10,15 @@ export async function writeStream(writable, input) {
10
10
  const writer = writable.getWriter()
11
11
 
12
12
  for (const buf of input) {
13
+ await writer.ready
13
14
  await writer.write(buf)
14
15
  }
15
16
 
16
- await writer.close()
17
+ await writer.ready
18
+ await writer.releaseLock()
19
+ try {
20
+ await writable.close()
21
+ } catch (error) {
22
+ console.log('Did we get a STOP_SENDING? ignore', error)
23
+ }
17
24
  }
package/test/index.js ADDED
@@ -0,0 +1,87 @@
1
+ import { execa } from 'execa'
2
+
3
+ /**
4
+ * Run server as separate process so we can kill it without
5
+ * getting stuck waiting for the custom event loop to end.
6
+ */
7
+ async function startServer() {
8
+ return new Promise((resolve, reject) => {
9
+ let foundAddress = false
10
+
11
+ const server = execa('node', ['./test/fixtures/server.js'], {
12
+ stdio: ['inherit', 'inherit', 'inherit', 'ipc']
13
+ })
14
+ server.on('message', (data) => {
15
+ if (!foundAddress) {
16
+ foundAddress = true
17
+
18
+ resolve(data)
19
+ }
20
+ })
21
+ })
22
+ }
23
+
24
+ /**
25
+ * @param {string} certificate
26
+ * @param {string} serverAddress
27
+ */
28
+ async function runTests(certificate, serverAddress) {
29
+ const env = process.argv[2]
30
+ /** @type {string} */
31
+ let command = ''
32
+ /** @type {string[]} */
33
+ let args = []
34
+
35
+ if (env === 'node') {
36
+ command = 'mocha'
37
+ args = [
38
+ process.env.CI ? '--no-colors' : '--colors',
39
+ './test/*.spec.js',
40
+ './test/*.node.js',
41
+ ...process.argv.slice(3)
42
+ ]
43
+ const tests = execa(command, args, {
44
+ env: {
45
+ DEBUG_COLORS: process.env.CI ? '' : 'true',
46
+ CERT_HASH: certificate,
47
+ SERVER_URL: serverAddress
48
+ },
49
+ stdio: ['inherit', 'inherit', 'inherit']
50
+ })
51
+
52
+ await tests
53
+ } else if (env === 'chromium') {
54
+ command = 'playwright-test'
55
+ args = ['./test/*.spec.js', ...process.argv.slice(3)]
56
+ const tests = execa(command, args, {
57
+ env: {
58
+ DEBUG_COLORS: process.env.CI ? '' : 'true',
59
+ CERT_HASH: certificate,
60
+ SERVER_URL: serverAddress
61
+ },
62
+ stdio: ['inherit', 'inherit', 'inherit']
63
+ })
64
+
65
+ await tests
66
+ }
67
+ }
68
+
69
+ let success = true
70
+
71
+ try {
72
+ const { address, certificate } = await startServer()
73
+
74
+ await runTests(certificate, address)
75
+ } catch (/** @type {any} */ err) {
76
+ if (err.command == null) {
77
+ // was not an execa error
78
+ throw err
79
+ }
80
+
81
+ if (err.failed || err.timedOut || err.isCancelled || err.isKilled) {
82
+ success = false
83
+ }
84
+ } finally {
85
+ // this will cause the server process to exit too as it is a child of this process
86
+ process.exit(success ? 0 : 1)
87
+ }
@@ -0,0 +1,159 @@
1
+ /* eslint-env mocha */
2
+
3
+ import { readCertHash } from './fixtures/read-cert-hash.js'
4
+ import WebTransport from './fixtures/webtransport.js'
5
+ import { expect } from './fixtures/chai.js'
6
+
7
+ /**
8
+ * @template T
9
+ * @typedef {import('../lib/types').Deferred<T>} Deferred<T>
10
+ */
11
+
12
+ describe('session', function () {
13
+ // FIXME: sometimes there are seemingly arbitrary 5s delays in
14
+ // communicating with the server under node.js
15
+ this.timeout(30000)
16
+
17
+ /** @type {import('../lib/dom').WebTransport | undefined} */
18
+ let client
19
+
20
+ // @ts-ignore
21
+ afterEach(async () => {
22
+ if (client != null) {
23
+ client.close()
24
+ client = undefined
25
+ }
26
+ })
27
+
28
+ it('should detect session closure', async () => {
29
+ client = new WebTransport(`${process.env.SERVER_URL}/session_close`, {
30
+ serverCertificateHashes: [
31
+ {
32
+ algorithm: 'sha-256',
33
+ value: readCertHash(process.env.CERT_HASH)
34
+ }
35
+ ]
36
+ })
37
+ await client.ready
38
+
39
+ const result = await client.closed
40
+ expect(result).to.have.property('closeCode', 0)
41
+ expect(result).to.have.property('reason', '')
42
+ })
43
+
44
+ it('should detect session closure with close info', async () => {
45
+ // client context - connects to the server, sends some datagrams and reads the response
46
+ client = new WebTransport(
47
+ `${process.env.SERVER_URL}/session_close_with_reason`,
48
+ {
49
+ serverCertificateHashes: [
50
+ {
51
+ algorithm: 'sha-256',
52
+ value: readCertHash(process.env.CERT_HASH)
53
+ }
54
+ ]
55
+ }
56
+ )
57
+ await client.ready
58
+
59
+ const result = await client.closed
60
+ expect(result).to.have.property('closeCode', 7)
61
+ expect(result).to.have.property('reason', 'this is the reason')
62
+ })
63
+
64
+ it('should error when connecting to a server that does not exist', async () => {
65
+ client = new WebTransport(`https://127.0.0.1:39821`, {
66
+ serverCertificateHashes: [
67
+ {
68
+ algorithm: 'sha-256',
69
+ value: readCertHash(process.env.CERT_HASH)
70
+ }
71
+ ],
72
+ quicConnectTimeout: 100,
73
+ webTransportConnectTimeout: 100
74
+ })
75
+
76
+ const [closedResult, readyResult] = await Promise.all([
77
+ client.closed.catch((err) => err),
78
+ client.ready.catch((err) => err)
79
+ ])
80
+
81
+ expect(closedResult)
82
+ .to.be.a('WebTransportError')
83
+ .with.property('message', 'Opening handshake failed.')
84
+ expect(readyResult)
85
+ .to.be.a('WebTransportError')
86
+ .with.property('message', 'Opening handshake failed.')
87
+ })
88
+
89
+ it('should error when connecting to a path that does not exist', async () => {
90
+ client = new WebTransport(`${process.env.SERVER_URL}/non_existant`, {
91
+ serverCertificateHashes: [
92
+ {
93
+ algorithm: 'sha-256',
94
+ value: readCertHash(process.env.CERT_HASH)
95
+ }
96
+ ]
97
+ })
98
+
99
+ const [closedResult, readyResult] = await Promise.all([
100
+ client.closed.catch((err) => err),
101
+ client.ready.catch((err) => err)
102
+ ])
103
+
104
+ expect(closedResult)
105
+ .to.be.a('WebTransportError')
106
+ .with.property('message', 'Opening handshake failed.')
107
+ expect(readyResult)
108
+ .to.be.a('WebTransportError')
109
+ .with.property('message', 'Opening handshake failed.')
110
+ })
111
+
112
+ it('should error when connecting with a bad certificate', async () => {
113
+ client = new WebTransport(`${process.env.SERVER_URL}/session_close`, {
114
+ serverCertificateHashes: [
115
+ {
116
+ algorithm: 'sha-256',
117
+ value: readCertHash(process.env.CERT_HASH + ':DE:AD:BE:EF')
118
+ }
119
+ ]
120
+ })
121
+
122
+ const [closedResult, readyResult] = await Promise.all([
123
+ client.closed.catch((err) => err),
124
+ client.ready.catch((err) => err)
125
+ ])
126
+
127
+ expect(closedResult)
128
+ .to.be.a('WebTransportError')
129
+ .with.property('message', 'Opening handshake failed.')
130
+ expect(readyResult)
131
+ .to.be.a('WebTransportError')
132
+ .with.property('message', 'Opening handshake failed.')
133
+ })
134
+
135
+ it('should error when connecting with the wrong certificate', async () => {
136
+ client = new WebTransport(`${process.env.SERVER_URL}/session_close`, {
137
+ serverCertificateHashes: [
138
+ {
139
+ algorithm: 'sha-256',
140
+ value: readCertHash(
141
+ 'DE:AD:BE:EF:' + process.env.CERT_HASH?.substring(12)
142
+ )
143
+ }
144
+ ]
145
+ })
146
+
147
+ const [closedResult, readyResult] = await Promise.all([
148
+ client.closed.catch((err) => err),
149
+ client.ready.catch((err) => err)
150
+ ])
151
+
152
+ expect(closedResult)
153
+ .to.be.a('WebTransportError')
154
+ .with.property('message', 'Opening handshake failed.')
155
+ expect(readyResult)
156
+ .to.be.a('WebTransportError')
157
+ .with.property('message', 'Opening handshake failed.')
158
+ })
159
+ })