Skip to content

Commit 0ff44dc

Browse files
quaesitor-scientiamRichard Wheelerclaude
authored
net.http: add TLS termination to Server (HTTPS support) (#27373)
net.http.Server has been plain-HTTP only. Add TLS termination so an HTTP/1.1 server can listen on https:// directly, without a separate reverse proxy. - Server gains three opt-in fields: `cert`, `cert_key`, and `in_memory_verification`, mirroring the client-side SSL config naming. When both `cert` and `cert_key` are set, `listen_and_serve` delegates to `listen_and_serve_tls`, which uses the mbedtls SSL listener to accept TLS connections, hands them off to a separate worker pool, and serves HTTP/1.1 requests over those TLS sockets with the existing Handler interface. - New default port `default_https_server_port = 9043` for HTTPS listeners. - The TLS path is provided only on the default mbedtls backend (server_tls_notd_use_openssl.v). On `-d use_openssl`, a matching server_tls_d_use_openssl.v provides a clear-error stub at runtime so the module builds and existing plain-HTTP servers keep working; an OpenSSL server listener is a follow-up. - Existing plain-HTTP behavior is completely unchanged when `cert` / `cert_key` are not set: no new code on that path, same Handler contract, same workers. Hermetic test: spins up a local TLS Server with an in-memory cert/key, exercises it via http.fetch(validate: false), and asserts the 200 response body. The test skips itself under -d use_openssl with a clear message. The full vlib/net/http suite is green on both backends, and the TLS test passes under -W -cstrict -cc clang. Co-authored-by: Richard Wheeler <quaesitor.scientiam@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent b6bd49b commit 0ff44dc

4 files changed

Lines changed: 279 additions & 0 deletions

File tree

vlib/net/http/server.v

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ mut:
2525

2626
pub const default_server_port = 9009
2727

28+
pub const default_https_server_port = 9043
29+
2830
pub struct Server {
2931
mut:
3032
state ServerStatus = .closed
@@ -39,6 +41,16 @@ pub mut:
3941
max_keep_alive_requests int = 100 // max requests per keep-alive connection (0 = unlimited)
4042
listener net.TcpListener
4143

44+
// TLS termination: when both `cert` and `cert_key` are set, the server
45+
// accepts HTTPS connections instead of plain HTTP. With
46+
// `in_memory_verification = true`, `cert` and `cert_key` are PEM strings;
47+
// otherwise they are filesystem paths. Currently implemented on the
48+
// default mbedtls backend; building with `-d use_openssl` reports a clear
49+
// runtime error from listen_and_serve.
50+
cert string
51+
cert_key string
52+
in_memory_verification bool
53+
4254
on_running fn (mut s Server) = unsafe { nil } // Blocking cb. If set, ran by the web server on transitions to its .running state.
4355
on_stopped fn (mut s Server) = unsafe { nil } // Blocking cb. If set, ran by the web server on transitions to its .stopped state.
4456
on_closed fn (mut s Server) = unsafe { nil } // Blocking cb. If set, ran by the web server on transitions to its .closed state.
@@ -53,6 +65,11 @@ pub fn (mut s Server) listen_and_serve() {
5365
eprintln('Server handler not set, using debug handler')
5466
}
5567

68+
if s.cert != '' && s.cert_key != '' {
69+
s.listen_and_serve_tls()
70+
return
71+
}
72+
5673
mut l := s.listener.addr() or {
5774
eprintln('Failed getting listener address, err: ${err}')
5875
return
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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.
4+
module http
5+
6+
// TLS termination for net.http.Server is currently provided by the default
7+
// mbedtls backend. Under `-d use_openssl`, the OpenSSL backend does not yet
8+
// expose a server-side listener; this file replaces the implementation in
9+
// `server_tls_notd_use_openssl.v` with a stub that reports a clear error.
10+
11+
fn (mut s Server) listen_and_serve_tls() {
12+
eprintln('net.http.Server: TLS termination is not supported on -d use_openssl yet; remove -d use_openssl or omit cert/cert_key to fall back to plain HTTP')
13+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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.
4+
module http
5+
6+
import io
7+
import time
8+
import net.mbedtls
9+
10+
// This file implements TLS termination for net.http.Server on top of the
11+
// mbedtls SSL listener. It is gated to the default TLS backend; the matching
12+
// `server_tls_d_use_openssl.v` provides a clear-error stub when the project is
13+
// built with `-d use_openssl`.
14+
15+
// listen_and_serve_tls is the TLS counterpart of listen_and_serve. It is
16+
// dispatched to by listen_and_serve when `s.cert` and `s.cert_key` are set.
17+
fn (mut s Server) listen_and_serve_tls() {
18+
// Pick a default port that's distinct from the plain-HTTP default if the
19+
// user hasn't overridden it.
20+
addr := if s.addr == '' || s.addr == ':${default_server_port}' {
21+
':${default_https_server_port}'
22+
} else {
23+
s.addr
24+
}
25+
26+
mut listener := mbedtls.new_ssl_listener(addr, mbedtls.SSLConnectConfig{
27+
cert: s.cert
28+
cert_key: s.cert_key
29+
in_memory_verification: s.in_memory_verification
30+
validate: false // accept any client; servers don't verify clients by default
31+
}) or {
32+
eprintln('Listening TLS on ${addr} failed, err: ${err}')
33+
return
34+
}
35+
defer {
36+
listener.shutdown() or {}
37+
}
38+
s.addr = addr
39+
40+
ch := chan &mbedtls.SSLConn{cap: s.pool_channel_slots}
41+
mut ws := []thread{cap: s.worker_num}
42+
for wid in 0 .. s.worker_num {
43+
ws << new_tls_handler_worker(wid, ch, s.handler, s.max_keep_alive_requests)
44+
}
45+
46+
if s.show_startup_message {
47+
println('Listening on https://${s.addr}/')
48+
flush_stdout()
49+
}
50+
51+
time.sleep(20 * time.millisecond)
52+
s.state = .running
53+
if s.on_running != unsafe { nil } {
54+
s.on_running(mut s)
55+
}
56+
for s.state == .running {
57+
mut conn := listener.accept() or {
58+
if s.state != .running {
59+
break
60+
}
61+
$if debug {
62+
eprintln('TLS accept failed: ${err}; skipping')
63+
}
64+
continue
65+
}
66+
if s.read_timeout > 0 {
67+
conn.set_read_timeout(s.read_timeout)
68+
}
69+
ch <- conn
70+
}
71+
if s.state == .stopped {
72+
s.close()
73+
}
74+
}
75+
76+
// TlsHandlerWorker serves HTTP/1.1 requests on TLS-wrapped connections.
77+
struct TlsHandlerWorker {
78+
id int
79+
ch chan &mbedtls.SSLConn
80+
max_keep_alive_requests int
81+
pub mut:
82+
handler Handler
83+
}
84+
85+
fn new_tls_handler_worker(wid int, ch chan &mbedtls.SSLConn, handler Handler, max_keep_alive_requests int) thread {
86+
mut w := &TlsHandlerWorker{
87+
id: wid
88+
ch: ch
89+
handler: handler
90+
max_keep_alive_requests: max_keep_alive_requests
91+
}
92+
return spawn w.process_requests()
93+
}
94+
95+
fn (mut w TlsHandlerWorker) process_requests() {
96+
for {
97+
mut conn := <-w.ch or { break }
98+
w.handle_conn(mut conn)
99+
}
100+
}
101+
102+
fn (mut w TlsHandlerWorker) handle_conn(mut conn mbedtls.SSLConn) {
103+
defer {
104+
conn.shutdown() or {}
105+
}
106+
mut reader := io.new_buffered_reader(reader: conn)
107+
defer {
108+
unsafe {
109+
reader.free()
110+
}
111+
}
112+
113+
mut request_count := 0
114+
for {
115+
mut req := parse_request(mut reader) or {
116+
if err !is io.Eof {
117+
$if debug {
118+
eprintln('error parsing TLS request: ${err}')
119+
}
120+
}
121+
return
122+
}
123+
request_count++
124+
// `conn.ip` is the peer's IPv4 address as populated by mbedtls'
125+
// accept(); blank for IPv6, which is acceptable for keep-alive logic.
126+
if conn.ip != '' {
127+
req.header.add_custom('Remote-Addr', conn.ip) or {}
128+
}
129+
130+
mut resp := w.handler.handle(req)
131+
normalize_server_response(mut resp, req)
132+
133+
if !resp.header.contains(.content_length) {
134+
resp.header.set(.content_length, '${resp.body.len}')
135+
}
136+
137+
max_reached := w.max_keep_alive_requests > 0 && request_count >= w.max_keep_alive_requests
138+
req_conn := (req.header.get(.connection) or { '' }).to_lower()
139+
resp_conn := (resp.header.get(.connection) or { '' }).to_lower()
140+
keep_alive := if max_reached {
141+
false
142+
} else if resp_conn == 'close' {
143+
false
144+
} else if resp_conn == 'keep-alive' {
145+
true
146+
} else if req_conn == 'close' {
147+
false
148+
} else if req_conn == 'keep-alive' {
149+
true
150+
} else {
151+
req.version == .v1_1
152+
}
153+
if max_reached || !resp.header.contains(.connection) {
154+
if keep_alive {
155+
resp.header.set(.connection, 'keep-alive')
156+
} else {
157+
resp.header.set(.connection, 'close')
158+
}
159+
}
160+
161+
conn.write(resp.bytes()) or {
162+
$if debug {
163+
eprintln('error sending TLS response: ${err}')
164+
}
165+
return
166+
}
167+
168+
if !keep_alive {
169+
return
170+
}
171+
}
172+
}

vlib/net/http/server_tls_test.v

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Hermetic TLS-termination test for net.http.Server: spin up a local HTTPS
2+
// server with an in-memory cert/key, hit it with http.fetch (validate: false),
3+
// and assert the round-trip.
4+
5+
module main
6+
7+
import net
8+
import net.http
9+
import time
10+
11+
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'
12+
13+
const server_tls_key = '-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----\n'
14+
15+
struct EchoHandler {
16+
mut:
17+
last_path string
18+
}
19+
20+
fn (mut h EchoHandler) handle(req http.Request) http.Response {
21+
h.last_path = req.url
22+
return http.Response{
23+
status_code: 200
24+
body: 'tls hello ${req.url}'
25+
}
26+
}
27+
28+
fn pick_port() !int {
29+
mut l := net.listen_tcp(.ip, '127.0.0.1:0')!
30+
port := l.addr()!.port()!
31+
l.close()!
32+
return port
33+
}
34+
35+
fn test_server_tls_round_trip() {
36+
$if use_openssl ? {
37+
// TLS termination for net.http.Server is not yet supported on the
38+
// OpenSSL backend; the listener stub reports a clear runtime error and
39+
// the test is skipped here so the suite stays green under
40+
// `-d use_openssl`.
41+
eprintln('skipping: TLS server not implemented for -d use_openssl yet')
42+
return
43+
}
44+
port := pick_port() or {
45+
assert false, 'pick_port: ${err}'
46+
return
47+
}
48+
mut srv := &http.Server{
49+
addr: '127.0.0.1:${port}'
50+
cert: server_tls_cert
51+
cert_key: server_tls_key
52+
in_memory_verification: true
53+
handler: EchoHandler{}
54+
show_startup_message: false
55+
}
56+
spawn srv.listen_and_serve()
57+
srv.wait_till_running() or {
58+
srv.close()
59+
assert false, 'server failed to start: ${err}'
60+
return
61+
}
62+
defer {
63+
srv.close()
64+
}
65+
// Give the listener a beat to come up.
66+
time.sleep(50 * time.millisecond)
67+
68+
resp := http.fetch(
69+
url: 'https://127.0.0.1:${port}/hello'
70+
validate: false
71+
) or {
72+
assert false, 'fetch failed: ${err}'
73+
return
74+
}
75+
assert resp.status_code == 200
76+
assert resp.body == 'tls hello /hello'
77+
}

0 commit comments

Comments
 (0)