vxx2 / vlib / net / http / h2_server_test.v
655 lines · 610 sloc · 17.71 KB · 02015a7ce111e4d0cfd5b5d78711ad0db7586936
Raw
1// Hermetic round-trip test for the server-side HTTP/2 driver. Uses an
2// in-memory blocking ReadWriter pair so client and server share the same
3// address space and no socket is required.
4module http
5
6import sync
7import time
8
9// PipeBuf is a one-way in-memory FIFO between a writer and a reader.
10// `read` blocks (poll-with-sleep) until data is available, mirroring socket
11// semantics so the h2 client and server can drive each other to completion.
12struct PipeBuf {
13mut:
14 mu &sync.Mutex = sync.new_mutex()
15 data []u8
16 closed bool
17}
18
19fn (mut p PipeBuf) write(buf []u8) !int {
20 p.mu.lock()
21 defer {
22 p.mu.unlock()
23 }
24 if p.closed {
25 return error('pipe: write to closed pipe')
26 }
27 p.data << buf
28 return buf.len
29}
30
31fn (mut p PipeBuf) read(mut buf []u8) !int {
32 for {
33 p.mu.lock()
34 if p.data.len > 0 {
35 mut n := p.data.len
36 if n > buf.len {
37 n = buf.len
38 }
39 for i in 0 .. n {
40 buf[i] = p.data[i]
41 }
42 p.data = p.data[n..].clone()
43 p.mu.unlock()
44 return n
45 }
46 if p.closed {
47 p.mu.unlock()
48 return error('eof')
49 }
50 p.mu.unlock()
51 time.sleep(time.millisecond)
52 }
53 return 0
54}
55
56// PipeEnd is one half of a bidirectional pipe: reads come from `incoming`,
57// writes go to `outgoing`.
58struct PipeEnd {
59mut:
60 incoming &PipeBuf
61 outgoing &PipeBuf
62}
63
64fn (mut p PipeEnd) read(mut buf []u8) !int {
65 return p.incoming.read(mut buf)!
66}
67
68fn (mut p PipeEnd) write(buf []u8) !int {
69 return p.outgoing.write(buf)!
70}
71
72fn new_pipe() (&PipeEnd, &PipeEnd) {
73 mut a := &PipeBuf{}
74 mut b := &PipeBuf{}
75 client := &PipeEnd{
76 incoming: b
77 outgoing: a
78 }
79 server := &PipeEnd{
80 incoming: a
81 outgoing: b
82 }
83 return client, server
84}
85
86struct ServerEchoHandler {
87mut:
88 last_method Method
89 last_url string
90 last_body string
91}
92
93fn (mut h ServerEchoHandler) handle(req Request) Response {
94 h.last_method = req.method
95 h.last_url = req.url
96 h.last_body = req.data
97 mut resp_header := new_header()
98 resp_header.add_custom('content-type', 'text/plain') or {}
99 // Echo the URL and body back so callers can verify end-to-end delivery
100 // without depending on Handler-state mutation across goroutines.
101 return Response{
102 status_code: 200
103 header: resp_header
104 body: 'echo: ${req.url} body=${req.data}'
105 }
106}
107
108fn test_h2_server_basic_request() {
109 mut client_end, mut server_end := new_pipe()
110
111 // Spawn the server-side h2 driver against the server end of the pipe.
112 mut handler := ServerEchoHandler{}
113 mut handler_iface := Handler(handler)
114 spawn fn [mut server_end, mut handler_iface] () {
115 mut transport := H2Transport(server_end)
116 serve_h2_conn(mut transport, mut handler_iface) or {}
117 }()
118
119 // Drive a client-side request through the same pipe.
120 mut conn := new_h2_conn(client_end)
121 resp := conn.do(H2ClientRequest{
122 method: 'GET'
123 authority: 'example.com'
124 path: '/hello'
125 }) or {
126 assert false, 'client do() failed: ${err}'
127 return
128 }
129 assert resp.status == 200
130 assert resp.body.bytestr() == 'echo: /hello body='
131 assert resp.headers.any(it.name == 'content-type' && it.value == 'text/plain')
132}
133
134fn test_h2_server_post_with_body() {
135 mut client_end, mut server_end := new_pipe()
136 mut handler := ServerEchoHandler{}
137 mut handler_iface := Handler(handler)
138 spawn fn [mut server_end, mut handler_iface] () {
139 mut transport := H2Transport(server_end)
140 serve_h2_conn(mut transport, mut handler_iface) or {}
141 }()
142
143 mut conn := new_h2_conn(client_end)
144 resp := conn.do(H2ClientRequest{
145 method: 'POST'
146 authority: 'svc.example'
147 path: '/upload'
148 body: 'hello world'.bytes()
149 }) or {
150 assert false, 'client do() failed: ${err}'
151 return
152 }
153 assert resp.status == 200
154 assert resp.body.bytestr() == 'echo: /upload body=hello world'
155}
156
157struct StatusHandler {
158 code int
159}
160
161fn (mut h StatusHandler) handle(req Request) Response {
162 return Response{
163 status_code: h.code
164 body: 'status ${h.code}'
165 }
166}
167
168fn test_h2_server_non_200_status() {
169 mut client_end, mut server_end := new_pipe()
170 mut handler := StatusHandler{
171 code: 404
172 }
173 mut handler_iface := Handler(handler)
174 spawn fn [mut server_end, mut handler_iface] () {
175 mut transport := H2Transport(server_end)
176 serve_h2_conn(mut transport, mut handler_iface) or {}
177 }()
178
179 mut conn := new_h2_conn(client_end)
180 resp := conn.do(H2ClientRequest{ authority: 'h.example' }) or {
181 assert false, 'client do() failed: ${err}'
182 return
183 }
184 assert resp.status == 404
185 assert resp.body.bytestr() == 'status 404'
186}
187
188struct BigBodyHandler {
189 size int
190}
191
192fn (mut h BigBodyHandler) handle(req Request) Response {
193 return Response{
194 status_code: 200
195 body: 'x'.repeat(h.size)
196 }
197}
198
199// FrameReader reads HTTP/2 frames one at a time off a PipeEnd, buffering any
200// leftover bytes between frames.
201struct FrameReader {
202mut:
203 end &PipeEnd
204 buf []u8
205}
206
207fn (mut r FrameReader) next() !H2Frame {
208 for {
209 if r.buf.len >= h2_frame_header_len {
210 hdr := h2_parse_frame_header(r.buf)!
211 total := h2_frame_header_len + int(hdr.length)
212 if r.buf.len >= total {
213 f := h2_parse_frame(hdr, r.buf[h2_frame_header_len..total])!
214 r.buf = r.buf[total..].clone()
215 return f
216 }
217 }
218 mut tmp := []u8{len: 4096}
219 n := r.end.read(mut tmp)!
220 r.buf << tmp[..n]
221 }
222 return error('unreachable')
223}
224
225fn test_h2_server_respects_send_window() {
226 mut client_end, mut server_end := new_pipe()
227 mut handler_iface := Handler(BigBodyHandler{
228 size: 100
229 })
230 spawn fn [mut server_end, mut handler_iface] () {
231 mut transport := H2Transport(server_end)
232 serve_h2_conn(mut transport, mut handler_iface) or {}
233 }()
234
235 // Raw client: preface + SETTINGS(initial_window_size = 10) + a GET, then a
236 // WINDOW_UPDATE(stream 1, +1000). The server must send at most 10 body
237 // bytes before consuming the WINDOW_UPDATE, then deliver the rest.
238 mut enc := H2HpackEncoder{}
239 block := enc.encode([
240 H2HeaderField{':method', 'GET'},
241 H2HeaderField{':scheme', 'https'},
242 H2HeaderField{':authority', 'h.example'},
243 H2HeaderField{':path', '/big'},
244 ])
245 mut out := []u8{}
246 out << h2_client_preface.bytes()
247 out << H2Frame(H2SettingsFrame{
248 settings: [H2Setting{h2_settings_initial_window_size, 10}]
249 }).encode()
250 out << H2Frame(H2HeadersFrame{
251 stream_id: 1
252 fragment: block
253 end_headers: true
254 end_stream: true
255 }).encode()
256 out << H2Frame(H2WindowUpdateFrame{
257 stream_id: 1
258 window_size_increment: 1000
259 }).encode()
260 client_end.write(out) or {
261 assert false, 'client write failed: ${err}'
262 return
263 }
264
265 mut fr := FrameReader{
266 end: client_end
267 }
268 mut body := []u8{}
269 mut status := 0
270 mut first_data_len := -1
271 mut got_end := false
272 mut dec := H2HpackDecoder{}
273 for !got_end {
274 f := fr.next() or {
275 assert false, 'frame read failed: ${err}'
276 return
277 }
278 match f {
279 H2HeadersFrame {
280 for hf in dec.decode(f.fragment) or { []H2HeaderField{} } {
281 if hf.name == ':status' {
282 status = hf.value.int()
283 }
284 }
285 if f.end_stream {
286 got_end = true
287 }
288 }
289 H2DataFrame {
290 if first_data_len < 0 {
291 first_data_len = f.data.len
292 }
293 body << f.data
294 if f.end_stream {
295 got_end = true
296 }
297 }
298 else {}
299 }
300 }
301 assert status == 200
302 assert body.len == 100
303 // The first DATA frame must not exceed the initial 10-byte window.
304 assert first_data_len <= 10
305}
306
307// spawn_h2_echo_server starts the h2 server driver on the server end of a fresh
308// pipe and returns the client end for raw-frame scripting.
309fn spawn_h2_echo_server() &PipeEnd {
310 mut client_end, mut server_end := new_pipe()
311 mut handler_iface := Handler(ServerEchoHandler{})
312 spawn fn [mut server_end, mut handler_iface] () {
313 mut transport := H2Transport(server_end)
314 serve_h2_conn(mut transport, mut handler_iface) or {}
315 }()
316 return client_end
317}
318
319// drive_until_rst_or_response writes `out` to the server, then reads frames back
320// until it sees a RST_STREAM on stream 1 (returns the error code) or a HEADERS
321// frame (a response — returns -1, meaning the request was accepted). It never
322// blocks indefinitely: the server always answers a malformed request with
323// RST_STREAM and a valid one with HEADERS.
324fn drive_until_rst_or_response(mut client_end PipeEnd, out []u8, label string) i64 {
325 client_end.write(out) or {
326 assert false, '${label}: client write failed: ${err}'
327 return -2
328 }
329 mut fr := FrameReader{
330 end: client_end
331 }
332 for _ in 0 .. 32 {
333 f := fr.next() or { break }
334 match f {
335 H2RstStreamFrame {
336 if f.stream_id == 1 {
337 return i64(f.error_code)
338 }
339 }
340 H2HeadersFrame {
341 return -1
342 }
343 else {}
344 }
345 }
346 assert false, '${label}: server sent neither RST_STREAM nor a response'
347 return -2
348}
349
350// malformed_headers_out builds preface + SETTINGS + a single complete HEADERS
351// frame (END_HEADERS, END_STREAM) carrying `fields`.
352fn malformed_headers_out(fields []H2HeaderField) []u8 {
353 mut enc := H2HpackEncoder{}
354 block := enc.encode(fields)
355 mut out := []u8{}
356 out << h2_client_preface.bytes()
357 out << H2Frame(H2SettingsFrame{}).encode()
358 out << H2Frame(H2HeadersFrame{
359 stream_id: 1
360 fragment: block
361 end_headers: true
362 end_stream: true
363 }).encode()
364 return out
365}
366
367fn assert_request_malformed(fields []H2HeaderField, label string) {
368 mut client_end := spawn_h2_echo_server()
369 code := drive_until_rst_or_response(mut client_end, malformed_headers_out(fields), label)
370 assert code == i64(u32(H2ErrorCode.protocol_error)), '${label}: expected RST_STREAM(PROTOCOL_ERROR), got ${code}'
371}
372
373const valid_get_pseudo = [
374 H2HeaderField{':method', 'GET'},
375 H2HeaderField{':scheme', 'https'},
376 H2HeaderField{':authority', 'h.example'},
377 H2HeaderField{':path', '/'},
378]
379
380// RFC 9113 §8.1.2: a field name with uppercase letters is malformed.
381fn test_h2_server_rejects_uppercase_header() {
382 mut fields := valid_get_pseudo.clone()
383 fields << H2HeaderField{'X-Test', 'v'}
384 assert_request_malformed(fields, 'uppercase field name')
385}
386
387// RFC 9113 §8.1.2.1: a pseudo-header after a regular field is malformed.
388fn test_h2_server_rejects_pseudo_after_regular() {
389 fields := [
390 H2HeaderField{'x-a', '1'},
391 H2HeaderField{':method', 'GET'},
392 H2HeaderField{':scheme', 'https'},
393 H2HeaderField{':authority', 'h.example'},
394 H2HeaderField{':path', '/'},
395 ]
396 assert_request_malformed(fields, 'pseudo after regular')
397}
398
399// RFC 9113 §8.1.2.2: TE may only carry the value "trailers".
400fn test_h2_server_rejects_te_not_trailers() {
401 mut fields := valid_get_pseudo.clone()
402 fields << H2HeaderField{'te', 'gzip'}
403 assert_request_malformed(fields, 'TE other than trailers')
404}
405
406// RFC 9113 §8.1.2.2: connection-specific fields are forbidden.
407fn test_h2_server_rejects_connection_specific_header() {
408 mut fields := valid_get_pseudo.clone()
409 fields << H2HeaderField{'connection', 'keep-alive'}
410 assert_request_malformed(fields, 'connection-specific field')
411}
412
413// RFC 9113 §8.1.2.3: omitting :scheme is malformed.
414fn test_h2_server_rejects_missing_scheme() {
415 fields := [
416 H2HeaderField{':method', 'GET'},
417 H2HeaderField{':authority', 'h.example'},
418 H2HeaderField{':path', '/'},
419 ]
420 assert_request_malformed(fields, 'missing :scheme')
421}
422
423// RFC 9113 §8.1.2.3: a duplicated request pseudo-header is malformed.
424fn test_h2_server_rejects_duplicate_pseudo() {
425 for dup in [
426 H2HeaderField{':method', 'GET'},
427 H2HeaderField{':path', '/'},
428 H2HeaderField{':scheme', 'https'},
429 ] {
430 mut fields := valid_get_pseudo.clone()
431 fields << dup
432 assert_request_malformed(fields, 'duplicate ${dup.name}')
433 }
434}
435
436// RFC 9113 §8.2.1: a field value containing NUL/CR/LF is malformed (and would be
437// a header-injection vector if forwarded to an HTTP/1.x peer).
438fn test_h2_server_rejects_control_char_in_value() {
439 mut fields := valid_get_pseudo.clone()
440 fields << H2HeaderField{'x-evil', 'a\r\nInjected: 1'}
441 assert_request_malformed(fields, 'CR/LF in field value')
442}
443
444// RFC 9113 §8.3.1: an empty :path pseudo-header is malformed for http/https.
445fn test_h2_server_rejects_empty_path() {
446 fields := [
447 H2HeaderField{':method', 'GET'},
448 H2HeaderField{':scheme', 'https'},
449 H2HeaderField{':authority', 'h.example'},
450 H2HeaderField{':path', ''},
451 ]
452 assert_request_malformed(fields, 'empty :path')
453}
454
455// RFC 9113 §8.1.2.6: content-length must equal the DATA payload length.
456fn test_h2_server_rejects_content_length_mismatch() {
457 mut enc := H2HpackEncoder{}
458 mut fields := [
459 H2HeaderField{':method', 'POST'},
460 H2HeaderField{':scheme', 'https'},
461 H2HeaderField{':authority', 'h.example'},
462 H2HeaderField{':path', '/upload'},
463 H2HeaderField{'content-length', '5'},
464 ]
465 block := enc.encode(fields)
466 mut out := []u8{}
467 out << h2_client_preface.bytes()
468 out << H2Frame(H2SettingsFrame{}).encode()
469 out << H2Frame(H2HeadersFrame{
470 stream_id: 1
471 fragment: block
472 end_headers: true
473 end_stream: false
474 }).encode()
475 // Two DATA frames totalling 2 bytes, not the declared 5 (covers the
476 // "sum of multiple DATA frames" variant too).
477 out << H2Frame(H2DataFrame{
478 stream_id: 1
479 data: 'h'.bytes()
480 end_stream: false
481 }).encode()
482 out << H2Frame(H2DataFrame{
483 stream_id: 1
484 data: 'i'.bytes()
485 end_stream: true
486 }).encode()
487 mut client_end := spawn_h2_echo_server()
488 code := drive_until_rst_or_response(mut client_end, out, 'content-length mismatch')
489 assert code == i64(u32(H2ErrorCode.protocol_error)), 'content-length mismatch: expected RST_STREAM(PROTOCOL_ERROR), got ${code}'
490}
491
492// RFC 9110 §8.6: conflicting duplicate content-length fields are malformed —
493// every occurrence is validated, not just the last.
494fn test_h2_server_rejects_conflicting_content_length() {
495 mut enc := H2HpackEncoder{}
496 block := enc.encode([
497 H2HeaderField{':method', 'POST'},
498 H2HeaderField{':scheme', 'https'},
499 H2HeaderField{':authority', 'h.example'},
500 H2HeaderField{':path', '/upload'},
501 H2HeaderField{'content-length', '5'},
502 H2HeaderField{'content-length', '0'},
503 ])
504 mut out := []u8{}
505 out << h2_client_preface.bytes()
506 out << H2Frame(H2SettingsFrame{}).encode()
507 out << H2Frame(H2HeadersFrame{
508 stream_id: 1
509 fragment: block
510 end_headers: true
511 end_stream: true
512 }).encode()
513 mut client_end := spawn_h2_echo_server()
514 code := drive_until_rst_or_response(mut client_end, out, 'conflicting content-length')
515 assert code == i64(u32(H2ErrorCode.protocol_error)), 'conflicting content-length: expected RST_STREAM(PROTOCOL_ERROR), got ${code}'
516}
517
518// RFC 9113 §4.3: an HPACK decoding error is a CONNECTION error
519// (GOAWAY COMPRESSION_ERROR), not a stream reset — the decoder's dynamic table is
520// desynced, so the whole connection must close.
521fn test_h2_server_hpack_error_closes_connection() {
522 mut out := []u8{}
523 out << h2_client_preface.bytes()
524 out << H2Frame(H2SettingsFrame{}).encode()
525 // HEADERS with an invalid HPACK block: indexed header field with index 0
526 // (0x80), which RFC 7541 §6.1 forbids -> decode error.
527 out << H2Frame(H2HeadersFrame{
528 stream_id: 1
529 fragment: [u8(0x80)]
530 end_headers: true
531 end_stream: true
532 }).encode()
533 mut client_end := spawn_h2_echo_server()
534 client_end.write(out) or {
535 assert false, 'hpack error: client write failed: ${err}'
536 return
537 }
538 mut fr := FrameReader{
539 end: client_end
540 }
541 mut code := i64(-1)
542 for _ in 0 .. 32 {
543 f := fr.next() or { break }
544 match f {
545 H2GoawayFrame {
546 code = i64(f.error_code)
547 break
548 }
549 H2RstStreamFrame {
550 // Wrong scope: a per-stream reset for an HPACK error.
551 code = -2
552 break
553 }
554 else {}
555 }
556 }
557 assert code == i64(u32(H2ErrorCode.compression_error)), 'HPACK decode error must be GOAWAY(COMPRESSION_ERROR), got ${code}'
558}
559
560// RFC 9113 §8.1: a trailer section that does not carry END_STREAM is malformed.
561// It must be a STREAM error (RST_STREAM) — the connection stays up — not a GOAWAY.
562fn test_h2_server_rejects_trailers_without_end_stream() {
563 mut enc := H2HpackEncoder{}
564 req_block := enc.encode([
565 H2HeaderField{':method', 'POST'},
566 H2HeaderField{':scheme', 'https'},
567 H2HeaderField{':authority', 'h.example'},
568 H2HeaderField{':path', '/upload'},
569 ])
570 trailer_block := enc.encode([
571 H2HeaderField{'x-checksum', 'abc123'},
572 ])
573 mut out := []u8{}
574 out << h2_client_preface.bytes()
575 out << H2Frame(H2SettingsFrame{}).encode()
576 out << H2Frame(H2HeadersFrame{
577 stream_id: 1
578 fragment: req_block
579 end_headers: true
580 end_stream: false
581 }).encode()
582 out << H2Frame(H2DataFrame{
583 stream_id: 1
584 data: 'hi'.bytes()
585 end_stream: false
586 }).encode()
587 // Trailer HEADERS missing END_STREAM.
588 out << H2Frame(H2HeadersFrame{
589 stream_id: 1
590 fragment: trailer_block
591 end_headers: true
592 end_stream: false
593 }).encode()
594 mut client_end := spawn_h2_echo_server()
595 code := drive_until_rst_or_response(mut client_end, out, 'trailers without END_STREAM')
596 assert code == i64(u32(H2ErrorCode.protocol_error)), 'trailers without END_STREAM: expected RST_STREAM(PROTOCOL_ERROR), got ${code}'
597}
598
599// RFC 9113 §8.1: a POST with a trailer section (a 2nd HEADERS block ending the
600// stream) is accepted and dispatched.
601fn test_h2_server_accepts_post_with_trailers() {
602 mut enc := H2HpackEncoder{}
603 req_block := enc.encode([
604 H2HeaderField{':method', 'POST'},
605 H2HeaderField{':scheme', 'https'},
606 H2HeaderField{':authority', 'h.example'},
607 H2HeaderField{':path', '/upload'},
608 ])
609 trailer_block := enc.encode([
610 H2HeaderField{'x-checksum', 'abc123'},
611 ])
612 mut out := []u8{}
613 out << h2_client_preface.bytes()
614 out << H2Frame(H2SettingsFrame{}).encode()
615 out << H2Frame(H2HeadersFrame{
616 stream_id: 1
617 fragment: req_block
618 end_headers: true
619 end_stream: false
620 }).encode()
621 out << H2Frame(H2DataFrame{
622 stream_id: 1
623 data: 'hi'.bytes()
624 end_stream: false
625 }).encode()
626 out << H2Frame(H2HeadersFrame{
627 stream_id: 1
628 fragment: trailer_block
629 end_headers: true
630 end_stream: true
631 }).encode()
632
633 mut client_end := spawn_h2_echo_server()
634 client_end.write(out) or {
635 assert false, 'trailers POST: client write failed: ${err}'
636 return
637 }
638 mut fr := FrameReader{
639 end: client_end
640 }
641 mut dec := H2HpackDecoder{}
642 mut status := 0
643 for _ in 0 .. 32 {
644 f := fr.next() or { break }
645 if f is H2HeadersFrame {
646 for hf in dec.decode(f.fragment) or { []H2HeaderField{} } {
647 if hf.name == ':status' {
648 status = hf.value.int()
649 }
650 }
651 break
652 }
653 }
654 assert status == 200, 'trailers POST should succeed, got status ${status}'
655}
656