Skip to content

Commit 1eda5d9

Browse files
committed
fix(net.http,v2,quic): harden protocol safety, fix 29 adversarial review findings
Critical fixes (17 resolved): - Header.add()/set() now return errors on overflow instead of silent header drops that could lose Content-Length/Authorization - Remove unsafe mutation of immutable request during 303 redirects; use local effective_data variable instead of UB cast - set_custom() iterates only populated header slots (cur_pos), not the full 50-element fixed array - Server worker threads receive channel close signal on shutdown instead of leaking permanently - HTTP/2 stream state violations now return PROTOCOL_ERROR per RFC 7540 §5.1, not silently ignored log messages - HTTP/2 stream ID overflow check (>0x7FFFFFFF) prevents reuse - Connection pool evicts stale/closed connections before returning - QUIC RAND_bytes failure detected with arc4random fallback - QUIC timestamps use monotonic clock (sys_mono_now) instead of wall clock that drifts with NTP/DST corrections (6 occurrences) - Negative offset/length bounds check in QUIC stream data events High fixes (12 resolved): - Retry loops return explicit max-retries error, not misleading "unsupported scheme" - Body boundary detection uses >= 0 (not > 0) for position check - URL params properly encoded with query_escape in fetch() - HTTP/2 unknown SETTINGS return Option (none) per RFC 7540 §6.5.2 - encode_optimized adds never-indexed check for sensitive headers (authorization, cookie) preventing intermediary indexing - ConnectionPool.size() acquires mutex for thread safety - Extension HTTP methods (PROPFIND, BREW) no longer rejected - IPv6 address parsing supports bracket notation [::1]:port - README Quick Start examples rewritten to match actual API - Empty/println-only QUIC tests replaced with real assertions - Certificate generation command fixed (separate key.pem/cert.pem)
1 parent 2ae0ca8 commit 1eda5d9

39 files changed

Lines changed: 790 additions & 158 deletions

examples/http2/01_simple_server.v

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//
55
// To generate test certificates:
66
// openssl req -x509 -newkey rsa:2048 -nodes \
7-
// -keyout cert.pem -out cert.pem -days 365 \
7+
// -keyout key.pem -out cert.pem -days 365 \
88
// -subj "/CN=localhost"
99
//
1010
// Test with: curl -k --http2 https://localhost:8080/

examples/http2/README.md

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ A simple HTTP/2 server demonstrating basic usage.
1818
v run examples/http2/01_simple_server.v
1919
```
2020

21-
Then visit: `http://localhost:8080`
21+
Then visit: `https://localhost:8080`
2222

2323
---
2424

@@ -66,32 +66,50 @@ Benchmark 4: Multiple Streams Simulation
6666
### Basic HTTP/2 Server
6767

6868
```v
69-
import net.http.v2
69+
import net.http
70+
71+
struct MyHandler {}
72+
73+
fn (h MyHandler) handle(req http.ServerRequest) http.ServerResponse {
74+
return http.ServerResponse{
75+
status_code: 200
76+
header: http.new_header_from_map({
77+
.content_type: 'text/html; charset=utf-8'
78+
})
79+
body: '<h1>Hello from HTTP/2!</h1>'.bytes()
80+
}
81+
}
7082
7183
fn main() {
72-
mut server := v2.new_server(port: 8080)
73-
74-
server.on('/', fn (req v2.Request) v2.Response {
75-
return v2.Response{
76-
status_code: 200
77-
body: 'Hello HTTP/2!'
78-
}
79-
})
80-
81-
server.listen()!
84+
mut server := http.Server{
85+
addr: '0.0.0.0:8080'
86+
handler: MyHandler{}
87+
cert_file: 'cert.pem'
88+
key_file: 'key.pem'
89+
}
90+
// HTTP/2 is enabled automatically over TLS (ALPN h2)
91+
server.listen_and_serve_tls() or { eprintln('Server error: ${err}') }
8292
}
8393
```
8494

8595
### Basic HTTP/2 Client
8696

8797
```v
88-
import net.http.v2
98+
import net.http
8999
90100
fn main() {
91-
mut client := v2.new_client()
92-
93-
resp := client.get('https://example.com')!
94-
println(resp.body)
101+
response := http.fetch(
102+
url: 'https://nghttp2.org/'
103+
method: .get
104+
header: http.new_header_from_map({
105+
.user_agent: 'V-HTTP2-Client/1.0'
106+
})
107+
) or {
108+
eprintln('Request failed: ${err}')
109+
return
110+
}
111+
println('Status: ${response.status_code}')
112+
println('Body: ${response.body[..200]}...')
95113
}
96114
```
97115

examples/http3/README.md

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -121,36 +121,56 @@ v run examples/http3/04_standalone_tests.v
121121
### Basic HTTP/3 Server
122122

123123
```v
124-
import net.http.v3
124+
import net.http
125+
126+
struct MyHandler {}
127+
128+
fn (h MyHandler) handle(req http.ServerRequest) http.ServerResponse {
129+
return http.ServerResponse{
130+
status_code: 200
131+
header: http.new_header_from_map({
132+
.content_type: 'text/html; charset=utf-8'
133+
})
134+
body: '<h1>Hello from HTTP/3!</h1>'.bytes()
135+
}
136+
}
125137
126138
fn main() {
127-
mut server := v3.new_server(
128-
port: 4433
129-
cert_file: 'cert.pem'
130-
key_file: 'key.pem'
131-
)
132-
133-
server.on('/', fn (req v3.Request) v3.Response {
134-
return v3.Response{
135-
status_code: 200
136-
body: 'Hello HTTP/3!'
137-
}
138-
})
139-
140-
server.listen()!
139+
mut server := http.Server{
140+
addr: '0.0.0.0:8080'
141+
tls_addr: ':4433'
142+
h3_addr: ':4433'
143+
handler: MyHandler{}
144+
cert_file: 'server.crt'
145+
key_file: 'server.key'
146+
enable_h3: true
147+
}
148+
// Starts HTTP/1.1 + HTTP/2 + HTTP/3 with the same handler
149+
server.listen_and_serve_all() or { eprintln('Server error: ${err}') }
141150
}
142151
```
143152

144153
### Basic HTTP/3 Client
145154

146155
```v
147-
import net.http.v3
156+
import net.http
148157
149158
fn main() {
150-
mut client := v3.new_client()
151-
152-
resp := client.get('https://example.com')!
153-
println(resp.body)
159+
// Protocol is negotiated automatically over TLS.
160+
// If the server advertises HTTP/3 via Alt-Svc, subsequent
161+
// requests can upgrade when using an Alt-Svc cache.
162+
response := http.fetch(
163+
url: 'https://cloudflare-quic.com/'
164+
method: .get
165+
header: http.new_header_from_map({
166+
.user_agent: 'V-HTTP3-Client/1.0'
167+
})
168+
) or {
169+
eprintln('Request failed: ${err}')
170+
return
171+
}
172+
println('Status: ${response.status_code}')
173+
println('Body: ${response.body[..200]}...')
154174
}
155175
```
156176

@@ -165,8 +185,8 @@ import net.http.v3
165185
166186
mut encoder := v3.new_qpack_encoder(4096, 100)
167187
headers := [
168-
v3.HeaderField{':method', 'GET'},
169-
v3.HeaderField{':path', '/'},
188+
v3.HeaderField{ name: ':method', value: 'GET' },
189+
v3.HeaderField{ name: ':path', value: '/' },
170190
]
171191
encoded := encoder.encode(headers)
172192
// Achieves 2-30x compression ratio
@@ -177,24 +197,35 @@ encoded := encoder.encode(headers)
177197
```v
178198
import net.quic
179199
200+
// Create a shared session cache for ticket storage
180201
mut cache := quic.new_session_cache()
202+
203+
// Store a session ticket after the first connection
181204
cache.store('example.com', ticket)
182205
183-
// Next connection uses 0-RTT
206+
// Subsequent connections can use 0-RTT with the cached ticket
184207
mut conn := quic.new_connection(
185-
server_name: 'example.com'
208+
remote_addr: 'example.com:4433'
209+
enable_0rtt: true
186210
session_cache: cache
187-
)
188-
// 50-70% latency reduction
211+
)!
212+
// 50-70% latency reduction on resumed connections
189213
```
190214

191215
### Connection Migration
192216

193217
```v
194218
import net.quic
219+
import net
220+
221+
// Create a migration manager for the current path
222+
local_addr := net.Addr{}
223+
remote_addr := net.Addr{}
224+
mut migration := quic.new_connection_migration(local_addr, remote_addr)
195225
196-
mut migration := quic.new_connection_migration(local, remote)
197-
migration.handle_network_change(new_local_addr)!
226+
// Probe a new path when the network changes
227+
new_local := net.Addr{}
228+
migration.probe_path(new_local, remote_addr)!
198229
// Seamless WiFi ↔ Cellular switching
199230
```
200231

vlib/net/http/alt_svc.v

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ fn parse_authority(authority string) (string, int) {
129129
colon := authority.last_index(':') or { return authority, 0 }
130130
host := authority[..colon]
131131
port := authority[colon + 1..].int()
132+
if port < 1 || port > 65535 {
133+
return host, 0
134+
}
132135
return host, port
133136
}
134137

vlib/net/http/backend.c.v

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ module http
66
import net.ssl
77
import strings
88

9-
fn (req &Request) ssl_do(port int, method Method, host_name string, path string) !Response {
9+
fn (req &Request) ssl_do(port int, method Method, host_name string, path string, effective_data string) !Response {
1010
$if windows && !no_vschannel ? {
11-
return vschannel_ssl_do(req, port, method, host_name, path)
11+
return vschannel_ssl_do(req, port, method, host_name, path, effective_data)
1212
}
13-
return net_ssl_do(req, port, method, host_name, path)
13+
return net_ssl_do(req, port, method, host_name, path, effective_data)
1414
}
1515

16-
fn net_ssl_do(req &Request, port int, method Method, host_name string, path string) !Response {
16+
fn net_ssl_do(req &Request, port int, method Method, host_name string, path string, effective_data string) !Response {
1717
mut ssl_conn := ssl.new_ssl_conn(
1818
verify: req.verify
1919
cert: req.cert
@@ -33,7 +33,7 @@ fn net_ssl_do(req &Request, port int, method Method, host_name string, path stri
3333
break
3434
}
3535

36-
req_headers := req.build_request_headers(method, host_name, port, path)
36+
req_headers := req.build_request_headers(method, host_name, port, path, effective_data)
3737
$if trace_http_request ? {
3838
eprint('> ')
3939
eprint(req_headers)

vlib/net/http/backend_vschannel_windows.c.v

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ pub struct C.TlsContext {}
1111

1212
fn C.new_tls_context() C.TlsContext
1313

14-
fn vschannel_ssl_do(req &Request, port int, method Method, host_name string, path string) !Response {
14+
fn vschannel_ssl_do(req &Request, port int, method Method, host_name string, path string, effective_data string) !Response {
1515
mut ctx := C.new_tls_context()
1616
C.vschannel_init(&ctx)
1717
mut buff := unsafe { malloc_noscan(C.vsc_init_resp_buff_size) }
1818
addr := host_name
19-
sdata := req.build_request_headers(method, host_name, port, path)
19+
sdata := req.build_request_headers(method, host_name, port, path, effective_data)
2020
$if trace_http_request ? {
2121
eprintln('> ${sdata}')
2222
}

vlib/net/http/bench_body_test.v

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ fn test_bench_body_round_trip_by_size() {
4747
fn test_bench_server_request_construction() {
4848
body_data := []u8{len: 16384, init: u8(index % 256)}
4949
mut header := common.new_header()
50-
header.add(.content_type, 'application/octet-stream')
51-
header.add(.host, 'localhost:8080')
50+
header.add(.content_type, 'application/octet-stream') or {}
51+
header.add(.host, 'localhost:8080') or {}
5252

5353
mut sw_old := time.new_stopwatch()
5454
for _ in 0 .. bench_iterations {

vlib/net/http/build_request_headers_test.v

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ fn test_build_request_headers_with_empty_body_adds_content_length_zero() {
66
// Build the headers for it. Ensure that Content-Length: 0 is added
77
// for requests without a body, which is required by some servers.
88
// We use a POST request, as it is most likely to be affected by this.
9-
headers := req.build_request_headers(.post, 'localhost', 80, '/')
9+
headers := req.build_request_headers(.post, 'localhost', 80, '/', '')
1010
assert headers.contains('Content-Length: 0\r\n')
1111
}

vlib/net/http/common/header.v

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ pub fn new_header(kvs ...HeaderConfig) Header {
368368

369369
pub fn new_header_from_map(kvs map[CommonHeader]string) Header {
370370
mut h := new_header()
371-
h.add_map(kvs)
371+
h.add_map(kvs) or {}
372372
return h
373373
}
374374

@@ -416,10 +416,10 @@ pub fn from_map(m map[string]string) Header {
416416
return h
417417
}
418418

419-
pub fn (mut h Header) add(key CommonHeader, value string) {
419+
pub fn (mut h Header) add(key CommonHeader, value string) ! {
420420
k := key.str()
421421
if !h.has_capacity() {
422-
return
422+
return error('maximum number of headers reached')
423423
}
424424
h.data[h.cur_pos] = HeaderKV{k, value}
425425
h.cur_pos++
@@ -434,9 +434,9 @@ pub fn (mut h Header) add_custom(key string, value string) ! {
434434
h.cur_pos++
435435
}
436436

437-
pub fn (mut h Header) add_map(kvs map[CommonHeader]string) {
437+
pub fn (mut h Header) add_map(kvs map[CommonHeader]string) ! {
438438
for k, v in kvs {
439-
h.add(k, v)
439+
h.add(k, v)!
440440
}
441441
}
442442

@@ -446,7 +446,7 @@ pub fn (mut h Header) add_custom_map(kvs map[string]string) ! {
446446
}
447447
}
448448

449-
pub fn (mut h Header) set(key CommonHeader, value string) {
449+
pub fn (mut h Header) set(key CommonHeader, value string) ! {
450450
key_str := key.str()
451451
for i := 0; i < h.cur_pos; i++ {
452452
if h.data[i].key == key_str && h.data[i].value != '' {
@@ -455,7 +455,7 @@ pub fn (mut h Header) set(key CommonHeader, value string) {
455455
}
456456
}
457457
if !h.has_capacity() {
458-
return
458+
return error('maximum number of headers reached')
459459
}
460460
h.data[h.cur_pos] = HeaderKV{key_str, value}
461461
h.cur_pos++
@@ -464,7 +464,8 @@ pub fn (mut h Header) set(key CommonHeader, value string) {
464464
pub fn (mut h Header) set_custom(key string, value string) ! {
465465
is_valid(key)!
466466
mut set := false
467-
for i, kv in h.data {
467+
for i := 0; i < h.cur_pos; i++ {
468+
kv := h.data[i]
468469
if kv.key == key {
469470
if !set {
470471
h.data[i] = HeaderKV{key, value}
@@ -620,7 +621,7 @@ pub:
620621

621622
@[manualfree]
622623
pub fn (h Header) render(flags HeaderRenderConfig) string {
623-
mut sb := strings.new_builder(h.data.len * 48)
624+
mut sb := strings.new_builder(h.cur_pos * 48)
624625
h.render_into_sb(mut sb, flags)
625626
res := sb.str()
626627
unsafe { sb.free() }

vlib/net/http/common/version.v

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub fn (v Version) str() string {
1313
return match v {
1414
.v1_1 { 'HTTP/1.1' }
1515
.v2_0 { 'HTTP/2.0' }
16-
.v3_0 { 'HTTP/3.0' }
16+
.v3_0 { 'HTTP/3' }
1717
.v1_0 { 'HTTP/1.0' }
1818
.unknown { 'unknown' }
1919
}

0 commit comments

Comments
 (0)