From 8e3e67eff2703ee6a931953575b6c236e7349712 Mon Sep 17 00:00:00 2001 From: Richard Wheeler Date: Fri, 5 Jun 2026 22:09:27 -0400 Subject: [PATCH] net.ssl: add ALPN protocol support to mbedtls and openssl backends (#27343) * net.ssl: add ALPN protocol support to mbedtls and openssl backends Add an `alpn_protocols []string` option to the SSL connection config and a `negotiated_alpn() string` method to query the protocol selected during the TLS handshake. This is the foundation needed for HTTP/2 (ALPN `h2`) and other protocol negotiation, without changing behaviour for existing callers (an empty list sends no ALPN extension, exactly as before). mbedtls: - Finish the previously commented-out, leaky ALPN block in SSLConn.init, driving it from config.alpn_protocols and freeing the C array in shutdown. - Add the same advertisement to SSLListener so accepted server connections can negotiate a protocol. - Add `negotiated_alpn()` via mbedtls_ssl_get_alpn_protocol. openssl: - Set the ALPN list (length-prefixed wire format) in SSLConn.init via SSL_set_alpn_protos. - Add `negotiated_alpn()` via SSL_get0_alpn_selected. The option and method are exposed through the net.ssl facade automatically (embedded config + SSLConn type alias). Add a hermetic net.ssl test that stands up a local mbedtls TLS server advertising ALPN and verifies negotiation for h2, http/1.1 fallback, and the no-ALPN case. It passes on both the mbedtls and OpenSSL (-d use_openssl) client backends. Co-Authored-By: Claude Opus 4.8 * net.ssl: make ALPN C interop portable (-cstrict and old OpenSSL) Two follow-up fixes to the ALPN support: 1. -cstrict / -Werror const-qualifier errors (clang): - mbedtls_ssl_conf_alpn_protocols takes `const char **` and mbedtls_ssl_get_alpn_protocol returns `const char *`; SSL_get0_alpn_selected takes `const unsigned char **`. Pass the nested-pointer arguments through voidptr at the call sites (void* converts cleanly to a const pointer in C) and cast the const char* return before use. 2. Linking against OpenSSL versions older than 1.0.2 (predating ALPN): route SSL_set_alpn_protos / SSL_get0_alpn_selected through small version-guarded shims in openssl_compat.h, matching the existing v_net_openssl_* pattern. On older OpenSSL the shims are no-ops (set returns non-zero -> a clear runtime error only if ALPN was actually requested; get reports no protocol), so HTTP/1.1-only users keep building and running. LibreSSL reports a high version number and uses the native path. Verified with `./vnew -W -cstrict -cc clang test vlib/net/ssl/ssl_alpn_test.v` on both the mbedtls and OpenSSL backends, plus the net.mbedtls suite. Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Richard Wheeler Co-authored-by: Claude Opus 4.8 --- vlib/net/mbedtls/mbedtls.c.v | 6 +- vlib/net/mbedtls/ssl_connection.c.v | 91 +++++++++++++++++++++++++---- vlib/net/openssl/openssl.c.v | 10 ++++ vlib/net/openssl/openssl_compat.h | 26 +++++++++ vlib/net/openssl/ssl_connection.c.v | 35 +++++++++++ vlib/net/ssl/ssl_alpn_test.v | 80 +++++++++++++++++++++++++ 6 files changed, 235 insertions(+), 13 deletions(-) create mode 100644 vlib/net/ssl/ssl_alpn_test.v diff --git a/vlib/net/mbedtls/mbedtls.c.v b/vlib/net/mbedtls/mbedtls.c.v index 063af9963..a6fb0dfbd 100644 --- a/vlib/net/mbedtls/mbedtls.c.v +++ b/vlib/net/mbedtls/mbedtls.c.v @@ -228,4 +228,8 @@ fn C.mbedtls_debug_set_threshold(level i32) fn C.mbedtls_ssl_conf_read_timeout(conf &C.mbedtls_ssl_config, timeout u32) -fn C.mbedtls_ssl_conf_alpn_protocols(&C.mbedtls_ssl_config, &&char) i32 +// protos is `const char **`; declared as voidptr so V emits a clean +// `(void*)` cast and avoids -cstrict nested-pointer const warnings. +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 diff --git a/vlib/net/mbedtls/ssl_connection.c.v b/vlib/net/mbedtls/ssl_connection.c.v index 86042dd54..ded7f5e6c 100644 --- a/vlib/net/mbedtls/ssl_connection.c.v +++ b/vlib/net/mbedtls/ssl_connection.c.v @@ -150,6 +150,10 @@ pub mut: read_timeout time.Duration owns_socket bool + // alpn_list is a NUL-terminated C array of pointers to the protocol + // strings in config.alpn_protocols. mbedtls stores this pointer without + // copying, so it must outlive the SSL config; it is freed in shutdown(). + alpn_list &&char = unsafe { nil } } // SSLListener listens on a TCP port and accepts connection secured with TLS @@ -164,6 +168,10 @@ mut: ctr_drbg C.mbedtls_ctr_drbg_context entropy C.mbedtls_entropy_context opened bool + // alpn_list is a NUL-terminated C array of pointers to the protocol + // strings in config.alpn_protocols, advertised by accepted connections. + // It must outlive the SSL config and is freed in shutdown(). + alpn_list &&char = unsafe { nil } // handle int // duration time.Duration } @@ -190,6 +198,12 @@ pub fn (mut l SSLListener) shutdown() ! { C.mbedtls_ssl_free(&l.ssl) C.mbedtls_ssl_config_free(&l.conf) free_rng(mut l.ctr_drbg, mut l.entropy) + if l.alpn_list != unsafe { nil } { + unsafe { + C.free(l.alpn_list) + l.alpn_list = nil + } + } if l.opened { C.mbedtls_net_free(&l.server_fd) } @@ -269,6 +283,27 @@ fn (mut l SSLListener) init() ! { ret) } + // Advertise ALPN protocols for accepted connections to select from. + // See the matching client-side logic in SSLConn.init for lifetime notes. + if l.config.alpn_protocols.len > 0 { + n := l.config.alpn_protocols.len + l.alpn_list = unsafe { &&char(C.malloc(isize((n + 1) * int(sizeof(voidptr))))) } + if l.alpn_list == unsafe { nil } { + return error('net.mbedtls SSLListener.init, failed to allocate ALPN list') + } + unsafe { + for i, proto in l.config.alpn_protocols { + l.alpn_list[i] = &char(proto.str) + } + l.alpn_list[n] = &char(0) + } + ret = C.mbedtls_ssl_conf_alpn_protocols(&l.conf, voidptr(l.alpn_list)) + if ret != 0 { + return error_with_code('net.mbedtls SSLListener.init, mbedtls_ssl_conf_alpn_protocols failed ret: ${ret}', + ret) + } + } + ret = C.mbedtls_ssl_setup(&l.ssl, &l.conf) if ret != 0 { return error_with_code("net.mbedtls SSLListener.init, mbedtls_ssl_setup can't setup ssl ret: ${ret}", @@ -357,6 +392,8 @@ pub: get_certificate ?fn (mut SSLListener, string) !&SSLCerts read_timeout time.Duration = default_mbedtls_client_read_timeout // the SSL client read timeout + + alpn_protocols []string // the list of ALPN protocols to advertise, e.g. ['h2', 'http/1.1']; empty means no ALPN extension is sent } fn ssl_read_timeout_ms(timeout time.Duration) u32 { @@ -438,12 +475,31 @@ pub fn (mut s SSLConn) shutdown() ! { C.mbedtls_ssl_free(&s.ssl) C.mbedtls_ssl_config_free(&s.conf) free_rng(mut s.ctr_drbg, mut s.entropy) + if s.alpn_list != unsafe { nil } { + unsafe { + C.free(s.alpn_list) + s.alpn_list = nil + } + } if s.owns_socket { net.shutdown(s.handle) net.close(s.handle)! } } +// negotiated_alpn returns the ALPN protocol selected during the TLS +// handshake (e.g. 'h2' or 'http/1.1'), or an empty string if no protocol +// was negotiated. +pub fn (s &SSLConn) negotiated_alpn() string { + // mbedtls_ssl_get_alpn_protocol returns a `const char *`; cast away const + // for V, since we only read from it (and copy it below). + p := &char(C.mbedtls_ssl_get_alpn_protocol(&s.ssl)) + if p == unsafe { nil } { + return '' + } + return unsafe { cstring_to_vstring(p) } +} + // connect to server using mbedtls fn (mut s SSLConn) init() ! { $if trace_ssl ? { @@ -467,19 +523,30 @@ fn (mut s SSLConn) init() ! { unsafe { C.mbedtls_ssl_conf_rng(&s.conf, C.mbedtls_ctr_drbg_random, &s.ctr_drbg) + } - // Enable ALPN for HTTP/1.1 (Required by strict servers like Rustls/Pijul) - // We allocate a small C array of strings: ["http/1.1", NULL] - // This memory must persist while the SSL config is active. - /* - alpn_list := &&char(C.malloc(2 * sizeof(voidptr))) - if alpn_list != 0 { - alpn_list[0] = c'http/1.1' - alpn_list[1] = &char(0) - C.mbedtls_ssl_conf_alpn_protocols(&s.conf, alpn_list) - } - TODO free alpn_list - */ + // Advertise ALPN protocols (e.g. ['h2', 'http/1.1']) when requested. + // mbedtls expects a NUL-terminated array of NUL-terminated C strings, and + // keeps the pointer without copying, so both the array and the backing + // strings must outlive the config. The strings live in s.config; the array + // is allocated here and freed in shutdown(). + if s.config.alpn_protocols.len > 0 { + n := s.config.alpn_protocols.len + s.alpn_list = unsafe { &&char(C.malloc(isize((n + 1) * int(sizeof(voidptr))))) } + if s.alpn_list == unsafe { nil } { + return error('net.mbedtls SSLConn.init, failed to allocate ALPN list') + } + unsafe { + for i, proto in s.config.alpn_protocols { + s.alpn_list[i] = &char(proto.str) + } + s.alpn_list[n] = &char(0) + } + ret = C.mbedtls_ssl_conf_alpn_protocols(&s.conf, voidptr(s.alpn_list)) + if ret != 0 { + return error_with_code('net.mbedtls SSLConn.init, mbedtls_ssl_conf_alpn_protocols failed ret: ${ret}', + ret) + } } if s.config.verify != '' || s.config.cert != '' || s.config.cert_key != '' { s.certs = &SSLCerts{} diff --git a/vlib/net/openssl/openssl.c.v b/vlib/net/openssl/openssl.c.v index 29c75e676..b87b0ceab 100644 --- a/vlib/net/openssl/openssl.c.v +++ b/vlib/net/openssl/openssl.c.v @@ -133,6 +133,16 @@ fn C.SSL_get_verify_result(ssl &C.SSL) i32 fn C.SSL_set_tlsext_host_name(s &C.SSL, name &char) i32 +// The ALPN calls go through small version-guarded shims in openssl_compat.h, +// so the module still links against OpenSSL versions older than 1.0.2 that +// predate ALPN. v_net_openssl_set_alpn_protos returns 0 on success (like the +// underlying SSL_set_alpn_protos) and non-zero when ALPN is unavailable. +// `data` is `const unsigned char **`; declared as voidptr so V emits a clean +// `(void*)` cast and avoids -cstrict nested-pointer const warnings. +fn C.v_net_openssl_set_alpn_protos(ssl &C.SSL, protos &u8, protos_len u32) i32 + +fn C.v_net_openssl_get0_alpn_selected(ssl &C.SSL, data voidptr, len &u32) + fn C.SSL_shutdown(&C.SSL) i32 fn C.SSL_free(&C.SSL) diff --git a/vlib/net/openssl/openssl_compat.h b/vlib/net/openssl/openssl_compat.h index eab26a1ce..e8b2959d4 100644 --- a/vlib/net/openssl/openssl_compat.h +++ b/vlib/net/openssl/openssl_compat.h @@ -23,6 +23,32 @@ static X509 *v_net_openssl_get1_peer_certificate(SSL *ssl) { } #endif +// ALPN (SSL_set_alpn_protos / SSL_get0_alpn_selected) is only available in +// OpenSSL 1.0.2 and later. On older OpenSSL-compatible headers, fall back to +// no-op shims so the module still links; ALPN is simply unavailable there. +// LibreSSL reports a high OPENSSL_VERSION_NUMBER and provides ALPN, so it uses +// the native path below. +#if !defined(OPENSSL_VERSION_NUMBER) || OPENSSL_VERSION_NUMBER < 0x10002000L +static int v_net_openssl_set_alpn_protos(SSL *ssl, const unsigned char *protos, unsigned int protos_len) { + (void)ssl; + (void)protos; + (void)protos_len; + return -1; // ALPN unsupported on this OpenSSL version +} +static void v_net_openssl_get0_alpn_selected(SSL *ssl, const unsigned char **data, unsigned int *len) { + (void)ssl; + *data = NULL; + *len = 0; +} +#else +static int v_net_openssl_set_alpn_protos(SSL *ssl, const unsigned char *protos, unsigned int protos_len) { + return SSL_set_alpn_protos(ssl, protos, protos_len); +} +static void v_net_openssl_get0_alpn_selected(SSL *ssl, const unsigned char **data, unsigned int *len) { + SSL_get0_alpn_selected(ssl, data, len); +} +#endif + // LibreSSL and older OpenSSL-compatible headers may not expose the async // SSL_ERROR constants, but V's SSLError enum needs stable values for them. #ifndef SSL_ERROR_WANT_ASYNC diff --git a/vlib/net/openssl/ssl_connection.c.v b/vlib/net/openssl/ssl_connection.c.v index 3ad7ce911..b7189e4d3 100644 --- a/vlib/net/openssl/ssl_connection.c.v +++ b/vlib/net/openssl/ssl_connection.c.v @@ -27,6 +27,8 @@ pub: validate bool // set this to true, if you want to stop requests, when their certificates are found to be invalid in_memory_verification bool // if true, verify, cert, and cert_key are read from memory, not from a file + + alpn_protocols []string // the list of ALPN protocols to advertise, e.g. ['h2', 'http/1.1']; empty means no ALPN extension is sent } // new_ssl_conn instance an new SSLCon struct @@ -70,6 +72,20 @@ pub fn (mut s SSLConn) close() ! { s.shutdown()! } +// negotiated_alpn returns the ALPN protocol selected during the TLS +// handshake (e.g. 'h2' or 'http/1.1'), or an empty string if no protocol +// was negotiated. +pub fn (s &SSLConn) negotiated_alpn() string { + mut data := &u8(unsafe { nil }) + mut length := u32(0) + C.v_net_openssl_get0_alpn_selected(voidptr(s.ssl), voidptr(&data), &length) + if length == 0 || data == unsafe { nil } { + return '' + } + // data points into OpenSSL-owned memory and is not NUL-terminated; copy it. + return unsafe { data.vbytes(int(length)).bytestr() } +} + // shutdown closes the ssl connection and does cleanup pub fn (mut s SSLConn) shutdown() ! { $if trace_ssl ? { @@ -134,6 +150,25 @@ fn (mut s SSLConn) init() ! { mut res := 0 + // Advertise ALPN protocols (e.g. ['h2', 'http/1.1']) when requested. + // OpenSSL expects the length-prefixed wire format: each protocol is a + // single length byte followed by that many bytes of protocol name. + if s.config.alpn_protocols.len > 0 { + mut wire := []u8{cap: 64} + for proto in s.config.alpn_protocols { + if proto.len == 0 || proto.len > 255 { + return error('net.openssl SSLConn.init, invalid ALPN protocol "${proto}"') + } + wire << u8(proto.len) + wire << proto.bytes() + } + // Returns 0 on success (opposite of most OpenSSL calls); non-zero also + // means ALPN is unavailable on OpenSSL versions older than 1.0.2. + if C.v_net_openssl_set_alpn_protos(voidptr(s.ssl), wire.data, u32(wire.len)) != 0 { + return error('net.openssl SSLConn.init, failed to set ALPN protocols (requires OpenSSL >= 1.0.2)') + } + } + if s.config.validate { mut verify := s.config.verify mut cert := s.config.cert diff --git a/vlib/net/ssl/ssl_alpn_test.v b/vlib/net/ssl/ssl_alpn_test.v new file mode 100644 index 000000000..37d806973 --- /dev/null +++ b/vlib/net/ssl/ssl_alpn_test.v @@ -0,0 +1,80 @@ +module main + +import net +import net.ssl +import net.mbedtls + +// Hermetic ALPN test for the net.ssl facade. A local mbedtls TLS server +// advertises a protocol list, and clients created via ssl.new_ssl_conn verify +// the negotiated result with SSLConn.negotiated_alpn(). The client exercises +// whichever backend net.ssl is built with (mbedtls by default, OpenSSL under +// -d use_openssl). No network access is required. + +const alpn_test_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' + +const alpn_test_key = '-----BEGIN RSA PRIVATE KEY-----\nMIIJKQIBAAKCAgEAuoAjh+pSL5AFVZyxeCWi4t04wvn7DT7WFJHUp07RDlc91ACz\n/JhYn4pSQPAb76t/L0NQ+rsU1EFW6tdCRwC2fqBd2WUZRKPBSIOj83iYw7JL/ay5\n8G5p7DDuNWKumkf7PA/W1SeNF3qSGXpuLUhqf1VLOvcjL0KRciDUZTgI9eQ7J0t6\nhB96lfFagrU5Q3BPdUoL+y0B9jeP80gh6IliTK5EYUKTGf8FSM34CxeJO6BFxgoQ\nUd0g+Q64JelhuF/6UuxzOnEKzet1kb1v83qYQMLNdOZ4eSayN09N/7WTkNlm8rNU\nEGuB3EKAFvvlHCMXm/1aachzffBaB8kLoVpTOP01jFhRs8cP6NPPSskH+OPi7/Em\nUz97k1GZd1bvSk4wpe1aNFKiUs4Ug8otjWqzcoxMBnEopY8MMXtaUwnExG3ueeeS\nznLDjc2iDgKMMuTVmAROc+kuCjeNuYl0i/44cpMmsGVW8OrtVPKLEKhLkbwqg6bJ\nAmFpcbo3ATaJWEfkkgqDOAJ9618OyePGon+q2sVc9rwUXZOyiYPQ0r8ZmYhyz705\n5UHvkmfm6EV8sdJymJNu67nDc/8sj8yqKEtEHpFE2dHM8fx8iDltgL1vvvS1r/wZ\nwVG5gTy35REa2pcmggqLFsOWpXniRqXwVDNjOERN/340zopv+UoR8b+myW8CAwEA\nAQKCAgEAkcoffF0JOBMOiHlAJhrNtSiX+ZruzNDlCxlgshUjyWEbfQG7sWbqSHUZ\njZflTrqyZqDpyca7Jp2ZM2Vocxa0klIMayfj08trCaOWY3pPeROE4d3HUJMPjEpH\nvEXTFdnVJIOBPgl3+vWfBfm17QIh9j4X3BVbVNNl3WCaiDGAl699Kl+Pe38cFeCh\nD3JZPEWsZ5SlvwjU8sNGbThjAWN8C1NjMuCXG4hGej5Ae3M/nPPR91jgnw4Me4Ut\nIL3K3RVyGqaqAPJjLsu0kWQUArJAGMfvUkXjwVklkaUV5SHtJBs+pdTXjyprTmJR\nvSXWWON5zkAEEJNY7QcZaeKYi96PFLUFI+ciEdnXn74CfSKhgZCBo+OyFZjDWW5R\nNmgAbZTN2RW0z+V54Lg36JfJrmiGs8TN06KwNjFo+iOJCdQnoUSIhTlmMfVbXPah\ntRfQvwqtfqVS9W/jkiGq9yDDqyXx093R/QTM/XqDlWJ2iOJFppOJefGFCWF6Fwll\nVT9povTAGQmXFiAxwFZxWtbFa0i8fP5QG80X6l/gRklSd6ZXAVvcLkaFGqxunDAe\nrYC2jBwHWRpVmbxw880SWRzlAsJXc7M8PQnBTlyX1mFZNnwAJgqplz0BQHQhQh4V\nqNfisUm9smtda+Hr9GBBUxs09ulery3I0lQjsArVxPqPVgUbFPECggEBANqLA5fH\n2LupOBoFH/fK5jixyGdSB8eJvU+XuS8RBBexnzTQApmDHiU7Axa/cKvxAfUgwBpU\n6OIsL6Lq6wowVInBgo7GraACwspGMIP8Z7+A8qDgSWIcpXP21Ny2RW+nukdH8ZnV\nTFtiFxLYU9GRfzSUcqvE0miKfMGP/S9Cqbew00K6CQ2xurLTR2AchfUQZJJIg7eF\nRBoftthXLQ+s1JoiLJX2gqCliFy32RMAUP+pKvKVJmVQh8bxEkoEzTV2eY7eTxsH\nJDH5hD66EZ5bW/nVAMruJ3iKjy3WvjDbnddNAz9IFKrd1RMP9dgSEKuSv/HhqwPe\n1q9Wm6LWZo8BlYcCggEBANp3M14QMcMxRlZE0TiSopi1CaE8OG0C9apToS1dol2s\n4lCsWHVPIC516LMPGU0bmCdtwJey1mgXQEKVxCWHkVhhoCKT/tN53o5qkptrhrXL\npbqmRfoMXI7LwJU+Vqi5fwSPGrSR/IzHwCUL7pHTbYN7wT5rr2rcC84XYSX31TFm\nNfMnbDuUk33ycAo07Vqts5A5FN+xViEUMFSDmfA2XmOAV77awz0l/3n3qOg9lQYe\nU4Av2nT19lGELirLInkB1ndLirWAcLaCBXKOLW4bzpNm9Bt8aiziVzcUzlJlLa+1\nnb/7//xzKi0eM/BhyJfhsmOz5B8AQ6Ca/keDk8M7JtkCggEARl8DDinE6VCpBv/l\ndlX4YgMlQ9fPN3pr4ig58iTpi3Ofj1L3s1TcLSLecMG+Vy9o8PTVxuTWhJWz1SMO\nAh7j6ePM1Yq2N9MLxDRrxOROyASOnCz8lEIjKL8vdc6fdz+sJO3OpzleuAJS6beM\n7euK6XRvpE3hbtZBK9bgsQonOkYPEOp0pds4AgM0dYdZvzrDF7OP7lVUQ5E4wFr5\n4JVHdEZS0wsoru/+g9STaqHscxaXBLvwPCl9Pxs7R2haZ7+5jr6Y/FwFVK5C3ivu\nJm7GpCDpe27KeO8tAZancXYWUlCzHfpo5Ug/Jz85a5UNlyHO+uUuuzVTLeyWew3M\nwnnBGwKCAQEAqGTBP3wUH3TX1p9s9cJxemvxZEra44woeIXF8wX9pV8hgzWVabb4\nA1f3ai31Pq5KdfnvPf8nrUxex/RRIOyCaDG4EW8qOS/zEKutHgef6nly4ZBQ2BC3\nN4pug5ttiNiSw5za5NyyYoGF5ghweA8UlwjJR6gRqri6kL0MsQt7VXyHkUmN787y\ncV5yZiut2PuTMVQOdu5miVDagAqAmdwOnXvMJtzRKU0kw4rWs0zklbbCfkhkh0sf\n9m2AeJPjmoqEGags3wKF3ugR8t8MvZbJgG0XNCiOXtKIj3iGIJTExm+jjNxd0OWk\nWOqy9lMpH4lky91ZtVuqxR0za0RMnWv24QKCAQBe8l0w9AYVNGDLv1jyPcbsncty\nNYI81yqe2mL+TC00sMCeil7C7WCP7kRklY01rH5q5gJ9Q1UV+bOj2fQdXDmQ5Bgo\n41jseh44gkbuXAeWcSDrDkJCrfvlNqFobTmUb8cdb9aQlHYfOJ31367LJspiw2SY\nmCbnLQ5sMnyBiMkcn0GfBV6IAkZVN73DPa8a1m/0Qrrv1GmBJFVbuZd9d/hAWpHa\nekhXPq0Sta+RNDfBR3aI5lAmVA17qRGiubQYJ+Ldq0aRJ40fGE51ctoSU/5RMcmh\n6+Qro+jSC94L46xMFp+1J5atgB1p/jVzTT/Ws7SLyotYUSL8zU7tcLiycQXs\n-----END RSA PRIVATE KEY-----\n' + +fn start_alpn_server(server_protos []string) !(&mbedtls.SSLListener, int) { + mut port_listener := net.listen_tcp(.ip, '127.0.0.1:0')! + port := port_listener.addr()!.port()! + port_listener.close()! + mut listener := mbedtls.new_ssl_listener('127.0.0.1:${port}', mbedtls.SSLConnectConfig{ + cert: alpn_test_cert + cert_key: alpn_test_key + validate: false + in_memory_verification: true + alpn_protocols: server_protos + })! + return listener, port +} + +fn accept_one(mut listener mbedtls.SSLListener) { + mut conn := listener.accept() or { return } + mut buf := []u8{len: 64} + _ := conn.read(mut buf) or { 0 } + conn.write_string('ok') or {} + conn.shutdown() or {} +} + +fn dial_and_get_alpn(port int, client_protos []string) !string { + mut client := ssl.new_ssl_conn( + validate: false + alpn_protocols: client_protos + )! + client.dial('127.0.0.1', port)! + selected := client.negotiated_alpn() + client.write_string('hi') or {} + client.shutdown() or {} + return selected +} + +fn test_alpn_negotiates_h2() { + mut listener, port := start_alpn_server(['h2', 'http/1.1'])! + defer { + listener.shutdown() or {} + } + spawn accept_one(mut listener) + selected := dial_and_get_alpn(port, ['h2', 'http/1.1'])! + assert selected == 'h2' +} + +fn test_alpn_falls_back_to_http1() { + mut listener, port := start_alpn_server(['h2', 'http/1.1'])! + defer { + listener.shutdown() or {} + } + spawn accept_one(mut listener) + selected := dial_and_get_alpn(port, ['http/1.1'])! + assert selected == 'http/1.1' +} + +fn test_alpn_empty_when_not_advertised() { + mut listener, port := start_alpn_server(['h2', 'http/1.1'])! + defer { + listener.shutdown() or {} + } + spawn accept_one(mut listener) + // Client advertises no ALPN protocols -> no extension -> empty result. + selected := dial_and_get_alpn(port, []string{})! + assert selected == '' +} -- 2.39.5