v / vlib / net / http / transport_test.v
543 lines · 514 sloc · 15.92 KB · 065a450b86f6459b1e4398fe7b0594bbfcc2d691
Raw
1module http
2
3// Tests for the connection-pooling Transport (transport.v): keep-alive reuse
4// over plain TCP and TLS, no-reuse on `Connection: close` / truncated reads,
5// transparent retry on stale pooled connections, idle eviction, and the
6// `disable_connection_reuse` opt-out. Each test runs its own loopback server
7// with an accept counter: the accept count is the proof of (non-)reuse.
8import net
9import net.mbedtls
10import sync
11import time
12
13const tls_test_cert_path = @VEXEROOT +
14 '/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.crt'
15const tls_test_key_path = @VEXEROOT +
16 '/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.key'
17
18@[heap]
19struct KaSrv {
20mut:
21 mu &sync.Mutex = sync.new_mutex()
22 accepts int
23 // connection_close: respond with `Connection: close` and close afterwards.
24 connection_close bool
25 // close_after_each: close after each response WITHOUT advertising it — the
26 // classic stale-pooled-connection scenario.
27 close_after_each bool
28 // split_connection_close: emit the close token in a SECOND, repeated
29 // `Connection` header (`keep-alive` first, then `close`) while keeping the
30 // socket open — a server that says "do not reuse" via a split field. The
31 // client must honor it and not pool, even though the connection stays usable.
32 split_connection_close bool
33 // drop_on_post: after reading a POST request head, close the connection
34 // without responding — a reused-connection failure after the bytes were
35 // written, for a non-idempotent method.
36 drop_on_post bool
37 posts int // POST request heads actually read by the server
38 body string = 'hello'
39}
40
41fn (mut s KaSrv) bump_accepts() {
42 s.mu.lock()
43 s.accepts++
44 s.mu.unlock()
45}
46
47fn (mut s KaSrv) accept_count() int {
48 s.mu.lock()
49 defer {
50 s.mu.unlock()
51 }
52 return s.accepts
53}
54
55fn (mut s KaSrv) post_count() int {
56 s.mu.lock()
57 defer {
58 s.mu.unlock()
59 }
60 return s.posts
61}
62
63// ka_read_request_head reads from `conn` until a full request head (terminated
64// by a blank line) has arrived, returning it. The test requests carry no bodies.
65fn ka_read_request_head(mut conn net.TcpConn) !string {
66 mut buf := []u8{len: 4096}
67 mut sofar := []u8{}
68 for {
69 n := conn.read(mut buf)!
70 if n <= 0 {
71 return error('closed')
72 }
73 sofar << buf[..n]
74 if sofar.bytestr().contains('\r\n\r\n') {
75 return sofar.bytestr()
76 }
77 }
78 return error('closed')
79}
80
81fn ka_srv_serve_conn(mut s KaSrv, mut conn net.TcpConn) {
82 defer {
83 conn.close() or {}
84 }
85 conn.set_read_timeout(10 * time.second)
86 for {
87 head := ka_read_request_head(mut conn) or { return }
88 if s.drop_on_post && head.starts_with('POST ') {
89 s.mu.lock()
90 s.posts++
91 s.mu.unlock()
92 // Drop the connection without responding: the request bytes were
93 // written, so this is not a stale-write.
94 return
95 }
96 mut resp := 'HTTP/1.1 200 OK\r\nContent-Length: ${s.body.len}\r\n'
97 if s.connection_close {
98 resp += 'Connection: close\r\n'
99 }
100 if s.split_connection_close {
101 resp += 'Connection: keep-alive\r\nConnection: close\r\n'
102 }
103 resp += '\r\n' + s.body
104 conn.write(resp.bytes()) or { return }
105 if s.connection_close || s.close_after_each {
106 return
107 }
108 }
109}
110
111fn ka_srv_loop(mut s KaSrv, mut listener net.TcpListener) {
112 for {
113 mut conn := listener.accept() or { return }
114 s.bump_accepts()
115 ka_srv_serve_conn(mut s, mut conn)
116 }
117}
118
119// start_ka_srv starts a keep-alive capable loopback HTTP server, returning its
120// port, listener and server thread.
121fn start_ka_srv(mut s KaSrv) !(int, &net.TcpListener, thread) {
122 mut listener := net.listen_tcp(.ip, '127.0.0.1:0')!
123 port := listener.addr()!.port()!
124 th := spawn ka_srv_loop(mut s, mut listener)
125 return port, listener, th
126}
127
128// stop_ka_srv tears a test server down fully before the test returns: closing
129// the idle pool unblocks a server thread reading a kept-alive connection,
130// closing the listener aborts its accept, and the join guarantees the thread
131// is gone before the next test creates sockets (otherwise the OS can recycle
132// this listener's handle for the next test's listener while the old thread is
133// still calling accept on it, stealing its connections).
134fn stop_ka_srv(mut listener net.TcpListener, th thread) {
135 close_idle_connections()
136 listener.close() or {}
137 th.wait()
138}
139
140// The pool key must isolate distinct TLS configurations even when a field value
141// contains the '|' separator, or a request could reuse a connection dialed with
142// the wrong cert/CA. cert='a|b',cert_key='c' and cert='a',cert_key='b|c' must
143// not collide.
144fn test_transport_pool_key_no_delimiter_collision() {
145 a := Request{
146 cert: 'a|b'
147 cert_key: 'c'
148 }
149 b := Request{
150 cert: 'a'
151 cert_key: 'b|c'
152 }
153 assert transport_pool_key(a, 'https', 'h', 443) != transport_pool_key(b, 'https', 'h', 443)
154 // Identical configs still share a key.
155 a2 := Request{
156 cert: 'a|b'
157 cert_key: 'c'
158 }
159 assert transport_pool_key(a, 'https', 'h', 443) == transport_pool_key(a2, 'https', 'h', 443)
160 // A host containing '|' must not collide with a different host/port split.
161 assert transport_pool_key(Request{}, 'https', 'h|x', 443) != transport_pool_key(Request{},
162 'https', 'h', 443)
163}
164
165fn test_h1_plain_reuse() {
166 mut srv := &KaSrv{}
167 port, mut listener, th := start_ka_srv(mut srv) or {
168 assert false, 'server: ${err}'
169 return
170 }
171 for i in 0 .. 3 {
172 resp := fetch(url: 'http://127.0.0.1:${port}/r${i}') or {
173 assert false, 'fetch ${i}: ${err}'
174 return
175 }
176 assert resp.status_code == 200
177 assert resp.body == 'hello'
178 }
179 // All three requests must have shared one connection.
180 assert srv.accept_count() == 1
181 stop_ka_srv(mut listener, th)
182}
183
184fn test_h1_no_reuse_on_connection_close_response() {
185 mut srv := &KaSrv{
186 connection_close: true
187 }
188 port, mut listener, th := start_ka_srv(mut srv) or {
189 assert false, 'server: ${err}'
190 return
191 }
192 for i in 0 .. 2 {
193 resp := fetch(url: 'http://127.0.0.1:${port}/r${i}') or {
194 assert false, 'fetch ${i}: ${err}'
195 return
196 }
197 assert resp.status_code == 200
198 }
199 // `Connection: close` responses must not be pooled.
200 assert srv.accept_count() == 2
201 stop_ka_srv(mut listener, th)
202}
203
204// A close token carried in a repeated `Connection` header (after a keep-alive
205// one) must still be honored: parse_headers stores repeats separately and get()
206// returns only the first, so response_allows_reuse joins all values. The server
207// keeps the socket open, so a wrongly-pooled connection would be reused (1
208// accept); honoring the close dials fresh each time (2 accepts).
209fn test_h1_no_reuse_on_split_connection_close() {
210 mut srv := &KaSrv{
211 split_connection_close: true
212 }
213 port, mut listener, th := start_ka_srv(mut srv) or {
214 assert false, 'server: ${err}'
215 return
216 }
217 for i in 0 .. 2 {
218 resp := fetch(url: 'http://127.0.0.1:${port}/r${i}') or {
219 assert false, 'fetch ${i}: ${err}'
220 return
221 }
222 assert resp.status_code == 200
223 }
224 assert srv.accept_count() == 2
225 stop_ka_srv(mut listener, th)
226}
227
228// The total idle pool is bounded by max_idle_conns across all pool keys, not
229// just per host: checking in more distinct-keyed connections than the cap
230// evicts the least-recently-used ones (here k0, k1) and keeps the newest.
231fn test_transport_global_idle_cap() {
232 mut t := new_transport()
233 t.max_idle_conns = 3
234 t.max_idle_conns_per_host = 10 // keep the per-host cap out of the way
235 for i in 0 .. 5 {
236 mut c := &H1PooledConn{
237 key: 'k${i}'
238 }
239 t.checkin(mut c)
240 }
241 mut total := 0
242 for _, list in t.h1_idle {
243 total += list.len
244 }
245 assert total == 3, 'global idle cap not enforced: ${total}'
246 assert 'k0' !in t.h1_idle
247 assert 'k1' !in t.h1_idle
248 assert 'k2' in t.h1_idle
249 assert 'k3' in t.h1_idle
250 assert 'k4' in t.h1_idle
251}
252
253fn test_h1_stale_pooled_connection_is_retried() {
254 mut srv := &KaSrv{
255 close_after_each: true
256 }
257 port, mut listener, th := start_ka_srv(mut srv) or {
258 assert false, 'server: ${err}'
259 return
260 }
261 // First request succeeds and the connection is pooled (the response did not
262 // advertise the close). The server then closes it. The second request picks
263 // up the stale connection, fails, and must transparently retry on a fresh
264 // one.
265 for i in 0 .. 2 {
266 resp := fetch(url: 'http://127.0.0.1:${port}/r${i}') or {
267 assert false, 'fetch ${i}: ${err}'
268 return
269 }
270 assert resp.status_code == 200
271 assert resp.body == 'hello'
272 }
273 assert srv.accept_count() == 2
274 stop_ka_srv(mut listener, th)
275}
276
277// A non-idempotent request that fails on a reused keep-alive connection after
278// its bytes were written must NOT be replayed: doing so could duplicate side
279// effects. The server reads the POST then drops the connection; the client must
280// surface the error without retrying on a fresh connection.
281fn test_h1_unsafe_pooled_post_is_not_retried() {
282 mut srv := &KaSrv{
283 drop_on_post: true
284 }
285 port, mut listener, th := start_ka_srv(mut srv) or {
286 assert false, 'server: ${err}'
287 return
288 }
289 // First, a GET to establish and pool a keep-alive connection.
290 resp := fetch(url: 'http://127.0.0.1:${port}/warmup') or {
291 assert false, 'warmup fetch: ${err}'
292 return
293 }
294 assert resp.status_code == 200
295 // The POST reuses that connection; the server reads it and drops the
296 // connection. The request must fail rather than be re-sent.
297 fetch(method: .post, url: 'http://127.0.0.1:${port}/submit', data: 'payload') or {
298 // expected: the POST failed and was not retried.
299 assert srv.post_count() == 1, 'POST was replayed ${srv.post_count()} times'
300 assert srv.accept_count() == 1, 'a fresh connection was opened to retry the POST'
301 stop_ka_srv(mut listener, th)
302 return
303 }
304 assert false, 'the POST unexpectedly succeeded'
305 stop_ka_srv(mut listener, th)
306}
307
308fn test_h1_opt_out_disables_reuse() {
309 mut srv := &KaSrv{}
310 port, mut listener, th := start_ka_srv(mut srv) or {
311 assert false, 'server: ${err}'
312 return
313 }
314 for i in 0 .. 2 {
315 resp := fetch(
316 url: 'http://127.0.0.1:${port}/r${i}'
317 disable_connection_reuse: true
318 ) or {
319 assert false, 'fetch ${i}: ${err}'
320 return
321 }
322 assert resp.status_code == 200
323 }
324 // Opt-out requests open one connection each.
325 assert srv.accept_count() == 2
326 stop_ka_srv(mut listener, th)
327}
328
329fn test_h1_truncated_read_poisons_connection() {
330 mut srv := &KaSrv{
331 // Larger than the 64KB read buffer, so the body needs several reads and
332 // the stop limit actually interrupts the transfer mid-stream.
333 body: 'x'.repeat(200 * 1024)
334 }
335 port, mut listener, th := start_ka_srv(mut srv) or {
336 assert false, 'server: ${err}'
337 return
338 }
339 // A stop_receiving_limit read leaves unread response bytes on the wire, so
340 // that connection must not be reused.
341 resp1 := fetch(url: 'http://127.0.0.1:${port}/big', stop_receiving_limit: 1000) or {
342 assert false, 'fetch 1: ${err}'
343 return
344 }
345 assert resp1.status_code == 200
346 resp2 := fetch(url: 'http://127.0.0.1:${port}/after') or {
347 assert false, 'fetch 2: ${err}'
348 return
349 }
350 assert resp2.status_code == 200
351 assert srv.accept_count() == 2
352 stop_ka_srv(mut listener, th)
353}
354
355fn test_h1_idle_eviction() {
356 mut srv := &KaSrv{}
357 port, mut listener, th := start_ka_srv(mut srv) or {
358 assert false, 'server: ${err}'
359 return
360 }
361 mut t := new_transport()
362 t.idle_timeout = 50 * time.millisecond
363 req := prepare(url: 'http://127.0.0.1:${port}/') or {
364 assert false, 'prepare: ${err}'
365 return
366 }
367 r1 := t.round_trip(req, .get, 'http', '127.0.0.1', port, '/', '', req.header) or {
368 assert false, 'round_trip 1: ${err}'
369 return
370 }
371 assert r1.status_code == 200
372 time.sleep(150 * time.millisecond)
373 // The pooled connection has sat idle past the timeout: it must be evicted
374 // and a fresh one dialled.
375 r2 := t.round_trip(req, .get, 'http', '127.0.0.1', port, '/', '', req.header) or {
376 assert false, 'round_trip 2: ${err}'
377 return
378 }
379 assert r2.status_code == 200
380 assert srv.accept_count() == 2
381 // This test pools in its own private Transport, so flush that one before
382 // the joint teardown.
383 t.close_idle()
384 stop_ka_srv(mut listener, th)
385}
386
387// --- TLS (h1 over mbedtls) reuse ---
388
389@[heap]
390struct TlsKaSrv {
391mut:
392 mu &sync.Mutex = sync.new_mutex()
393 accepts int
394}
395
396fn (mut s TlsKaSrv) bump_accepts() {
397 s.mu.lock()
398 s.accepts++
399 s.mu.unlock()
400}
401
402fn (mut s TlsKaSrv) accept_count() int {
403 s.mu.lock()
404 defer {
405 s.mu.unlock()
406 }
407 return s.accepts
408}
409
410fn tls_ka_srv_serve_conn(mut conn mbedtls.SSLConn) {
411 defer {
412 conn.shutdown() or {}
413 }
414 body := 'tls hello'
415 for {
416 mut buf := []u8{len: 4096}
417 mut sofar := []u8{}
418 for !sofar.bytestr().contains('\r\n\r\n') {
419 n := conn.read(mut buf) or { return }
420 if n <= 0 {
421 return
422 }
423 sofar << buf[..n]
424 }
425 conn.write_string('HTTP/1.1 200 OK\r\nContent-Length: ${body.len}\r\n\r\n${body}') or {
426 return
427 }
428 }
429}
430
431fn tls_ka_srv_loop(mut s TlsKaSrv, mut listener mbedtls.SSLListener) {
432 for {
433 mut conn := listener.accept() or { return }
434 s.bump_accepts()
435 // Serve each connection on its own thread: a pooled idle connection
436 // keeps its serve loop parked in read, which must not block accepting
437 // the next connection. (The serve threads exit when the test closes
438 // the pooled connections via close_idle_connections.)
439 spawn tls_ka_srv_serve_conn(mut conn)
440 }
441}
442
443fn test_h1_tls_reuse() {
444 $if windows && !no_vschannel ? {
445 // The default Windows TLS backend (SChannel) keeps its one-shot path
446 // until SChannel pooling lands.
447 eprintln('skipping: SChannel connection pooling is not implemented yet')
448 return
449 }
450 mut port_listener := net.listen_tcp(.ip, '127.0.0.1:0') or {
451 assert false, 'port: ${err}'
452 return
453 }
454 port := port_listener.addr() or {
455 assert false, 'addr: ${err}'
456 return
457 }.port() or {
458 assert false, 'port: ${err}'
459 return
460 }
461 port_listener.close() or {}
462 mut listener := mbedtls.new_ssl_listener('127.0.0.1:${port}', mbedtls.SSLConnectConfig{
463 cert: tls_test_cert_path
464 cert_key: tls_test_key_path
465 validate: false
466 }) or {
467 assert false, 'listener: ${err}'
468 return
469 }
470 mut srv := &TlsKaSrv{}
471 th := spawn tls_ka_srv_loop(mut srv, mut listener)
472 for i in 0 .. 2 {
473 resp := fetch(url: 'https://127.0.0.1:${port}/r${i}', validate: false) or {
474 assert false, 'fetch ${i}: ${err}'
475 return
476 }
477 assert resp.status_code == 200
478 assert resp.body == 'tls hello'
479 }
480 // Both https requests must have shared one TLS connection.
481 assert srv.accept_count() == 1
482 // Free the pooled TLS connection (unblocking the server read), then abort
483 // the accept and join the server thread before the test returns.
484 close_idle_connections()
485 listener.shutdown() or {}
486 th.wait()
487}
488
489// A TLS connection dialled with HTTP/2 disabled (no ALPN) must not satisfy an
490// HTTP/2-enabled request to the same origin — the ALPN preference is part of
491// the pool key. Forced-h1 requests still share among themselves.
492fn test_h1_tls_no_reuse_across_alpn_preference() {
493 $if windows && !no_vschannel ? {
494 eprintln('skipping: SChannel connection pooling is not implemented yet')
495 return
496 }
497 mut port_listener := net.listen_tcp(.ip, '127.0.0.1:0') or {
498 assert false, 'port: ${err}'
499 return
500 }
501 port := port_listener.addr() or {
502 assert false, 'addr: ${err}'
503 return
504 }.port() or {
505 assert false, 'port: ${err}'
506 return
507 }
508 port_listener.close() or {}
509 mut listener := mbedtls.new_ssl_listener('127.0.0.1:${port}', mbedtls.SSLConnectConfig{
510 cert: tls_test_cert_path
511 cert_key: tls_test_key_path
512 validate: false
513 }) or {
514 assert false, 'listener: ${err}'
515 return
516 }
517 mut srv := &TlsKaSrv{}
518 th := spawn tls_ka_srv_loop(mut srv, mut listener)
519 // 1. Forced-HTTP/1.1 request: dialled without ALPN, pooled under its key.
520 r1 := fetch(url: 'https://127.0.0.1:${port}/h1', validate: false, enable_http2: false) or {
521 assert false, 'fetch 1: ${err}'
522 return
523 }
524 assert r1.status_code == 200
525 // 2. Default (HTTP/2-enabled) request: must NOT reuse the no-ALPN
526 // connection; it dials fresh and advertises h2.
527 r2 := fetch(url: 'https://127.0.0.1:${port}/h2pref', validate: false) or {
528 assert false, 'fetch 2: ${err}'
529 return
530 }
531 assert r2.status_code == 200
532 assert srv.accept_count() == 2
533 // 3. Another forced-HTTP/1.1 request: reuses connection 1.
534 r3 := fetch(url: 'https://127.0.0.1:${port}/h1again', validate: false, enable_http2: false) or {
535 assert false, 'fetch 3: ${err}'
536 return
537 }
538 assert r3.status_code == 200
539 assert srv.accept_count() == 2
540 close_idle_connections()
541 listener.shutdown() or {}
542 th.wait()
543}
544