Skip to content

Commit c9e9ade

Browse files
committed
quic: add wt bidi from server test
1 parent 8b5c181 commit c9e9ade

2 files changed

Lines changed: 454 additions & 0 deletions

File tree

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
// Flags: --experimental-quic --experimental-stream-iter --no-warnings
2+
3+
// Test: HTTP/3 webtransport client initiated bidi stream
4+
// Server creates a bidi stream and client send data that is echoed back to the client
5+
6+
import { hasQuic, skip, mustCall, mustNotCall, mustCallAtLeast } from '../common/index.mjs';
7+
import assert from 'node:assert';
8+
import * as fixtures from '../common/fixtures.mjs';
9+
10+
if (!hasQuic) {
11+
skip('QUIC is not enabled');
12+
}
13+
14+
const { listen, connect } = await import('node:quic');
15+
const { createPrivateKey } = await import('node:crypto');
16+
const { drainableProtocol: dp } = await import('stream/iter');
17+
18+
const key = createPrivateKey(fixtures.readKey('agent1-key.pem'));
19+
const cert = fixtures.readKey('agent1-cert.pem');
20+
21+
const chunkSizes = [60000, 12, 1000000, 50000, 1600, 20000, 1000000, 30000, 0, 100];
22+
const numChunks = chunkSizes.length;
23+
const byteLength = chunkSizes.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
24+
25+
26+
// Build a deterministic payload so we can verify integrity.
27+
function buildChunk(index) {
28+
const chunk = new Uint8Array(chunkSizes[index]);
29+
// Fill with a pattern derived from the chunk index.
30+
const val = index & 0xff;
31+
for (let i = 0; i < chunkSizes[index]; i++) {
32+
chunk[i] = (val + i) & 0xff;
33+
}
34+
return chunk;
35+
}
36+
37+
function checksum(data) {
38+
let sum = 0;
39+
for (let i = 0; i < data.byteLength; i++) {
40+
sum = (sum + data[i]) | 0;
41+
}
42+
return sum;
43+
}
44+
45+
// Compute expected checksum.
46+
let expectedChecksum = 0;
47+
for (let i = 0; i < numChunks; i++) {
48+
const chunk = buildChunk(i);
49+
expectedChecksum = (expectedChecksum + checksum(chunk)) | 0;
50+
}
51+
52+
const serverSessionOpened = Promise.withResolvers();
53+
54+
55+
const serverEndpoint = await listen(mustCall(async (ss) => {
56+
ss.onapplication = mustCall((aopts) => {
57+
assert.strictEqual(!aopts.enableDatagrams, false);
58+
});
59+
ss.onhandshake = mustCall(function() {
60+
assert.strictEqual(BigInt(this?.remoteTransportParams?.maxDatagramFrameSize) > 0, true);
61+
});
62+
ss.onstream = mustCall((stream) => {
63+
stream.onsessionid = mustNotCall(async (sessionid) => {
64+
console.log('No client initiated stream expected!');
65+
});
66+
});
67+
}), {
68+
sni: { '*': { keys: [key], certs: [cert] } },
69+
application: {
70+
enableConnectProtocol: true,
71+
enableDatagrams: true,
72+
enableWebtransport: true
73+
},
74+
transportParams: {
75+
maxDatagramFrameSize: 1000,
76+
initialMaxStreamsBidi: 100, // default value according to spec
77+
initialMaxStreamsUni: 100, // especially important as limit default is 0
78+
},
79+
onheaders: mustCall(async function(headers) {
80+
try {
81+
assert.strictEqual(headers[':scheme'], 'https');
82+
assert.strictEqual(headers[':method'], 'CONNECT');
83+
assert.strictEqual(headers[':protocol'], 'webtransport'); // depends on the draft
84+
assert.strictEqual(headers[':path'], '/testwtpath');
85+
// We could also check for wt-available protocols
86+
this.sendHeaders(
87+
{ ':status': '200' },
88+
{ terminal: false, webtransport: true }
89+
);
90+
serverSessionOpened.resolve(this.id);
91+
} catch (error) {
92+
serverSessionOpened.reject(error);
93+
}
94+
// Should only be installed on wt streams
95+
this.onwtsessionclose = mustCall((code, reason) => {
96+
assert.strictEqual(code, 200);
97+
assert.strictEqual(reason, 'all perfect');
98+
this.session.close();
99+
});
100+
101+
// Well let's get a bidi stream and echo all
102+
const serverBidiStream = await this.session.createBidirectionalStream({
103+
incremental: true,
104+
webtransportSession: this // Associate it with the sessionStream
105+
});
106+
// Now we can echo all data.
107+
const writer = serverBidiStream.writer;
108+
let echoedData = 0;
109+
for await (const chunks of serverBidiStream) {
110+
for (const chunk of chunks) {
111+
echoedData += chunk.byteLength
112+
while (!writer.writeSync(chunk)) {
113+
// Flow controlled — wait for drain before retrying.
114+
const drainable = writer[dp]();
115+
if (drainable) await drainable;
116+
}
117+
}
118+
}
119+
writer.end();
120+
}),
121+
});
122+
123+
const clientSession = await connect(serverEndpoint.address, {
124+
servername: 'localhost',
125+
verifyPeer: 'manual',
126+
application: {
127+
enableConnectProtocol: true,
128+
enableDatagrams: true,
129+
enableWebtransport: true
130+
},
131+
transportParams: {
132+
maxDatagramFrameSize: 1000,
133+
initialMaxStreamsBidi: 100, // default value according to spec
134+
initialMaxStreamsUni: 100, // especially important as limit default is 0
135+
},
136+
});
137+
138+
const webtransportSupport = Promise.withResolvers();
139+
140+
clientSession.onapplication = mustCall((aopts) => {
141+
try {
142+
// Test for webtransport support
143+
assert.strictEqual(aopts.enableConnectProtocol, true);
144+
assert.strictEqual(aopts.enableDatagrams, true);
145+
assert.strictEqual(aopts.enableWebtransport, true);
146+
// Ok we have wt support
147+
webtransportSupport.resolve();
148+
} catch (error) {
149+
webtransportSupport.reject(error);
150+
}
151+
});
152+
153+
clientSession.onstream = mustNotCall((stream) => {
154+
stream.onheaders = mustNotCall((stream) => {
155+
// Well this should not happen on client side
156+
console.log('Called onheaders on the client side!');
157+
});
158+
});
159+
160+
await clientSession.opened;
161+
await webtransportSupport.promise;
162+
163+
clientSession.onstream = mustCall((stream) => {
164+
stream.onsessionid = mustCall(async (sessionid) => {
165+
assert.strictEqual(sessionid, wtSessionStream.id);
166+
await Promise.all([readFromStream(stream), writeToStream(stream)]);
167+
await wtSessionStream.closeWebtransportSessionStream(200, 'all perfect');
168+
});
169+
});
170+
171+
// Now we open a webtransport session, which is actually
172+
// a special bidirectional stream
173+
const wtSessionStream = await clientSession.createBidirectionalStream({
174+
body: '',
175+
});
176+
wtSessionStream.sendHeaders({
177+
':method': 'CONNECT',
178+
':scheme': 'https',
179+
// This one depends on draft, draft14 says "webtransport", draft15 says "webtransport-h3"
180+
':protocol': 'webtransport',
181+
':path': '/testwtpath',
182+
':authority': 'testserver:' + serverEndpoint.address.port
183+
}, {
184+
webtransport: true // Tell nghttp3 to treat the stream as a WT session stream
185+
});
186+
187+
188+
// Next step send some data
189+
190+
191+
const readFromStream = mustCallAtLeast(async (stream) => {
192+
const readChunks = [];
193+
let readdata = 0;
194+
for await (const chunks of stream) {
195+
for (const chunk of chunks) {
196+
readdata += chunk.byteLength;
197+
}
198+
readChunks.push(...chunks);
199+
}
200+
const receivedBytes = readChunks.reduce((accu, curVal) => accu + curVal.byteLength, 0);
201+
202+
assert.strictEqual(receivedBytes, byteLength);
203+
let receivedChecksum = 0;
204+
for (const chunk of readChunks) {
205+
receivedChecksum = (receivedChecksum + checksum(chunk)) | 0;
206+
}
207+
assert.strictEqual(receivedChecksum, expectedChecksum);
208+
}, 1);
209+
210+
211+
const writeToStream = mustCallAtLeast(async (stream) => {
212+
const w = stream.writer;
213+
let writtenData = 0;
214+
for (let i = 0; i < numChunks; i++) {
215+
const chunk = buildChunk(i);
216+
writtenData += chunk.byteLength;
217+
while (!w.writeSync(chunk)) {
218+
// Flow controlled — wait for drain before retrying.
219+
const drainable = w[dp]();
220+
if (drainable) await drainable;
221+
}
222+
}
223+
w.endSync();
224+
}, 1);
225+
226+
await serverSessionOpened.promise;
227+
228+
try {
229+
await wtSessionStream.closed;
230+
} catch (error) {
231+
assert.strictEqual(error.errorCode, 200n);
232+
assert.strictEqual(error.reason, 'all perfect');
233+
}
234+
await clientSession.close();
235+
await serverEndpoint.close();

0 commit comments

Comments
 (0)