| 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 | // ALPN (RFC 7301) support for the Windows SChannel backend. The handshake-level |
| 7 | // plumbing lives in thirdparty/vschannel/vschannel.c; this exposes it to V. |
| 8 | // |
| 9 | // This adds the *capability* to advertise ALPN and read the negotiated protocol |
| 10 | // on SChannel. The one-shot vschannel request() path still speaks HTTP/1.1, so |
| 11 | // it is not wired into fetch() yet (advertising `h2` without an HTTP/2 driver |
| 12 | // would let a server pick a protocol we cannot speak). Negotiation is exercised |
| 13 | // via schannel_alpn_probe(). Wiring the request path to HTTP/2 is the follow-up. |
| 14 | // See vlang/v#27383. |
| 15 | |
| 16 | fn C.vschannel_set_alpn(tls_ctx &C.TlsContext, wire &char, len int) |
| 17 | fn C.vschannel_get_alpn(tls_ctx &C.TlsContext, out &char, out_cap int) int |
| 18 | fn C.vschannel_alpn_probe(tls_ctx &C.TlsContext, iport int, host &u16, out &char, out_cap int) int |
| 19 | |
| 20 | // alpn_wire encodes ALPN protocol names into the wire format SChannel expects: |
| 21 | // each name preceded by a single length byte, e.g. `['h2', 'http/1.1']` becomes |
| 22 | // "\x02h2\x08http/1.1". Empty names, or names longer than 255 bytes, are skipped. |
| 23 | fn alpn_wire(protocols []string) []u8 { |
| 24 | mut out := []u8{} |
| 25 | for p in protocols { |
| 26 | if p.len == 0 || p.len > 255 { |
| 27 | continue |
| 28 | } |
| 29 | out << u8(p.len) |
| 30 | out << p.bytes() |
| 31 | } |
| 32 | return out |
| 33 | } |
| 34 | |
| 35 | // schannel_alpn_probe performs a TLS handshake to `host`:`port` advertising |
| 36 | // `protocols` via ALPN and returns the protocol the server selected (e.g. 'h2'), |
| 37 | // or '' if none was negotiated. It sends no application data, so it works as a |
| 38 | // pure ALPN-negotiation check independent of the HTTP/1.1 request path. |
| 39 | // Windows/SChannel only. Pass `validate` = false to skip certificate validation |
| 40 | // (e.g. against a local test server with a self-signed cert). |
| 41 | fn schannel_alpn_probe(host string, port int, protocols []string, validate bool) string { |
| 42 | mut ctx := C.new_tls_context() |
| 43 | C.vschannel_use_tls12_client_protocol() |
| 44 | C.vschannel_init(&ctx, C.BOOL(if validate { 1 } else { 0 })) |
| 45 | wire := alpn_wire(protocols) |
| 46 | if wire.len > 0 { |
| 47 | C.vschannel_set_alpn(&ctx, &char(wire.data), wire.len) |
| 48 | } |
| 49 | mut buf := []u8{len: 256} |
| 50 | n := C.vschannel_alpn_probe(&ctx, port, host.to_wide(), &char(buf.data), buf.len) |
| 51 | C.vschannel_cleanup(&ctx) |
| 52 | if n <= 0 { |
| 53 | return '' |
| 54 | } |
| 55 | return buf[..n].bytestr() |
| 56 | } |
| 57 | |