v4 / vlib / net / http / server_tls_notd_use_openssl.v
303 lines · 283 sloc · 10.34 KB · 6e2d911004257b2b6384e3a12ad0c050275f4ed6
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module http
5
6import io
7import net
8import time
9import net.mbedtls
10
11const tls_accept_poll_timeout = 100 * time.millisecond
12
13// tls_handshake_timeout is the default value for Server.tls_handshake_timeout,
14// used as the fallback handshake budget when Server.accept_timeout is zero or
15// net.infinite_timeout. The handshake now runs on a worker thread (the accept
16// thread only does the raw TCP accept, polled at tls_accept_poll_timeout, so
17// stop() is observed promptly regardless of handshakes); without a finite bound a
18// client that completes the TCP connect and then stalls mid-handshake would tie
19// up a worker indefinitely.
20const tls_handshake_timeout = 30 * time.second
21
22fn tls_accept_timeouts(accept_timeout time.Duration, handshake_fallback time.Duration) (time.Duration, time.Duration) {
23 // A finite `accept_timeout` doubles as the handshake budget; when it is
24 // zero or net.infinite_timeout (i64.max), fall back to `handshake_fallback`
25 // so the handshake still times out and shutdown stays responsive.
26 // net.infinite_timeout is positive (i64.max), so the > 0 check alone is
27 // not enough — mbedtls's ssl_timeout_deadline treats it as an infinite
28 // deadline exactly like 0 or negative values.
29 is_finite := accept_timeout > 0 && accept_timeout != net.infinite_timeout
30 handshake_timeout := if is_finite { accept_timeout } else { handshake_fallback }
31 accept_poll_timeout := if is_finite && accept_timeout < tls_accept_poll_timeout {
32 accept_timeout
33 } else {
34 tls_accept_poll_timeout
35 }
36 return accept_poll_timeout, handshake_timeout
37}
38
39// This file implements TLS termination for net.http.Server on top of the
40// mbedtls SSL listener. It is gated to the default TLS backend; the matching
41// `server_tls_d_use_openssl.v` provides a clear-error stub when the project is
42// built with `-d use_openssl`.
43
44// listen_and_serve_tls is the TLS counterpart of listen_and_serve. It is
45// dispatched to by listen_and_serve when `s.cert` and `s.cert_key` are set.
46fn (mut s Server) listen_and_serve_tls() {
47 // Pick a default port that's distinct from the plain-HTTP default if the
48 // user hasn't overridden it.
49 addr := if s.addr == '' || s.addr == ':${default_server_port}' {
50 ':${default_https_server_port}'
51 } else {
52 s.addr
53 }
54
55 // When HTTP/2 is enabled, advertise ALPN `h2, http/1.1` on the listener.
56 // Clients that select `h2` are dispatched to the HTTP/2 driver after the
57 // handshake; clients that select `http/1.1` (or send no ALPN extension)
58 // keep the existing HTTP/1.1 worker path.
59 alpn := if s.enable_http2 { ['h2', 'http/1.1'] } else { []string{} }
60 mut listener := mbedtls.new_ssl_listener(addr, mbedtls.SSLConnectConfig{
61 cert: s.cert
62 cert_key: s.cert_key
63 in_memory_verification: s.in_memory_verification
64 validate: false // accept any client; servers don't verify clients by default
65 read_timeout: s.read_timeout
66 alpn_protocols: alpn
67 }) or {
68 eprintln('Listening TLS on ${addr} failed, err: ${err}')
69 return
70 }
71 defer {
72 listener.shutdown() or {}
73 if s.state == .stopped {
74 s.state = .closed
75 if s.on_closed != unsafe { nil } {
76 s.on_closed(mut s)
77 }
78 }
79 }
80 s.addr = addr
81
82 accept_poll_timeout, handshake_timeout := tls_accept_timeouts(s.accept_timeout,
83 s.tls_handshake_timeout)
84 ch := chan &mbedtls.SSLConn{cap: s.pool_channel_slots}
85 mut idle_conns := &TlsIdleConnTracker{}
86 mut ws := []thread{cap: s.worker_num}
87 for wid in 0 .. s.worker_num {
88 ws << new_tls_handler_worker(wid, ch, s.handler, s.max_keep_alive_requests,
89 handshake_timeout, idle_conns)
90 }
91
92 if s.show_startup_message {
93 println('Listening on https://${s.addr}/')
94 flush_stdout()
95 }
96
97 time.sleep(20 * time.millisecond)
98 s.state = .running
99 if s.on_running != unsafe { nil } {
100 s.on_running(mut s)
101 }
102 for s.state == .running {
103 mut conn := listener.accept_raw_with_timeout(accept_poll_timeout) or {
104 if s.state != .running {
105 break
106 }
107 if err.code() == net.err_timed_out_code {
108 continue
109 }
110 $if debug {
111 eprintln('TLS accept failed: ${err}; skipping')
112 }
113 continue
114 }
115 // Hand the raw (not-yet-handshaked) conn to a worker. Don't use a bare
116 // blocking `ch <- conn`: the accept loop, this send, and the post-loop
117 // shutdown (ch.close + close_idle + ws.wait) all run on this one thread,
118 // while stop()/close() only flip s.state from another thread. Under a slow-
119 // handshake flood -- every worker blocked in complete_handshake and the
120 // channel buffer full of untracked conns -- a blocking send would wedge the
121 // accept thread so it never re-checks s.state nor reaches close_idle(),
122 // delaying shutdown until a worker handshake times out. Poll s.state via a
123 // select timeout so shutdown is still observed promptly.
124 mut queued := false
125 for s.state == .running && !queued {
126 select {
127 ch <- conn {
128 queued = true
129 }
130 accept_poll_timeout {
131 // channel full; loop re-checks s.state
132 }
133 }
134 }
135 if !queued {
136 // Shutting down before this conn could be handed off. It was never
137 // mark_idle'd, so close_idle() won't see it; close it here (exactly
138 // once -- no worker ever received it) to avoid leaking the fd.
139 conn.shutdown() or {}
140 }
141 }
142 ch.close()
143 idle_conns.close_idle()
144 ws.wait()
145}
146
147// TlsHandlerWorker serves HTTP/1.1 requests on TLS-wrapped connections.
148struct TlsHandlerWorker {
149 id int
150 ch chan &mbedtls.SSLConn
151 max_keep_alive_requests int
152 handshake_timeout time.Duration
153mut:
154 idle_conns &TlsIdleConnTracker = unsafe { nil }
155pub mut:
156 handler Handler
157}
158
159fn new_tls_handler_worker(wid int, ch chan &mbedtls.SSLConn, handler Handler, max_keep_alive_requests int, handshake_timeout time.Duration, idle_conns &TlsIdleConnTracker) thread {
160 mut w := &TlsHandlerWorker{
161 id: wid
162 ch: ch
163 handler: handler
164 max_keep_alive_requests: max_keep_alive_requests
165 handshake_timeout: handshake_timeout
166 idle_conns: idle_conns
167 }
168 return spawn w.process_requests()
169}
170
171fn (mut w TlsHandlerWorker) process_requests() {
172 for {
173 mut conn := <-w.ch or { break }
174 w.handle_conn(mut conn)
175 }
176}
177
178fn (mut w TlsHandlerWorker) handle_conn(mut conn mbedtls.SSLConn) {
179 // For H2 connections, serve_h2_conn_with_idle_tracker's serve() owns the
180 // mark_idle/unmark_idle lifetime (it marks idle once and unmarks in its
181 // own defer). Calling unmark_idle here a second time would race: after
182 // serve()'s defer fires the OS can recycle conn.handle for a new
183 // connection that has already called mark_idle, and this stale unmark
184 // would silently evict it, preventing close_idle from ever shutting it
185 // down and leaking the reader goroutine.
186 mut is_h2 := false
187 defer {
188 if !is_h2 {
189 w.idle_conns.unmark_idle(conn.handle)
190 }
191 // close_idle may have already closed conn.handle on Windows (a forced
192 // closesocket to wake this worker from a blocked read). conn.shutdown()
193 // both frees the mbedtls TLS resources (SSL ctx, config, certs, RNG,
194 // ALPN alloc) AND closes the socket; we must always do the former but
195 // must not close the socket a second time — that would race process-wide
196 // SOCKET reuse and could close an unrelated socket. So relinquish socket
197 // ownership and still call shutdown for the TLS cleanup. On non-Windows
198 // was_force_closed is always false and the worker remains the sole closer.
199 if w.idle_conns.was_force_closed(conn.handle) {
200 conn.owns_socket = false
201 }
202 conn.shutdown() or {}
203 }
204 // Run the TLS handshake here on the worker thread (the accept thread only does
205 // the raw TCP accept), so handshakes proceed in parallel up to worker_num and a
206 // client that stalls mid-handshake can't wedge the accept loop or delay stop().
207 // mark_idle first so close_idle can interrupt a stalled handshake by force-
208 // closing the fd, and so a conn handed off while the server is shutting down is
209 // closed (mark_idle returns false) rather than handshaked. On success, unmark
210 // before the serve path does its own idle marking, keeping each handle tracked
211 // once. On any early return the defer above performs the single shutdown.
212 if !w.idle_conns.mark_idle(conn.handle) {
213 return
214 }
215 conn.complete_handshake(w.handshake_timeout) or {
216 $if debug {
217 eprintln('TLS handshake failed: ${err}')
218 }
219 return
220 }
221 w.idle_conns.unmark_idle(conn.handle)
222 // If the TLS handshake negotiated HTTP/2 via ALPN, switch to the HTTP/2
223 // driver; otherwise fall through to the existing HTTP/1.1 path unchanged.
224 if conn.negotiated_alpn() == 'h2' {
225 is_h2 = true
226 serve_h2_conn_with_idle_tracker(mut conn, mut w.handler, w.idle_conns, conn.handle) or {
227 $if debug {
228 eprintln('h2 server error: ${err}')
229 }
230 }
231 return
232 }
233 mut reader := io.new_buffered_reader(reader: conn)
234 defer {
235 unsafe {
236 reader.free()
237 }
238 }
239
240 mut request_count := 0
241 for {
242 if !w.idle_conns.mark_idle(conn.handle) {
243 return
244 }
245 mut req := parse_request(mut reader) or {
246 if err !is io.Eof {
247 $if debug {
248 eprintln('error parsing TLS request: ${err}')
249 }
250 }
251 return
252 }
253 w.idle_conns.unmark_idle(conn.handle)
254 request_count++
255 // `conn.ip` is the peer's IPv4 address as populated by mbedtls'
256 // accept(); blank for IPv6, which is acceptable for keep-alive logic.
257 if conn.ip != '' {
258 req.header.add_custom('Remote-Addr', conn.ip) or {}
259 }
260
261 mut resp := w.handler.handle(req)
262 normalize_server_response(mut resp, req)
263
264 if !resp.header.contains(.content_length) {
265 resp.header.set(.content_length, '${resp.body.len}')
266 }
267
268 max_reached := w.max_keep_alive_requests > 0 && request_count >= w.max_keep_alive_requests
269 req_conn := (req.header.get(.connection) or { '' }).to_lower()
270 resp_conn := (resp.header.get(.connection) or { '' }).to_lower()
271 keep_alive := if max_reached {
272 false
273 } else if resp_conn == 'close' {
274 false
275 } else if resp_conn == 'keep-alive' {
276 true
277 } else if req_conn == 'close' {
278 false
279 } else if req_conn == 'keep-alive' {
280 true
281 } else {
282 req.version == .v1_1
283 }
284 if max_reached || !resp.header.contains(.connection) {
285 if keep_alive {
286 resp.header.set(.connection, 'keep-alive')
287 } else {
288 resp.header.set(.connection, 'close')
289 }
290 }
291
292 conn.write(resp.bytes()) or {
293 $if debug {
294 eprintln('error sending TLS response: ${err}')
295 }
296 return
297 }
298
299 if !keep_alive {
300 return
301 }
302 }
303}
304