From 5a1b7b030ce9ca6eb47c6eb3ee92cfa44608c49d Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Fri, 12 Jun 2026 15:30:01 +0300 Subject: [PATCH] net.http: fix TLS server listener shutdown (#27429) --- vlib/net/http/h2_server.v | 45 +- vlib/net/http/server.v | 9 +- vlib/net/http/server_tls_idle.v | 49 +++ vlib/net/http/server_tls_notd_use_openssl.v | 46 +- vlib/net/http/server_tls_test.v | 405 +++++++++++++++++- ...server_tls_timeout_notd_use_openssl_test.v | 21 + vlib/net/mbedtls/mbedtls.c.v | 3 + vlib/net/mbedtls/mbedtls_helpers.h | 9 + vlib/net/mbedtls/mbedtls_read_timeout_test.v | 5 + vlib/net/mbedtls/ssl_connection.c.v | 100 ++++- 10 files changed, 659 insertions(+), 33 deletions(-) create mode 100644 vlib/net/http/server_tls_idle.v create mode 100644 vlib/net/http/server_tls_timeout_notd_use_openssl_test.v create mode 100644 vlib/net/mbedtls/mbedtls_helpers.h diff --git a/vlib/net/http/h2_server.v b/vlib/net/http/h2_server.v index 9a32cf3ac..461e674ea 100644 --- a/vlib/net/http/h2_server.v +++ b/vlib/net/http/h2_server.v @@ -48,14 +48,22 @@ mut: last_stream_id u32 awaiting_cont u32 // non-zero when mid-CONTINUATION on this stream closing bool + idle_conns &TlsIdleConnTracker = unsafe { nil } + idle_handle int } // serve_h2_conn drives a single HTTP/2 server-side connection until the // transport closes or a protocol error forces a GOAWAY. `handler` is invoked // once per fully-received request stream. fn serve_h2_conn(mut transport H2Transport, mut handler Handler) ! { + serve_h2_conn_with_idle_tracker(mut transport, mut handler, unsafe { nil }, 0)! +} + +fn serve_h2_conn_with_idle_tracker(mut transport H2Transport, mut handler Handler, idle_conns &TlsIdleConnTracker, idle_handle int) ! { mut c := &H2ServerConn{ - transport: transport + transport: transport + idle_conns: idle_conns + idle_handle: idle_handle } c.serve(mut handler) or { // Best-effort GOAWAY before bailing. @@ -65,10 +73,10 @@ fn serve_h2_conn(mut transport H2Transport, mut handler Handler) ! { } fn (mut c H2ServerConn) serve(mut handler Handler) ! { - c.read_client_preface()! + c.read_client_preface_idle()! c.send_initial_settings()! for !c.closing { - frame := c.read_frame() or { + frame := c.read_idle_frame() or { // Treat a clean transport close as end of session. return } @@ -76,6 +84,37 @@ fn (mut c H2ServerConn) serve(mut handler Handler) ! { } } +fn (mut c H2ServerConn) should_track_idle_read() bool { + return c.idle_handle > 0 && c.idle_conns != unsafe { nil } +} + +fn (mut c H2ServerConn) read_client_preface_idle() ! { + if !c.should_track_idle_read() { + c.read_client_preface()! + return + } + if !c.idle_conns.mark_idle(c.idle_handle) { + return error('h2 server: connection is shutting down') + } + defer { + c.idle_conns.unmark_idle(c.idle_handle) + } + c.read_client_preface()! +} + +fn (mut c H2ServerConn) read_idle_frame() !H2Frame { + if !c.should_track_idle_read() { + return c.read_frame()! + } + if !c.idle_conns.mark_idle(c.idle_handle) { + return error('h2 server: connection is shutting down') + } + defer { + c.idle_conns.unmark_idle(c.idle_handle) + } + return c.read_frame()! +} + fn (mut c H2ServerConn) read_client_preface() ! { c.fill_at_least(h2_client_preface.len)! got := c.rbuf[..h2_client_preface.len].bytestr() diff --git a/vlib/net/http/server.v b/vlib/net/http/server.v index c8eb67900..942fcd694 100644 --- a/vlib/net/http/server.v +++ b/vlib/net/http/server.v @@ -29,7 +29,8 @@ pub const default_https_server_port = 9043 pub struct Server { mut: - state ServerStatus = .closed + state ServerStatus = .closed + listener_opened bool pub mut: addr string = ':${default_server_port}' handler Handler = DebugHandler{} @@ -89,6 +90,7 @@ pub fn (mut s Server) listen_and_serve() { } } s.addr = l.str() + s.listener_opened = true s.listener.set_accept_timeout(s.accept_timeout) // Create tcp connection channel @@ -140,7 +142,10 @@ pub fn (mut s Server) stop() { @[inline] pub fn (mut s Server) close() { s.state = .closed - s.listener.close() or { return } + if s.listener_opened { + s.listener.close() or { return } + s.listener_opened = false + } if s.on_closed != unsafe { nil } { s.on_closed(mut s) } diff --git a/vlib/net/http/server_tls_idle.v b/vlib/net/http/server_tls_idle.v new file mode 100644 index 000000000..dcc1190bd --- /dev/null +++ b/vlib/net/http/server_tls_idle.v @@ -0,0 +1,49 @@ +// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module http + +import net +import sync + +@[heap] +struct TlsIdleConnTracker { + mu &sync.Mutex = sync.new_mutex() +mut: + handles []int + closing bool +} + +fn (mut t TlsIdleConnTracker) mark_idle(handle int) bool { + t.mu.lock() + defer { + t.mu.unlock() + } + if t.closing { + return false + } + t.handles << handle + return true +} + +fn (mut t TlsIdleConnTracker) unmark_idle(handle int) { + t.mu.lock() + defer { + t.mu.unlock() + } + idx := t.handles.index(handle) + if idx >= 0 { + t.handles.delete(idx) + } +} + +fn (mut t TlsIdleConnTracker) close_idle() { + t.mu.lock() + t.closing = true + handles := t.handles.clone() + t.handles.clear() + t.mu.unlock() + for handle in handles { + net.shutdown(handle) + } +} diff --git a/vlib/net/http/server_tls_notd_use_openssl.v b/vlib/net/http/server_tls_notd_use_openssl.v index c4b36984c..a04b84046 100644 --- a/vlib/net/http/server_tls_notd_use_openssl.v +++ b/vlib/net/http/server_tls_notd_use_openssl.v @@ -4,9 +4,22 @@ module http import io +import net import time import net.mbedtls +const tls_accept_poll_timeout = 100 * time.millisecond + +fn tls_accept_timeouts(accept_timeout time.Duration) (time.Duration, time.Duration) { + handshake_timeout := accept_timeout + accept_poll_timeout := if accept_timeout > 0 && accept_timeout < tls_accept_poll_timeout { + accept_timeout + } else { + tls_accept_poll_timeout + } + return accept_poll_timeout, handshake_timeout +} + // This file implements TLS termination for net.http.Server on top of the // mbedtls SSL listener. It is gated to the default TLS backend; the matching // `server_tls_d_use_openssl.v` provides a clear-error stub when the project is @@ -40,13 +53,20 @@ fn (mut s Server) listen_and_serve_tls() { } defer { listener.shutdown() or {} + if s.state == .stopped { + s.state = .closed + if s.on_closed != unsafe { nil } { + s.on_closed(mut s) + } + } } s.addr = addr ch := chan &mbedtls.SSLConn{cap: s.pool_channel_slots} + mut idle_conns := &TlsIdleConnTracker{} mut ws := []thread{cap: s.worker_num} for wid in 0 .. s.worker_num { - ws << new_tls_handler_worker(wid, ch, s.handler, s.max_keep_alive_requests) + ws << new_tls_handler_worker(wid, ch, s.handler, s.max_keep_alive_requests, idle_conns) } if s.show_startup_message { @@ -59,11 +79,15 @@ fn (mut s Server) listen_and_serve_tls() { if s.on_running != unsafe { nil } { s.on_running(mut s) } + accept_poll_timeout, handshake_timeout := tls_accept_timeouts(s.accept_timeout) for s.state == .running { - mut conn := listener.accept() or { + mut conn := listener.accept_with_timeouts(accept_poll_timeout, handshake_timeout) or { if s.state != .running { break } + if err.code() == net.err_timed_out_code { + continue + } $if debug { eprintln('TLS accept failed: ${err}; skipping') } @@ -74,9 +98,9 @@ fn (mut s Server) listen_and_serve_tls() { } ch <- conn } - if s.state == .stopped { - s.close() - } + ch.close() + idle_conns.close_idle() + ws.wait() } // TlsHandlerWorker serves HTTP/1.1 requests on TLS-wrapped connections. @@ -84,16 +108,19 @@ struct TlsHandlerWorker { id int ch chan &mbedtls.SSLConn max_keep_alive_requests int +mut: + idle_conns &TlsIdleConnTracker = unsafe { nil } pub mut: handler Handler } -fn new_tls_handler_worker(wid int, ch chan &mbedtls.SSLConn, handler Handler, max_keep_alive_requests int) thread { +fn new_tls_handler_worker(wid int, ch chan &mbedtls.SSLConn, handler Handler, max_keep_alive_requests int, idle_conns &TlsIdleConnTracker) thread { mut w := &TlsHandlerWorker{ id: wid ch: ch handler: handler max_keep_alive_requests: max_keep_alive_requests + idle_conns: idle_conns } return spawn w.process_requests() } @@ -107,12 +134,13 @@ fn (mut w TlsHandlerWorker) process_requests() { fn (mut w TlsHandlerWorker) handle_conn(mut conn mbedtls.SSLConn) { defer { + w.idle_conns.unmark_idle(conn.handle) conn.shutdown() or {} } // If the TLS handshake negotiated HTTP/2 via ALPN, switch to the HTTP/2 // driver; otherwise fall through to the existing HTTP/1.1 path unchanged. if conn.negotiated_alpn() == 'h2' { - serve_h2_conn(mut conn, mut w.handler) or { + serve_h2_conn_with_idle_tracker(mut conn, mut w.handler, w.idle_conns, conn.handle) or { $if debug { eprintln('h2 server error: ${err}') } @@ -128,6 +156,9 @@ fn (mut w TlsHandlerWorker) handle_conn(mut conn mbedtls.SSLConn) { mut request_count := 0 for { + if !w.idle_conns.mark_idle(conn.handle) { + return + } mut req := parse_request(mut reader) or { if err !is io.Eof { $if debug { @@ -136,6 +167,7 @@ fn (mut w TlsHandlerWorker) handle_conn(mut conn mbedtls.SSLConn) { } return } + w.idle_conns.unmark_idle(conn.handle) request_count++ // `conn.ip` is the peer's IPv4 address as populated by mbedtls' // accept(); blank for IPv6, which is acceptable for keep-alive logic. diff --git a/vlib/net/http/server_tls_test.v b/vlib/net/http/server_tls_test.v index f936c399d..f781d1af5 100644 --- a/vlib/net/http/server_tls_test.v +++ b/vlib/net/http/server_tls_test.v @@ -6,6 +6,7 @@ module main import net import net.http +import net.mbedtls import time const server_tls_cert = '-----BEGIN CERTIFICATE-----\nMIIEOTCCAyECFG64Q2g46jZb3kRbDOJWX/BwjSp6MA0GCSqGSIb3DQEBCwUAMEUx\nCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl\ncm5ldCBXaWRnaXRzIFB0eSBMdGQwIBcNMjMwODAyMTcyOTQyWhgPMjA1MDEyMTcx\nNzI5NDJaMGsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYD\nVQQHDAtMb3MgQW5nZWxlczEdMBsGA1UECgwUQ2F0YWx5c3QgRGV2ZWxvcG1lbnQx\nEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC\nggIBALqAI4fqUi+QBVWcsXglouLdOML5+w0+1hSR1KdO0Q5XPdQAs/yYWJ+KUkDw\nG++rfy9DUPq7FNRBVurXQkcAtn6gXdllGUSjwUiDo/N4mMOyS/2sufBuaeww7jVi\nrppH+zwP1tUnjRd6khl6bi1Ian9VSzr3Iy9CkXIg1GU4CPXkOydLeoQfepXxWoK1\nOUNwT3VKC/stAfY3j/NIIeiJYkyuRGFCkxn/BUjN+AsXiTugRcYKEFHdIPkOuCXp\nYbhf+lLsczpxCs3rdZG9b/N6mEDCzXTmeHkmsjdPTf+1k5DZZvKzVBBrgdxCgBb7\n5RwjF5v9WmnIc33wWgfJC6FaUzj9NYxYUbPHD+jTz0rJB/jj4u/xJlM/e5NRmXdW\n70pOMKXtWjRSolLOFIPKLY1qs3KMTAZxKKWPDDF7WlMJxMRt7nnnks5yw43Nog4C\njDLk1ZgETnPpLgo3jbmJdIv+OHKTJrBlVvDq7VTyixCoS5G8KoOmyQJhaXG6NwE2\niVhH5JIKgzgCfetfDsnjxqJ/qtrFXPa8FF2TsomD0NK/GZmIcs+9OeVB75Jn5uhF\nfLHScpiTbuu5w3P/LI/MqihLRB6RRNnRzPH8fIg5bYC9b770ta/8GcFRuYE8t+UR\nGtqXJoIKixbDlqV54kal8FQzYzhETf9+NM6Kb/lKEfG/pslvAgMBAAEwDQYJKoZI\nhvcNAQELBQADggEBALI3uNiNO0QE1brA3QYFK+d9ZroB72NrJ0UNkzYHDg2Fc6xg\n4aVVfaxY08+TmKc0JlMOW+pUxeCW/+UBSngdQiR9EE9xm0k0XIrAsy9RXxRvEtPu\nM1VI2h7ayp1Y2BrnQinevTSgtqLRyS1VbOFRl1FiyVvinw2I0KsDdAMNevAPXcOa\nQ8pUgUq6f56DkhocQaj+hxD/uV8HryNxuoSXnPhvfTN3z4YRGzsaWevJ9EYJliOM\n+XugcqfFJ+W7/QCEcAHCL+Bw6OydG5NFORr3p57PXjjcL/uKmxPBrWg2Bz6uT4uR\nMhj0zttiFHLAt9jGfyk6W57UNUja1e1ggftJJhs=\n-----END CERTIFICATE-----\n' @@ -25,6 +26,21 @@ fn (mut h EchoHandler) handle(req http.Request) http.Response { } } +struct BlockingHandler { + started chan bool + release chan bool +} + +fn (mut h BlockingHandler) handle(req http.Request) http.Response { + h.started <- true + _ := <-h.release + return http.Response{ + status_code: 200 + header: http.new_header(key: .connection, value: 'close') + body: 'released' + } +} + fn pick_port() !int { mut l := net.listen_tcp(.ip, '127.0.0.1:0')! port := l.addr()!.port()! @@ -50,17 +66,20 @@ fn test_server_tls_round_trip() { cert: server_tls_cert cert_key: server_tls_key in_memory_verification: true + accept_timeout: time.second handler: EchoHandler{} show_startup_message: false } - spawn srv.listen_and_serve() + t := spawn srv.listen_and_serve() srv.wait_till_running() or { srv.close() + t.wait() assert false, 'server failed to start: ${err}' return } defer { srv.close() + t.wait() } // Give the listener a beat to come up. time.sleep(50 * time.millisecond) @@ -76,6 +95,385 @@ fn test_server_tls_round_trip() { assert resp.body == 'tls hello /hello' } +fn test_server_tls_stop() { + $if use_openssl ? { + eprintln('skipping: TLS server not implemented for -d use_openssl yet') + return + } + port := pick_port() or { + assert false, 'pick_port: ${err}' + return + } + mut srv := &http.Server{ + addr: '127.0.0.1:${port}' + cert: server_tls_cert + cert_key: server_tls_key + in_memory_verification: true + accept_timeout: 100 * time.millisecond + handler: EchoHandler{} + show_startup_message: false + } + t := spawn srv.listen_and_serve() + srv.wait_till_running() or { + srv.close() + t.wait() + assert false, 'server failed to start: ${err}' + return + } + srv.stop() + assert srv.status() == .stopped + t.wait() + assert srv.status() == .closed +} + +fn test_server_tls_close_caps_default_accept_poll() { + $if use_openssl ? { + eprintln('skipping: TLS server not implemented for -d use_openssl yet') + return + } + port := pick_port() or { + assert false, 'pick_port: ${err}' + return + } + mut srv := &http.Server{ + addr: '127.0.0.1:${port}' + cert: server_tls_cert + cert_key: server_tls_key + in_memory_verification: true + handler: EchoHandler{} + show_startup_message: false + } + t := spawn srv.listen_and_serve() + srv.wait_till_running() or { + srv.close() + t.wait() + assert false, 'server failed to start: ${err}' + return + } + sw := time.new_stopwatch() + srv.close() + t.wait() + assert sw.elapsed() < time.second + assert srv.status() == .closed +} + +fn test_server_tls_close_waits_for_active_request() { + $if use_openssl ? { + eprintln('skipping: TLS server not implemented for -d use_openssl yet') + return + } + port := pick_port() or { + assert false, 'pick_port: ${err}' + return + } + started := chan bool{cap: 1} + release := chan bool{cap: 1} + done := chan string{cap: 1} + mut srv := &http.Server{ + addr: '127.0.0.1:${port}' + cert: server_tls_cert + cert_key: server_tls_key + in_memory_verification: true + accept_timeout: time.second + handler: BlockingHandler{ + started: started + release: release + } + show_startup_message: false + } + t := spawn srv.listen_and_serve() + srv.wait_till_running() or { + srv.close() + t.wait() + assert false, 'server failed to start: ${err}' + return + } + spawn fn [done, port] () { + resp := http.fetch( + url: 'https://127.0.0.1:${port}/blocked' + enable_http2: false + validate: false + ) or { + done <- 'error: ${err}' + return + } + done <- resp.body + }() + select { + _ := <-started {} + msg := <-done { + srv.close() + t.wait() + assert false, 'client finished before handler started: ${msg}' + return + } + 2 * time.second { + srv.close() + t.wait() + assert false, 'timed out waiting for handler to start' + return + } + } + srv.close() + time.sleep(50 * time.millisecond) + release <- true + assert (<-done) == 'released' + t.wait() + assert srv.status() == .closed +} + +fn test_server_tls_close_during_silent_handshake() { + $if use_openssl ? { + eprintln('skipping: TLS server not implemented for -d use_openssl yet') + return + } + port := pick_port() or { + assert false, 'pick_port: ${err}' + return + } + mut srv := &http.Server{ + addr: '127.0.0.1:${port}' + cert: server_tls_cert + cert_key: server_tls_key + in_memory_verification: true + accept_timeout: 100 * time.millisecond + handler: EchoHandler{} + show_startup_message: false + } + t := spawn srv.listen_and_serve() + srv.wait_till_running() or { + srv.close() + t.wait() + assert false, 'server failed to start: ${err}' + return + } + mut client := net.dial_tcp('127.0.0.1:${port}') or { + srv.close() + t.wait() + assert false, 'tcp dial failed: ${err}' + return + } + defer { + client.close() or {} + } + time.sleep(50 * time.millisecond) + sw := time.new_stopwatch() + srv.close() + t.wait() + assert sw.elapsed() < time.second + assert srv.status() == .closed +} + +fn test_server_tls_close_interrupts_idle_keep_alive() { + $if use_openssl ? { + eprintln('skipping: TLS server not implemented for -d use_openssl yet') + return + } + port := pick_port() or { + assert false, 'pick_port: ${err}' + return + } + mut srv := &http.Server{ + addr: '127.0.0.1:${port}' + cert: server_tls_cert + cert_key: server_tls_key + in_memory_verification: true + accept_timeout: time.second + read_timeout: 5 * time.second + handler: EchoHandler{} + show_startup_message: false + } + t := spawn srv.listen_and_serve() + srv.wait_till_running() or { + srv.close() + t.wait() + assert false, 'server failed to start: ${err}' + return + } + mut client := mbedtls.new_ssl_conn(mbedtls.SSLConnectConfig{}) or { + srv.close() + t.wait() + assert false, 'ssl client init failed: ${err}' + return + } + defer { + client.shutdown() or {} + } + client.dial('127.0.0.1', port) or { + srv.close() + t.wait() + assert false, 'ssl dial failed: ${err}' + return + } + request := 'GET /idle HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\nConnection: keep-alive\r\n\r\n' + client.write_string(request) or { + srv.close() + t.wait() + assert false, 'ssl write failed: ${err}' + return + } + mut buf := []u8{len: 4096} + n := client.read(mut buf) or { + srv.close() + t.wait() + assert false, 'ssl read failed: ${err}' + return + } + response := buf[..n].bytestr() + assert response.to_lower().contains('connection: keep-alive') + assert response.contains('tls hello /idle') + + sw := time.new_stopwatch() + srv.close() + t.wait() + assert sw.elapsed() < 2 * time.second + assert srv.status() == .closed +} + +fn test_server_tls_close_interrupts_idle_h2() { + $if use_openssl ? { + eprintln('skipping: TLS server not implemented for -d use_openssl yet') + return + } + port := pick_port() or { + assert false, 'pick_port: ${err}' + return + } + mut srv := &http.Server{ + addr: '127.0.0.1:${port}' + cert: server_tls_cert + cert_key: server_tls_key + in_memory_verification: true + accept_timeout: time.second + read_timeout: 5 * time.second + enable_http2: true + handler: EchoHandler{} + show_startup_message: false + } + t := spawn srv.listen_and_serve() + srv.wait_till_running() or { + srv.close() + t.wait() + assert false, 'server failed to start: ${err}' + return + } + mut client := mbedtls.new_ssl_conn(mbedtls.SSLConnectConfig{ + alpn_protocols: ['h2'] + }) or { + srv.close() + t.wait() + assert false, 'ssl client init failed: ${err}' + return + } + defer { + client.shutdown() or {} + } + client.dial('127.0.0.1', port) or { + srv.close() + t.wait() + assert false, 'ssl dial failed: ${err}' + return + } + assert client.negotiated_alpn() == 'h2' + mut h2 := http.new_h2_conn(client) + resp := h2.do(http.H2ClientRequest{ + method: 'GET' + scheme: 'https' + authority: '127.0.0.1:${port}' + path: '/h2-idle' + }) or { + srv.close() + t.wait() + assert false, 'h2 request failed: ${err}' + return + } + assert resp.status == 200 + assert resp.body.bytestr() == 'tls hello /h2-idle' + + sw := time.new_stopwatch() + srv.close() + t.wait() + assert sw.elapsed() < 2 * time.second + assert srv.status() == .closed +} + +fn test_server_tls_close_interrupts_incomplete_h2_request() { + $if use_openssl ? { + eprintln('skipping: TLS server not implemented for -d use_openssl yet') + return + } + port := pick_port() or { + assert false, 'pick_port: ${err}' + return + } + mut srv := &http.Server{ + addr: '127.0.0.1:${port}' + cert: server_tls_cert + cert_key: server_tls_key + in_memory_verification: true + accept_timeout: time.second + read_timeout: 2 * time.second + enable_http2: true + handler: EchoHandler{} + show_startup_message: false + } + t := spawn srv.listen_and_serve() + srv.wait_till_running() or { + srv.close() + t.wait() + assert false, 'server failed to start: ${err}' + return + } + mut client := mbedtls.new_ssl_conn(mbedtls.SSLConnectConfig{ + alpn_protocols: ['h2'] + }) or { + srv.close() + t.wait() + assert false, 'ssl client init failed: ${err}' + return + } + defer { + client.shutdown() or {} + } + client.dial('127.0.0.1', port) or { + srv.close() + t.wait() + assert false, 'ssl dial failed: ${err}' + return + } + assert client.negotiated_alpn() == 'h2' + mut enc := http.H2HpackEncoder{} + block := enc.encode([ + http.H2HeaderField{':method', 'POST'}, + http.H2HeaderField{':scheme', 'https'}, + http.H2HeaderField{':authority', '127.0.0.1:${port}'}, + http.H2HeaderField{':path', '/h2-incomplete'}, + ]) + mut out := []u8{} + out << http.h2_client_preface.bytes() + out << http.H2Frame(http.H2SettingsFrame{}).encode() + out << http.H2Frame(http.H2HeadersFrame{ + stream_id: 1 + fragment: block + end_headers: true + end_stream: false + }).encode() + written := client.write(out) or { + srv.close() + t.wait() + assert false, 'ssl write failed: ${err}' + return + } + assert written == out.len + time.sleep(100 * time.millisecond) + + sw := time.new_stopwatch() + srv.close() + t.wait() + assert sw.elapsed() < time.second + assert srv.status() == .closed +} + fn test_server_tls_h2_negotiation() { $if use_openssl ? { eprintln('skipping: TLS server not implemented for -d use_openssl yet') @@ -90,18 +488,21 @@ fn test_server_tls_h2_negotiation() { cert: server_tls_cert cert_key: server_tls_key in_memory_verification: true + accept_timeout: time.second enable_http2: true handler: EchoHandler{} show_startup_message: false } - spawn srv.listen_and_serve() + t := spawn srv.listen_and_serve() srv.wait_till_running() or { srv.close() + t.wait() assert false, 'server failed to start: ${err}' return } defer { srv.close() + t.wait() } time.sleep(50 * time.millisecond) diff --git a/vlib/net/http/server_tls_timeout_notd_use_openssl_test.v b/vlib/net/http/server_tls_timeout_notd_use_openssl_test.v new file mode 100644 index 000000000..2e98c1866 --- /dev/null +++ b/vlib/net/http/server_tls_timeout_notd_use_openssl_test.v @@ -0,0 +1,21 @@ +module http + +import time + +fn test_tls_accept_timeouts_preserve_zero_handshake_timeout() { + accept_poll_timeout, handshake_timeout := tls_accept_timeouts(0) + assert accept_poll_timeout == tls_accept_poll_timeout + assert handshake_timeout == 0 +} + +fn test_tls_accept_timeouts_cap_poll_without_changing_handshake_timeout() { + accept_poll_timeout, handshake_timeout := tls_accept_timeouts(time.second) + assert accept_poll_timeout == tls_accept_poll_timeout + assert handshake_timeout == time.second +} + +fn test_tls_accept_timeouts_keep_short_accept_timeout() { + accept_poll_timeout, handshake_timeout := tls_accept_timeouts(50 * time.millisecond) + assert accept_poll_timeout == 50 * time.millisecond + assert handshake_timeout == 50 * time.millisecond +} diff --git a/vlib/net/mbedtls/mbedtls.c.v b/vlib/net/mbedtls/mbedtls.c.v index a6fb0dfbd..2bb2ba3ce 100644 --- a/vlib/net/mbedtls/mbedtls.c.v +++ b/vlib/net/mbedtls/mbedtls.c.v @@ -135,6 +135,7 @@ $if prod && opt_size ? { #include #include #include +#insert "@VEXEROOT/vlib/net/mbedtls/mbedtls_helpers.h" @[typedef] pub struct C.mbedtls_net_context { @@ -233,3 +234,5 @@ fn C.mbedtls_ssl_conf_read_timeout(conf &C.mbedtls_ssl_config, timeout u32) fn C.mbedtls_ssl_conf_alpn_protocols(conf &C.mbedtls_ssl_config, protos voidptr) i32 fn C.mbedtls_ssl_get_alpn_protocol(&C.mbedtls_ssl_context) voidptr + +fn C.v_mbedtls_ssl_set_bio_nonblocking(&C.mbedtls_ssl_context, &C.mbedtls_net_context) diff --git a/vlib/net/mbedtls/mbedtls_helpers.h b/vlib/net/mbedtls/mbedtls_helpers.h new file mode 100644 index 000000000..0cf8bdf2a --- /dev/null +++ b/vlib/net/mbedtls/mbedtls_helpers.h @@ -0,0 +1,9 @@ +#ifndef V_NET_MBEDTLS_HELPERS_H +#define V_NET_MBEDTLS_HELPERS_H + +static inline void v_mbedtls_ssl_set_bio_nonblocking(mbedtls_ssl_context *ssl, mbedtls_net_context *net) +{ + mbedtls_ssl_set_bio(ssl, net, mbedtls_net_send, mbedtls_net_recv, NULL); +} + +#endif diff --git a/vlib/net/mbedtls/mbedtls_read_timeout_test.v b/vlib/net/mbedtls/mbedtls_read_timeout_test.v index ab78eda49..9f20f57d1 100644 --- a/vlib/net/mbedtls/mbedtls_read_timeout_test.v +++ b/vlib/net/mbedtls/mbedtls_read_timeout_test.v @@ -18,3 +18,8 @@ fn test_ssl_conn_read_timeout_can_be_configured_at_runtime() ! { conn.set_read_timeout(net.infinite_timeout) assert conn.read_timeout() == net.infinite_timeout } + +fn test_ssl_remaining_timeout_clamps_expired_deadlines() { + assert ssl_remaining_timeout(time.unix(0)) == net.infinite_timeout + assert ssl_remaining_timeout(time.now().add(-time.second)) == time.nanosecond +} diff --git a/vlib/net/mbedtls/ssl_connection.c.v b/vlib/net/mbedtls/ssl_connection.c.v index ded7f5e6c..7df540668 100644 --- a/vlib/net/mbedtls/ssl_connection.c.v +++ b/vlib/net/mbedtls/ssl_connection.c.v @@ -332,15 +332,32 @@ fn (mut l SSLListener) init_sni(get_cert_callback fn (mut SSLListener, string) ! // accepts a new connection and returns a SSLConn of the connected client pub fn (mut l SSLListener) accept() !&SSLConn { + mut conn := l.accept_tcp_connection()! + + C.mbedtls_ssl_init(&conn.ssl) + C.mbedtls_ssl_config_init(&conn.conf) + ret := C.mbedtls_ssl_setup(&conn.ssl, &l.conf) + if ret != 0 { + conn.shutdown() or {} + return error_with_code('net.mbedtls SSLListener.accept, mbedtls_ssl_setup SSL setup failed ret: ${ret}', + ret) + } + + C.mbedtls_ssl_set_bio(&conn.ssl, &conn.server_fd, C.mbedtls_net_send, C.mbedtls_net_recv, + C.mbedtls_net_recv_timeout) + conn.server_handshake(net.infinite_timeout)! + return conn +} + +fn (mut l SSLListener) accept_tcp_connection() !&SSLConn { mut conn := &SSLConn{ config: l.config opened: true } - ip := [16]u8{} iplen := usize(0) - mut ret := C.mbedtls_net_accept(&l.server_fd, &conn.server_fd, &ip, 16, &iplen) + ret := C.mbedtls_net_accept(&l.server_fd, &conn.server_fd, &ip, 16, &iplen) if ret != 0 { return error_with_code("net.mbedtls SSLListener.accept, mbedtls_net_accept can't accept connection ret: ${ret}", ret) @@ -350,32 +367,73 @@ pub fn (mut l SSLListener) accept() !&SSLConn { if iplen == 4 { conn.ip = '${ip[0]}.${ip[1]}.${ip[2]}.${ip[3]}' } + return conn +} + +fn (mut conn SSLConn) server_handshake(timeout time.Duration) ! { + deadline := ssl_timeout_deadline(timeout) + mut ret := C.mbedtls_ssl_handshake(&conn.ssl) + for ret != 0 { + match ret { + C.MBEDTLS_ERR_SSL_WANT_READ { + conn.wait_for_read(ssl_remaining_timeout(deadline)) or { + conn.shutdown() or {} + return err + } + } + C.MBEDTLS_ERR_SSL_WANT_WRITE { + conn.wait_for_write(ssl_remaining_timeout(deadline)) or { + conn.shutdown() or {} + return err + } + } + else { + conn.shutdown() or { + $if trace_ssl ? { + eprintln('${@METHOD} shutdown ---> res: ${err}') + } + } + return error_with_code('net.mbedtls SSLListener.accept, mbedtls_ssl_handshake failed 1; handshake ret: ${ret}', + ret) + } + } + + ret = C.mbedtls_ssl_handshake(&conn.ssl) + } +} + +// accept_with_timeout waits up to `timeout` for a new client before accepting it. +pub fn (mut l SSLListener) accept_with_timeout(timeout time.Duration) !&SSLConn { + return l.accept_with_timeouts(timeout, timeout) +} + +// accept_with_timeouts waits up to `accept_timeout` for a new client, then +// waits up to `handshake_timeout` for the TLS server handshake to complete. +pub fn (mut l SSLListener) accept_with_timeouts(accept_timeout time.Duration, handshake_timeout time.Duration) !&SSLConn { + wait_for(l.server_fd.fd, .read, accept_timeout)! + mut conn := l.accept_tcp_connection()! C.mbedtls_ssl_init(&conn.ssl) C.mbedtls_ssl_config_init(&conn.conf) - ret = C.mbedtls_ssl_setup(&conn.ssl, &l.conf) + net.set_blocking(conn.handle, false) or { + conn.shutdown() or {} + return err + } + ret := C.mbedtls_ssl_setup(&conn.ssl, &l.conf) if ret != 0 { + conn.shutdown() or {} return error_with_code('net.mbedtls SSLListener.accept, mbedtls_ssl_setup SSL setup failed ret: ${ret}', ret) } + C.v_mbedtls_ssl_set_bio_nonblocking(&conn.ssl, &conn.server_fd) + conn.server_handshake(handshake_timeout)! + net.set_blocking(conn.handle, true) or { + conn.shutdown() or {} + return err + } C.mbedtls_ssl_set_bio(&conn.ssl, &conn.server_fd, C.mbedtls_net_send, C.mbedtls_net_recv, C.mbedtls_net_recv_timeout) - - ret = C.mbedtls_ssl_handshake(&conn.ssl) - for ret != 0 { - if ret != C.MBEDTLS_ERR_SSL_WANT_READ && ret != C.MBEDTLS_ERR_SSL_WANT_WRITE { - conn.shutdown() or { - $if trace_ssl ? { - eprintln('${@METHOD} shutdown ---> res: ${err}') - } - } - return error_with_code('net.mbedtls SSLListener.accept, mbedtls_ssl_handshake failed 1; handshake ret: ${ret}', - ret) - } - ret = C.mbedtls_ssl_handshake(&conn.ssl) - } - return conn } @@ -418,7 +476,11 @@ fn ssl_remaining_timeout(deadline time.Time) time.Duration { if deadline.unix() == 0 { return net.infinite_timeout } - return deadline - time.now() + remaining := deadline - time.now() + if remaining <= 0 { + return time.nanosecond + } + return remaining } // read_timeout returns the current SSL read timeout. -- 2.39.5