diff --git a/Cargo.lock b/Cargo.lock index d13a80a4..4c521af6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -297,7 +297,6 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", ] [[package]] @@ -521,6 +520,7 @@ dependencies = [ "tempfile", "tokio", "tower-service", + "webpki-roots", "windows-sys 0.59.0", ] diff --git a/Cargo.toml b/Cargo.toml index 04aafefb..3e00f8bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,15 +27,13 @@ libsqlite3-sys = { version = "=0.33.0", features = ["bundled"] } # backend, and WebPKI roots keep the static host self-contained for Roc linking. hyper = { version = "=1.6.0", default-features = false, features = ["http1", "client"] } hyper-util = { version = "=0.1.12", default-features = false, features = ["client", "client-legacy", "http1", "tokio"] } -hyper-rustls = { version = "=0.27.6", default-features = false, features = ["http1", "tls12", "webpki-tokio", "ring"] } +hyper-rustls = { version = "=0.27.6", default-features = false, features = ["http1", "tls12", "ring"] } http-body-util = "=0.1.3" tokio = { version = "=1.45.0", default-features = false, features = ["rt", "net", "time", "process", "sync", "io-util", "macros"] } bytes = "=1.12.1" tower-service = "=0.3.3" - -[dev-dependencies] -# Exercise the production ring provider against a local TLS server. rustls = { version = "=0.23.46", default-features = false, features = ["std", "ring", "tls12"] } +webpki-roots = "=1.0.8" # Keep freed small-allocation pages on the statically linked Linux hosts. # Generated Roc ABI helpers already allocate through Rust's global allocator. diff --git a/README.md b/README.md index 436cc1ec..0148348d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A Roc [platform](https://github.com/roc-lang/roc/wiki/Roc-concepts-explained#platform) for command-line programs. -`basic-cli` supports command execution, directories, environment variables, files, HTTP, locales, paths, random seeds, sleeping, SQLite, standard input/output/error, TCP, terminal raw mode, and UTC time. +`basic-cli` supports command execution, directories, environment variables, files, HTTP, locales, paths, random seeds, sleeping, SQLite, standard input/output/error, TCP (including TLS client connections), terminal raw mode, and UTC time. ## Supported targets @@ -35,10 +35,15 @@ These host behaviors may affect application design: - HTTP and HTTPS use HTTP/1 only. [Issue #455](https://github.com/roc-lang/basic-cli/issues/455) tracks research into HTTP/2 support and its tradeoffs. -- HTTPS certificates are validated against bundled WebPKI roots, not the - operating system trust store. A certificate trusted only through a locally - installed OS root will therefore be rejected. [Issue #454](https://github.com/roc-lang/basic-cli/issues/454) - tracks research into the appropriate trust configuration for basic-cli. +- HTTPS and `Tcp.connect_tls!` validate certificates against bundled WebPKI + roots by default, not the operating system trust store (`SSL_CERT_FILE` is + not read either). A certificate trusted only through a locally installed OS + root will therefore be rejected. An application can add its own roots from + PEM bytes with `tls: { trust: AddPem(bytes) }`, or accept any certificate + while developing with `tls: { trust: DangerAcceptInvalidCerts }`, in the + options of `Http.send!`, `Http.send_streaming!` or `Tcp.connect_tls!`. The + trust applies to every redirect hop. [Issue #454](https://github.com/roc-lang/basic-cli/issues/454) + tracks research into the appropriate default trust source for basic-cli. - The timeout configured on an HTTP `Request` is the deadline for the whole exchange, response body included; `NoTimeout` leaves it unbounded. `Http.send!` and the functions built on it buffer the response completely @@ -86,8 +91,9 @@ These host behaviors may affect application design: creating a new regular file. Existing files retain their access grants; use `RequireNew` with `OwnerOnly` for private files. Explicit permissions replace ACL grants and fail if the filesystem cannot enforce the policy. -- Several network and TLS failures currently use a generic HTTP transport - error ([issue #438](https://github.com/roc-lang/basic-cli/issues/438)). +- A rejected certificate or failed TLS handshake is `HttpErr(TlsErr(err))`, + naming the cause. Several other network failures currently use a generic + HTTP transport error ([issue #438](https://github.com/roc-lang/basic-cli/issues/438)). - TCP reads and writes always have a timeout: a `Tcp.Connection` holds the `io_timeout` it was created with, which bounds each read or write as a whole, and `with_timeout` returns the same connection with another. The wait @@ -96,6 +102,13 @@ These host behaviors may affect application design: `TcpErr(TimedOut)`. - A TCP read that times out or is interrupted consumes nothing: the bytes that had arrived stay buffered for the next read. A write that times out may have sent any prefix. +- `Tcp.connect_tls!` returns an ordinary `Tcp.Connection`: its + `connect_timeout` covers resolution, connecting and the handshake, and reads + and writes keep every TCP guarantee above. A bad PEM in `AddPem` is + `TlsErr(BadPem(_))`, reported before any network activity. A TLS failure + after the handshake is `TcpErr(Other(message))`, and so is a peer that closes + without TLS `close_notify`, rather than a clean end of input. Releasing the + connection sends `close_notify` without waiting. - `Tcp.Connection.read_until!`, `read_line!` and `lines` stop at the connection's line limit (1 MiB unless changed with `with_line_limit`), preventing an untrusted peer from causing unbounded buffering. The rest of diff --git a/examples/tcp-tls.roc b/examples/tcp-tls.roc new file mode 100644 index 00000000..59f4fe70 --- /dev/null +++ b/examples/tcp-tls.roc @@ -0,0 +1,57 @@ +## Exchange lines with a TLS echo server whose certificate is signed by a +## private certificate authority, read from a PEM file. +app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.27.0/9yjxqdGTP4xSpSEmDazwTmRsehfRZzA19pwEKkR6fj1g.tar.zst" } + +import pf.Duration +import pf.OsStr +import pf.Path +import pf.Stdout +import pf.Tcp + +# The test runner starts a local TLS echo server on port 8443 and passes the +# certificate it trusts. To try it by hand, run scripts/tls_test_server.py in +# another terminal, then: +# +# $ roc examples/tcp-tls.roc -- tests/tls/localhost-cert.pem +# +# Without an argument only the bundled public roots are trusted, so the same +# server is rejected with `TlsErr(UnknownIssuer)`. +main! : List(OsStr) => Try({}, _) +main! = |args| { + trust = match args { + [ca_file] => AddPem(Path.from_os_str(ca_file).read_bytes!()?) + _ => Default + } + + # The handshake verifies the certificate against the host name. It is part + # of the connect, so `connect_timeout` bounds it as well. + conn = Tcp.connect_tls!("localhost", 8443, { connect_timeout: Duration.seconds(5), io_timeout: Duration.seconds(5), tls: { trust } })? + Stdout.line!("Connected over TLS")? + + # From here on it is an ordinary `Tcp.Connection`. + conn.write_utf8!("hello\nover tls\n")? + var $pending = 2 + for! item in conn.lines() { + match item { + Ok(line) => Stdout.line!("< ${line}")? + # These two concern one line; the stream carries on with the next. + Err(BadUtf8({ index, .. })) => Stdout.line!("< (invalid UTF-8 at byte ${index.to_str()})")? + Err(ReadLimitExceeded(limit)) => Stdout.line!("< (a line longer than ${limit.to_str()} bytes)")? + # The connection failed or timed out, and this is the last item. + Err(TcpErr(err)) => { + return Err(TcpErr(err)) + } + Err(Interrupted) => { + return Err(Interrupted) + } + } + # The echo server keeps the connection open, so stop at the last reply + # instead of waiting for the end of input. + $pending = $pending - 1 + if $pending == 0 { + break + } + } + + Ok({}) +} diff --git a/platform/Host.roc b/platform/Host.roc index e1e86cde..bdc75f6f 100644 --- a/platform/Host.roc +++ b/platform/Host.roc @@ -3,6 +3,7 @@ import InternalHttp import InternalSqlite import Resource import System +import Tls ## Declare the hosted effects and ABI-safe data exchanged with the native host. Host :: [].{ @@ -166,7 +167,7 @@ Host :: [].{ # reads from the one cursor the response's streams share: `Chunk` is the # next non-empty chunk of at most 64 KiB, `ToEnd(max)` is everything unread, # or `ReadLimitExceeded` once more than `max` bytes of it have arrived. - http_send_streaming! : InternalHttp.RequestToAndFromHost, [NoTimeout, After(U64)], [NoTimeout, At(U64)] => Try(InternalHttp.StreamingResponseFromHost, InternalHttp.TransportErr) + http_send_streaming! : InternalHttp.RequestToAndFromHost, [NoTimeout, After(U64)], [NoTimeout, At(U64)], Tls.Trust => Try(InternalHttp.StreamingResponseFromHost, InternalHttp.TransportErr) http_read_body! : Resource.HttpResponse, [Chunk, ToEnd(U64)] => Try(List(U8), [EndOfBody, ReadLimitExceeded, HttpErr(InternalHttp.TransportErr)]) signal_catch! : List([Interrupt, Terminate(System.Unix), User1(System.Unix), User2(System.Unix), WindowResize(System.Unix)]) => Try({}, [SignalErr(IOErr)]) signal_poll! : () => List([Interrupt, Terminate, User1, User2, WindowResize]) @@ -188,4 +189,8 @@ Host :: [].{ unix_socket_listen! : NativePath => Try(Resource.UnixSocketListener, [UnixSocketErr(IOErr)]) unix_socket_accept! : Resource.UnixSocketListener, [NoTimeout, After(U64)] => Try(Resource.UnixSocketConnection, [Closed, UnixSocketErr(IOErr)]) unix_socket_listener_close! : Resource.UnixSocketListener => Try({}, [UnixSocketErr(IOErr)]) + + # TLS over TCP. The timeout, in nanoseconds, bounds resolution, connecting + # and the handshake. `TlsErr` is a failed handshake or unusable trust roots. + tcp_connect_tls! : Str, U16, U64, Tls.Trust => Try(Resource.TcpConnection, [TcpErr(IOErr), TlsErr(Tls.Err)]) } diff --git a/platform/Http.roc b/platform/Http.roc index 9caa9ea7..64667711 100644 --- a/platform/Http.roc +++ b/platform/Http.roc @@ -2,6 +2,7 @@ import Duration import Host import InternalHttp import Resource +import Tls import Url import http.Method import http.Header @@ -24,17 +25,21 @@ import http.Response ## adds no content type. Explicit content types are always preserved. ## ## See the [host runtime behavior](https://github.com/roc-lang/basic-cli#host-runtime-behavior) -## for HTTP protocol, TLS trust-store, and timeout details. +## for HTTP protocol, TLS trust, and timeout details. Http :: [].{ ## Failures while sending a request, reading its body or following redirects. - TransportErr : [Timeout, NetworkError, BadBody, Other(List(U8)), RedirectLimitExceeded(U64), InvalidRedirectUrl(Url.ParseErr), AmbiguousRedirectLocation, InsecureRedirect] + TransportErr : [Timeout, NetworkError, BadBody, Other(List(U8)), RedirectLimitExceeded(U64), InvalidRedirectUrl(Url.ParseErr), AmbiguousRedirectLocation, InsecureRedirect, TlsErr(Tls.Err)] ## Redirects are opt-in; `{}` returns the first response unchanged. ## A positive maximum follows 301/302/303/307/308, using one request ## deadline across every hop and the final body. Cross-origin requests ## discard credentials; HTTPS-to-HTTP redirects are rejected. - SendOptions := { max_redirects : U64 ?? 0 } + ## + ## `tls` chooses which server certificates HTTPS accepts, on the first + ## request and on every redirect hop; `{}` keeps the bundled roots. A + ## cross-origin redirect is checked with the same trust. + SendOptions := { max_redirects : U64 ?? 0, tls : Tls.Options ?? {} } ## Validate and send an HTTP request. ## @@ -48,7 +53,7 @@ Http :: [].{ ## ``` send! : Request, SendOptions => Try(Response, [Interrupted, InvalidUrl(Url.ParseErr), HttpErr(TransportErr)]) send! = |request, options| { - response = send_streaming!(request, { idle_timeout: NoTimeout, max_redirects: options.max_redirects })? + response = send_streaming!(request, { idle_timeout: NoTimeout, max_redirects: options.max_redirects, tls: options.tls })? body = match response.read_to_end!(NoLimit) { Ok(bytes) => bytes Err(ReadLimitExceeded(_)) => { @@ -145,10 +150,11 @@ Http :: [].{ ## leave the request at `NoTimeout` and rely on `idle_timeout`. ## ## `max_redirects` has the same meaning as the `max_redirects` field of [SendOptions]. + ## `tls` has the same meaning as in [SendOptions]. ## A caught pending signal returns bare `Interrupted` from a wait. Body ## reads preserve their cursor and collected bytes after interruption; ## acknowledge the signal before reading again. The deadline keeps running. - StreamingOptions := { idle_timeout : [NoTimeout, After(Duration)] ?? After(Duration.seconds(30)), max_redirects : U64 ?? 0 } + StreamingOptions := { idle_timeout : [NoTimeout, After(Duration)] ?? After(Duration.seconds(30)), max_redirects : U64 ?? 0, tls : Tls.Options ?? {} } ## Validate and send an HTTP request, returning once the status and ## headers have arrived. The body is read afterwards, through the @@ -308,6 +314,7 @@ transport_error = |error| NetworkError => HttpErr(NetworkError) BadBody => HttpErr(BadBody) Other(message) => HttpErr(Other(message)) + TlsErr(err) => HttpErr(TlsErr(err)) } ## The same driver serves buffered and streamed responses. The deadline is @@ -319,7 +326,7 @@ send_redirected! = |request, url, options, remaining, deadline| { After(duration) => After(duration.to_nanos()) } hop = request.with_headers(sanitize_headers(Request.headers(request), False, False)) - raw = match Host.http_send_streaming!(InternalHttp.to_host_request(hop), idle, deadline) { + raw = match Host.http_send_streaming!(InternalHttp.to_host_request(hop), idle, deadline, options.tls.trust) { Ok(value) => value Err(error) => { return Err(transport_error(error)) @@ -420,6 +427,8 @@ sanitize_headers = |headers, cross_origin, drop_body| { expect Http.SendOptions.{}.max_redirects == 0 expect Http.StreamingOptions.{}.max_redirects == 0 +expect Http.SendOptions.{}.tls.trust == Default +expect Http.StreamingOptions.{ tls: { trust: DangerAcceptInvalidCerts } }.tls.trust == DangerAcceptInvalidCerts expect redirect_url("https://example.com/a/b", "../c#part") == Url.parse("https://example.com/c") expect redirect_url("https://example.com/a", "//other.example/b") == Url.parse("https://other.example/b") expect same_origin("https://EXAMPLE.com:443/a", "https://example.com/b") diff --git a/platform/InternalHttp.roc b/platform/InternalHttp.roc index e4841c46..0f4b3ccb 100644 --- a/platform/InternalHttp.roc +++ b/platform/InternalHttp.roc @@ -3,6 +3,7 @@ import http.Method import http.Request import http.Response import Resource +import Tls ## Define host-ABI HTTP types and convert them to the shared HTTP package types. ## These records map directly to the generated Rust glue types. @@ -10,7 +11,7 @@ InternalHttp :: [].{ ## Errors raised by the host while sending a request, before a real HTTP ## response is available. - TransportErr : [Interrupted, Timeout, NetworkError, BadBody, Other(List(U8))] + TransportErr : [Interrupted, Timeout, NetworkError, BadBody, Other(List(U8)), TlsErr(Tls.Err)] # Generated Rust glue uses tuple headers at the host ABI boundary. HostHeaderTuple : (Str, Str) diff --git a/platform/Tcp.roc b/platform/Tcp.roc index cb35d498..ad46eaf8 100644 --- a/platform/Tcp.roc +++ b/platform/Tcp.roc @@ -2,6 +2,7 @@ import Duration import Host import IOErr import Resource +import Tls ## The line limit a new connection holds: 1 MiB. default_line_limit : U64 @@ -24,6 +25,7 @@ accept_wait = |accept_timeout| ## results compare with `==`: `result == Err(TcpErr(ConnectionRefused))`. ## A caught, pending signal returns bare `Interrupted` from waits. A read that ## is interrupted keeps its unread bytes; a write may have sent a prefix. +## `connect_tls!` adds `TlsErr(Tls.Err)` for a failed handshake. ## ## Timeouts are [Duration]s. The wait to connect or to accept bounds that one ## call. The connection it produces holds its own I/O timeout, which bounds @@ -372,6 +374,42 @@ Tcp :: [].{ Err(TcpErr(Interrupted)) => Err(Interrupted) Err(TcpErr(err)) => Err(TcpErr(err)) } + + ## Options for [connect_tls!]: the two timeouts of [connect!], and TLS + ## settings that default to `{}`. + ConnectTlsOptions := { connect_timeout : Duration, io_timeout : Duration, tls : Tls.Options ?? {} } + + ## Open a TLS client connection. + ## + ## The handshake verifies the server's certificate for `host`, which is also + ## sent as the server name (SNI) unless it is an IP address. An IP address is + ## verified against the certificate's IP address entries. + ## + ## `connect_timeout` bounds this call: hostname resolution, connecting, and + ## the handshake. `io_timeout` is the timeout the new [Connection] holds, as + ## for [connect!]. The connection behaves exactly like one from [connect!]: + ## reads that time out or are interrupted consume nothing, and the line + ## limit applies. + ## + ## A failed handshake is `TlsErr(err)`, with [Tls.Err] naming the cause. + ## `BadPem` is reported before any network activity. A TLS failure later, + ## while reading or writing, is `TcpErr(Other(message))`. So is a peer + ## that closes without TLS's `close_notify`: data could have been cut off. + ## Releasing the connection sends `close_notify` without waiting. + ## + ## ```roc + ## conn = Tcp.connect_tls!("example.com", 443, { connect_timeout: Duration.seconds(5), io_timeout: Duration.seconds(30) })? + ## conn.write_utf8!("HEAD / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n")? + ## status_line = conn.read_line!()? + ## ``` + connect_tls! : Str, U16, ConnectTlsOptions => Try(Connection, [Interrupted, TlsErr(Tls.Err), TcpErr(IOErr)]) + connect_tls! = |host, port, options| + match Host.tcp_connect_tls!(host, port, options.connect_timeout.to_nanos(), options.tls.trust) { + Ok(handle) => Ok(Connection.{ host: handle, timeout: options.io_timeout, line_limit: default_line_limit }) + Err(TcpErr(Interrupted)) => Err(Interrupted) + Err(TcpErr(err)) => Err(TcpErr(err)) + Err(TlsErr(err)) => Err(TlsErr(err)) + } } expect default_line_limit == 1_048_576 diff --git a/platform/Tls.roc b/platform/Tls.roc new file mode 100644 index 00000000..77414f8e --- /dev/null +++ b/platform/Tls.roc @@ -0,0 +1,66 @@ +## Choose which server certificates a TLS client accepts, for [Tcp.connect_tls!] +## and for HTTPS through [Http.send!] and [Http.send_streaming!]. +## +## See the [host runtime behavior](https://github.com/roc-lang/basic-cli#host-runtime-behavior) +## for where the default roots come from. +Tls :: [].{ + + ## Which server certificates a client accepts. + ## + ## - `Default`: certificates that chain to the bundled WebPKI (Mozilla) roots. + ## The operating system's trust store is not consulted. + ## - `AddPem(bytes)`: the bundled roots plus every `CERTIFICATE` in the PEM + ## bytes, such as a private certificate authority. Other PEM sections are + ## ignored. Bytes without any certificate are `BadPem`, before anything is + ## sent. + ## - `DangerAcceptInvalidCerts`: any certificate, for any name, expired or + ## not. The connection is still encrypted and the server must still hold + ## the key of the certificate it presents, but anyone on the network path + ## can present their own. Only for development against a server with a + ## self-signed certificate. + Trust : [Default, AddPem(List(U8)), DangerAcceptInvalidCerts] + + ## TLS settings. `{}` selects the defaults. + ## + ## ```roc + ## ca = Path.read_bytes!("ca.pem")? + ## conn = Tcp.connect_tls!("db.internal", 6380, { connect_timeout: Duration.seconds(5), io_timeout: Duration.seconds(30), tls: { trust: AddPem(ca) } })? + ## ``` + Options := { trust : Trust ?? Default } + + ## Why a TLS handshake failed, or why the settings were rejected before + ## connecting. + ## + ## - `UnknownIssuer`: the certificate does not chain to a trusted root. + ## - `Expired`: the certificate, or one it chains to, has expired. + ## - `NameMismatch`: the certificate is not valid for the host name or IP + ## address that was connected to. + ## - `BadPem(message)`: the `AddPem` bytes hold no usable certificate. + ## - `Other(message)`: any other handshake failure, such as no common + ## protocol version or an alert from the server. + Err := [UnknownIssuer, Expired, NameMismatch, BadPem(Str), Other(Str)].{ + + ## Compare two TLS errors, so results that contain one work with `==`. + is_eq : _ + + ## Describe the error for display. + to_str : Err -> Str + to_str = |err| + match err { + UnknownIssuer => "the server certificate is not issued by a trusted certificate authority" + Expired => "the server certificate has expired" + NameMismatch => "the server certificate is not valid for the requested host" + BadPem(message) => "the PEM trust roots cannot be used: ${message}" + Other(message) => message + } + } +} + +expect Tls.Options.{}.trust == Default +expect Tls.Options.{ trust: AddPem([1]) }.trust == AddPem([1]) +expect Tls.Err.to_str(Expired) == "the server certificate has expired" +expect { + result : Try({}, [TlsErr(Tls.Err)]) + result = Err(TlsErr(NameMismatch)) + result == Err(TlsErr(NameMismatch)) and result != Err(TlsErr(Expired)) +} diff --git a/platform/main.roc b/platform/main.roc index f198e3dc..dc1ad0e8 100644 --- a/platform/main.roc +++ b/platform/main.roc @@ -6,7 +6,7 @@ platform "" ## launcher-supplied program name is available from Env.program_name!. main! : List([Utf8(Str), UnixBytes(List(U8)), WindowsU16s(List(U16))]) => Try({}, [Exit(I32), Interrupted, ..]) } - exposes [Cmd, Duration, Env, File, Http, IOErr, Locale, Monotonic, OsStr, Path, Random, Signal, Sleep, Sqlite, Stderr, Stdin, Stdout, System, Tcp, Tty, UnixSocket, Url, Utc] + exposes [Cmd, Duration, Env, File, Http, IOErr, Locale, Monotonic, OsStr, Path, Random, Signal, Sleep, Sqlite, Stderr, Stdin, Stdout, System, Tcp, Tls, Tty, UnixSocket, Url, Utc] packages { # HTTP data types (Method, Request, Response) come from the shared # roc-lang/http package so apps and other packages using it see the same @@ -115,6 +115,7 @@ platform "" "hosted_tcp_accept": Host.tcp_accept!, # TCP hosted functions are likewise kept at the end to avoid renumbering. "hosted_tcp_connect": Host.tcp_connect!, + "hosted_tcp_connect_tls": Host.tcp_connect_tls!, "hosted_tcp_listen": Host.tcp_listen!, "hosted_tcp_listener_close": Host.tcp_listener_close!, "hosted_tcp_local_port": Host.tcp_local_port!, @@ -166,6 +167,7 @@ import Stdin import Stdout import Stderr import Tcp +import Tls import Tty import UnixSocket import Url diff --git a/scripts/test.py b/scripts/test.py index 5cc8013a..c2bb9cb4 100755 --- a/scripts/test.py +++ b/scripts/test.py @@ -239,6 +239,9 @@ def helper_server(name: str | None) -> Iterator[None]: elif name == "tcp": args = [sys.executable, str(ROOT / "scripts" / "tcp_echo_server.py")] port = 8085 + elif name == "tls": + args = [sys.executable, str(ROOT / "scripts" / "tls_test_server.py")] + port = 8443 else: raise SystemExit(f"Unknown helper server: {name}") process = subprocess.Popen(args, cwd=ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) diff --git a/scripts/test_spec.json b/scripts/test_spec.json index 7003551d..aa76b194 100644 --- a/scripts/test_spec.json +++ b/scripts/test_spec.json @@ -548,6 +548,34 @@ } ] }, + { + "path": "examples/tcp-tls.roc", + "cases": [ + { + "name": "private-ca", + "helper": "tls", + "timeout": 20, + "live_handles": 0, + "args": ["{root}/tests/tls/localhost-cert.pem"], + "stdout_contains": ["Connected over TLS\n< hello\n< over tls\n"] + }, + { + "name": "default-roots-reject-private-ca", + "helper": "tls", + "timeout": 20, + "live_handles": 0, + "exit_code": 1, + "contains": ["Program exited with error: TlsErr(UnknownIssuer)"] + }, + { + "name": "bad-pem-before-connecting", + "live_handles": 0, + "args": ["{root}/LICENSE"], + "exit_code": 1, + "contains": ["Program exited with error: TlsErr(BadPem("] + } + ] + }, { "path": "examples/temp-dir.roc", "cases": [{ "name": "happy", "regex": ["The temp dir path is .+"] }] @@ -1231,6 +1259,33 @@ } ] }, + { + "path": "tests/tls-trust.roc", + "cases": [ + { + "name": "trust-options", + "helper": "tls", + "timeout": 30, + "live_handles": 0, + "args": ["{root}/tests/tls/localhost-cert.pem"], + "stdout_contains": [ + "tcp trust: ok\n", + "tcp bad pem before any network I/O: ok\n", + "tcp reads over tls: ok\n", + "http trust: ok\n", + "http redirects use the trust on every hop: ok\n", + "http streaming trust: ok\n", + "http bad pem before any network I/O: ok\n" + ] + }, + { + "name": "missing-ca-file", + "live_handles": 0, + "exit_code": 1, + "contains": ["Program exited with error: MissingCaFile"] + } + ] + }, { "path": "tests/sqlite.roc", "cases": [ diff --git a/scripts/tls_test_server.py b/scripts/tls_test_server.py new file mode 100644 index 00000000..8bc76c37 --- /dev/null +++ b/scripts/tls_test_server.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Local TLS helper servers used by the TLS example test. + +Serves the self-signed `localhost` certificate from tests/tls over TLS: + * 8443: echo server; echoes bytes until the client closes, then sends + close_notify and closes. + * 9443: HTTPS server; GET /hello -> 200 "ok", GET /redirect -> 302 to + /hello, GET /redirect-cross -> 302 to https://127.0.0.1:9443/hello (the + same server under a name the certificate does not cover), anything else + -> 404. + +Both bind ::1 (best effort) and 127.0.0.1. Windows resolves `localhost` to ::1 +first, while scripts/test.py polls 127.0.0.1 for readiness, so ::1 is bound +first and 127.0.0.1 last. Handshake failures (clients rejecting the +certificate, plain-TCP readiness probes) never stop the servers. +""" +import socket +import ssl +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + +TLS_DIR = Path(__file__).resolve().parent.parent / "tests" / "tls" +ECHO_PORT = 8443 +HTTPS_PORT = 9443 + +context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) +context.load_cert_chain(TLS_DIR / "localhost-cert.pem", TLS_DIR / "localhost-key.pem") + + +def echo(conn: socket.socket) -> None: + try: + with conn: + conn.settimeout(30) + try: + tls = context.wrap_socket(conn, server_side=True) + except (OSError, ValueError): + return + try: + while True: + data = tls.recv(4096) + if not data: + break + tls.sendall(data) + tls.unwrap() + except (OSError, ValueError): + pass + except OSError: + pass + + +def serve_echo(listener: socket.socket) -> None: + while True: + try: + conn, _ = listener.accept() + except OSError: + continue + threading.Thread(target=echo, args=(conn,), daemon=True).start() + + +class Handler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def do_GET(self) -> None: + if self.path == "/hello": + self.reply(200, b"ok") + elif self.path == "/redirect": + self.reply(302, b"", {"Location": "/hello"}) + elif self.path == "/redirect-cross": + self.reply(302, b"", {"Location": f"https://127.0.0.1:{HTTPS_PORT}/hello"}) + else: + self.reply(404, b"not found") + + def reply(self, status: int, body: bytes, headers: dict[str, str] | None = None) -> None: + self.send_response(status) + for name, value in (headers or {}).items(): + self.send_header(name, value) + self.send_header("Content-Length", str(len(body))) + self.send_header("Connection", "close") + self.end_headers() + self.wfile.write(body) + + def log_message(self, format: str, *args: object) -> None: + pass + + +class HttpsServer(ThreadingHTTPServer): + daemon_threads = True + + def __init__(self, family: socket.AddressFamily, host: str) -> None: + self.address_family = family + super().__init__((host, HTTPS_PORT), Handler) + + def finish_request(self, request, client_address) -> None: # type: ignore[no-untyped-def] + # Handshake in the per-connection thread so a bad client cannot block accept. + request.settimeout(30) + super().finish_request(context.wrap_socket(request, server_side=True), client_address) + + def handle_error(self, request, client_address) -> None: # type: ignore[no-untyped-def] + pass + + +def listen_echo(family: socket.AddressFamily, host: str) -> socket.socket: + listener = socket.socket(family, socket.SOCK_STREAM) + listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + listener.bind((host, ECHO_PORT)) + listener.listen(16) + return listener + + +def main() -> None: + echo_listeners = [] + https_servers = [] + # ::1 first: the runner's readiness probe polls 127.0.0.1, which must come up last. + for family, host in [(socket.AF_INET6, "::1"), (socket.AF_INET, "127.0.0.1")]: + try: + echo_listeners.append(listen_echo(family, host)) + https_servers.append(HttpsServer(family, host)) + except OSError: + if family == socket.AF_INET: + raise + for server in https_servers: + threading.Thread(target=server.serve_forever, daemon=True).start() + threads = [threading.Thread(target=serve_echo, args=(l,), daemon=True) for l in echo_listeners] + for thread in threads: + thread.start() + for thread in threads: + thread.join() + + +if __name__ == "__main__": + main() diff --git a/src/http.rs b/src/http.rs index 3e783b1a..6c34fe07 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,5 +1,5 @@ use core::mem::ManuallyDrop; -use std::sync::LazyLock; +use std::sync::{Arc, LazyLock}; use std::time::{Duration, Instant}; use crate::roc_platform_abi::*; @@ -13,9 +13,9 @@ type HttpHeader = HostHttpSendRequestArg0Headers; type HttpResult = HostHttpSendRequestResult; type HttpResultPayload = HostHttpSendRequestResultPayload; type HttpResultTag = HostHttpSendRequestResultTag; -type HttpTransportErr = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout; -type HttpTransportErrPayload = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutPayload; -type HttpTransportErrTag = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag; +type HttpTransportErr = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr; +type HttpTransportErrPayload = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrPayload; +type HttpTransportErrTag = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag; // Deliberately a `static`, so this runtime is never dropped. // @@ -196,7 +196,9 @@ fn build_roc_headers(pairs: &[(String, String)], roc_host: &RocHost) -> RocList< list } -fn https_connector() -> hyper_rustls::HttpsConnector< +fn https_connector( + config: Arc, +) -> hyper_rustls::HttpsConnector< hyper_util::client::legacy::connect::HttpConnector, > { let mut http = hyper_util::client::legacy::connect::HttpConnector::new_with_resolver( @@ -204,7 +206,7 @@ fn https_connector() -> hyper_rustls::HttpsConnector< ); http.enforce_http(false); hyper_rustls::HttpsConnectorBuilder::new() - .with_webpki_roots() + .with_tls_config(rustls::ClientConfig::clone(&config)) .https_or_http() .enable_http1() .wrap_connector(http) @@ -216,7 +218,7 @@ async fn async_send_request(request: HyperRequest, roc_host: &RocHost) -> HttpRe use hyper_util::rt::TokioExecutor; let client: Client<_, http_body_util::Full> = - Client::builder(TokioExecutor::new()).build(https_connector()); + Client::builder(TokioExecutor::new()).build(https_connector(crate::tls::default_config())); match client.request(request).await { Ok(response) => { @@ -309,6 +311,7 @@ enum Failure { Timeout, BadBody, Other(String), + Tls(crate::tls::TlsFailure), } impl Failure { @@ -318,6 +321,12 @@ impl Failure { Failure::Timeout => http_err_timeout(), Failure::BadBody => http_err_bad_body(), Failure::Other(message) => http_err_other(message, roc_host), + Failure::Tls(failure) => HttpTransportErr { + payload: HttpTransportErrPayload { + tls_err: ManuallyDrop::new(failure.to_roc(roc_host)), + }, + tag: HttpTransportErrTag::TlsErr, + }, } } } @@ -604,13 +613,27 @@ fn send_streaming( 0 => None, ms => Instant::now().checked_add(Duration::from_millis(ms)), }; - send_streaming_at(request, idle_timeout, deadline) + send_streaming_at( + request, + idle_timeout, + deadline, + crate::tls::default_config(), + ) +} + +/// A failed `client.request`: a TLS handshake failure names its cause. +fn client_failure(error: &hyper_util::client::legacy::Error) -> Failure { + match crate::tls::rustls_error_in(error) { + Some(tls) => Failure::Tls(crate::tls::TlsFailure::from_rustls(tls)), + None => Failure::Other(error.to_string()), + } } fn send_streaming_at( request: HyperRequest, idle_timeout: Option, deadline: Option, + config: Arc, ) -> Result<(StreamingHead, StreamingBody), Failure> { use hyper_util::client::legacy::Client; use hyper_util::rt::TokioExecutor; @@ -632,12 +655,12 @@ fn send_streaming_at( let client: Client<_, http_body_util::Full> = Client::builder(TokioExecutor::new()) .pool_max_idle_per_host(0) - .build(https_connector()); + .build(https_connector(config)); await_io(until, client.request(request)).await }); let response = match response { Ok(Ok(response)) => response, - Ok(Err(error)) => return Err(Failure::Other(error.to_string())), + Ok(Err(error)) => return Err(client_failure(&error)), Err(failure) => { settle(); return Err(failure); @@ -689,8 +712,19 @@ pub extern "C" fn hosted_http_send_streaming( request: HostHttpSendStreamingArg0, idle_timeout: AfterOrNoTimeout, deadline: AtOrNoTimeout, + trust: AddPemOrDangerAcceptInvalidCertsOrDefault, ) -> HostHttpSendStreamingResult { - let roc_host = roc_host(); + http_send_streaming(request, idle_timeout, deadline, trust, roc_host()) +} + +fn http_send_streaming( + request: HostHttpSendStreamingArg0, + idle_timeout: AfterOrNoTimeout, + deadline: AtOrNoTimeout, + trust: AddPemOrDangerAcceptInvalidCertsOrDefault, + roc_host: &RocHost, +) -> HostHttpSendStreamingResult { + let trust = crate::tls::Trust::from_roc(trust, roc_host); let deadline = match deadline.tag { AtOrNoTimeoutTag::At => crate::monotonic_origin() .checked_add(Duration::from_nanos(unsafe { *deadline.payload.at })), @@ -707,9 +741,10 @@ pub extern "C" fn hosted_http_send_streaming( let hyper_request = build_hyper_request((&request).into()); unsafe { request.decref(roc_host) }; - let sent = hyper_request - .map_err(Failure::Other) - .and_then(|request| send_streaming_at(request, idle_timeout, deadline)); + let sent = hyper_request.map_err(Failure::Other).and_then(|request| { + let config = crate::tls::client_config(&trust).map_err(Failure::Tls)?; + send_streaming_at(request, idle_timeout, deadline, config) + }); let (head, body) = match sent { Ok(sent) => sent, Err(failure) => return streaming_err(&failure, roc_host), @@ -926,7 +961,7 @@ mod tests { TOKIO_RUNTIME.block_on(async { let client: hyper_util::client::legacy::Client<_, http_body_util::Full> = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) - .build(https_connector()); + .build(https_connector(crate::tls::default_config())); let request = hyper::Request::builder() .uri(format!("https://localhost:{}/", address.port())) .body(http_body_util::Full::new(bytes::Bytes::new())) @@ -1824,4 +1859,233 @@ mod tests { "a thread that used the HTTP runtime hung on exit while a blocking task was running" ); } + + // ---- TLS trust ----------------------------------------------------------- + + use crate::tls::{ + client_config, + testing::{server_config, LOCALHOST_PEM}, + TlsFailure, Trust, + }; + + /// An HTTPS server for one connection, bound to `host`. It answers any + /// request with `200` and `ok`. A client that rejects the certificate ends + /// the connection during the handshake, which the server tolerates. + fn https_server(host: &str) -> (u16, thread::JoinHandle<()>) { + let listener = TcpListener::bind((host, 0)).unwrap(); + let port = listener.local_addr().unwrap().port(); + let server = thread::spawn(move || { + let (socket, _) = listener.accept().unwrap(); + socket + .set_read_timeout(Some(Duration::from_secs(5))) + .unwrap(); + socket + .set_write_timeout(Some(Duration::from_secs(5))) + .unwrap(); + let connection = rustls::ServerConnection::new(server_config()).unwrap(); + let mut tls = rustls::StreamOwned::new(connection, socket); + let mut head = Vec::new(); + let mut byte = [0u8]; + while !head.ends_with(b"\r\n\r\n") { + match tls.read(&mut byte) { + Ok(1) => head.push(byte[0]), + _ => return, + } + } + let _ = tls + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok"); + let _ = tls.flush(); + tls.conn.send_close_notify(); + let _ = tls.flush(); + }); + (port, server) + } + + fn https_request(host: &str, port: u16) -> HyperRequest { + hyper::Request::builder() + .uri(format!("https://{host}:{port}/")) + .body(http_body_util::Full::default()) + .unwrap() + } + + /// Send one request with `trust` to a fresh HTTPS server at `host`. + fn https_get(host: &str, trust: &Trust) -> Result<(StreamingHead, StreamingBody), Failure> { + let (port, server) = https_server(host); + let deadline = Instant::now() + Duration::from_secs(10); + let sent = send_streaming_at( + https_request(host, port), + None, + Some(deadline), + client_config(trust).unwrap(), + ); + // A rejected certificate may leave the server without a request to read. + let sent = sent.map(|(head, mut body)| { + assert_eq!(drain(&mut body), b"ok"); + (head, body) + }); + server.join().unwrap(); + sent + } + + fn failure_of( + result: Result<(StreamingHead, StreamingBody), Failure>, + ) -> crate::tls::TlsFailure { + match result { + Err(Failure::Tls(failure)) => failure, + Err(Failure::Other(message)) => panic!("expected a TLS failure, got Other({message})"), + Err(_) => panic!("expected a TLS failure, got another failure"), + Ok((head, _)) => panic!("expected a TLS failure, got status {}", head.status), + } + } + + #[test] + fn https_uses_the_requested_trust() { + // Added roots trust the localhost certificate. + let (head, _) = https_get("localhost", &Trust::AddPem(LOCALHOST_PEM.to_vec())).unwrap(); + assert_eq!(head.status, 200); + + // The default roots do not, and the failure names its cause rather + // than arriving as `Failure::Other`. + assert_eq!( + failure_of(https_get("localhost", &Trust::Default)), + TlsFailure::UnknownIssuer + ); + + let (head, _) = https_get("localhost", &Trust::DangerAcceptInvalidCerts).unwrap(); + assert_eq!(head.status, 200); + } + + #[test] + fn https_checks_the_name_the_url_gives() { + // The certificate is for `localhost` only, so its address does not match, + // however much the roots are trusted. + let pem = Trust::AddPem(LOCALHOST_PEM.to_vec()); + assert_eq!( + failure_of(https_get("127.0.0.1", &pem)), + TlsFailure::NameMismatch + ); + let (head, _) = https_get("127.0.0.1", &Trust::DangerAcceptInvalidCerts).unwrap(); + assert_eq!(head.status, 200); + } + + #[test] + fn tls_failures_cross_as_transport_tls_err() { + let host = crate::resources::make_host(); + let tls_err = |failure: TlsFailure| { + let error = Failure::Tls(failure).to_roc(&host); + assert_eq!(error.tag, HttpTransportErrTag::TlsErr); + unsafe { ManuallyDrop::into_inner(error.payload.tls_err) } + }; + + assert_eq!( + tls_err(TlsFailure::UnknownIssuer).tag, + TlsErrTag::UnknownIssuer + ); + assert_eq!(tls_err(TlsFailure::Expired).tag, TlsErrTag::Expired); + assert_eq!( + tls_err(TlsFailure::NameMismatch).tag, + TlsErrTag::NameMismatch + ); + + let bad_pem = tls_err(TlsFailure::BadPem("no CERTIFICATE section found".into())); + assert_eq!(bad_pem.tag, TlsErrTag::BadPem); + let message = unsafe { ManuallyDrop::into_inner(bad_pem.payload.bad_pem) }; + assert_eq!(message.as_str(), "no CERTIFICATE section found"); + unsafe { message.decref(&host) }; + + let other = tls_err(TlsFailure::Other("handshake failure".into())); + assert_eq!(other.tag, TlsErrTag::Other); + let message = unsafe { ManuallyDrop::into_inner(other.payload.other) }; + assert_eq!(message.as_str(), "handshake failure"); + unsafe { message.decref(&host) }; + } + + /// Call `http_send_streaming` with a GET of `uri` and no timeouts. + fn hosted_get(uri: &str, trust: Trust, host: &RocHost) -> HostHttpSendStreamingResult { + let request = HostHttpSendStreamingArg0 { + timeout_ms: 0, + body: roc_u8_list_from_slice(b"", host), + headers: build_roc_headers(&[], host), + method_ext: RocStr::from_str("", host), + uri: RocStr::from_str(uri, host), + method: 3, + }; + let trust = match trust { + Trust::AddPem(pem) => AddPemOrDangerAcceptInvalidCertsOrDefault { + payload: AddPemOrDangerAcceptInvalidCertsOrDefaultPayload { + add_pem: ManuallyDrop::new(roc_u8_list_from_slice(&pem, host)), + }, + tag: AddPemOrDangerAcceptInvalidCertsOrDefaultTag::AddPem, + }, + Trust::DangerAcceptInvalidCerts => AddPemOrDangerAcceptInvalidCertsOrDefault { + payload: AddPemOrDangerAcceptInvalidCertsOrDefaultPayload { + danger_accept_invalid_certs: [], + }, + tag: AddPemOrDangerAcceptInvalidCertsOrDefaultTag::DangerAcceptInvalidCerts, + }, + Trust::Default => AddPemOrDangerAcceptInvalidCertsOrDefault { + payload: AddPemOrDangerAcceptInvalidCertsOrDefaultPayload { default: [] }, + tag: AddPemOrDangerAcceptInvalidCertsOrDefaultTag::Default, + }, + }; + http_send_streaming( + request, + AfterOrNoTimeout { + payload: AfterOrNoTimeoutPayload { no_timeout: [] }, + tag: AfterOrNoTimeoutTag::NoTimeout, + }, + AtOrNoTimeout { + payload: AtOrNoTimeoutPayload { no_timeout: [] }, + tag: AtOrNoTimeoutTag::NoTimeout, + }, + trust, + host, + ) + } + + /// The `TlsErr` tag of a failed send, releasing the result. + fn hosted_tls_err_tag(result: HostHttpSendStreamingResult, host: &RocHost) -> TlsErrTag { + assert_eq!(result.tag, HostHttpSendStreamingResultTag::Err); + let error = unsafe { result.borrow_payload_err_unchecked() }; + assert_eq!(error.tag, HttpTransportErrTag::TlsErr); + let tag = unsafe { &error.payload.tls_err }.tag; + unsafe { result.decref(host) }; + tag + } + + #[test] + fn hosted_send_streaming_applies_the_trust_it_is_given() { + let host = crate::resources::make_host(); + let (port, server) = https_server("localhost"); + let uri = format!("https://localhost:{port}/"); + let result = hosted_get(&uri, Trust::AddPem(LOCALHOST_PEM.to_vec()), &host); + assert_eq!(result.tag, HostHttpSendStreamingResultTag::Ok); + assert_eq!(unsafe { result.borrow_payload_ok_unchecked() }.status, 200); + unsafe { result.decref(&host) }; + server.join().unwrap(); + + let (port, server) = https_server("localhost"); + let uri = format!("https://localhost:{port}/"); + assert_eq!( + hosted_tls_err_tag(hosted_get(&uri, Trust::Default, &host), &host), + TlsErrTag::UnknownIssuer + ); + server.join().unwrap(); + } + + #[test] + fn unusable_pem_fails_before_any_connection() { + // Nothing listens here, and the URL is plain HTTP: the PEM is still + // reported, so a bad CA file does not hide behind an `http://` setup. + let host = crate::resources::make_host(); + let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap(); + let port = listener.local_addr().unwrap().port(); + drop(listener); + let result = hosted_get( + &format!("http://127.0.0.1:{port}/"), + Trust::AddPem(b"not a certificate".to_vec()), + &host, + ); + assert_eq!(hosted_tls_err_tag(result, &host), TlsErrTag::BadPem); + } } diff --git a/src/lib.rs b/src/lib.rs index 7372677a..6577eb5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,7 @@ mod unix_socket; mod roc_platform_abi; mod sqlite; mod tcp; +mod tls; mod tty; use crate::roc_platform_abi::*; diff --git a/src/roc_platform_abi.rs b/src/roc_platform_abi.rs index aa9c7518..7efc9fba 100644 --- a/src/roc_platform_abi.rs +++ b/src/roc_platform_abi.rs @@ -3641,7 +3641,7 @@ pub enum HostHttpSendRequestResultTag { #[repr(C)] #[derive(Clone, Copy)] pub union HostHttpSendRequestResultPayload { - pub err: core::mem::ManuallyDrop, + pub err: core::mem::ManuallyDrop, pub ok: core::mem::ManuallyDrop, } @@ -3675,8 +3675,8 @@ impl HostHttpSendRequestResult { /// # Safety /// `self.tag` must be `HostHttpSendRequestResultTag::Err` and the payload must still be initialized. #[cfg(target_pointer_width = "32")] - pub unsafe fn borrow_payload_err_unchecked(&self) -> &BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { - unsafe { &*(self.payload.as_ptr() as *const BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout) } + pub unsafe fn borrow_payload_err_unchecked(&self) -> &BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr { + unsafe { &*(self.payload.as_ptr() as *const BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr) } } /// Borrow the `Err` payload without creating another owner. @@ -3684,8 +3684,8 @@ impl HostHttpSendRequestResult { /// # Safety /// `self.tag` must be `HostHttpSendRequestResultTag::Err` and the payload must still be initialized. #[cfg(not(target_pointer_width = "32"))] - pub unsafe fn borrow_payload_err_unchecked(&self) -> &BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { - unsafe { &*(&self.payload.err as *const core::mem::ManuallyDrop as *const BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout) } + pub unsafe fn borrow_payload_err_unchecked(&self) -> &BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr { + unsafe { &*(&self.payload.err as *const core::mem::ManuallyDrop as *const BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr) } } /// Move the `Err` payload out of one owned tag-union shell. @@ -3693,8 +3693,8 @@ impl HostHttpSendRequestResult { /// # Safety /// `self.tag` must be `HostHttpSendRequestResultTag::Err`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(target_pointer_width = "32")] - pub unsafe fn take_payload_err_unchecked(&mut self) -> BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { - unsafe { core::ptr::read(self.payload.as_ptr() as *const BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout) } + pub unsafe fn take_payload_err_unchecked(&mut self) -> BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr) } } /// Move the `Err` payload out of one owned tag-union shell. @@ -3702,7 +3702,7 @@ impl HostHttpSendRequestResult { /// # Safety /// `self.tag` must be `HostHttpSendRequestResultTag::Err`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(not(target_pointer_width = "32"))] - pub unsafe fn take_payload_err_unchecked(&mut self) -> BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { + pub unsafe fn take_payload_err_unchecked(&mut self) -> BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr { unsafe { core::mem::ManuallyDrop::take(&mut self.payload.err) } } @@ -3757,56 +3757,58 @@ const _: () = assert!(core::mem::align_of::() == 4, " #[cfg(target_pointer_width = "32")] const _: () = assert!(core::mem::offset_of!(HostHttpSendRequestResult, tag) == 28, "HostHttpSendRequestResult tag offset mismatch"); -/// Tag discriminant for BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout. +/// Tag discriminant for BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag { +pub enum BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag { BadBody = 0, Interrupted = 1, NetworkError = 2, Other = 3, Timeout = 4, + TlsErr = 5, } #[repr(C)] #[derive(Clone, Copy)] -pub union BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutPayload { +pub union BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrPayload { pub bad_body: [u8; 0], pub interrupted: [u8; 0], pub network_error: [u8; 0], pub other: core::mem::ManuallyDrop>, pub timeout: [u8; 0], + pub tls_err: core::mem::ManuallyDrop, } #[cfg(target_pointer_width = "32")] #[repr(align(4))] #[derive(Clone, Copy)] -pub struct BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutPayloadAlignment; +pub struct BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrPayloadAlignment; -/// Tag union: BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout +/// Tag union: BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr #[cfg(target_pointer_width = "32")] #[repr(C)] #[derive(Clone, Copy)] -pub struct BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { - pub _payload_alignment: [BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutPayloadAlignment; 0], - pub payload: [u8; 12], - pub tag: BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag, +pub struct BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr { + pub _payload_alignment: [BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag, } -/// Tag union: BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout +/// Tag union: BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr #[cfg(not(target_pointer_width = "32"))] #[repr(C)] #[derive(Clone, Copy)] -pub struct BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { - pub payload: BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutPayload, - pub tag: BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag, +pub struct BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr { + pub payload: BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrPayload, + pub tag: BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag, } -impl BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { +impl BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr { /// Borrow the `Other` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag::Other` and the payload must still be initialized. + /// `self.tag` must be `BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::Other` and the payload must still be initialized. #[cfg(target_pointer_width = "32")] pub unsafe fn borrow_payload_other_unchecked(&self) -> &RocListWith { unsafe { &*(self.payload.as_ptr() as *const RocListWith) } @@ -3815,7 +3817,7 @@ impl BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { /// Borrow the `Other` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag::Other` and the payload must still be initialized. + /// `self.tag` must be `BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::Other` and the payload must still be initialized. #[cfg(not(target_pointer_width = "32"))] pub unsafe fn borrow_payload_other_unchecked(&self) -> &RocListWith { unsafe { &*(&self.payload.other as *const core::mem::ManuallyDrop> as *const RocListWith) } @@ -3824,7 +3826,7 @@ impl BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { /// Move the `Other` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag::Other`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::Other`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(target_pointer_width = "32")] pub unsafe fn take_payload_other_unchecked(&mut self) -> RocListWith { unsafe { core::ptr::read(self.payload.as_ptr() as *const RocListWith) } @@ -3833,26 +3835,195 @@ impl BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { /// Move the `Other` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag::Other`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::Other`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(not(target_pointer_width = "32"))] pub unsafe fn take_payload_other_unchecked(&mut self) -> RocListWith { unsafe { core::mem::ManuallyDrop::take(&mut self.payload.other) } } + /// Borrow the `TlsErr` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::TlsErr` and the payload must still be initialized. + #[cfg(target_pointer_width = "32")] + pub unsafe fn borrow_payload_tls_err_unchecked(&self) -> &TlsErr { + unsafe { &*(self.payload.as_ptr() as *const TlsErr) } + } + + /// Borrow the `TlsErr` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::TlsErr` and the payload must still be initialized. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn borrow_payload_tls_err_unchecked(&self) -> &TlsErr { + unsafe { &*(&self.payload.tls_err as *const core::mem::ManuallyDrop as *const TlsErr) } + } + + /// Move the `TlsErr` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::TlsErr`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(target_pointer_width = "32")] + pub unsafe fn take_payload_tls_err_unchecked(&mut self) -> TlsErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const TlsErr) } + } + + /// Move the `TlsErr` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::TlsErr`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn take_payload_tls_err_unchecked(&mut self) -> TlsErr { + unsafe { core::mem::ManuallyDrop::take(&mut self.payload.tls_err) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr, tag) == 32, "BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr, tag) == 16, "BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr tag offset mismatch"); + +/// Tag discriminant for Tls.Err. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TlsErrTag { + BadPem = 0, + Expired = 1, + NameMismatch = 2, + Other = 3, + UnknownIssuer = 4, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TlsErrPayload { + pub bad_pem: core::mem::ManuallyDrop, + pub expired: [u8; 0], + pub name_mismatch: [u8; 0], + pub other: core::mem::ManuallyDrop, + pub unknown_issuer: [u8; 0], +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct TlsErrPayloadAlignment; + +/// Tag union: Tls.Err +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TlsErr { + pub _payload_alignment: [TlsErrPayloadAlignment; 0], + pub payload: [u8; 12], + pub tag: TlsErrTag, +} + +/// Tag union: Tls.Err +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TlsErr { + pub payload: TlsErrPayload, + pub tag: TlsErrTag, +} + +impl TlsErr { + /// Borrow the `BadPem` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `TlsErrTag::BadPem` and the payload must still be initialized. + #[cfg(target_pointer_width = "32")] + pub unsafe fn borrow_payload_bad_pem_unchecked(&self) -> &RocStr { + unsafe { &*(self.payload.as_ptr() as *const RocStr) } + } + + /// Borrow the `BadPem` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `TlsErrTag::BadPem` and the payload must still be initialized. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn borrow_payload_bad_pem_unchecked(&self) -> &RocStr { + unsafe { &*(&self.payload.bad_pem as *const core::mem::ManuallyDrop as *const RocStr) } + } + + /// Move the `BadPem` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `TlsErrTag::BadPem`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(target_pointer_width = "32")] + pub unsafe fn take_payload_bad_pem_unchecked(&mut self) -> RocStr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocStr) } + } + + /// Move the `BadPem` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `TlsErrTag::BadPem`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn take_payload_bad_pem_unchecked(&mut self) -> RocStr { + unsafe { core::mem::ManuallyDrop::take(&mut self.payload.bad_pem) } + } + + /// Borrow the `Other` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `TlsErrTag::Other` and the payload must still be initialized. + #[cfg(target_pointer_width = "32")] + pub unsafe fn borrow_payload_other_unchecked(&self) -> &RocStr { + unsafe { &*(self.payload.as_ptr() as *const RocStr) } + } + + /// Borrow the `Other` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `TlsErrTag::Other` and the payload must still be initialized. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn borrow_payload_other_unchecked(&self) -> &RocStr { + unsafe { &*(&self.payload.other as *const core::mem::ManuallyDrop as *const RocStr) } + } + + /// Move the `Other` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `TlsErrTag::Other`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(target_pointer_width = "32")] + pub unsafe fn take_payload_other_unchecked(&mut self) -> RocStr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocStr) } + } + + /// Move the `Other` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `TlsErrTag::Other`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn take_payload_other_unchecked(&mut self) -> RocStr { + unsafe { core::mem::ManuallyDrop::take(&mut self.payload.other) } + } + } #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 32, "BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout size mismatch"); +const _: () = assert!(core::mem::size_of::() == 32, "TlsErr size mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::align_of::() == 8, "BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "TlsErr alignment mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::offset_of!(BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout, tag) == 24, "BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout tag offset mismatch"); +const _: () = assert!(core::mem::offset_of!(TlsErr, tag) == 24, "TlsErr tag offset mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 16, "BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout size mismatch"); +const _: () = assert!(core::mem::size_of::() == 16, "TlsErr size mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::align_of::() == 4, "BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 4, "TlsErr alignment mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::offset_of!(BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout, tag) == 12, "BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout tag offset mismatch"); +const _: () = assert!(core::mem::offset_of!(TlsErr, tag) == 12, "TlsErr tag offset mismatch"); /// Tag discriminant for Try. #[repr(u8)] @@ -4373,7 +4544,7 @@ pub enum HostSqliteOpenResultTag { #[repr(C)] #[derive(Clone, Copy)] pub union HostSqliteOpenResultPayload { - pub err: core::mem::ManuallyDrop, + pub err: core::mem::ManuallyDrop, pub ok: core::mem::ManuallyDrop<*mut u64>, } @@ -4407,8 +4578,8 @@ impl HostSqliteOpenResult { /// # Safety /// `self.tag` must be `HostSqliteOpenResultTag::Err` and the payload must still be initialized. #[cfg(target_pointer_width = "32")] - pub unsafe fn borrow_payload_err_unchecked(&self) -> &FailedOrInterruptedType115 { - unsafe { &*(self.payload.as_ptr() as *const FailedOrInterruptedType115) } + pub unsafe fn borrow_payload_err_unchecked(&self) -> &FailedOrInterruptedType117 { + unsafe { &*(self.payload.as_ptr() as *const FailedOrInterruptedType117) } } /// Borrow the `Err` payload without creating another owner. @@ -4416,8 +4587,8 @@ impl HostSqliteOpenResult { /// # Safety /// `self.tag` must be `HostSqliteOpenResultTag::Err` and the payload must still be initialized. #[cfg(not(target_pointer_width = "32"))] - pub unsafe fn borrow_payload_err_unchecked(&self) -> &FailedOrInterruptedType115 { - unsafe { &*(&self.payload.err as *const core::mem::ManuallyDrop as *const FailedOrInterruptedType115) } + pub unsafe fn borrow_payload_err_unchecked(&self) -> &FailedOrInterruptedType117 { + unsafe { &*(&self.payload.err as *const core::mem::ManuallyDrop as *const FailedOrInterruptedType117) } } /// Move the `Err` payload out of one owned tag-union shell. @@ -4425,8 +4596,8 @@ impl HostSqliteOpenResult { /// # Safety /// `self.tag` must be `HostSqliteOpenResultTag::Err`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(target_pointer_width = "32")] - pub unsafe fn take_payload_err_unchecked(&mut self) -> FailedOrInterruptedType115 { - unsafe { core::ptr::read(self.payload.as_ptr() as *const FailedOrInterruptedType115) } + pub unsafe fn take_payload_err_unchecked(&mut self) -> FailedOrInterruptedType117 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const FailedOrInterruptedType117) } } /// Move the `Err` payload out of one owned tag-union shell. @@ -4434,7 +4605,7 @@ impl HostSqliteOpenResult { /// # Safety /// `self.tag` must be `HostSqliteOpenResultTag::Err`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(not(target_pointer_width = "32"))] - pub unsafe fn take_payload_err_unchecked(&mut self) -> FailedOrInterruptedType115 { + pub unsafe fn take_payload_err_unchecked(&mut self) -> FailedOrInterruptedType117 { unsafe { core::mem::ManuallyDrop::take(&mut self.payload.err) } } @@ -4492,14 +4663,14 @@ const _: () = assert!(core::mem::offset_of!(HostSqliteOpenResult, tag) == 32, "H /// Tag discriminant for FailedOrInterrupted. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FailedOrInterruptedType115Tag { +pub enum FailedOrInterruptedType117Tag { Failed = 0, Interrupted = 1, } #[repr(C)] #[derive(Clone, Copy)] -pub union FailedOrInterruptedType115Payload { +pub union FailedOrInterruptedType117Payload { pub failed: core::mem::ManuallyDrop, pub interrupted: [u8; 0], } @@ -4507,32 +4678,32 @@ pub union FailedOrInterruptedType115Payload { #[cfg(target_pointer_width = "32")] #[repr(align(8))] #[derive(Clone, Copy)] -pub struct FailedOrInterruptedType115PayloadAlignment; +pub struct FailedOrInterruptedType117PayloadAlignment; /// Tag union: FailedOrInterrupted #[cfg(target_pointer_width = "32")] #[repr(C)] #[derive(Clone, Copy)] -pub struct FailedOrInterruptedType115 { - pub _payload_alignment: [FailedOrInterruptedType115PayloadAlignment; 0], +pub struct FailedOrInterruptedType117 { + pub _payload_alignment: [FailedOrInterruptedType117PayloadAlignment; 0], pub payload: [u8; 24], - pub tag: FailedOrInterruptedType115Tag, + pub tag: FailedOrInterruptedType117Tag, } /// Tag union: FailedOrInterrupted #[cfg(not(target_pointer_width = "32"))] #[repr(C)] #[derive(Clone, Copy)] -pub struct FailedOrInterruptedType115 { - pub payload: FailedOrInterruptedType115Payload, - pub tag: FailedOrInterruptedType115Tag, +pub struct FailedOrInterruptedType117 { + pub payload: FailedOrInterruptedType117Payload, + pub tag: FailedOrInterruptedType117Tag, } -impl FailedOrInterruptedType115 { +impl FailedOrInterruptedType117 { /// Borrow the `Failed` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `FailedOrInterruptedType115Tag::Failed` and the payload must still be initialized. + /// `self.tag` must be `FailedOrInterruptedType117Tag::Failed` and the payload must still be initialized. #[cfg(target_pointer_width = "32")] pub unsafe fn borrow_payload_failed_unchecked(&self) -> &AnonStruct8dda646697632dbf { unsafe { &*(self.payload.as_ptr() as *const AnonStruct8dda646697632dbf) } @@ -4541,7 +4712,7 @@ impl FailedOrInterruptedType115 { /// Borrow the `Failed` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `FailedOrInterruptedType115Tag::Failed` and the payload must still be initialized. + /// `self.tag` must be `FailedOrInterruptedType117Tag::Failed` and the payload must still be initialized. #[cfg(not(target_pointer_width = "32"))] pub unsafe fn borrow_payload_failed_unchecked(&self) -> &AnonStruct8dda646697632dbf { unsafe { &*(&self.payload.failed as *const core::mem::ManuallyDrop as *const AnonStruct8dda646697632dbf) } @@ -4550,7 +4721,7 @@ impl FailedOrInterruptedType115 { /// Move the `Failed` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `FailedOrInterruptedType115Tag::Failed`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `FailedOrInterruptedType117Tag::Failed`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(target_pointer_width = "32")] pub unsafe fn take_payload_failed_unchecked(&mut self) -> AnonStruct8dda646697632dbf { unsafe { core::ptr::read(self.payload.as_ptr() as *const AnonStruct8dda646697632dbf) } @@ -4559,7 +4730,7 @@ impl FailedOrInterruptedType115 { /// Move the `Failed` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `FailedOrInterruptedType115Tag::Failed`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `FailedOrInterruptedType117Tag::Failed`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(not(target_pointer_width = "32"))] pub unsafe fn take_payload_failed_unchecked(&mut self) -> AnonStruct8dda646697632dbf { unsafe { core::mem::ManuallyDrop::take(&mut self.payload.failed) } @@ -4568,17 +4739,17 @@ impl FailedOrInterruptedType115 { } #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 40, "FailedOrInterruptedType115 size mismatch"); +const _: () = assert!(core::mem::size_of::() == 40, "FailedOrInterruptedType117 size mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::align_of::() == 8, "FailedOrInterruptedType115 alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "FailedOrInterruptedType117 alignment mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::offset_of!(FailedOrInterruptedType115, tag) == 32, "FailedOrInterruptedType115 tag offset mismatch"); +const _: () = assert!(core::mem::offset_of!(FailedOrInterruptedType117, tag) == 32, "FailedOrInterruptedType117 tag offset mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 32, "FailedOrInterruptedType115 size mismatch"); +const _: () = assert!(core::mem::size_of::() == 32, "FailedOrInterruptedType117 size mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::align_of::() == 8, "FailedOrInterruptedType115 alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "FailedOrInterruptedType117 alignment mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::offset_of!(FailedOrInterruptedType115, tag) == 24, "FailedOrInterruptedType115 tag offset mismatch"); +const _: () = assert!(core::mem::offset_of!(FailedOrInterruptedType117, tag) == 24, "FailedOrInterruptedType117 tag offset mismatch"); /// Tag union: ReadOnlyOrReadWriteOrReadWriteCreate #[repr(u8)] @@ -11544,7 +11715,7 @@ pub enum HostHttpSendStreamingResultTag { #[repr(C)] #[derive(Clone, Copy)] pub union HostHttpSendStreamingResultPayload { - pub err: core::mem::ManuallyDrop, + pub err: core::mem::ManuallyDrop, pub ok: core::mem::ManuallyDrop, } @@ -11578,8 +11749,8 @@ impl HostHttpSendStreamingResult { /// # Safety /// `self.tag` must be `HostHttpSendStreamingResultTag::Err` and the payload must still be initialized. #[cfg(target_pointer_width = "32")] - pub unsafe fn borrow_payload_err_unchecked(&self) -> &BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { - unsafe { &*(self.payload.as_ptr() as *const BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout) } + pub unsafe fn borrow_payload_err_unchecked(&self) -> &BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr { + unsafe { &*(self.payload.as_ptr() as *const BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr) } } /// Borrow the `Err` payload without creating another owner. @@ -11587,8 +11758,8 @@ impl HostHttpSendStreamingResult { /// # Safety /// `self.tag` must be `HostHttpSendStreamingResultTag::Err` and the payload must still be initialized. #[cfg(not(target_pointer_width = "32"))] - pub unsafe fn borrow_payload_err_unchecked(&self) -> &BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { - unsafe { &*(&self.payload.err as *const core::mem::ManuallyDrop as *const BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout) } + pub unsafe fn borrow_payload_err_unchecked(&self) -> &BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr { + unsafe { &*(&self.payload.err as *const core::mem::ManuallyDrop as *const BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr) } } /// Move the `Err` payload out of one owned tag-union shell. @@ -11596,8 +11767,8 @@ impl HostHttpSendStreamingResult { /// # Safety /// `self.tag` must be `HostHttpSendStreamingResultTag::Err`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(target_pointer_width = "32")] - pub unsafe fn take_payload_err_unchecked(&mut self) -> BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { - unsafe { core::ptr::read(self.payload.as_ptr() as *const BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout) } + pub unsafe fn take_payload_err_unchecked(&mut self) -> BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr) } } /// Move the `Err` payload out of one owned tag-union shell. @@ -11605,7 +11776,7 @@ impl HostHttpSendStreamingResult { /// # Safety /// `self.tag` must be `HostHttpSendStreamingResultTag::Err`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(not(target_pointer_width = "32"))] - pub unsafe fn take_payload_err_unchecked(&mut self) -> BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { + pub unsafe fn take_payload_err_unchecked(&mut self) -> BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr { unsafe { core::mem::ManuallyDrop::take(&mut self.payload.err) } } @@ -11933,6 +12104,99 @@ const _: () = assert!(core::mem::align_of::() == 8, "AtOrNoTimeou #[cfg(target_pointer_width = "32")] const _: () = assert!(core::mem::offset_of!(AtOrNoTimeout, tag) == 8, "AtOrNoTimeout tag offset mismatch"); +/// Tag discriminant for AddPemOrDangerAcceptInvalidCertsOrDefault. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AddPemOrDangerAcceptInvalidCertsOrDefaultTag { + AddPem = 0, + DangerAcceptInvalidCerts = 1, + Default = 2, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union AddPemOrDangerAcceptInvalidCertsOrDefaultPayload { + pub add_pem: core::mem::ManuallyDrop>, + pub danger_accept_invalid_certs: [u8; 0], + pub default: [u8; 0], +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct AddPemOrDangerAcceptInvalidCertsOrDefaultPayloadAlignment; + +/// Tag union: AddPemOrDangerAcceptInvalidCertsOrDefault +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AddPemOrDangerAcceptInvalidCertsOrDefault { + pub _payload_alignment: [AddPemOrDangerAcceptInvalidCertsOrDefaultPayloadAlignment; 0], + pub payload: [u8; 12], + pub tag: AddPemOrDangerAcceptInvalidCertsOrDefaultTag, +} + +/// Tag union: AddPemOrDangerAcceptInvalidCertsOrDefault +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AddPemOrDangerAcceptInvalidCertsOrDefault { + pub payload: AddPemOrDangerAcceptInvalidCertsOrDefaultPayload, + pub tag: AddPemOrDangerAcceptInvalidCertsOrDefaultTag, +} + +impl AddPemOrDangerAcceptInvalidCertsOrDefault { + /// Borrow the `AddPem` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `AddPemOrDangerAcceptInvalidCertsOrDefaultTag::AddPem` and the payload must still be initialized. + #[cfg(target_pointer_width = "32")] + pub unsafe fn borrow_payload_add_pem_unchecked(&self) -> &RocListWith { + unsafe { &*(self.payload.as_ptr() as *const RocListWith) } + } + + /// Borrow the `AddPem` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `AddPemOrDangerAcceptInvalidCertsOrDefaultTag::AddPem` and the payload must still be initialized. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn borrow_payload_add_pem_unchecked(&self) -> &RocListWith { + unsafe { &*(&self.payload.add_pem as *const core::mem::ManuallyDrop> as *const RocListWith) } + } + + /// Move the `AddPem` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `AddPemOrDangerAcceptInvalidCertsOrDefaultTag::AddPem`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(target_pointer_width = "32")] + pub unsafe fn take_payload_add_pem_unchecked(&mut self) -> RocListWith { + unsafe { core::ptr::read(self.payload.as_ptr() as *const RocListWith) } + } + + /// Move the `AddPem` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `AddPemOrDangerAcceptInvalidCertsOrDefaultTag::AddPem`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn take_payload_add_pem_unchecked(&mut self) -> RocListWith { + unsafe { core::mem::ManuallyDrop::take(&mut self.payload.add_pem) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 32, "AddPemOrDangerAcceptInvalidCertsOrDefault size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "AddPemOrDangerAcceptInvalidCertsOrDefault alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(AddPemOrDangerAcceptInvalidCertsOrDefault, tag) == 24, "AddPemOrDangerAcceptInvalidCertsOrDefault tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 16, "AddPemOrDangerAcceptInvalidCertsOrDefault size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "AddPemOrDangerAcceptInvalidCertsOrDefault alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(AddPemOrDangerAcceptInvalidCertsOrDefault, tag) == 12, "AddPemOrDangerAcceptInvalidCertsOrDefault tag offset mismatch"); + /// Tag discriminant for Try. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -11959,7 +12223,7 @@ pub struct HostHttpReadBodyResultPayloadAlignment; #[derive(Clone, Copy)] pub struct HostHttpReadBodyResult { pub _payload_alignment: [HostHttpReadBodyResultPayloadAlignment; 0], - pub payload: [u8; 20], + pub payload: [u8; 24], pub tag: HostHttpReadBodyResultTag, } @@ -12048,17 +12312,17 @@ impl HostHttpReadBodyResult { } #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 48, "HostHttpReadBodyResult size mismatch"); +const _: () = assert!(core::mem::size_of::() == 56, "HostHttpReadBodyResult size mismatch"); #[cfg(target_pointer_width = "64")] const _: () = assert!(core::mem::align_of::() == 8, "HostHttpReadBodyResult alignment mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::offset_of!(HostHttpReadBodyResult, tag) == 40, "HostHttpReadBodyResult tag offset mismatch"); +const _: () = assert!(core::mem::offset_of!(HostHttpReadBodyResult, tag) == 48, "HostHttpReadBodyResult tag offset mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 24, "HostHttpReadBodyResult size mismatch"); +const _: () = assert!(core::mem::size_of::() == 28, "HostHttpReadBodyResult size mismatch"); #[cfg(target_pointer_width = "32")] const _: () = assert!(core::mem::align_of::() == 4, "HostHttpReadBodyResult alignment mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::offset_of!(HostHttpReadBodyResult, tag) == 20, "HostHttpReadBodyResult tag offset mismatch"); +const _: () = assert!(core::mem::offset_of!(HostHttpReadBodyResult, tag) == 24, "HostHttpReadBodyResult tag offset mismatch"); /// Tag discriminant for EndOfBodyOrHttpErrOrReadLimitExceeded. #[repr(u8)] @@ -12073,7 +12337,7 @@ pub enum EndOfBodyOrHttpErrOrReadLimitExceededTag { #[derive(Clone, Copy)] pub union EndOfBodyOrHttpErrOrReadLimitExceededPayload { pub end_of_body: [u8; 0], - pub http_err: core::mem::ManuallyDrop, + pub http_err: core::mem::ManuallyDrop, pub read_limit_exceeded: [u8; 0], } @@ -12088,7 +12352,7 @@ pub struct EndOfBodyOrHttpErrOrReadLimitExceededPayloadAlignment; #[derive(Clone, Copy)] pub struct EndOfBodyOrHttpErrOrReadLimitExceeded { pub _payload_alignment: [EndOfBodyOrHttpErrOrReadLimitExceededPayloadAlignment; 0], - pub payload: [u8; 16], + pub payload: [u8; 20], pub tag: EndOfBodyOrHttpErrOrReadLimitExceededTag, } @@ -12107,8 +12371,8 @@ impl EndOfBodyOrHttpErrOrReadLimitExceeded { /// # Safety /// `self.tag` must be `EndOfBodyOrHttpErrOrReadLimitExceededTag::HttpErr` and the payload must still be initialized. #[cfg(target_pointer_width = "32")] - pub unsafe fn borrow_payload_http_err_unchecked(&self) -> &BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { - unsafe { &*(self.payload.as_ptr() as *const BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout) } + pub unsafe fn borrow_payload_http_err_unchecked(&self) -> &BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr { + unsafe { &*(self.payload.as_ptr() as *const BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr) } } /// Borrow the `HttpErr` payload without creating another owner. @@ -12116,8 +12380,8 @@ impl EndOfBodyOrHttpErrOrReadLimitExceeded { /// # Safety /// `self.tag` must be `EndOfBodyOrHttpErrOrReadLimitExceededTag::HttpErr` and the payload must still be initialized. #[cfg(not(target_pointer_width = "32"))] - pub unsafe fn borrow_payload_http_err_unchecked(&self) -> &BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { - unsafe { &*(&self.payload.http_err as *const core::mem::ManuallyDrop as *const BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout) } + pub unsafe fn borrow_payload_http_err_unchecked(&self) -> &BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr { + unsafe { &*(&self.payload.http_err as *const core::mem::ManuallyDrop as *const BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr) } } /// Move the `HttpErr` payload out of one owned tag-union shell. @@ -12125,8 +12389,8 @@ impl EndOfBodyOrHttpErrOrReadLimitExceeded { /// # Safety /// `self.tag` must be `EndOfBodyOrHttpErrOrReadLimitExceededTag::HttpErr`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(target_pointer_width = "32")] - pub unsafe fn take_payload_http_err_unchecked(&mut self) -> BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { - unsafe { core::ptr::read(self.payload.as_ptr() as *const BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout) } + pub unsafe fn take_payload_http_err_unchecked(&mut self) -> BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr) } } /// Move the `HttpErr` payload out of one owned tag-union shell. @@ -12134,24 +12398,24 @@ impl EndOfBodyOrHttpErrOrReadLimitExceeded { /// # Safety /// `self.tag` must be `EndOfBodyOrHttpErrOrReadLimitExceededTag::HttpErr`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(not(target_pointer_width = "32"))] - pub unsafe fn take_payload_http_err_unchecked(&mut self) -> BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { + pub unsafe fn take_payload_http_err_unchecked(&mut self) -> BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr { unsafe { core::mem::ManuallyDrop::take(&mut self.payload.http_err) } } } #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 40, "EndOfBodyOrHttpErrOrReadLimitExceeded size mismatch"); +const _: () = assert!(core::mem::size_of::() == 48, "EndOfBodyOrHttpErrOrReadLimitExceeded size mismatch"); #[cfg(target_pointer_width = "64")] const _: () = assert!(core::mem::align_of::() == 8, "EndOfBodyOrHttpErrOrReadLimitExceeded alignment mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::offset_of!(EndOfBodyOrHttpErrOrReadLimitExceeded, tag) == 32, "EndOfBodyOrHttpErrOrReadLimitExceeded tag offset mismatch"); +const _: () = assert!(core::mem::offset_of!(EndOfBodyOrHttpErrOrReadLimitExceeded, tag) == 40, "EndOfBodyOrHttpErrOrReadLimitExceeded tag offset mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 20, "EndOfBodyOrHttpErrOrReadLimitExceeded size mismatch"); +const _: () = assert!(core::mem::size_of::() == 24, "EndOfBodyOrHttpErrOrReadLimitExceeded size mismatch"); #[cfg(target_pointer_width = "32")] const _: () = assert!(core::mem::align_of::() == 4, "EndOfBodyOrHttpErrOrReadLimitExceeded alignment mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::offset_of!(EndOfBodyOrHttpErrOrReadLimitExceeded, tag) == 16, "EndOfBodyOrHttpErrOrReadLimitExceeded tag offset mismatch"); +const _: () = assert!(core::mem::offset_of!(EndOfBodyOrHttpErrOrReadLimitExceeded, tag) == 20, "EndOfBodyOrHttpErrOrReadLimitExceeded tag offset mismatch"); /// Tag discriminant for ChunkOrToEnd. #[repr(u8)] @@ -12338,7 +12602,7 @@ const _: () = assert!(core::mem::offset_of!(HostSignalCatchResult, tag) == 16, " /// Tag discriminant for InterruptOrTerminateOrUser1OrUser2OrWindowResize. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag { +pub enum InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag { Interrupt = 0, Terminate = 1, User1 = 2, @@ -12348,7 +12612,7 @@ pub enum InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag { #[repr(C)] #[derive(Clone, Copy)] -pub union InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Payload { +pub union InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Payload { pub interrupt: [u8; 0], pub terminate: core::mem::ManuallyDrop, pub user1: core::mem::ManuallyDrop, @@ -12359,32 +12623,32 @@ pub union InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Payload { #[cfg(target_pointer_width = "32")] #[repr(align(1))] #[derive(Clone, Copy)] -pub struct InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380PayloadAlignment; +pub struct InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385PayloadAlignment; /// Tag union: InterruptOrTerminateOrUser1OrUser2OrWindowResize #[cfg(target_pointer_width = "32")] #[repr(C)] #[derive(Clone, Copy)] -pub struct InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { - pub _payload_alignment: [InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380PayloadAlignment; 0], +pub struct InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385 { + pub _payload_alignment: [InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385PayloadAlignment; 0], pub payload: [u8; 1], - pub tag: InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag, + pub tag: InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag, } /// Tag union: InterruptOrTerminateOrUser1OrUser2OrWindowResize #[cfg(not(target_pointer_width = "32"))] #[repr(C)] #[derive(Clone, Copy)] -pub struct InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { - pub payload: InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Payload, - pub tag: InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag, +pub struct InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385 { + pub payload: InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Payload, + pub tag: InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag, } -impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { +impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385 { /// Borrow the `Terminate` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::Terminate` and the payload must still be initialized. + /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::Terminate` and the payload must still be initialized. #[cfg(target_pointer_width = "32")] pub unsafe fn borrow_payload_terminate_unchecked(&self) -> &SystemUnix { unsafe { &*(self.payload.as_ptr() as *const SystemUnix) } @@ -12393,7 +12657,7 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { /// Borrow the `Terminate` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::Terminate` and the payload must still be initialized. + /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::Terminate` and the payload must still be initialized. #[cfg(not(target_pointer_width = "32"))] pub unsafe fn borrow_payload_terminate_unchecked(&self) -> &SystemUnix { unsafe { &*(&self.payload.terminate as *const core::mem::ManuallyDrop as *const SystemUnix) } @@ -12402,7 +12666,7 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { /// Move the `Terminate` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::Terminate`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::Terminate`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(target_pointer_width = "32")] pub unsafe fn take_payload_terminate_unchecked(&mut self) -> SystemUnix { unsafe { core::ptr::read(self.payload.as_ptr() as *const SystemUnix) } @@ -12411,7 +12675,7 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { /// Move the `Terminate` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::Terminate`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::Terminate`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(not(target_pointer_width = "32"))] pub unsafe fn take_payload_terminate_unchecked(&mut self) -> SystemUnix { unsafe { core::mem::ManuallyDrop::take(&mut self.payload.terminate) } @@ -12420,7 +12684,7 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { /// Borrow the `User1` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::User1` and the payload must still be initialized. + /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::User1` and the payload must still be initialized. #[cfg(target_pointer_width = "32")] pub unsafe fn borrow_payload_user1_unchecked(&self) -> &SystemUnix { unsafe { &*(self.payload.as_ptr() as *const SystemUnix) } @@ -12429,7 +12693,7 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { /// Borrow the `User1` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::User1` and the payload must still be initialized. + /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::User1` and the payload must still be initialized. #[cfg(not(target_pointer_width = "32"))] pub unsafe fn borrow_payload_user1_unchecked(&self) -> &SystemUnix { unsafe { &*(&self.payload.user1 as *const core::mem::ManuallyDrop as *const SystemUnix) } @@ -12438,7 +12702,7 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { /// Move the `User1` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::User1`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::User1`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(target_pointer_width = "32")] pub unsafe fn take_payload_user1_unchecked(&mut self) -> SystemUnix { unsafe { core::ptr::read(self.payload.as_ptr() as *const SystemUnix) } @@ -12447,7 +12711,7 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { /// Move the `User1` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::User1`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::User1`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(not(target_pointer_width = "32"))] pub unsafe fn take_payload_user1_unchecked(&mut self) -> SystemUnix { unsafe { core::mem::ManuallyDrop::take(&mut self.payload.user1) } @@ -12456,7 +12720,7 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { /// Borrow the `User2` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::User2` and the payload must still be initialized. + /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::User2` and the payload must still be initialized. #[cfg(target_pointer_width = "32")] pub unsafe fn borrow_payload_user2_unchecked(&self) -> &SystemUnix { unsafe { &*(self.payload.as_ptr() as *const SystemUnix) } @@ -12465,7 +12729,7 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { /// Borrow the `User2` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::User2` and the payload must still be initialized. + /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::User2` and the payload must still be initialized. #[cfg(not(target_pointer_width = "32"))] pub unsafe fn borrow_payload_user2_unchecked(&self) -> &SystemUnix { unsafe { &*(&self.payload.user2 as *const core::mem::ManuallyDrop as *const SystemUnix) } @@ -12474,7 +12738,7 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { /// Move the `User2` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::User2`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::User2`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(target_pointer_width = "32")] pub unsafe fn take_payload_user2_unchecked(&mut self) -> SystemUnix { unsafe { core::ptr::read(self.payload.as_ptr() as *const SystemUnix) } @@ -12483,7 +12747,7 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { /// Move the `User2` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::User2`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::User2`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(not(target_pointer_width = "32"))] pub unsafe fn take_payload_user2_unchecked(&mut self) -> SystemUnix { unsafe { core::mem::ManuallyDrop::take(&mut self.payload.user2) } @@ -12492,7 +12756,7 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { /// Borrow the `WindowResize` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::WindowResize` and the payload must still be initialized. + /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::WindowResize` and the payload must still be initialized. #[cfg(target_pointer_width = "32")] pub unsafe fn borrow_payload_window_resize_unchecked(&self) -> &SystemUnix { unsafe { &*(self.payload.as_ptr() as *const SystemUnix) } @@ -12501,7 +12765,7 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { /// Borrow the `WindowResize` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::WindowResize` and the payload must still be initialized. + /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::WindowResize` and the payload must still be initialized. #[cfg(not(target_pointer_width = "32"))] pub unsafe fn borrow_payload_window_resize_unchecked(&self) -> &SystemUnix { unsafe { &*(&self.payload.window_resize as *const core::mem::ManuallyDrop as *const SystemUnix) } @@ -12510,7 +12774,7 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { /// Move the `WindowResize` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::WindowResize`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::WindowResize`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(target_pointer_width = "32")] pub unsafe fn take_payload_window_resize_unchecked(&mut self) -> SystemUnix { unsafe { core::ptr::read(self.payload.as_ptr() as *const SystemUnix) } @@ -12519,7 +12783,7 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { /// Move the `WindowResize` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::WindowResize`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::WindowResize`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(not(target_pointer_width = "32"))] pub unsafe fn take_payload_window_resize_unchecked(&mut self) -> SystemUnix { unsafe { core::mem::ManuallyDrop::take(&mut self.payload.window_resize) } @@ -12528,22 +12792,22 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { } #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 2, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 size mismatch"); +const _: () = assert!(core::mem::size_of::() == 2, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385 size mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::align_of::() == 1, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 1, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385 alignment mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::offset_of!(InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380, tag) == 1, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 tag offset mismatch"); +const _: () = assert!(core::mem::offset_of!(InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385, tag) == 1, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385 tag offset mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 2, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 size mismatch"); +const _: () = assert!(core::mem::size_of::() == 2, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385 size mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::align_of::() == 1, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 1, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385 alignment mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::offset_of!(InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380, tag) == 1, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 tag offset mismatch"); +const _: () = assert!(core::mem::offset_of!(InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385, tag) == 1, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385 tag offset mismatch"); /// Tag union: InterruptOrTerminateOrUser1OrUser2OrWindowResize #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum InterruptOrTerminateOrUser1OrUser2OrWindowResizeType384 { +pub enum InterruptOrTerminateOrUser1OrUser2OrWindowResizeType389 { Interrupt = 0, Terminate = 1, User1 = 2, @@ -12552,25 +12816,25 @@ pub enum InterruptOrTerminateOrUser1OrUser2OrWindowResizeType384 { } #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 1, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType384 size mismatch"); +const _: () = assert!(core::mem::size_of::() == 1, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType389 size mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::align_of::() == 1, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType384 alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 1, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType389 alignment mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 1, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType384 size mismatch"); +const _: () = assert!(core::mem::size_of::() == 1, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType389 size mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::align_of::() == 1, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType384 alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 1, "InterruptOrTerminateOrUser1OrUser2OrWindowResizeType389 alignment mismatch"); /// Tag discriminant for FailedOrInterrupted. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FailedOrInterruptedType386Tag { +pub enum FailedOrInterruptedType391Tag { Failed = 0, Interrupted = 1, } #[repr(C)] #[derive(Clone, Copy)] -pub union FailedOrInterruptedType386Payload { +pub union FailedOrInterruptedType391Payload { pub failed: core::mem::ManuallyDrop, pub interrupted: [u8; 0], } @@ -12578,32 +12842,32 @@ pub union FailedOrInterruptedType386Payload { #[cfg(target_pointer_width = "32")] #[repr(align(4))] #[derive(Clone, Copy)] -pub struct FailedOrInterruptedType386PayloadAlignment; +pub struct FailedOrInterruptedType391PayloadAlignment; /// Tag union: FailedOrInterrupted #[cfg(target_pointer_width = "32")] #[repr(C)] #[derive(Clone, Copy)] -pub struct FailedOrInterruptedType386 { - pub _payload_alignment: [FailedOrInterruptedType386PayloadAlignment; 0], +pub struct FailedOrInterruptedType391 { + pub _payload_alignment: [FailedOrInterruptedType391PayloadAlignment; 0], pub payload: [u8; 16], - pub tag: FailedOrInterruptedType386Tag, + pub tag: FailedOrInterruptedType391Tag, } /// Tag union: FailedOrInterrupted #[cfg(not(target_pointer_width = "32"))] #[repr(C)] #[derive(Clone, Copy)] -pub struct FailedOrInterruptedType386 { - pub payload: FailedOrInterruptedType386Payload, - pub tag: FailedOrInterruptedType386Tag, +pub struct FailedOrInterruptedType391 { + pub payload: FailedOrInterruptedType391Payload, + pub tag: FailedOrInterruptedType391Tag, } -impl FailedOrInterruptedType386 { +impl FailedOrInterruptedType391 { /// Borrow the `Failed` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `FailedOrInterruptedType386Tag::Failed` and the payload must still be initialized. + /// `self.tag` must be `FailedOrInterruptedType391Tag::Failed` and the payload must still be initialized. #[cfg(target_pointer_width = "32")] pub unsafe fn borrow_payload_failed_unchecked(&self) -> &IOErr { unsafe { &*(self.payload.as_ptr() as *const IOErr) } @@ -12612,7 +12876,7 @@ impl FailedOrInterruptedType386 { /// Borrow the `Failed` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `FailedOrInterruptedType386Tag::Failed` and the payload must still be initialized. + /// `self.tag` must be `FailedOrInterruptedType391Tag::Failed` and the payload must still be initialized. #[cfg(not(target_pointer_width = "32"))] pub unsafe fn borrow_payload_failed_unchecked(&self) -> &IOErr { unsafe { &*(&self.payload.failed as *const core::mem::ManuallyDrop as *const IOErr) } @@ -12621,7 +12885,7 @@ impl FailedOrInterruptedType386 { /// Move the `Failed` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `FailedOrInterruptedType386Tag::Failed`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `FailedOrInterruptedType391Tag::Failed`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(target_pointer_width = "32")] pub unsafe fn take_payload_failed_unchecked(&mut self) -> IOErr { unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } @@ -12630,7 +12894,7 @@ impl FailedOrInterruptedType386 { /// Move the `Failed` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `FailedOrInterruptedType386Tag::Failed`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `FailedOrInterruptedType391Tag::Failed`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(not(target_pointer_width = "32"))] pub unsafe fn take_payload_failed_unchecked(&mut self) -> IOErr { unsafe { core::mem::ManuallyDrop::take(&mut self.payload.failed) } @@ -12639,17 +12903,17 @@ impl FailedOrInterruptedType386 { } #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 40, "FailedOrInterruptedType386 size mismatch"); +const _: () = assert!(core::mem::size_of::() == 40, "FailedOrInterruptedType391 size mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::align_of::() == 8, "FailedOrInterruptedType386 alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "FailedOrInterruptedType391 alignment mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::offset_of!(FailedOrInterruptedType386, tag) == 32, "FailedOrInterruptedType386 tag offset mismatch"); +const _: () = assert!(core::mem::offset_of!(FailedOrInterruptedType391, tag) == 32, "FailedOrInterruptedType391 tag offset mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 20, "FailedOrInterruptedType386 size mismatch"); +const _: () = assert!(core::mem::size_of::() == 20, "FailedOrInterruptedType391 size mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::align_of::() == 4, "FailedOrInterruptedType386 alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 4, "FailedOrInterruptedType391 alignment mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::offset_of!(FailedOrInterruptedType386, tag) == 16, "FailedOrInterruptedType386 tag offset mismatch"); +const _: () = assert!(core::mem::offset_of!(FailedOrInterruptedType391, tag) == 16, "FailedOrInterruptedType391 tag offset mismatch"); /// Tag union: OwnerOnlyOrPublicRead #[repr(u8)] @@ -13940,126 +14204,380 @@ const _: () = assert!(core::mem::align_of::() == 4, "Clos #[cfg(target_pointer_width = "32")] const _: () = assert!(core::mem::offset_of!(ClosedOrUnixSocketErr, tag) == 16, "ClosedOrUnixSocketErr tag offset mismatch"); -/// Tag union: Arm64OrX64 -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Arm64OrX64 { - Arm64 = 0, - X64 = 1, -} - -#[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 1, "Arm64OrX64 size mismatch"); -#[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::align_of::() == 1, "Arm64OrX64 alignment mismatch"); -#[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 1, "Arm64OrX64 size mismatch"); -#[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::align_of::() == 1, "Arm64OrX64 alignment mismatch"); - -/// Tag discriminant for LinuxOrMacOSOrWindows. +/// Tag discriminant for Try. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum LinuxOrMacOSOrWindowsTag { - Linux = 0, - MacOS = 1, - Windows = 2, +pub enum HostTcpConnectTlsResultTag { + Err = 0, + Ok = 1, } #[repr(C)] #[derive(Clone, Copy)] -pub union LinuxOrMacOSOrWindowsPayload { - pub linux: core::mem::ManuallyDrop, - pub mac_os: core::mem::ManuallyDrop, - pub windows: core::mem::ManuallyDrop, +pub union HostTcpConnectTlsResultPayload { + pub err: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop<*mut u64>, } #[cfg(target_pointer_width = "32")] -#[repr(align(1))] +#[repr(align(4))] #[derive(Clone, Copy)] -pub struct LinuxOrMacOSOrWindowsPayloadAlignment; +pub struct HostTcpConnectTlsResultPayloadAlignment; -/// Tag union: LinuxOrMacOSOrWindows +/// Tag union: Try #[cfg(target_pointer_width = "32")] #[repr(C)] #[derive(Clone, Copy)] -pub struct LinuxOrMacOSOrWindows { - pub _payload_alignment: [LinuxOrMacOSOrWindowsPayloadAlignment; 0], - pub payload: [u8; 1], - pub tag: LinuxOrMacOSOrWindowsTag, +pub struct HostTcpConnectTlsResult { + pub _payload_alignment: [HostTcpConnectTlsResultPayloadAlignment; 0], + pub payload: [u8; 20], + pub tag: HostTcpConnectTlsResultTag, } -/// Tag union: LinuxOrMacOSOrWindows +/// Tag union: Try #[cfg(not(target_pointer_width = "32"))] #[repr(C)] #[derive(Clone, Copy)] -pub struct LinuxOrMacOSOrWindows { - pub payload: LinuxOrMacOSOrWindowsPayload, - pub tag: LinuxOrMacOSOrWindowsTag, +pub struct HostTcpConnectTlsResult { + pub payload: HostTcpConnectTlsResultPayload, + pub tag: HostTcpConnectTlsResultTag, } -impl LinuxOrMacOSOrWindows { - /// Borrow the `Linux` payload without creating another owner. +impl HostTcpConnectTlsResult { + /// Borrow the `Err` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `LinuxOrMacOSOrWindowsTag::Linux` and the payload must still be initialized. + /// `self.tag` must be `HostTcpConnectTlsResultTag::Err` and the payload must still be initialized. #[cfg(target_pointer_width = "32")] - pub unsafe fn borrow_payload_linux_unchecked(&self) -> &SystemLinux { - unsafe { &*(self.payload.as_ptr() as *const SystemLinux) } + pub unsafe fn borrow_payload_err_unchecked(&self) -> &TcpErrOrTlsErr { + unsafe { &*(self.payload.as_ptr() as *const TcpErrOrTlsErr) } } - /// Borrow the `Linux` payload without creating another owner. + /// Borrow the `Err` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `LinuxOrMacOSOrWindowsTag::Linux` and the payload must still be initialized. + /// `self.tag` must be `HostTcpConnectTlsResultTag::Err` and the payload must still be initialized. #[cfg(not(target_pointer_width = "32"))] - pub unsafe fn borrow_payload_linux_unchecked(&self) -> &SystemLinux { - unsafe { &*(&self.payload.linux as *const core::mem::ManuallyDrop as *const SystemLinux) } + pub unsafe fn borrow_payload_err_unchecked(&self) -> &TcpErrOrTlsErr { + unsafe { &*(&self.payload.err as *const core::mem::ManuallyDrop as *const TcpErrOrTlsErr) } } - /// Move the `Linux` payload out of one owned tag-union shell. + /// Move the `Err` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `LinuxOrMacOSOrWindowsTag::Linux`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `HostTcpConnectTlsResultTag::Err`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(target_pointer_width = "32")] - pub unsafe fn take_payload_linux_unchecked(&mut self) -> SystemLinux { - unsafe { core::ptr::read(self.payload.as_ptr() as *const SystemLinux) } + pub unsafe fn take_payload_err_unchecked(&mut self) -> TcpErrOrTlsErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const TcpErrOrTlsErr) } } - /// Move the `Linux` payload out of one owned tag-union shell. + /// Move the `Err` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `LinuxOrMacOSOrWindowsTag::Linux`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `HostTcpConnectTlsResultTag::Err`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(not(target_pointer_width = "32"))] - pub unsafe fn take_payload_linux_unchecked(&mut self) -> SystemLinux { - unsafe { core::mem::ManuallyDrop::take(&mut self.payload.linux) } + pub unsafe fn take_payload_err_unchecked(&mut self) -> TcpErrOrTlsErr { + unsafe { core::mem::ManuallyDrop::take(&mut self.payload.err) } } - /// Borrow the `MacOS` payload without creating another owner. + /// Borrow the `Ok` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `LinuxOrMacOSOrWindowsTag::MacOS` and the payload must still be initialized. + /// `self.tag` must be `HostTcpConnectTlsResultTag::Ok` and the payload must still be initialized. #[cfg(target_pointer_width = "32")] - pub unsafe fn borrow_payload_mac_os_unchecked(&self) -> &SystemMacOS { - unsafe { &*(self.payload.as_ptr() as *const SystemMacOS) } + pub unsafe fn borrow_payload_ok_unchecked(&self) -> &*mut u64 { + unsafe { &*(self.payload.as_ptr() as *const *mut u64) } } - /// Borrow the `MacOS` payload without creating another owner. + /// Borrow the `Ok` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `LinuxOrMacOSOrWindowsTag::MacOS` and the payload must still be initialized. + /// `self.tag` must be `HostTcpConnectTlsResultTag::Ok` and the payload must still be initialized. #[cfg(not(target_pointer_width = "32"))] - pub unsafe fn borrow_payload_mac_os_unchecked(&self) -> &SystemMacOS { - unsafe { &*(&self.payload.mac_os as *const core::mem::ManuallyDrop as *const SystemMacOS) } + pub unsafe fn borrow_payload_ok_unchecked(&self) -> &*mut u64 { + unsafe { &*(&self.payload.ok as *const core::mem::ManuallyDrop<*mut u64> as *const *mut u64) } } - /// Move the `MacOS` payload out of one owned tag-union shell. + /// Move the `Ok` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `LinuxOrMacOSOrWindowsTag::MacOS`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `HostTcpConnectTlsResultTag::Ok`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(target_pointer_width = "32")] - pub unsafe fn take_payload_mac_os_unchecked(&mut self) -> SystemMacOS { - unsafe { core::ptr::read(self.payload.as_ptr() as *const SystemMacOS) } + pub unsafe fn take_payload_ok_unchecked(&mut self) -> *mut u64 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const *mut u64) } + } + + /// Move the `Ok` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `HostTcpConnectTlsResultTag::Ok`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn take_payload_ok_unchecked(&mut self) -> *mut u64 { + unsafe { core::mem::ManuallyDrop::take(&mut self.payload.ok) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 48, "HostTcpConnectTlsResult size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "HostTcpConnectTlsResult alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(HostTcpConnectTlsResult, tag) == 40, "HostTcpConnectTlsResult tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 24, "HostTcpConnectTlsResult size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "HostTcpConnectTlsResult alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(HostTcpConnectTlsResult, tag) == 20, "HostTcpConnectTlsResult tag offset mismatch"); + +/// Tag discriminant for TcpErrOrTlsErr. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TcpErrOrTlsErrTag { + TcpErr = 0, + TlsErr = 1, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TcpErrOrTlsErrPayload { + pub tcp_err: core::mem::ManuallyDrop, + pub tls_err: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct TcpErrOrTlsErrPayloadAlignment; + +/// Tag union: TcpErrOrTlsErr +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TcpErrOrTlsErr { + pub _payload_alignment: [TcpErrOrTlsErrPayloadAlignment; 0], + pub payload: [u8; 16], + pub tag: TcpErrOrTlsErrTag, +} + +/// Tag union: TcpErrOrTlsErr +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TcpErrOrTlsErr { + pub payload: TcpErrOrTlsErrPayload, + pub tag: TcpErrOrTlsErrTag, +} + +impl TcpErrOrTlsErr { + /// Borrow the `TcpErr` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `TcpErrOrTlsErrTag::TcpErr` and the payload must still be initialized. + #[cfg(target_pointer_width = "32")] + pub unsafe fn borrow_payload_tcp_err_unchecked(&self) -> &IOErr { + unsafe { &*(self.payload.as_ptr() as *const IOErr) } + } + + /// Borrow the `TcpErr` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `TcpErrOrTlsErrTag::TcpErr` and the payload must still be initialized. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn borrow_payload_tcp_err_unchecked(&self) -> &IOErr { + unsafe { &*(&self.payload.tcp_err as *const core::mem::ManuallyDrop as *const IOErr) } + } + + /// Move the `TcpErr` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `TcpErrOrTlsErrTag::TcpErr`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(target_pointer_width = "32")] + pub unsafe fn take_payload_tcp_err_unchecked(&mut self) -> IOErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } + } + + /// Move the `TcpErr` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `TcpErrOrTlsErrTag::TcpErr`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn take_payload_tcp_err_unchecked(&mut self) -> IOErr { + unsafe { core::mem::ManuallyDrop::take(&mut self.payload.tcp_err) } + } + + /// Borrow the `TlsErr` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `TcpErrOrTlsErrTag::TlsErr` and the payload must still be initialized. + #[cfg(target_pointer_width = "32")] + pub unsafe fn borrow_payload_tls_err_unchecked(&self) -> &TlsErr { + unsafe { &*(self.payload.as_ptr() as *const TlsErr) } + } + + /// Borrow the `TlsErr` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `TcpErrOrTlsErrTag::TlsErr` and the payload must still be initialized. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn borrow_payload_tls_err_unchecked(&self) -> &TlsErr { + unsafe { &*(&self.payload.tls_err as *const core::mem::ManuallyDrop as *const TlsErr) } + } + + /// Move the `TlsErr` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `TcpErrOrTlsErrTag::TlsErr`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(target_pointer_width = "32")] + pub unsafe fn take_payload_tls_err_unchecked(&mut self) -> TlsErr { + unsafe { core::ptr::read(self.payload.as_ptr() as *const TlsErr) } + } + + /// Move the `TlsErr` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `TcpErrOrTlsErrTag::TlsErr`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn take_payload_tls_err_unchecked(&mut self) -> TlsErr { + unsafe { core::mem::ManuallyDrop::take(&mut self.payload.tls_err) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "TcpErrOrTlsErr size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "TcpErrOrTlsErr alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(TcpErrOrTlsErr, tag) == 32, "TcpErrOrTlsErr tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "TcpErrOrTlsErr size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "TcpErrOrTlsErr alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(TcpErrOrTlsErr, tag) == 16, "TcpErrOrTlsErr tag offset mismatch"); + +/// Tag union: Arm64OrX64 +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Arm64OrX64 { + Arm64 = 0, + X64 = 1, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 1, "Arm64OrX64 size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 1, "Arm64OrX64 alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 1, "Arm64OrX64 size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 1, "Arm64OrX64 alignment mismatch"); + +/// Tag discriminant for LinuxOrMacOSOrWindows. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LinuxOrMacOSOrWindowsTag { + Linux = 0, + MacOS = 1, + Windows = 2, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union LinuxOrMacOSOrWindowsPayload { + pub linux: core::mem::ManuallyDrop, + pub mac_os: core::mem::ManuallyDrop, + pub windows: core::mem::ManuallyDrop, +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(1))] +#[derive(Clone, Copy)] +pub struct LinuxOrMacOSOrWindowsPayloadAlignment; + +/// Tag union: LinuxOrMacOSOrWindows +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct LinuxOrMacOSOrWindows { + pub _payload_alignment: [LinuxOrMacOSOrWindowsPayloadAlignment; 0], + pub payload: [u8; 1], + pub tag: LinuxOrMacOSOrWindowsTag, +} + +/// Tag union: LinuxOrMacOSOrWindows +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct LinuxOrMacOSOrWindows { + pub payload: LinuxOrMacOSOrWindowsPayload, + pub tag: LinuxOrMacOSOrWindowsTag, +} + +impl LinuxOrMacOSOrWindows { + /// Borrow the `Linux` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `LinuxOrMacOSOrWindowsTag::Linux` and the payload must still be initialized. + #[cfg(target_pointer_width = "32")] + pub unsafe fn borrow_payload_linux_unchecked(&self) -> &SystemLinux { + unsafe { &*(self.payload.as_ptr() as *const SystemLinux) } + } + + /// Borrow the `Linux` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `LinuxOrMacOSOrWindowsTag::Linux` and the payload must still be initialized. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn borrow_payload_linux_unchecked(&self) -> &SystemLinux { + unsafe { &*(&self.payload.linux as *const core::mem::ManuallyDrop as *const SystemLinux) } + } + + /// Move the `Linux` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `LinuxOrMacOSOrWindowsTag::Linux`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(target_pointer_width = "32")] + pub unsafe fn take_payload_linux_unchecked(&mut self) -> SystemLinux { + unsafe { core::ptr::read(self.payload.as_ptr() as *const SystemLinux) } + } + + /// Move the `Linux` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `LinuxOrMacOSOrWindowsTag::Linux`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn take_payload_linux_unchecked(&mut self) -> SystemLinux { + unsafe { core::mem::ManuallyDrop::take(&mut self.payload.linux) } + } + + /// Borrow the `MacOS` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `LinuxOrMacOSOrWindowsTag::MacOS` and the payload must still be initialized. + #[cfg(target_pointer_width = "32")] + pub unsafe fn borrow_payload_mac_os_unchecked(&self) -> &SystemMacOS { + unsafe { &*(self.payload.as_ptr() as *const SystemMacOS) } + } + + /// Borrow the `MacOS` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `LinuxOrMacOSOrWindowsTag::MacOS` and the payload must still be initialized. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn borrow_payload_mac_os_unchecked(&self) -> &SystemMacOS { + unsafe { &*(&self.payload.mac_os as *const core::mem::ManuallyDrop as *const SystemMacOS) } + } + + /// Move the `MacOS` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `LinuxOrMacOSOrWindowsTag::MacOS`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(target_pointer_width = "32")] + pub unsafe fn take_payload_mac_os_unchecked(&mut self) -> SystemMacOS { + unsafe { core::ptr::read(self.payload.as_ptr() as *const SystemMacOS) } } /// Move the `MacOS` payload out of one owned tag-union shell. @@ -14935,7 +15453,7 @@ pub struct HostHttpCloseBodyArgs { } /// Arguments for Host.http_read_body! -/// Roc signature: Resource.Handle([HttpResponseKind]), [Chunk, ToEnd(U64)] => Try(List(U8), [EndOfBody, HttpErr([BadBody, Interrupted, NetworkError, Other(List(U8)), Timeout]), ReadLimitExceeded]) +/// Roc signature: Resource.Handle([HttpResponseKind]), [Chunk, ToEnd(U64)] => Try(List(U8), [EndOfBody, HttpErr([BadBody, Interrupted, NetworkError, Other(List(U8)), Timeout, TlsErr(Tls.Err)]), ReadLimitExceeded]) /// Refcounted fields are owned by the hosted function. #[repr(C)] #[derive(Clone, Copy)] @@ -14945,7 +15463,7 @@ pub struct HostHttpReadBodyArgs { } /// Arguments for Host.http_send_request! -/// Roc signature: { body : List(U8), headers : List((Str, Str)), method : U8, method_ext : Str, timeout_ms : U64, uri : Str } => Try({ body : List(U8), headers : List((Str, Str)), status : U16 }, [BadBody, Interrupted, NetworkError, Other(List(U8)), Timeout]) +/// Roc signature: { body : List(U8), headers : List((Str, Str)), method : U8, method_ext : Str, timeout_ms : U64, uri : Str } => Try({ body : List(U8), headers : List((Str, Str)), status : U16 }, [BadBody, Interrupted, NetworkError, Other(List(U8)), Timeout, TlsErr(Tls.Err)]) /// Refcounted fields are owned by the hosted function. #[cfg(target_pointer_width = "32")] #[repr(C)] @@ -14960,7 +15478,7 @@ pub struct HostHttpSendRequestArgs { } /// Arguments for Host.http_send_request! -/// Roc signature: { body : List(U8), headers : List((Str, Str)), method : U8, method_ext : Str, timeout_ms : U64, uri : Str } => Try({ body : List(U8), headers : List((Str, Str)), status : U16 }, [BadBody, Interrupted, NetworkError, Other(List(U8)), Timeout]) +/// Roc signature: { body : List(U8), headers : List((Str, Str)), method : U8, method_ext : Str, timeout_ms : U64, uri : Str } => Try({ body : List(U8), headers : List((Str, Str)), status : U16 }, [BadBody, Interrupted, NetworkError, Other(List(U8)), Timeout, TlsErr(Tls.Err)]) /// Refcounted fields are owned by the hosted function. #[cfg(not(target_pointer_width = "32"))] #[repr(C)] @@ -15019,7 +15537,7 @@ unsafe impl RocRelease for HostHttpSendRequestArgsRelea } /// Arguments for Host.http_send_streaming! -/// Roc signature: { body : List(U8), headers : List((Str, Str)), method : U8, method_ext : Str, timeout_ms : U64, uri : Str }, [After(U64), NoTimeout], [At(U64), NoTimeout] => Try({ body : Resource.Handle([HttpResponseKind]), content_length : [Known(U64), Unknown], headers : List((Str, Str)), invalid_location : [None, Some(U8)], status : U16 }, [BadBody, Interrupted, NetworkError, Other(List(U8)), Timeout]) +/// Roc signature: { body : List(U8), headers : List((Str, Str)), method : U8, method_ext : Str, timeout_ms : U64, uri : Str }, [After(U64), NoTimeout], [At(U64), NoTimeout], [AddPem(List(U8)), DangerAcceptInvalidCerts, Default] => Try({ body : Resource.Handle([HttpResponseKind]), content_length : [Known(U64), Unknown], headers : List((Str, Str)), invalid_location : [None, Some(U8)], status : U16 }, [BadBody, Interrupted, NetworkError, Other(List(U8)), Timeout, TlsErr(Tls.Err)]) /// Refcounted fields are owned by the hosted function. #[repr(C)] #[derive(Clone, Copy)] @@ -15027,6 +15545,7 @@ pub struct HostHttpSendStreamingArgs { pub arg0: AnonStruct8bbc5017d7a8cb36, pub arg1: AfterOrNoTimeout, pub arg2: AtOrNoTimeout, + pub arg3: AddPemOrDangerAcceptInvalidCertsOrDefault, } /// Arguments for Host.path_absolute! @@ -15113,7 +15632,7 @@ pub struct HostPathTypeArgs { #[repr(C)] #[derive(Clone, Copy)] pub struct HostSignalCatchArgs { - pub arg0: RocListWith, + pub arg0: RocListWith, } /// Arguments for Host.sleep_nanos! @@ -15279,6 +15798,18 @@ pub struct HostTcpConnectArgs { pub arg2: u64, } +/// Arguments for Host.tcp_connect_tls! +/// Roc signature: Str, U16, U64, [AddPem(List(U8)), DangerAcceptInvalidCerts, Default] => Try(Resource.Handle([TcpConnectionKind]), [TcpErr(IOErr), TlsErr(Tls.Err)]) +/// Refcounted fields are owned by the hosted function. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct HostTcpConnectTlsArgs { + pub arg0: RocStr, + pub arg1: u16, + pub arg2: u64, + pub arg3: AddPemOrDangerAcceptInvalidCertsOrDefault, +} + /// Arguments for Host.tcp_listen! /// Roc signature: Str, U16, U64 => Try(Resource.Handle([TcpListenerKind]), [TcpErr(IOErr)]) /// Refcounted fields are owned by the hosted function. @@ -15545,9 +16076,9 @@ pub type HostChildWriteErrCmdErrTimedOut = AnonStruct3e7554e024207e25; pub type HostCmdExecArg2 = UnixBytesOrUtf8OrWindowsU16s; pub type HostCmdExecArg2Payload = UnixBytesOrUtf8OrWindowsU16sPayload; pub type HostCmdExecArg2Tag = UnixBytesOrUtf8OrWindowsU16sTag; -pub type FailedOrInterrupted = FailedOrInterruptedType386; -pub type FailedOrInterruptedPayload = FailedOrInterruptedType386Payload; -pub type FailedOrInterruptedTag = FailedOrInterruptedType386Tag; +pub type FailedOrInterrupted = FailedOrInterruptedType391; +pub type FailedOrInterruptedPayload = FailedOrInterruptedType391Payload; +pub type FailedOrInterruptedTag = FailedOrInterruptedType391Tag; pub type HostCmdRunArg0 = AnonStruct1027aaeb0344dea1; pub type HostCmdRunArg0Args = UnixBytesOrUtf8OrWindowsU16s; pub type HostCmdRunArg0ArgsPayload = UnixBytesOrUtf8OrWindowsU16sPayload; @@ -15692,24 +16223,39 @@ pub type HostFileWriterWriteUtf8ResultTag = HostFileAppendBytesResultTag; pub type HostHttpReadBodyErr = EndOfBodyOrHttpErrOrReadLimitExceeded; pub type HostHttpReadBodyErrPayload = EndOfBodyOrHttpErrOrReadLimitExceededPayload; pub type HostHttpReadBodyErrTag = EndOfBodyOrHttpErrOrReadLimitExceededTag; -pub type HostHttpReadBodyErrHttpErr = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout; -pub type HostHttpReadBodyErrHttpErrPayload = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutPayload; -pub type HostHttpReadBodyErrHttpErrTag = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag; -pub type EndOfBodyOrHttpErrOrReadLimitExceededHttpErr = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout; -pub type EndOfBodyOrHttpErrOrReadLimitExceededHttpErrPayload = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutPayload; -pub type EndOfBodyOrHttpErrOrReadLimitExceededHttpErrTag = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag; +pub type HostHttpReadBodyErrHttpErr = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr; +pub type HostHttpReadBodyErrHttpErrPayload = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrPayload; +pub type HostHttpReadBodyErrHttpErrTag = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag; +pub type HostHttpReadBodyErrHttpErrTlsErr = TlsErr; +pub type HostHttpReadBodyErrHttpErrTlsErrPayload = TlsErrPayload; +pub type HostHttpReadBodyErrHttpErrTlsErrTag = TlsErrTag; +pub type EndOfBodyOrHttpErrOrReadLimitExceededHttpErr = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr; +pub type EndOfBodyOrHttpErrOrReadLimitExceededHttpErrPayload = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrPayload; +pub type EndOfBodyOrHttpErrOrReadLimitExceededHttpErrTag = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag; +pub type EndOfBodyOrHttpErrOrReadLimitExceededHttpErrTlsErr = TlsErr; +pub type EndOfBodyOrHttpErrOrReadLimitExceededHttpErrTlsErrPayload = TlsErrPayload; +pub type EndOfBodyOrHttpErrOrReadLimitExceededHttpErrTlsErrTag = TlsErrTag; +pub type BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTlsErr = TlsErr; +pub type BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTlsErrPayload = TlsErrPayload; +pub type BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTlsErrTag = TlsErrTag; pub type HostHttpSendRequestArg0 = AnonStruct8bbc5017d7a8cb36; pub type HostHttpSendRequestArg0Headers = AnonStruct77eaba63dfee299d; -pub type HostHttpSendRequestErr = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout; -pub type HostHttpSendRequestErrPayload = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutPayload; -pub type HostHttpSendRequestErrTag = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag; +pub type HostHttpSendRequestErr = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr; +pub type HostHttpSendRequestErrPayload = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrPayload; +pub type HostHttpSendRequestErrTag = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag; +pub type HostHttpSendRequestErrTlsErr = TlsErr; +pub type HostHttpSendRequestErrTlsErrPayload = TlsErrPayload; +pub type HostHttpSendRequestErrTlsErrTag = TlsErrTag; pub type HostHttpSendRequestOk = AnonStructBe6bcbc15f8a1360; pub type HostHttpSendRequestOkHeaders = AnonStruct77eaba63dfee299d; pub type HostHttpSendStreamingArg0 = AnonStruct8bbc5017d7a8cb36; pub type HostHttpSendStreamingArg0Headers = AnonStruct77eaba63dfee299d; -pub type HostHttpSendStreamingErr = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout; -pub type HostHttpSendStreamingErrPayload = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutPayload; -pub type HostHttpSendStreamingErrTag = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag; +pub type HostHttpSendStreamingErr = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr; +pub type HostHttpSendStreamingErrPayload = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrPayload; +pub type HostHttpSendStreamingErrTag = BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag; +pub type HostHttpSendStreamingErrTlsErr = TlsErr; +pub type HostHttpSendStreamingErrTlsErrPayload = TlsErrPayload; +pub type HostHttpSendStreamingErrTlsErrTag = TlsErrTag; pub type HostHttpSendStreamingOk = AnonStruct60cbed700549ed13; pub type HostHttpSendStreamingOkHeaders = AnonStruct77eaba63dfee299d; pub type HostPathAbsoluteResult = HostEnvCreateTempDirResult; @@ -15743,21 +16289,21 @@ pub type HostPathSymlinkResult = HostEnvSetCwdResult; pub type HostPathSymlinkResultPayload = HostEnvSetCwdResultPayload; pub type HostPathSymlinkResultTag = HostEnvSetCwdResultTag; pub type HostPathTypeOk = DirOrFileOrOtherOrSymLink; -pub type HostSignalCatchArg0 = InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380; -pub type HostSignalCatchArg0Payload = InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Payload; -pub type HostSignalCatchArg0Tag = InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag; +pub type HostSignalCatchArg0 = InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385; +pub type HostSignalCatchArg0Payload = InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Payload; +pub type HostSignalCatchArg0Tag = InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag; pub type HostSignalCatchArg0Terminate = SystemUnix; pub type HostSignalCatchArg0User1 = SystemUnix; pub type HostSignalCatchArg0User2 = SystemUnix; pub type HostSignalCatchArg0WindowResize = SystemUnix; -pub type InterruptOrTerminateOrUser1OrUser2OrWindowResize = InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380; -pub type InterruptOrTerminateOrUser1OrUser2OrWindowResizePayload = InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Payload; -pub type InterruptOrTerminateOrUser1OrUser2OrWindowResizeTag = InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag; +pub type InterruptOrTerminateOrUser1OrUser2OrWindowResize = InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385; +pub type InterruptOrTerminateOrUser1OrUser2OrWindowResizePayload = InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Payload; +pub type InterruptOrTerminateOrUser1OrUser2OrWindowResizeTag = InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag; pub type InterruptOrTerminateOrUser1OrUser2OrWindowResizeTerminate = SystemUnix; pub type InterruptOrTerminateOrUser1OrUser2OrWindowResizeUser1 = SystemUnix; pub type InterruptOrTerminateOrUser1OrUser2OrWindowResizeUser2 = SystemUnix; pub type InterruptOrTerminateOrUser1OrUser2OrWindowResizeWindowResize = SystemUnix; -pub type HostSignalPoll = InterruptOrTerminateOrUser1OrUser2OrWindowResizeType384; +pub type HostSignalPoll = InterruptOrTerminateOrUser1OrUser2OrWindowResizeType389; pub type HostSqliteCloseErr = AnonStruct8dda646697632dbf; pub type HostSqliteExecuteArg2 = BytesOrIntegerOrNullOrRealOrString; pub type HostSqliteExecuteArg2Payload = BytesOrIntegerOrNullOrRealOrStringPayload; @@ -15785,9 +16331,9 @@ pub type HostSqliteNextOk = BytesOrIntegerOrNullOrRealOrText; pub type HostSqliteNextOkPayload = BytesOrIntegerOrNullOrRealOrTextPayload; pub type HostSqliteNextOkTag = BytesOrIntegerOrNullOrRealOrTextTag; pub type ClosedOrDoneOrFailedOrInterruptedFailed = AnonStruct8dda646697632dbf; -pub type HostSqliteOpenErr = FailedOrInterruptedType115; -pub type HostSqliteOpenErrPayload = FailedOrInterruptedType115Payload; -pub type HostSqliteOpenErrTag = FailedOrInterruptedType115Tag; +pub type HostSqliteOpenErr = FailedOrInterruptedType117; +pub type HostSqliteOpenErrPayload = FailedOrInterruptedType117Payload; +pub type HostSqliteOpenErrTag = FailedOrInterruptedType117Tag; pub type HostSqliteOpenErrFailed = AnonStruct8dda646697632dbf; pub type FailedOrInterruptedFailed = AnonStruct8dda646697632dbf; pub type HostSqliteQueryArg2 = BytesOrIntegerOrNullOrRealOrString; @@ -15854,6 +16400,15 @@ pub type LinuxOrMacOSOrWindowsWindows = SystemWindows; pub type HostTcpAcceptErr = ClosedOrTcpErr; pub type HostTcpAcceptErrPayload = ClosedOrTcpErrPayload; pub type HostTcpAcceptErrTag = ClosedOrTcpErrTag; +pub type HostTcpConnectTlsErr = TcpErrOrTlsErr; +pub type HostTcpConnectTlsErrPayload = TcpErrOrTlsErrPayload; +pub type HostTcpConnectTlsErrTag = TcpErrOrTlsErrTag; +pub type HostTcpConnectTlsErrTlsErr = TlsErr; +pub type HostTcpConnectTlsErrTlsErrPayload = TlsErrPayload; +pub type HostTcpConnectTlsErrTlsErrTag = TlsErrTag; +pub type TcpErrOrTlsErrTlsErr = TlsErr; +pub type TcpErrOrTlsErrTlsErrPayload = TlsErrPayload; +pub type TcpErrOrTlsErrTlsErrTag = TlsErrTag; pub type HostTcpLocalPortErr = ClosedOrTcpErr; pub type HostTcpLocalPortErrPayload = ClosedOrTcpErrPayload; pub type HostTcpLocalPortErrTag = ClosedOrTcpErrTag; @@ -16770,7 +17325,7 @@ unsafe impl RocRelease for HostHttpSendRequestResultR } } -impl BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { +impl BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr { /// Recursively decrement Roc-owned payloads. /// /// # Safety @@ -16779,14 +17334,18 @@ impl BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { let mut value = self; let _ = roc_host; match value.tag { - BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag::BadBody => {}, - BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag::Interrupted => {}, - BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag::NetworkError => {}, - BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag::Other => { + BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::BadBody => {}, + BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::Interrupted => {}, + BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::NetworkError => {}, + BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::Other => { let payload = unsafe { value.take_payload_other_unchecked() }; unsafe { payload.decref(roc_host); } }, - BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag::Timeout => {}, + BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::Timeout => {}, + BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::TlsErr => { + let payload = unsafe { value.take_payload_tls_err_unchecked() }; + unsafe { payload.decref(roc_host); } + }, } } @@ -16799,22 +17358,81 @@ impl BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout { let value = self; let _ = amount; match value.tag { - BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag::BadBody => {}, - BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag::Interrupted => {}, - BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag::NetworkError => {}, - BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag::Other => { + BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::BadBody => {}, + BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::Interrupted => {}, + BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::NetworkError => {}, + BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::Other => { let payload = unsafe { core::ptr::read(value.borrow_payload_other_unchecked()) }; unsafe { payload.incref(amount); } }, - BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutTag::Timeout => {}, + BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::Timeout => {}, + BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrTag::TlsErr => { + let payload = unsafe { core::ptr::read(value.borrow_payload_tls_err_unchecked()) }; + unsafe { payload.incref(amount); } + }, } } } -pub struct BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutRelease; +pub struct BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrRelease; -unsafe impl RocRelease for BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutRelease { - unsafe fn release(value: BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeout, roc_host: &RocHost) { +unsafe impl RocRelease for BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErrRelease { + unsafe fn release(value: BadBodyOrInterruptedOrNetworkErrorOrOtherOrTimeoutOrTlsErr, roc_host: &RocHost) { + unsafe { value.decref(roc_host); } + } +} + +impl TlsErr { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let mut value = self; + let _ = roc_host; + match value.tag { + TlsErrTag::BadPem => { + let payload = unsafe { value.take_payload_bad_pem_unchecked() }; + unsafe { payload.decref(roc_host); } + }, + TlsErrTag::Expired => {}, + TlsErrTag::NameMismatch => {}, + TlsErrTag::Other => { + let payload = unsafe { value.take_payload_other_unchecked() }; + unsafe { payload.decref(roc_host); } + }, + TlsErrTag::UnknownIssuer => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + TlsErrTag::BadPem => { + let payload = unsafe { core::ptr::read(value.borrow_payload_bad_pem_unchecked()) }; + unsafe { payload.incref(amount); } + }, + TlsErrTag::Expired => {}, + TlsErrTag::NameMismatch => {}, + TlsErrTag::Other => { + let payload = unsafe { core::ptr::read(value.borrow_payload_other_unchecked()) }; + unsafe { payload.incref(amount); } + }, + TlsErrTag::UnknownIssuer => {}, + } + } +} + +pub struct TlsErrRelease; + +unsafe impl RocRelease for TlsErrRelease { + unsafe fn release(value: TlsErr, roc_host: &RocHost) { unsafe { value.decref(roc_host); } } } @@ -17201,7 +17819,7 @@ unsafe impl RocRelease for HostSqliteOpenResultRelease { } } -impl FailedOrInterruptedType115 { +impl FailedOrInterruptedType117 { /// Recursively decrement Roc-owned payloads. /// /// # Safety @@ -17210,11 +17828,11 @@ impl FailedOrInterruptedType115 { let mut value = self; let _ = roc_host; match value.tag { - FailedOrInterruptedType115Tag::Failed => { + FailedOrInterruptedType117Tag::Failed => { let payload = unsafe { value.take_payload_failed_unchecked() }; unsafe { payload.decref(roc_host); } }, - FailedOrInterruptedType115Tag::Interrupted => {}, + FailedOrInterruptedType117Tag::Interrupted => {}, } } @@ -17227,19 +17845,19 @@ impl FailedOrInterruptedType115 { let value = self; let _ = amount; match value.tag { - FailedOrInterruptedType115Tag::Failed => { + FailedOrInterruptedType117Tag::Failed => { let payload = unsafe { core::ptr::read(value.borrow_payload_failed_unchecked()) }; unsafe { payload.incref(amount); } }, - FailedOrInterruptedType115Tag::Interrupted => {}, + FailedOrInterruptedType117Tag::Interrupted => {}, } } } -pub struct FailedOrInterruptedType115Release; +pub struct FailedOrInterruptedType117Release; -unsafe impl RocRelease for FailedOrInterruptedType115Release { - unsafe fn release(value: FailedOrInterruptedType115, roc_host: &RocHost) { +unsafe impl RocRelease for FailedOrInterruptedType117Release { + unsafe fn release(value: FailedOrInterruptedType117, roc_host: &RocHost) { unsafe { value.decref(roc_host); } } } @@ -20710,6 +21328,51 @@ unsafe impl RocRelease for AtOrNoTimeoutRelease { } } +impl AddPemOrDangerAcceptInvalidCertsOrDefault { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let mut value = self; + let _ = roc_host; + match value.tag { + AddPemOrDangerAcceptInvalidCertsOrDefaultTag::AddPem => { + let payload = unsafe { value.take_payload_add_pem_unchecked() }; + unsafe { payload.decref(roc_host); } + }, + AddPemOrDangerAcceptInvalidCertsOrDefaultTag::DangerAcceptInvalidCerts => {}, + AddPemOrDangerAcceptInvalidCertsOrDefaultTag::Default => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + AddPemOrDangerAcceptInvalidCertsOrDefaultTag::AddPem => { + let payload = unsafe { core::ptr::read(value.borrow_payload_add_pem_unchecked()) }; + unsafe { payload.incref(amount); } + }, + AddPemOrDangerAcceptInvalidCertsOrDefaultTag::DangerAcceptInvalidCerts => {}, + AddPemOrDangerAcceptInvalidCertsOrDefaultTag::Default => {}, + } + } +} + +pub struct AddPemOrDangerAcceptInvalidCertsOrDefaultRelease; + +unsafe impl RocRelease for AddPemOrDangerAcceptInvalidCertsOrDefaultRelease { + unsafe fn release(value: AddPemOrDangerAcceptInvalidCertsOrDefault, roc_host: &RocHost) { + unsafe { value.decref(roc_host); } + } +} + impl HostHttpReadBodyResult { /// Recursively decrement Roc-owned payloads. /// @@ -20884,7 +21547,7 @@ unsafe impl RocRelease for HostSignalCatchResultRelease { } } -impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { +impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385 { /// Recursively decrement Roc-owned payloads. /// /// # Safety @@ -20893,20 +21556,20 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { let mut value = self; let _ = roc_host; match value.tag { - InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::Interrupt => {}, - InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::Terminate => { + InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::Interrupt => {}, + InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::Terminate => { let payload = unsafe { value.take_payload_terminate_unchecked() }; unsafe { payload.decref(roc_host); } }, - InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::User1 => { + InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::User1 => { let payload = unsafe { value.take_payload_user1_unchecked() }; unsafe { payload.decref(roc_host); } }, - InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::User2 => { + InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::User2 => { let payload = unsafe { value.take_payload_user2_unchecked() }; unsafe { payload.decref(roc_host); } }, - InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::WindowResize => { + InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::WindowResize => { let payload = unsafe { value.take_payload_window_resize_unchecked() }; unsafe { payload.decref(roc_host); } }, @@ -20922,20 +21585,20 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { let value = self; let _ = amount; match value.tag { - InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::Interrupt => {}, - InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::Terminate => { + InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::Interrupt => {}, + InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::Terminate => { let payload = unsafe { core::ptr::read(value.borrow_payload_terminate_unchecked()) }; unsafe { payload.incref(amount); } }, - InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::User1 => { + InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::User1 => { let payload = unsafe { core::ptr::read(value.borrow_payload_user1_unchecked()) }; unsafe { payload.incref(amount); } }, - InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::User2 => { + InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::User2 => { let payload = unsafe { core::ptr::read(value.borrow_payload_user2_unchecked()) }; unsafe { payload.incref(amount); } }, - InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Tag::WindowResize => { + InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Tag::WindowResize => { let payload = unsafe { core::ptr::read(value.borrow_payload_window_resize_unchecked()) }; unsafe { payload.incref(amount); } }, @@ -20943,10 +21606,10 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380 { } } -pub struct InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Release; +pub struct InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Release; -unsafe impl RocRelease for InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380Release { - unsafe fn release(value: InterruptOrTerminateOrUser1OrUser2OrWindowResizeType380, roc_host: &RocHost) { +unsafe impl RocRelease for InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385Release { + unsafe fn release(value: InterruptOrTerminateOrUser1OrUser2OrWindowResizeType385, roc_host: &RocHost) { unsafe { value.decref(roc_host); } } } @@ -20982,7 +21645,7 @@ unsafe impl RocRelease for SystemUnixRelease { } } -impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType384 { +impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType389 { /// Recursively decrement Roc-owned payloads. /// /// # Safety @@ -21003,15 +21666,15 @@ impl InterruptOrTerminateOrUser1OrUser2OrWindowResizeType384 { } } -pub struct InterruptOrTerminateOrUser1OrUser2OrWindowResizeType384Release; +pub struct InterruptOrTerminateOrUser1OrUser2OrWindowResizeType389Release; -unsafe impl RocRelease for InterruptOrTerminateOrUser1OrUser2OrWindowResizeType384Release { - unsafe fn release(value: InterruptOrTerminateOrUser1OrUser2OrWindowResizeType384, roc_host: &RocHost) { +unsafe impl RocRelease for InterruptOrTerminateOrUser1OrUser2OrWindowResizeType389Release { + unsafe fn release(value: InterruptOrTerminateOrUser1OrUser2OrWindowResizeType389, roc_host: &RocHost) { unsafe { value.decref(roc_host); } } } -impl FailedOrInterruptedType386 { +impl FailedOrInterruptedType391 { /// Recursively decrement Roc-owned payloads. /// /// # Safety @@ -21020,11 +21683,11 @@ impl FailedOrInterruptedType386 { let mut value = self; let _ = roc_host; match value.tag { - FailedOrInterruptedType386Tag::Failed => { + FailedOrInterruptedType391Tag::Failed => { let payload = unsafe { value.take_payload_failed_unchecked() }; unsafe { payload.decref(roc_host); } }, - FailedOrInterruptedType386Tag::Interrupted => {}, + FailedOrInterruptedType391Tag::Interrupted => {}, } } @@ -21037,19 +21700,19 @@ impl FailedOrInterruptedType386 { let value = self; let _ = amount; match value.tag { - FailedOrInterruptedType386Tag::Failed => { + FailedOrInterruptedType391Tag::Failed => { let payload = unsafe { core::ptr::read(value.borrow_payload_failed_unchecked()) }; unsafe { payload.incref(amount); } }, - FailedOrInterruptedType386Tag::Interrupted => {}, + FailedOrInterruptedType391Tag::Interrupted => {}, } } } -pub struct FailedOrInterruptedType386Release; +pub struct FailedOrInterruptedType391Release; -unsafe impl RocRelease for FailedOrInterruptedType386Release { - unsafe fn release(value: FailedOrInterruptedType386, roc_host: &RocHost) { +unsafe impl RocRelease for FailedOrInterruptedType391Release { + unsafe fn release(value: FailedOrInterruptedType391, roc_host: &RocHost) { unsafe { value.decref(roc_host); } } } @@ -21623,6 +22286,104 @@ unsafe impl RocRelease for ClosedOrUnixSocketErrRelease { } } +impl HostTcpConnectTlsResult { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let mut value = self; + let _ = roc_host; + match value.tag { + HostTcpConnectTlsResultTag::Err => { + let payload = unsafe { value.take_payload_err_unchecked() }; + unsafe { payload.decref(roc_host); } + }, + HostTcpConnectTlsResultTag::Ok => { + let payload = unsafe { value.take_payload_ok_unchecked() }; + unsafe { decref_box_with(payload as RocBox, core::mem::align_of::(), false, None, roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + HostTcpConnectTlsResultTag::Err => { + let payload = unsafe { core::ptr::read(value.borrow_payload_err_unchecked()) }; + unsafe { payload.incref(amount); } + }, + HostTcpConnectTlsResultTag::Ok => { + let payload = unsafe { core::ptr::read(value.borrow_payload_ok_unchecked()) }; + unsafe { incref_box(payload as RocBox, amount); } + }, + } + } +} + +pub struct HostTcpConnectTlsResultRelease; + +unsafe impl RocRelease for HostTcpConnectTlsResultRelease { + unsafe fn release(value: HostTcpConnectTlsResult, roc_host: &RocHost) { + unsafe { value.decref(roc_host); } + } +} + +impl TcpErrOrTlsErr { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let mut value = self; + let _ = roc_host; + match value.tag { + TcpErrOrTlsErrTag::TcpErr => { + let payload = unsafe { value.take_payload_tcp_err_unchecked() }; + unsafe { payload.decref(roc_host); } + }, + TcpErrOrTlsErrTag::TlsErr => { + let payload = unsafe { value.take_payload_tls_err_unchecked() }; + unsafe { payload.decref(roc_host); } + }, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + TcpErrOrTlsErrTag::TcpErr => { + let payload = unsafe { core::ptr::read(value.borrow_payload_tcp_err_unchecked()) }; + unsafe { payload.incref(amount); } + }, + TcpErrOrTlsErrTag::TlsErr => { + let payload = unsafe { core::ptr::read(value.borrow_payload_tls_err_unchecked()) }; + unsafe { payload.incref(amount); } + }, + } + } +} + +pub struct TcpErrOrTlsErrRelease; + +unsafe impl RocRelease for TcpErrOrTlsErrRelease { + unsafe fn release(value: TcpErrOrTlsErr, roc_host: &RocHost) { + unsafe { value.decref(roc_host); } + } +} + impl SystemTarget { /// Recursively decrement Roc-owned fields. /// @@ -22103,7 +22864,7 @@ unsafe extern "C" { /// unsafe { arg1.decref(roc_host); } /// unsafe { decref_list_of_unix_bytes_or_utf8or_windows_u16s(arg2, roc_host); } /// The result is owned by Roc: return exactly one owned reference. - pub fn hosted_cmd_exec(arg0: SystemUnix, arg1: UnixBytesOrUtf8OrWindowsU16s, arg2: RocList) -> FailedOrInterruptedType386; + pub fn hosted_cmd_exec(arg0: SystemUnix, arg1: UnixBytesOrUtf8OrWindowsU16s, arg2: RocList) -> FailedOrInterruptedType391; /// Hosted symbol for Host.cmd_run! /// Roc signature: { args : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, cwd : [Dir([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), Inherit], envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), manage_tree : Bool, output_limit : [MaxBytes(U64), NoLimit], pending_limit : U64, program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], stderr : [Capture, File({ create_permissions : [Default, OwnerOnly, PublicRead], mode : [Append, RequireNew, Truncate], path : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] }), Inherit, MergeWithStdout, Null, Pipe, Tee], stdin : [Bytes(List(U8)), Inherit, Null, Pipe], stdout : [Capture, File({ create_permissions : [Default, OwnerOnly, PublicRead], mode : [Append, RequireNew, Truncate], path : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] }), Inherit, InheritStderr, Null, Pipe, Tee], timeout : [AfterNanos(U64), NoTimeout] } => Try({ status : [Exited(I32), Signaled(I32)], stderr_bytes : List(U8), stdout_bytes : List(U8) }, [CmdErr([IOErr(IOErr), OutputLimitExceeded({ stderr_bytes : List(U8), stdout_bytes : List(U8) }), StartFailed(IOErr), TimedOut({ stderr_bytes : List(U8), stdout_bytes : List(U8) })]), Interrupted]) @@ -22446,7 +23207,7 @@ unsafe extern "C" { pub fn hosted_http_close_body(arg0: *mut u64); /// Hosted symbol for Host.http_read_body! - /// Roc signature: Resource.Handle([HttpResponseKind]), [Chunk, ToEnd(U64)] => Try(List(U8), [EndOfBody, HttpErr([BadBody, Interrupted, NetworkError, Other(List(U8)), Timeout]), ReadLimitExceeded]) + /// Roc signature: Resource.Handle([HttpResponseKind]), [Chunk, ToEnd(U64)] => Try(List(U8), [EndOfBody, HttpErr([BadBody, Interrupted, NetworkError, Other(List(U8)), Timeout, TlsErr(Tls.Err)]), ReadLimitExceeded]) /// Owned arguments. Release each exactly once before returning, unless it is /// moved into storage or into the result: /// unsafe { decref_box_with(arg0 as RocBox, core::mem::align_of::(), false, None, roc_host); } @@ -22455,7 +23216,7 @@ unsafe extern "C" { pub fn hosted_http_read_body(arg0: *mut u64, arg1: ChunkOrToEnd) -> HostHttpReadBodyResult; /// Hosted symbol for Host.http_send_request! - /// Roc signature: { body : List(U8), headers : List((Str, Str)), method : U8, method_ext : Str, timeout_ms : U64, uri : Str } => Try({ body : List(U8), headers : List((Str, Str)), status : U16 }, [BadBody, Interrupted, NetworkError, Other(List(U8)), Timeout]) + /// Roc signature: { body : List(U8), headers : List((Str, Str)), method : U8, method_ext : Str, timeout_ms : U64, uri : Str } => Try({ body : List(U8), headers : List((Str, Str)), status : U16 }, [BadBody, Interrupted, NetworkError, Other(List(U8)), Timeout, TlsErr(Tls.Err)]) /// Owned arguments. Release each exactly once before returning, unless it is /// moved into storage or into the result: /// unsafe { arg0.decref(roc_host); } @@ -22463,14 +23224,15 @@ unsafe extern "C" { pub fn hosted_http_send_request(arg0: HostHttpSendRequestArgs) -> HostHttpSendRequestResult; /// Hosted symbol for Host.http_send_streaming! - /// Roc signature: { body : List(U8), headers : List((Str, Str)), method : U8, method_ext : Str, timeout_ms : U64, uri : Str }, [After(U64), NoTimeout], [At(U64), NoTimeout] => Try({ body : Resource.Handle([HttpResponseKind]), content_length : [Known(U64), Unknown], headers : List((Str, Str)), invalid_location : [None, Some(U8)], status : U16 }, [BadBody, Interrupted, NetworkError, Other(List(U8)), Timeout]) + /// Roc signature: { body : List(U8), headers : List((Str, Str)), method : U8, method_ext : Str, timeout_ms : U64, uri : Str }, [After(U64), NoTimeout], [At(U64), NoTimeout], [AddPem(List(U8)), DangerAcceptInvalidCerts, Default] => Try({ body : Resource.Handle([HttpResponseKind]), content_length : [Known(U64), Unknown], headers : List((Str, Str)), invalid_location : [None, Some(U8)], status : U16 }, [BadBody, Interrupted, NetworkError, Other(List(U8)), Timeout, TlsErr(Tls.Err)]) /// Owned arguments. Release each exactly once before returning, unless it is /// moved into storage or into the result: /// unsafe { arg0.decref(roc_host); } /// unsafe { arg1.decref(roc_host); } /// unsafe { arg2.decref(roc_host); } + /// unsafe { arg3.decref(roc_host); } /// The result is owned by Roc: return exactly one owned reference. - pub fn hosted_http_send_streaming(arg0: AnonStruct8bbc5017d7a8cb36, arg1: AfterOrNoTimeout, arg2: AtOrNoTimeout) -> HostHttpSendStreamingResult; + pub fn hosted_http_send_streaming(arg0: AnonStruct8bbc5017d7a8cb36, arg1: AfterOrNoTimeout, arg2: AtOrNoTimeout, arg3: AddPemOrDangerAcceptInvalidCertsOrDefault) -> HostHttpSendStreamingResult; /// Hosted symbol for Host.locale_all! /// Roc signature: {} => List(Str) @@ -22572,7 +23334,7 @@ unsafe extern "C" { /// moved into storage or into the result: /// unsafe { arg0.decref(roc_host); } /// The result is owned by Roc: return exactly one owned reference. - pub fn hosted_signal_catch(arg0: RocListWith) -> HostSignalCatchResult; + pub fn hosted_signal_catch(arg0: RocListWith) -> HostSignalCatchResult; /// Hosted symbol for Host.signal_interrupted_exit_code! /// Roc signature: {} => I32 @@ -22581,7 +23343,7 @@ unsafe extern "C" { /// Hosted symbol for Host.signal_poll! /// Roc signature: {} => List([Interrupt, Terminate, User1, User2, WindowResize]) /// The result is owned by Roc: return exactly one owned reference. - pub fn hosted_signal_poll() -> RocListWith; + pub fn hosted_signal_poll() -> RocListWith; /// Hosted symbol for Host.sleep_nanos! /// Roc signature: U64 => Try({}, [Interrupted]) @@ -22741,6 +23503,15 @@ unsafe extern "C" { /// The result is owned by Roc: return exactly one owned reference. pub fn hosted_tcp_connect(arg0: RocStr, arg1: u16, arg2: u64) -> HostTcpConnectResult; + /// Hosted symbol for Host.tcp_connect_tls! + /// Roc signature: Str, U16, U64, [AddPem(List(U8)), DangerAcceptInvalidCerts, Default] => Try(Resource.Handle([TcpConnectionKind]), [TcpErr(IOErr), TlsErr(Tls.Err)]) + /// Owned arguments. Release each exactly once before returning, unless it is + /// moved into storage or into the result: + /// unsafe { arg0.decref(roc_host); } + /// unsafe { arg3.decref(roc_host); } + /// The result is owned by Roc: return exactly one owned reference. + pub fn hosted_tcp_connect_tls(arg0: RocStr, arg1: u16, arg2: u64, arg3: AddPemOrDangerAcceptInvalidCertsOrDefault) -> HostTcpConnectTlsResult; + /// Hosted symbol for Host.tcp_listen! /// Roc signature: Str, U16, U64 => Try(Resource.Handle([TcpListenerKind]), [TcpErr(IOErr)]) /// Owned arguments. Release each exactly once before returning, unless it is diff --git a/src/socket.rs b/src/socket.rs index b9091e60..aaefb16f 100644 --- a/src/socket.rs +++ b/src/socket.rs @@ -1,5 +1,7 @@ -//! Buffered, deadline-bounded, interruptible stream sockets shared by TCP and AF_UNIX. +//! Buffered, deadline-bounded, interruptible stream sockets shared by TCP (optionally over TLS) +//! and AF_UNIX. use std::io::{self, Read, Write}; +use std::sync::Arc; use std::time::{Duration, Instant}; pub(crate) fn check_interrupt() -> io::Result<()> { @@ -14,10 +16,36 @@ pub(crate) fn check_interrupt() -> io::Result<()> { /// when its console event cannot participate in the socket provider's poll set. const POLL_SLICE: Duration = Duration::from_millis(10); +/// What `wait_for` waits for. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum Interest { + Readable, + Writable, + /// Whichever comes first. A TLS session that must send before it can receive waits for this, + /// so a peer that is itself blocked writing to us is noticed while our send buffer is full. + Either, +} + pub(crate) fn wait_ready( socket: &socket2::Socket, write: bool, deadline: Deadline, +) -> io::Result<()> { + wait_for( + socket, + if write { + Interest::Writable + } else { + Interest::Readable + }, + deadline, + ) +} + +pub(crate) fn wait_for( + socket: &socket2::Socket, + interest: Interest, + deadline: Deadline, ) -> io::Result<()> { loop { let duration = deadline.remaining()?.unwrap_or(POLL_SLICE).min(POLL_SLICE); @@ -30,7 +58,11 @@ pub(crate) fn wait_ready( use std::os::fd::AsRawFd; let mut fd = libc::pollfd { fd: socket.as_raw_fd(), - events: if write { libc::POLLOUT } else { libc::POLLIN }, + events: match interest { + Interest::Readable => libc::POLLIN, + Interest::Writable => libc::POLLOUT, + Interest::Either => libc::POLLIN | libc::POLLOUT, + }, revents: 0, }; let count = unsafe { libc::poll(&mut fd, 1, millis) }; @@ -48,7 +80,11 @@ pub(crate) fn wait_ready( }; let mut fd = WSAPOLLFD { fd: socket.as_raw_socket() as _, - events: if write { POLLWRNORM } else { POLLRDNORM }, + events: match interest { + Interest::Readable => POLLRDNORM, + Interest::Writable => POLLWRNORM, + Interest::Either => POLLRDNORM | POLLWRNORM, + }, revents: 0, }; let count = unsafe { WSAPoll(&mut fd, 1, millis) }; @@ -191,6 +227,9 @@ impl From for ReadUntilError { /// A connected socket and the bytes received from it but not yet returned. pub(crate) struct BufferedSocket { stream: socket2::Socket, + /// The TLS session over `stream`, for a `Tcp.connect_tls!` connection. Plaintext it has + /// decrypted is not in `buffer` until `fill` takes it, which it does before any wait. + tls: Option>, /// Received bytes. Those before `start` have been returned already. buffer: Vec, start: usize, @@ -204,12 +243,30 @@ impl BufferedSocket { stream.set_nonblocking(true)?; Ok(Self { stream, + tls: None, buffer: Vec::new(), start: 0, discarding: None, }) } + /// Start TLS on a new connection and finish the handshake within `deadline`. + pub(crate) fn new_tls( + stream: socket2::Socket, + config: Arc, + name: rustls::pki_types::ServerName<'static>, + deadline: Deadline, + ) -> Result { + let mut tls = rustls::ClientConnection::new(config, name).map_err(|error| { + crate::tls::ConnectError::Tls(crate::tls::TlsFailure::from_rustls(&error)) + })?; + stream.set_nonblocking(true)?; + crate::tls::handshake(&mut tls, &stream, deadline)?; + let mut socket = Self::new(stream)?; + socket.tls = Some(Box::new(tls)); + Ok(socket) + } + pub(crate) fn available(&self) -> &[u8] { &self.buffer[self.start..] } @@ -245,9 +302,21 @@ impl BufferedSocket { if let Err(error) = deadline.remaining() { break Err(error); } - match (&self.stream).read(&mut self.buffer[filled..]) { + let attempt = match &mut self.tls { + None => (&self.stream).read(&mut self.buffer[filled..]), + Some(tls) => crate::tls::read(tls, &self.stream, &mut self.buffer[filled..]), + }; + match attempt { Err(error) if error.kind() == io::ErrorKind::WouldBlock => { - if let Err(error) = wait_ready(&self.stream, false, deadline) { + // A TLS session can need to send before it can receive. + // Then readable data must still wake us: the peer may be blocked writing to + // us while our send buffer is full. + let interest = if self.tls.as_ref().is_some_and(|tls| tls.wants_write()) { + Interest::Either + } else { + Interest::Readable + }; + if let Err(error) = wait_for(&self.stream, interest, deadline) { break Err(error); } } @@ -402,6 +471,9 @@ impl BufferedSocket { /// Write all of `bytes`. On failure any prefix may have been sent. pub(crate) fn write(&mut self, bytes: &[u8], timeout_nanos: u64) -> io::Result<()> { let deadline = Deadline::after_nanos(timeout_nanos)?; + if let Some(tls) = &mut self.tls { + return crate::tls::write_all(tls, &self.stream, bytes, deadline); + } let mut written = 0; while written < bytes.len() { deadline.remaining()?; @@ -418,3 +490,47 @@ impl BufferedSocket { Ok(()) } } + +impl Drop for BufferedSocket { + fn drop(&mut self) { + // Best effort and never waiting: a peer still reading sees a clean end rather than a + // truncation. + if let Some(tls) = &mut self.tls { + crate::tls::close(tls, &self.stream); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::{TcpListener, TcpStream}; + + #[test] + fn either_interest_wakes_for_readable_data_while_the_send_buffer_is_full() { + let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap(); + let client = TcpStream::connect(listener.local_addr().unwrap()).unwrap(); + let (mut peer, _) = listener.accept().unwrap(); + let socket = socket2::Socket::from(client); + socket.set_nonblocking(true).unwrap(); + // The peer reads nothing, so this ends when both socket buffers are full. + loop { + match (&socket).write(&[0; 64 * 1024]) { + Ok(_) => {} + Err(error) if error.kind() == io::ErrorKind::WouldBlock => break, + Err(error) => panic!("{error}"), + } + } + let short = || Deadline::after_nanos(200_000_000).unwrap(); + let wait = |interest| wait_for(&socket, interest, short()).map_err(|error| error.kind()); + // Nothing to read and no room to write. + assert_eq!(wait(Interest::Either), Err(io::ErrorKind::TimedOut)); + assert_eq!(wait(Interest::Writable), Err(io::ErrorKind::TimedOut)); + peer.write_all(b"x").unwrap(); + peer.flush().unwrap(); + assert_eq!(wait(Interest::Either), Ok(())); + assert_eq!(wait(Interest::Readable), Ok(())); + // Still no room to write: a wait for that alone would stall here. + assert_eq!(wait(Interest::Writable), Err(io::ErrorKind::TimedOut)); + } +} diff --git a/src/tcp.rs b/src/tcp.rs index aaa4d524..ea4a15e3 100644 --- a/src/tcp.rs +++ b/src/tcp.rs @@ -13,12 +13,14 @@ use std::io; #[cfg(test)] use std::io::{Read, Write}; use std::net::TcpStream; +use std::sync::Arc; #[cfg(test)] use std::thread; #[cfg(test)] use std::time::{Duration, Instant}; use crate::roc_platform_abi::*; +use crate::tls::{ConnectError, Trust}; use crate::{roc_host, roc_u8_list_from_slice}; use crate::socket::normalize; @@ -35,6 +37,16 @@ impl Connection { pub(crate) fn new(stream: TcpStream) -> io::Result { crate::socket::BufferedSocket::new(stream.into()).map(Self) } + + /// A TLS client connection to `name` over `stream`, its handshake finished within `deadline`. + pub(crate) fn new_tls( + stream: TcpStream, + config: Arc, + name: rustls::pki_types::ServerName<'static>, + deadline: Deadline, + ) -> Result { + crate::socket::BufferedSocket::new_tls(stream.into(), config, name, deadline).map(Self) + } } impl std::ops::Deref for Connection { type Target = crate::socket::BufferedSocket; @@ -57,7 +69,10 @@ pub(crate) fn resolve_with_deadline( } fn connect(host: String, port: u16, timeout_nanos: u64) -> io::Result { - let deadline = Deadline::after_nanos(timeout_nanos)?; + connect_until(host, port, Deadline::after_nanos(timeout_nanos)?) +} + +fn connect_until(host: String, port: u16, deadline: Deadline) -> io::Result { let addresses = resolve_with_deadline(host, port, deadline)?; let mut last_error = None; @@ -83,6 +98,21 @@ fn connect(host: String, port: u16, timeout_nanos: u64) -> io::Result })) } +/// `Tcp.connect_tls!`. The trust and the name are checked before anything touches the network; +/// resolution, connecting and the handshake then share one deadline. +fn connect_tls( + host: String, + port: u16, + timeout_nanos: u64, + trust: &Trust, +) -> Result { + let config = crate::tls::client_config(trust).map_err(ConnectError::Tls)?; + let name = crate::tls::server_name(&host).map_err(ConnectError::Tls)?; + let deadline = Deadline::after_nanos(timeout_nanos)?; + let stream = connect_until(host, port, deadline)?; + Connection::new_tls(stream, config, name, deadline) +} + /// Run `operation` on the connection behind `handle`, then drop the reference /// the hosted call was passed. Final release closes the socket. fn with_connection( @@ -176,6 +206,55 @@ pub extern "C" fn hosted_tcp_connect( } } +/// The error of `Tcp.connect_tls!`. An I/O failure normalises as for `Tcp.connect!`. +fn connect_tls_err(error: ConnectError, roc_host: &RocHost) -> TcpErrOrTlsErr { + match error { + ConnectError::Io(error) => TcpErrOrTlsErr { + payload: TcpErrOrTlsErrPayload { + tcp_err: ManuallyDrop::new(tcp_io_err(error, roc_host)), + }, + tag: TcpErrOrTlsErrTag::TcpErr, + }, + ConnectError::Tls(failure) => TcpErrOrTlsErr { + payload: TcpErrOrTlsErrPayload { + tls_err: ManuallyDrop::new(failure.to_roc(roc_host)), + }, + tag: TcpErrOrTlsErrTag::TlsErr, + }, + } +} + +#[no_mangle] +pub extern "C" fn hosted_tcp_connect_tls( + host: RocStr, + port: u16, + timeout_nanos: u64, + trust: AddPemOrDangerAcceptInvalidCertsOrDefault, +) -> HostTcpConnectTlsResult { + let roc_host = roc_host(); + let host_string = host.as_str().to_owned(); + unsafe { host.decref(roc_host) }; + let trust = Trust::from_roc(trust, roc_host); + + let handle = connect_tls(host_string, port, timeout_nanos, &trust).and_then(|connection| { + crate::resources::box_resource(connection, roc_host).map_err(ConnectError::Io) + }); + match handle { + Ok(handle) => HostTcpConnectTlsResult { + payload: HostTcpConnectTlsResultPayload { + ok: ManuallyDrop::new(handle), + }, + tag: HostTcpConnectTlsResultTag::Ok, + }, + Err(error) => HostTcpConnectTlsResult { + payload: HostTcpConnectTlsResultPayload { + err: ManuallyDrop::new(connect_tls_err(error, roc_host)), + }, + tag: HostTcpConnectTlsResultTag::Err, + }, + } +} + #[no_mangle] pub extern "C" fn hosted_tcp_read_up_to( handle: *mut u64, @@ -312,6 +391,7 @@ pub extern "C" fn hosted_tcp_write( mod tests { use super::*; use std::net::{Shutdown, TcpListener}; + use std::sync::mpsc; const LONG: u64 = 10_000_000_000; const SHORT: u64 = 30_000_000; @@ -767,4 +847,375 @@ mod tests { assert_eq!(message.as_str(), "odd failure"); unsafe { message.decref(&host) }; } + + // TLS connections: the same buffered socket, with a rustls session over it. + + use crate::tls::testing::{ + config_at, finish_handshake, mismatched_key_server_config, serve_one, server_config, + tls_pair, LOCALHOST_PEM, + }; + use crate::tls::TlsFailure; + + fn pem() -> Trust { + Trust::AddPem(LOCALHOST_PEM.to_vec()) + } + + fn tls_failure(result: Result) -> TlsFailure { + match result { + Err(ConnectError::Tls(failure)) => failure, + Err(ConnectError::Io(error)) => panic!("expected a TLS failure, got {error:?}"), + Ok(_) => panic!("expected a TLS failure, got a connection"), + } + } + + fn connect_kind(result: Result) -> io::ErrorKind { + match result { + Err(ConnectError::Io(error)) => error.kind(), + Err(ConnectError::Tls(failure)) => panic!("expected an I/O error, got {failure:?}"), + Ok(_) => panic!("expected an I/O error, got a connection"), + } + } + + /// Handshake with a localhost server, verifying it as `name` under `config`. The server + /// ignores a handshake the client gives up on. + fn handshake_as( + name: &str, + config: Arc, + ) -> Result { + let (port, server) = serve_one(server_config(), |mut stream| { + let _ = finish_handshake(&mut stream); + }); + let stream = TcpStream::connect(("localhost", port)).unwrap(); + let name = crate::tls::server_name(name).unwrap(); + let result = Connection::new_tls(stream, config, name, Deadline::after_nanos(LONG)?); + server.join().unwrap(); + result + } + + #[test] + fn tls_connect_trusts_added_roots_and_exchanges_data() { + let (port, server) = serve_one(server_config(), |mut stream| { + let mut line = Vec::new(); + while line.last() != Some(&b'\n') { + let mut byte = [0]; + stream.read_exact(&mut byte).unwrap(); + line.push(byte[0]); + } + stream.write_all(b"you said ").unwrap(); + stream.write_all(&line).unwrap(); + stream.flush().unwrap(); + }); + let mut connection = connect_tls("localhost".to_string(), port, LONG, &pem()).unwrap(); + connection.write(b"hello\n", LONG).unwrap(); + assert_eq!(line(&mut connection, NO_LIMIT).unwrap(), b"you said hello"); + server.join().unwrap(); + } + + #[test] + fn tls_connect_names_the_failure() { + let config = |trust: &Trust| crate::tls::client_config(trust).unwrap(); + // A self-signed certificate is not issued by any bundled root. + assert_eq!( + tls_failure(handshake_as("localhost", config(&Trust::Default))), + TlsFailure::UnknownIssuer + ); + // The certificate names only `localhost`; the name verified is the one given. + assert_eq!( + tls_failure(handshake_as("127.0.0.1", config(&pem()))), + TlsFailure::NameMismatch + ); + // The certificate is valid until 2036. + const JANUARY_2040: u64 = 2_208_988_800; + assert_eq!( + tls_failure(handshake_as("localhost", config_at(&pem(), JANUARY_2040))), + TlsFailure::Expired + ); + // Accepting any certificate accepts each of these. + let any = config(&Trust::DangerAcceptInvalidCerts); + assert!(handshake_as("127.0.0.1", any.clone()).is_ok()); + assert!(handshake_as( + "localhost", + config_at(&Trust::DangerAcceptInvalidCerts, JANUARY_2040) + ) + .is_ok()); + } + + #[test] + fn bad_pem_fails_before_connecting() { + // Nothing listens on a port that was just released. + let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap(); + let port = listener.local_addr().unwrap().port(); + drop(listener); + let junk = Trust::AddPem(b"junk".to_vec()); + assert!(matches!( + tls_failure(connect_tls("127.0.0.1".to_string(), port, LONG, &junk)), + TlsFailure::BadPem(_) + )); + // Not even an expired deadline is consulted first. + assert!(matches!( + tls_failure(connect_tls("127.0.0.1".to_string(), port, 0, &junk)), + TlsFailure::BadPem(_) + )); + } + + #[test] + fn insecure_mode_still_checks_handshake_signatures() { + let any = Trust::DangerAcceptInvalidCerts; + let (port, server) = serve_one(mismatched_key_server_config(), |mut stream| { + let _ = finish_handshake(&mut stream); + }); + assert!(matches!( + tls_failure(connect_tls("localhost".to_string(), port, LONG, &any)), + TlsFailure::Other(_) + )); + server.join().unwrap(); + + let (port, server) = serve_one(server_config(), |mut stream| { + finish_handshake(&mut stream).unwrap(); + }); + assert!(connect_tls("localhost".to_string(), port, LONG, &any).is_ok()); + server.join().unwrap(); + } + + /// A server that accepts one connection and says nothing until told to go. + fn silent_server() -> (u16, mpsc::Sender<()>, thread::JoinHandle>) { + let listener = TcpListener::bind(("localhost", 0)).unwrap(); + let port = listener.local_addr().unwrap().port(); + let (go, wait) = mpsc::channel(); + let server = thread::spawn(move || { + let (mut socket, _) = listener.accept().unwrap(); + let _ = wait.recv_timeout(Duration::from_secs(3)); + socket.set_nonblocking(true).unwrap(); + let mut received = vec![0; 4096]; + let count = socket.read(&mut received).unwrap_or(0); + received.truncate(count); + received + }); + (port, go, server) + } + + #[test] + fn tls_handshake_respects_the_connect_deadline() { + let (port, go, server) = silent_server(); + let started = Instant::now(); + assert_eq!( + connect_kind(connect_tls("localhost".to_string(), port, SHORT, &pem())), + io::ErrorKind::TimedOut + ); + assert!(started.elapsed() < Duration::from_secs(1)); + go.send(()).unwrap(); + // The client did start the handshake. + assert!(!server.join().unwrap().is_empty()); + } + + #[test] + fn tls_handshake_fails_when_the_server_closes_the_connection() { + let listener = TcpListener::bind(("localhost", 0)).unwrap(); + let port = listener.local_addr().unwrap().port(); + let server = thread::spawn(move || { + let (mut socket, _) = listener.accept().unwrap(); + // Read the ClientHello first: closing with it unread would reset the connection + // instead of ending it. + let mut hello = [0; 4096]; + assert_ne!(socket.read(&mut hello).unwrap(), 0); + }); + let started = Instant::now(); + match tls_failure(connect_tls("localhost".to_string(), port, LONG, &pem())) { + TlsFailure::Other(message) => assert!(message.contains("closed the connection")), + other => panic!("expected Other, got {other:?}"), + } + assert!(started.elapsed() < Duration::from_secs(5)); + server.join().unwrap(); + } + + #[test] + fn tls_handshake_is_interrupted() { + struct Reset; + impl Drop for Reset { + fn drop(&mut self) { + crate::signals::set_test_pending(false); + } + } + let (port, go, server) = silent_server(); + let deadline = Deadline::after_nanos(LONG).unwrap(); + let stream = connect_until("localhost".to_string(), port, deadline).unwrap(); + let config = crate::tls::client_config(&pem()).unwrap(); + let name = crate::tls::server_name("localhost").unwrap(); + let reset = Reset; + crate::signals::set_test_pending(true); + assert_eq!( + connect_kind(Connection::new_tls(stream, config, name, deadline)), + io::ErrorKind::Interrupted + ); + drop(reset); + go.send(()).unwrap(); + // Nothing was sent: not even the ClientHello. + assert!(server.join().unwrap().is_empty()); + } + + #[test] + fn tls_timed_out_reads_consume_nothing() { + let (mut connection, mut peer) = tls_pair(); + peer.write_all(b"par").unwrap(); + peer.flush().unwrap(); + time_out_until_buffered(&mut connection, b"par"); + + assert_eq!( + until_kind(connection.read_until(b'|', NO_LIMIT, SHORT)), + io::ErrorKind::TimedOut + ); + assert_eq!( + kind(connection.read_exactly(5, SHORT)), + io::ErrorKind::TimedOut + ); + assert_eq!(kind(connection.read_up_to(3, 0)), io::ErrorKind::TimedOut); + assert_eq!(connection.available(), b"par"); + + peer.write_all(b"tial\r\nnext").unwrap(); + peer.flush().unwrap(); + assert_eq!(line(&mut connection, NO_LIMIT).unwrap(), b"partial"); + while connection.available() != b"next" { + assert_eq!( + kind(connection.read_exactly(10, SHORT)), + io::ErrorKind::TimedOut + ); + } + assert_eq!(connection.read_up_to(4, LONG).unwrap().unwrap(), b"next"); + } + + #[test] + fn tls_reads_take_decrypted_data_before_waiting() { + // The server's greeting follows its Finished message in the same flight, so the + // handshake decrypts it. Without session tickets nothing follows on the socket to wake a + // wait. + let mut config = rustls::ServerConfig::clone(&server_config()); + config.send_half_rtt_data = true; + config.send_tls13_tickets = 0; + let (port, server) = serve_one(Arc::new(config), |mut stream| { + stream.conn.writer().write_all(b"greeting\n").unwrap(); + finish_handshake(&mut stream).unwrap(); + // Stay open and silent until the client has read. + let mut byte = [0]; + let _ = stream.read(&mut byte); + }); + let mut connection = connect_tls("localhost".to_string(), port, LONG, &pem()).unwrap(); + assert_eq!( + connection + .read_line(NO_LIMIT, 2_000_000_000) + .unwrap() + .unwrap(), + b"greeting" + ); + drop(connection); + server.join().unwrap(); + } + + #[test] + fn tls_interrupted_reads_preserve_the_prefix() { + struct Reset; + impl Drop for Reset { + fn drop(&mut self) { + crate::signals::set_test_pending(false); + } + } + let (mut connection, mut peer) = tls_pair(); + peer.write_all(b"prefix").unwrap(); + peer.flush().unwrap(); + time_out_until_buffered(&mut connection, b"prefix"); + let reset = Reset; + crate::signals::set_test_pending(true); + assert_eq!( + until_kind(connection.read_line(NO_LIMIT, LONG)), + io::ErrorKind::Interrupted + ); + assert_eq!( + kind(connection.read_exactly(7, LONG)), + io::ErrorKind::Interrupted + ); + assert_eq!( + kind(connection.write(b"not written", LONG)), + io::ErrorKind::Interrupted + ); + assert_eq!(connection.available(), b"prefix"); + drop(reset); + + peer.write_all(b" tail\n").unwrap(); + peer.flush().unwrap(); + assert_eq!(line(&mut connection, NO_LIMIT).unwrap(), b"prefix tail"); + } + + #[test] + fn tls_line_limit_applies() { + let (mut connection, mut peer) = tls_pair(); + peer.write_all(b"too long\nok\n").unwrap(); + peer.flush().unwrap(); + assert!(matches!( + connection.read_line(4, LONG), + Err(ReadUntilError::LimitExceeded) + )); + assert_eq!(line(&mut connection, 4).unwrap(), b"ok"); + } + + #[test] + fn tls_end_of_input_requires_close_notify() { + // A clean close: the data, then the end, as often as it is asked for. + let (mut connection, mut peer) = tls_pair(); + peer.write_all(b"bye").unwrap(); + peer.conn.send_close_notify(); + peer.flush().unwrap(); + drop(peer); + assert_eq!(connection.read_up_to(8, LONG).unwrap().unwrap(), b"bye"); + assert_eq!(connection.read_up_to(8, LONG).unwrap(), None); + assert_eq!(line(&mut connection, NO_LIMIT), None); + + // The TCP connection ends without close_notify: the data may have been cut off. + let (mut connection, mut peer) = tls_pair(); + peer.write_all(b"cut").unwrap(); + peer.flush().unwrap(); + peer.sock.shutdown(Shutdown::Write).unwrap(); + assert_eq!(connection.read_up_to(8, LONG).unwrap().unwrap(), b"cut"); + let truncated = connection.read_up_to(8, LONG).unwrap_err(); + assert_eq!(truncated.kind(), io::ErrorKind::Other); + assert!( + truncated.to_string().contains("without close_notify"), + "{truncated}" + ); + // Not `UnexpectedEof`, which `Tcp.read_exactly!` reports as the end of input. + assert_eq!(kind(connection.read_exactly(1, LONG)), io::ErrorKind::Other); + } + + #[test] + fn dropping_a_tls_connection_sends_close_notify() { + let (mut connection, mut peer) = tls_pair(); + // Take the server's session tickets, so closing does not reset the connection. + assert_eq!( + kind(connection.read_up_to(1, SHORT)), + io::ErrorKind::TimedOut + ); + drop(connection); + let mut received = Vec::new(); + assert_eq!(peer.read_to_end(&mut received).unwrap(), 0); + } + + #[test] + fn tls_failures_cross_as_tls_err() { + let host = crate::resources::make_host(); + + let expired = connect_tls_err(ConnectError::Tls(TlsFailure::Expired), &host); + assert_eq!(expired.tag, TcpErrOrTlsErrTag::TlsErr); + assert_eq!(unsafe { expired.payload.tls_err.tag }, TlsErrTag::Expired); + + let bad_pem = connect_tls_err(ConnectError::Tls(TlsFailure::BadPem("x".into())), &host); + assert_eq!(bad_pem.tag, TcpErrOrTlsErrTag::TlsErr); + let tls_err = unsafe { ManuallyDrop::into_inner(bad_pem.payload.tls_err) }; + assert_eq!(tls_err.tag, TlsErrTag::BadPem); + let message = unsafe { ManuallyDrop::into_inner(tls_err.payload.bad_pem) }; + assert_eq!(message.as_str(), "x"); + unsafe { message.decref(&host) }; + + // Connection failures normalise as for `Tcp.connect!`. + let stalled = connect_tls_err(ConnectError::Io(io::ErrorKind::WouldBlock.into()), &host); + assert_eq!(stalled.tag, TcpErrOrTlsErrTag::TcpErr); + assert_eq!(unsafe { stalled.payload.tcp_err.tag }, IOErrTag::TimedOut); + } } diff --git a/src/tls.rs b/src/tls.rs new file mode 100644 index 00000000..d9ddac81 --- /dev/null +++ b/src/tls.rs @@ -0,0 +1,652 @@ +//! TLS client sessions for `Tcp.connect_tls!` and HTTPS: the trust an app chose, the rustls +//! configurations built for it, the failures a handshake reports, and the non-blocking driving of a +//! session over the buffered socket. +//! +//! A session is driven step by step on the connection's non-blocking socket, so the deadline, +//! signal polling and buffer of `socket.rs` apply unchanged. Plaintext that rustls has already +//! decrypted is always taken before the socket is waited on. +use core::mem::ManuallyDrop; +use std::io::{self, Read, Write}; +use std::sync::{Arc, LazyLock, Mutex, PoisonError}; + +use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; +use rustls::crypto::{ring, CryptoProvider}; +use rustls::pki_types::{pem::PemObject, CertificateDer, ServerName, UnixTime}; +use rustls::time_provider::{DefaultTimeProvider, TimeProvider}; +use rustls::{ + CertificateError, ClientConfig, ClientConnection, DigitallySignedStruct, RootCertStore, + SignatureScheme, +}; + +use crate::roc_platform_abi::*; +use crate::socket::{wait_ready, Deadline}; + +/// `Tls.Trust`, copied out of Roc. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum Trust { + Default, + AddPem(Vec), + DangerAcceptInvalidCerts, +} + +impl Trust { + /// Copy the trust out of its Roc value and release that value. + pub(crate) fn from_roc( + trust: AddPemOrDangerAcceptInvalidCertsOrDefault, + roc_host: &RocHost, + ) -> Self { + let copied = match trust.tag { + AddPemOrDangerAcceptInvalidCertsOrDefaultTag::Default => Self::Default, + AddPemOrDangerAcceptInvalidCertsOrDefaultTag::AddPem => Self::AddPem( + unsafe { trust.borrow_payload_add_pem_unchecked() } + .as_slice() + .to_vec(), + ), + AddPemOrDangerAcceptInvalidCertsOrDefaultTag::DangerAcceptInvalidCerts => { + Self::DangerAcceptInvalidCerts + } + }; + unsafe { trust.decref(roc_host) }; + copied + } +} + +/// `Tls.Err` as the host produces it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum TlsFailure { + UnknownIssuer, + Expired, + NameMismatch, + BadPem(String), + Other(String), +} + +impl TlsFailure { + /// Name the certificate failures Roc can match on; keep rustls's description of the rest. + /// webpki reports the `…Context` spellings, so both forms of each are named. + pub(crate) fn from_rustls(error: &rustls::Error) -> Self { + match error { + rustls::Error::InvalidCertificate(CertificateError::UnknownIssuer) => { + Self::UnknownIssuer + } + rustls::Error::InvalidCertificate( + CertificateError::Expired | CertificateError::ExpiredContext { .. }, + ) => Self::Expired, + rustls::Error::InvalidCertificate( + CertificateError::NotValidForName | CertificateError::NotValidForNameContext { .. }, + ) => Self::NameMismatch, + other => Self::Other(other.to_string()), + } + } + + pub(crate) fn to_roc(&self, roc_host: &RocHost) -> TlsErr { + let message = |text: &str| ManuallyDrop::new(RocStr::from_str(text, roc_host)); + match self { + Self::UnknownIssuer => TlsErr { + payload: TlsErrPayload { unknown_issuer: [] }, + tag: TlsErrTag::UnknownIssuer, + }, + Self::Expired => TlsErr { + payload: TlsErrPayload { expired: [] }, + tag: TlsErrTag::Expired, + }, + Self::NameMismatch => TlsErr { + payload: TlsErrPayload { name_mismatch: [] }, + tag: TlsErrTag::NameMismatch, + }, + Self::BadPem(text) => TlsErr { + payload: TlsErrPayload { + bad_pem: message(text), + }, + tag: TlsErrTag::BadPem, + }, + Self::Other(text) => TlsErr { + payload: TlsErrPayload { + other: message(text), + }, + tag: TlsErrTag::Other, + }, + } + } +} + +/// Why `Tcp.connect_tls!` failed: the connection itself, or TLS. +#[derive(Debug)] +pub(crate) enum ConnectError { + Io(io::Error), + Tls(TlsFailure), +} + +impl From for ConnectError { + fn from(error: io::Error) -> Self { + Self::Io(error) + } +} + +static DEFAULT: LazyLock> = LazyLock::new(|| { + Arc::new( + build_config(&Trust::Default, Arc::new(DefaultTimeProvider)) + .expect("the bundled roots build a configuration"), + ) +}); +static ACCEPT_ANY: LazyLock> = LazyLock::new(|| { + Arc::new( + build_config( + &Trust::DangerAcceptInvalidCerts, + Arc::new(DefaultTimeProvider), + ) + .expect("accepting any certificate builds a configuration"), + ) +}); +/// `AddPem` configurations by their exact bytes, most recently used last. +static WITH_PEM: Mutex, Arc)>> = Mutex::new(Vec::new()); +const CACHED_PEM_CONFIGS: usize = 8; + +/// The configuration for `trust`. Built once per distinct trust and shared, so connections with +/// the same trust share a session cache and can resume. `AddPem` is parsed here, before any I/O. +pub(crate) fn client_config(trust: &Trust) -> Result, TlsFailure> { + let pem = match trust { + Trust::Default => return Ok(default_config()), + Trust::DangerAcceptInvalidCerts => return Ok(ACCEPT_ANY.clone()), + Trust::AddPem(pem) => pem, + }; + let mut cached = WITH_PEM.lock().unwrap_or_else(PoisonError::into_inner); + if let Some(index) = cached.iter().position(|(bytes, _)| bytes == pem) { + let hit = cached.remove(index); + let config = hit.1.clone(); + cached.push(hit); + return Ok(config); + } + let config = Arc::new(build_config(trust, Arc::new(DefaultTimeProvider))?); + if cached.len() == CACHED_PEM_CONFIGS { + cached.remove(0); + } + cached.push((pem.clone(), config.clone())); + Ok(config) +} + +/// `client_config(&Trust::Default)`, which cannot fail. +pub(crate) fn default_config() -> Arc { + DEFAULT.clone() +} + +/// The configuration for `trust` with an explicit clock. Production passes `DefaultTimeProvider`; +/// tests move the clock to reach an expired certificate. +fn build_config(trust: &Trust, time: Arc) -> Result { + // The provider and protocol versions of `hyper_rustls`'s `with_webpki_roots()`. + let provider = Arc::new(ring::default_provider()); + let builder = ClientConfig::builder_with_details(provider.clone(), time) + .with_safe_default_protocol_versions() + .map_err(|error| TlsFailure::Other(error.to_string()))?; + let mut roots = RootCertStore::empty(); + roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + let builder = match trust { + Trust::Default => builder.with_root_certificates(roots), + Trust::AddPem(pem) => { + let mut added = 0; + for certificate in CertificateDer::pem_slice_iter(pem) { + let certificate = certificate + .map_err(|error| TlsFailure::BadPem(format!("invalid PEM: {error}")))?; + added += 1; + roots.add(certificate).map_err(|error| { + TlsFailure::BadPem(format!( + "certificate {added} is not a usable trust anchor: {error}" + )) + })?; + } + if added == 0 { + return Err(TlsFailure::BadPem( + "no CERTIFICATE section found".to_string(), + )); + } + builder.with_root_certificates(roots) + } + Trust::DangerAcceptInvalidCerts => builder + .dangerous() + .with_custom_certificate_verifier(Arc::new(AcceptAnyCertificate(provider))), + }; + Ok(builder.with_no_client_auth()) +} + +/// The name `host` is verified against: a DNS name (also sent as SNI) or an IP address. +pub(crate) fn server_name(host: &str) -> Result, TlsFailure> { + ServerName::try_from(host) + .map(|name| name.to_owned()) + .map_err(|_| { + TlsFailure::Other(format!( + "{host:?} is not a valid name for TLS server verification" + )) + }) +} + +/// The rustls error somewhere in `error`'s chain, looking inside `io::Error` payloads too: +/// `io::Error::source()` skips its own payload, which is where hyper-rustls puts it. +pub(crate) fn rustls_error_in<'a>( + mut error: &'a (dyn std::error::Error + 'static), +) -> Option<&'a rustls::Error> { + loop { + if let Some(found) = error.downcast_ref::() { + return Some(found); + } + let payload = error + .downcast_ref::() + .and_then(io::Error::get_ref) + .map(|inner| inner as &(dyn std::error::Error + 'static)); + error = payload.or_else(|| error.source())?; + } +} + +/// Send what the session owes, as far as the socket takes it now. +fn flush_without_waiting(conn: &mut ClientConnection, socket: &socket2::Socket) -> io::Result<()> { + while conn.wants_write() { + match conn.write_tls(&mut &*socket) { + Ok(_) => {} + Err(error) if error.kind() == io::ErrorKind::WouldBlock => return Ok(()), + Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) => return Err(error), + } + } + Ok(()) +} + +/// Complete the handshake within `deadline`, then flush the client's last flight. +pub(crate) fn handshake( + conn: &mut ClientConnection, + socket: &socket2::Socket, + deadline: Deadline, +) -> Result<(), ConnectError> { + while conn.is_handshaking() || conn.wants_write() { + deadline.remaining()?; + if conn.wants_write() { + match conn.write_tls(&mut &*socket) { + Ok(_) => {} + Err(error) if error.kind() == io::ErrorKind::WouldBlock => { + wait_ready(socket, true, deadline)? + } + Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) => return Err(error.into()), + } + continue; + } + match conn.read_tls(&mut &*socket) { + Ok(0) => { + return Err(ConnectError::Tls(TlsFailure::Other( + "the server closed the connection during the TLS handshake".to_string(), + ))) + } + Ok(_) => { + if let Err(error) = conn.process_new_packets() { + // Let the server see our alert, if the socket takes it now. + let _ = conn.write_tls(&mut &*socket); + return Err(ConnectError::Tls(TlsFailure::from_rustls(&error))); + } + } + Err(error) if error.kind() == io::ErrorKind::WouldBlock => { + wait_ready(socket, false, deadline)? + } + Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) => return Err(error.into()), + } + } + Ok(()) +} + +/// One attempt to read plaintext into `dst` that never waits. `WouldBlock` asks the caller to +/// wait for the socket; `Ok(0)` is the peer's `close_notify`. The caller checks the deadline +/// before each attempt. +pub(crate) fn read( + conn: &mut ClientConnection, + socket: &socket2::Socket, + dst: &mut [u8], +) -> io::Result { + loop { + // Plaintext already decrypted comes first: waiting on the socket while it is here could + // block with data available. + match conn.reader().read(dst) { + Ok(count) => return Ok(count), + Err(error) if error.kind() == io::ErrorKind::WouldBlock => {} + // Not `UnexpectedEof`, which a fixed-size read reports as the end of input. + Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => { + return Err(io::Error::other( + "the peer closed the TLS connection without close_notify; data may be missing", + )) + } + Err(error) => return Err(error), + } + // Send what the session owes (alerts, key update replies) if the socket takes it now. A + // failure here is not fatal: records already received still need processing, and + // `read_tls` reports the socket's state. + let _ = flush_without_waiting(conn, socket); + // Records are processed whatever the socket read gives, so a failed session reports its + // error again on every later read. `Ok(0)` is the end of TCP, which the reader reports. + let received = conn.read_tls(&mut &*socket); + conn.process_new_packets().map_err(|error| { + let _ = flush_without_waiting(conn, socket); + io::Error::other(format!("TLS: {error}")) + })?; + received?; + } +} + +/// Write all of `bytes` within `deadline`. On failure any prefix may have been sent. +pub(crate) fn write_all( + conn: &mut ClientConnection, + socket: &socket2::Socket, + bytes: &[u8], + deadline: Deadline, +) -> io::Result<()> { + let mut accepted = 0; + loop { + deadline.remaining()?; + if accepted < bytes.len() { + // Zero when the session's send buffer is full. + accepted += conn.writer().write(&bytes[accepted..])?; + } + if !conn.wants_write() { + if accepted == bytes.len() { + return Ok(()); + } + continue; + } + match conn.write_tls(&mut &*socket) { + Ok(_) => {} + Err(error) if error.kind() == io::ErrorKind::WouldBlock => { + wait_ready(socket, true, deadline)? + } + Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) => return Err(error), + } + } +} + +/// Queue `close_notify` and write what the socket takes now, without waiting. +pub(crate) fn close(conn: &mut ClientConnection, socket: &socket2::Socket) { + conn.send_close_notify(); + let _ = flush_without_waiting(conn, socket); +} + +/// `DangerAcceptInvalidCerts`: accept any certificate, but still require the handshake to be +/// signed by its key, so the session is encrypted to whoever holds it. +#[derive(Debug)] +struct AcceptAnyCertificate(Arc); + +impl ServerCertVerifier for AcceptAnyCertificate { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp_response: &[u8], + _now: UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls12_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls13_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.0.signature_verification_algorithms.supported_schemes() + } +} + +/// Local TLS servers for the tests of this module, `tcp` and `http`. +#[cfg(test)] +pub(crate) mod testing { + use super::*; + use rustls::pki_types::PrivateKeyDer; + use rustls::server::{ClientHello, ResolvesServerCert}; + use rustls::sign::CertifiedKey; + use rustls::{ServerConfig, ServerConnection, StreamOwned}; + use std::net::{TcpListener, TcpStream}; + use std::thread::{self, JoinHandle}; + use std::time::Duration; + + pub(crate) const LOCALHOST_PEM: &[u8] = include_bytes!("../tests/tls/localhost-cert.pem"); + pub(crate) type ServerStream = StreamOwned; + + fn certificate() -> CertificateDer<'static> { + CertificateDer::from(include_bytes!("../tests/tls/localhost-cert.der").to_vec()) + } + + fn key(der: &[u8]) -> PrivateKeyDer<'static> { + PrivateKeyDer::try_from(der.to_vec()).unwrap() + } + + fn server_builder() -> rustls::ConfigBuilder { + ServerConfig::builder_with_provider(Arc::new(ring::default_provider())) + .with_safe_default_protocol_versions() + .unwrap() + .with_no_client_auth() + } + + /// The localhost certificate and its key. + pub(crate) fn server_config() -> Arc { + let key = key(include_bytes!("../tests/tls/localhost-key.der")); + Arc::new( + server_builder() + .with_single_cert(vec![certificate()], key) + .unwrap(), + ) + } + + /// Presents the localhost certificate but signs with a key that does not match it. + /// `with_single_cert` would refuse the pair, so the resolver hands it over unchecked. + #[derive(Debug)] + struct MismatchedKey(Arc); + impl ResolvesServerCert for MismatchedKey { + fn resolve(&self, _: ClientHello<'_>) -> Option> { + Some(self.0.clone()) + } + } + + pub(crate) fn mismatched_key_server_config() -> Arc { + let signing_key = ring::default_provider() + .key_provider + .load_private_key(key(include_bytes!("../tests/tls/unrelated-key.der"))) + .unwrap(); + let certified = CertifiedKey::new(vec![certificate()], signing_key); + Arc::new(server_builder().with_cert_resolver(Arc::new(MismatchedKey(Arc::new(certified))))) + } + + /// A client configuration for `trust` whose clock reads `unix_seconds`. + pub(crate) fn config_at(trust: &Trust, unix_seconds: u64) -> Arc { + #[derive(Debug)] + struct FixedTime(UnixTime); + impl TimeProvider for FixedTime { + fn current_time(&self) -> Option { + Some(self.0) + } + } + let time = UnixTime::since_unix_epoch(Duration::from_secs(unix_seconds)); + Arc::new(build_config(trust, Arc::new(FixedTime(time))).unwrap()) + } + + /// Accept one client on a fresh port bound to `localhost`, the way the client resolves it + /// (see `connect_timeout_includes_localhost_resolution` in tcp.rs), and hand the server + /// session to `serve` on a thread. The socket blocks, with 5 s timeouts. + pub(crate) fn serve_one( + config: Arc, + serve: impl FnOnce(ServerStream) + Send + 'static, + ) -> (u16, JoinHandle<()>) { + let listener = TcpListener::bind(("localhost", 0)).unwrap(); + let port = listener.local_addr().unwrap().port(); + let server = thread::spawn(move || { + let (socket, _) = listener.accept().unwrap(); + socket + .set_read_timeout(Some(Duration::from_secs(5))) + .unwrap(); + socket + .set_write_timeout(Some(Duration::from_secs(5))) + .unwrap(); + serve(StreamOwned::new( + ServerConnection::new(config).unwrap(), + socket, + )); + }); + (port, server) + } + + /// Drive the server's side of the handshake to its end. + pub(crate) fn finish_handshake(stream: &mut ServerStream) -> Result<(), io::Error> { + while stream.conn.is_handshaking() { + stream.conn.complete_io(&mut stream.sock)?; + } + Ok(()) + } + + /// Like `pair()` in tcp.rs: a connected client `Connection` over TLS and the server's end, + /// the handshake done. + pub(crate) fn tls_pair() -> (crate::tcp::Connection, ServerStream) { + let (sender, receiver) = std::sync::mpsc::channel(); + let (port, server) = serve_one(server_config(), move |mut stream| { + finish_handshake(&mut stream).unwrap(); + sender.send(stream).unwrap(); + }); + let stream = TcpStream::connect(("localhost", port)).unwrap(); + let config = client_config(&Trust::AddPem(LOCALHOST_PEM.to_vec())).unwrap(); + let name = server_name("localhost").unwrap(); + let deadline = Deadline::after_nanos(10_000_000_000).unwrap(); + let connection = crate::tcp::Connection::new_tls(stream, config, name, deadline).unwrap(); + let peer = receiver.recv().unwrap(); + server.join().unwrap(); + (connection, peer) + } +} + +#[cfg(test)] +mod tests { + use super::testing::LOCALHOST_PEM; + use super::*; + + #[test] + fn pem_roots_are_parsed_before_any_connection() { + let bad_pem = |pem: &[u8]| match client_config(&Trust::AddPem(pem.to_vec())) { + Err(TlsFailure::BadPem(message)) => message, + other => panic!("expected BadPem, got {:?}", other.map(|_| ())), + }; + assert_eq!(bad_pem(b"junk"), "no CERTIFICATE section found"); + assert!( + bad_pem(b"-----BEGIN CERTIFICATE-----\n!!!\n-----END CERTIFICATE-----\n") + .starts_with("invalid PEM") + ); + // A key alone is not a root, and a certificate that does not parse is not an anchor. + assert_eq!( + bad_pem(include_bytes!("../tests/tls/localhost-key.pem")), + "no CERTIFICATE section found" + ); + assert!( + bad_pem(b"-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----\n") + .starts_with("certificate 1 is not a usable trust anchor") + ); + assert!(client_config(&Trust::AddPem(LOCALHOST_PEM.to_vec())).is_ok()); + } + + #[test] + fn certificate_errors_are_named() { + use rustls::Error::InvalidCertificate; + let named = |error: rustls::Error| TlsFailure::from_rustls(&error); + let now = UnixTime::now(); + assert_eq!( + named(InvalidCertificate(CertificateError::UnknownIssuer)), + TlsFailure::UnknownIssuer + ); + assert_eq!( + named(InvalidCertificate(CertificateError::Expired)), + TlsFailure::Expired + ); + assert_eq!( + named(InvalidCertificate(CertificateError::ExpiredContext { + time: now, + not_after: now, + })), + TlsFailure::Expired + ); + assert_eq!( + named(InvalidCertificate(CertificateError::NotValidForName)), + TlsFailure::NameMismatch + ); + assert_eq!( + named(InvalidCertificate( + CertificateError::NotValidForNameContext { + expected: server_name("localhost").unwrap(), + presented: vec!["DNS:example.com".to_string()], + } + )), + TlsFailure::NameMismatch + ); + // Anything else keeps rustls's description. + let other = rustls::Error::InvalidCertificate(CertificateError::NotValidYet); + assert_eq!(named(other.clone()), TlsFailure::Other(other.to_string())); + assert_eq!( + server_name("not a host name"), + Err(TlsFailure::Other( + "\"not a host name\" is not a valid name for TLS server verification".to_string() + )) + ); + } + + #[test] + fn rustls_errors_are_found_inside_io_errors() { + // How hyper-rustls reports a failed handshake to hyper. + let wrapped = io::Error::other(io::Error::new( + io::ErrorKind::InvalidData, + rustls::Error::InvalidCertificate(CertificateError::UnknownIssuer), + )); + assert_eq!( + rustls_error_in(&wrapped), + Some(&rustls::Error::InvalidCertificate( + CertificateError::UnknownIssuer + )) + ); + assert_eq!(rustls_error_in(&io::Error::other("no TLS here")), None); + } + + #[test] + fn configs_are_shared_per_trust() { + let config = |trust: &Trust| client_config(trust).unwrap(); + let pem = Trust::AddPem(LOCALHOST_PEM.to_vec()); + assert!(Arc::ptr_eq(&config(&pem), &config(&pem))); + assert!(Arc::ptr_eq( + &config(&Trust::Default), + &config(&Trust::Default) + )); + assert!(Arc::ptr_eq( + &config(&Trust::DangerAcceptInvalidCerts), + &config(&Trust::DangerAcceptInvalidCerts) + )); + // Different bytes are a different trust, even when they hold the same certificate. + let mut repeated = LOCALHOST_PEM.to_vec(); + repeated.extend_from_slice(LOCALHOST_PEM); + assert!(!Arc::ptr_eq( + &config(&pem), + &config(&Trust::AddPem(repeated)) + )); + assert!(!Arc::ptr_eq(&config(&pem), &config(&Trust::Default))); + } +} diff --git a/tests/tls-trust.roc b/tests/tls-trust.roc new file mode 100644 index 00000000..660c41f2 --- /dev/null +++ b/tests/tls-trust.roc @@ -0,0 +1,241 @@ +## Exercise the TLS trust options of `Tcp.connect_tls!` and `Http` against the +## local helper servers of scripts/tls_test_server.py, so that scripts/test.py +## can check each trust choice end to end. The only argument is the PEM file of +## the certificate the helper servers present. +app [main!] { + http: "https://github.com/roc-lang/http/releases/download/1.0.0/6ZUwqYhCS8PU9Mo6MF7oV82ET2o7KYb57CLKDq4cq4sS.tar.zst", + pf: platform "../platform/main.roc", +} + +import pf.Duration +import pf.Http +import pf.OsStr +import pf.Path +import pf.Stdout +import pf.Tcp +import pf.Tls +import http.Request +import http.Response + +check : Bool, Str -> Try({}, [CheckFailed(Str)]) +check = |condition, message| if (condition) Ok({}) else Err(CheckFailed(message)) + +main! : List(OsStr) => Try({}, _) +main! = |args| { + ca = match args { + [ca_file] => Path.from_os_str(ca_file).read_bytes!()? + _ => { + return Err(MissingCaFile) + } + } + + tcp_trust!(ca)? + Stdout.line!("tcp trust: ok")? + tcp_bad_pem!(ca)? + Stdout.line!("tcp bad pem before any network I/O: ok")? + tcp_streams!(ca)? + Stdout.line!("tcp reads over tls: ok")? + http_trust!(ca)? + Stdout.line!("http trust: ok")? + http_redirects!(ca)? + Stdout.line!("http redirects use the trust on every hop: ok")? + http_streaming!(ca)? + Stdout.line!("http streaming trust: ok")? + http_bad_pem!()? + Stdout.line!("http bad pem before any network I/O: ok") +} + +## Connect to the helper's echo server on `host` and port 8443. +connect! : Str, Tls.Trust => Try(Tcp.Connection, [Interrupted, TlsErr(Tls.Err), TcpErr(_)]) +connect! = |host, trust| Tcp.connect_tls!(host, 8443, { connect_timeout: Duration.seconds(5), io_timeout: Duration.seconds(5), tls: { trust } }) + +## Connect and send one line through the echo server. +echoes! : Str, Tls.Trust => Try(Str, _) +echoes! = |host, trust| { + conn = connect!(host, trust)? + conn.write_utf8!("ping\n")? + conn.read_line!() +} + +tcp_trust! : List(U8) => Try({}, _) +tcp_trust! = |ca| { + # The private authority is trusted only when it is added. + check(echoes!("localhost", AddPem(ca)) == Ok("ping"), "AddPem echoes")? + check(echoes!("localhost", Default) == Err(TlsErr(UnknownIssuer)), "Default rejects the private certificate")? + check(echoes!("localhost", DangerAcceptInvalidCerts) == Ok("ping"), "DangerAcceptInvalidCerts echoes")? + + # The certificate names `localhost` only, so an address is a mismatch + # unless every certificate is accepted. + check(echoes!("127.0.0.1", AddPem(ca)) == Err(TlsErr(NameMismatch)), "AddPem still checks the name")? + check(echoes!("127.0.0.1", DangerAcceptInvalidCerts) == Ok("ping"), "DangerAcceptInvalidCerts accepts any name")? + + # The default options trust nothing but the bundled roots. + defaults = Tcp.connect_tls!("localhost", 8443, { connect_timeout: Duration.seconds(5), io_timeout: Duration.seconds(5) }).map_ok(|_| {}) + check(defaults == Err(TlsErr(UnknownIssuer)), "omitted tls is Default")? + check(Tls.Err.to_str(UnknownIssuer) != "", "UnknownIssuer describes itself") +} + +tcp_bad_pem! : List(U8) => Try({}, _) +tcp_bad_pem! = |_ca| { + bad = "this is not a PEM file".to_utf8() + empty_section = "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n".to_utf8() + + # The helper is listening, and nothing is sent to it. + check(rejects_roots!(AddPem(bad)), "BadPem for text")? + check(rejects_roots!(AddPem([])), "BadPem for no bytes")? + check(rejects_roots!(AddPem(empty_section)), "BadPem for a certificate section without a certificate")? + + # Nothing listens on port 1. A connection attempt would be refused, so + # `BadPem` shows that the roots are rejected before any network I/O. + options = { connect_timeout: Duration.seconds(5), io_timeout: Duration.seconds(5), tls: { trust: AddPem(bad) } } + unreachable = match Tcp.connect_tls!("127.0.0.1", 1, options) { + Err(TlsErr(BadPem(_))) => True + _ => False + } + check(unreachable, "BadPem before connecting")? + plain = Tcp.connect!("127.0.0.1", 1, { connect_timeout: Duration.seconds(5), io_timeout: Duration.seconds(5) }).map_ok(|_| {}) + check(plain == Err(TcpErr(ConnectionRefused)), "port 1 refuses connections") +} + +## Whether connecting to the echo server fails with a described `BadPem`. +rejects_roots! : Tls.Trust => Bool +rejects_roots! = |trust| + match connect!("localhost", trust) { + Err(TlsErr(BadPem(message))) => message != "" + _ => False + } + +tcp_streams! : List(U8) => Try({}, _) +tcp_streams! = |ca| { + conn = connect!("localhost", AddPem(ca))? + + # Lines, partial lines and bytes arrive as over plain TCP. + conn.write_utf8!("first\nsecond\npart")? + check(conn.read_line!() == Ok("first"), "read_line")? + check(conn.read_until!('\n') == Ok("second\n".to_utf8()), "read_until")? + conn.write_utf8!("ial\n")? + check(conn.read_line!() == Ok("partial"), "a line split across writes")? + + conn.write_bytes!([0, 255, 128, 10])? + check(conn.read_exactly!(3) == Ok([0, 255, 128]), "read_exactly")? + check(conn.read_up_to!(1) == Ok([10]), "read_up_to")? + + # Nothing more was sent, so a short timeout expires and consumes nothing. + idle = conn.with_timeout(Duration.millis(50)).read_line!() + check(idle == Err(TcpErr(TimedOut)), "idle read times out")? + + conn.write_utf8!("a\nb\nc\n")? + var $seen = [] + for! item in conn.lines() { + match item { + Ok(line) => { + $seen = $seen.append(line) + } + Err(_) => { + return Err(CheckFailed("lines item")) + } + } + if $seen.len() == 3 { + break + } + } + check($seen == ["a", "b", "c"], "lines")? + + # 64 KiB spans several TLS records in each direction. + big = Str.repeat("0123456789abcdef", 4096) + conn.write_utf8!("${big}\n")? + check(conn.read_line!() == Ok(big), "a 64 KiB line") +} + +get : Str -> Request +get = |path| Request.from_method(GET).with_uri("https://localhost:9443${path}") + +text : Response -> Str +text = |response| Str.from_utf8_lossy(Response.body(response)) + +http_trust! : List(U8) => Try({}, _) +http_trust! = |ca| { + trusted = Http.send!(get("/hello"), { tls: { trust: AddPem(ca) } })? + check(Response.status(trusted) == 200 and text(trusted) == "ok", "AddPem fetches /hello")? + + missing = Http.send!(get("/missing"), { tls: { trust: AddPem(ca) } })? + check(Response.status(missing) == 404 and text(missing) == "not found", "AddPem reaches a 404")? + + untrusted = Http.send!(get("/hello"), {}).map_ok(|_| {}) + check(untrusted == Err(HttpErr(TlsErr(UnknownIssuer))), "Default rejects the private certificate")? + explicit_default = Http.send!(get("/hello"), { tls: { trust: Default } }).map_ok(|_| {}) + check(explicit_default == Err(HttpErr(TlsErr(UnknownIssuer))), "Default is the same as omitting tls")? + + accepted = Http.send!(get("/hello"), { tls: { trust: DangerAcceptInvalidCerts } })? + check(Response.status(accepted) == 200 and text(accepted) == "ok", "DangerAcceptInvalidCerts fetches /hello")? + body = Http.get_utf8!("https://localhost:9443/hello", { tls: { trust: DangerAcceptInvalidCerts } })? + check(body == "ok", "get_utf8!")? + + # The certificate is for `localhost`, so the address does not match. + by_address = Request.from_method(GET).with_uri("https://127.0.0.1:9443/hello") + mismatch = Http.send!(by_address, { tls: { trust: AddPem(ca) } }).map_ok(|_| {}) + check(mismatch == Err(HttpErr(TlsErr(NameMismatch))), "AddPem still checks the name") +} + +http_redirects! : List(U8) => Try({}, _) +http_redirects! = |ca| { + # Redirects are opt-in, and the first response needs the trust as well. + first = Http.send!(get("/redirect"), { tls: { trust: AddPem(ca) } })? + check(Response.status(first) == 302, "the redirect is not followed by default")? + + # The second request is a new TLS connection that must use the same trust. + followed = Http.send!(get("/redirect"), { max_redirects: 1, tls: { trust: AddPem(ca) } })? + check(Response.status(followed) == 200 and text(followed) == "ok", "AddPem on both hops")? + + accepted = Http.send!(get("/redirect"), { max_redirects: 1, tls: { trust: DangerAcceptInvalidCerts } })? + check(Response.status(accepted) == 200 and text(accepted) == "ok", "DangerAcceptInvalidCerts on both hops")? + + untrusted = Http.send!(get("/redirect"), { max_redirects: 1 }).map_ok(|_| {}) + check(untrusted == Err(HttpErr(TlsErr(UnknownIssuer))), "Default rejects the first hop")? + + # The redirect leads to another origin: the same server by address, which the + # certificate does not name. Trust is checked again on that hop and not + # inherited from the first. + cross = Http.send!(get("/redirect-cross"), { max_redirects: 1, tls: { trust: AddPem(ca) } }).map_ok(|_| {}) + check(cross == Err(HttpErr(TlsErr(NameMismatch))), "AddPem checks the name on a cross-origin hop")? + cross_accepted = Http.send!(get("/redirect-cross"), { max_redirects: 1, tls: { trust: DangerAcceptInvalidCerts } })? + check(Response.status(cross_accepted) == 200 and text(cross_accepted) == "ok", "DangerAcceptInvalidCerts on a cross-origin hop") +} + +http_streaming! : List(U8) => Try({}, _) +http_streaming! = |ca| { + response = Http.send_streaming!(get("/redirect"), { max_redirects: 1, tls: { trust: AddPem(ca) } })? + check(response.status() == 200, "streamed status after the redirect")? + body = response.read_to_end!(NoLimit)? + check(body == "ok".to_utf8(), "streamed body")? + + untrusted = Http.send_streaming!(get("/hello"), {}).map_ok(|_| {}) + check(untrusted == Err(HttpErr(TlsErr(UnknownIssuer))), "Default rejects a streamed request")? + + accepted = Http.send_streaming!(get("/hello"), { tls: { trust: DangerAcceptInvalidCerts } })? + check(accepted.read_to_end!(NoLimit) == Ok("ok".to_utf8()), "DangerAcceptInvalidCerts streams") +} + +http_bad_pem! : () => Try({}, _) +http_bad_pem! = || { + # Nothing listens on port 1; the roots are rejected before a connection. + request = Request.from_method(GET).with_uri("https://127.0.0.1:1/") + sent = match Http.send!(request, { tls: { trust: AddPem("x".to_utf8()) } }) { + Err(HttpErr(TlsErr(BadPem(_)))) => True + _ => False + } + check(sent, "send! with a bad PEM")? + streamed = match Http.send_streaming!(request, { tls: { trust: AddPem([]) } }) { + Err(HttpErr(TlsErr(BadPem(_)))) => True + _ => False + } + check(streamed, "send_streaming! with a bad PEM")? + + # With the default roots a connection is attempted, and it fails. + refused = match Http.send!(request, {}) { + Err(HttpErr(Other(_))) => True + Err(HttpErr(NetworkError)) => True + _ => False + } + check(refused, "port 1 refuses connections") +} diff --git a/tests/tls/README.md b/tests/tls/README.md index e2920a8c..0b28505c 100644 --- a/tests/tls/README.md +++ b/tests/tls/README.md @@ -9,3 +9,13 @@ Generated with OpenSSL `req -x509 -newkey rsa:2048 -nodes -subj /CN=localhost -addext keyUsage=critical,digitalSignature,keyEncipherment -addext extendedKeyUsage=serverAuth -days 3650`, then converted with `x509 -outform DER` and `pkcs8 -topk8 -nocrypt -outform DER`. + +`localhost-cert.pem` and `localhost-key.pem` are PEM forms of the DER files, +loaded by `scripts/tls_test_server.py`: `openssl x509 -inform DER -in +localhost-cert.der -out localhost-cert.pem` and `openssl pkcs8 -topk8 -nocrypt +-inform DER -in localhost-key.der -out localhost-key.pem`. + +`unrelated-key.der` is an RSA key that does not match the certificate. It checks +that a client which accepts any certificate still verifies handshake signatures. +Generated with `openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 | +openssl pkcs8 -topk8 -nocrypt -outform DER`. diff --git a/tests/tls/localhost-cert.pem b/tests/tls/localhost-cert.pem new file mode 100644 index 00000000..58ba1baa --- /dev/null +++ b/tests/tls/localhost-cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDQzCCAiugAwIBAgIUUtCydS8G1oAiieyIJqmPaOOMroYwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MTAxMjA1MzcxMFoXDTM2MTAw +OTA1MzcxMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAwfsWjALxTLiwSBVdZJkPsZ5GTaL4dP9nrdT4CrAKUXy3 +SdzpdDkqowelPyhmk4Y3lHC8fIRoSWnjs2z2+DRRmgC+lHda04byGl04mWu/uzSD +JF/Bpc6/aN2Hw329kXxwrqnADSMuNH5QBI8BOzJYC6hZ2QW84X7GTNBQv6kaYOAS +ZO3oDJFR9fY6HFzvsd9UW0H6RAfoEvqtgDbdhUMM3efslu8pWCS0ZiqRLXfJApZD +Jcx7PwLKI/6Hu0edk5cK21unfhzQfIiuMkxxvXcNBFIjG/H3eBYoilpDFjeFrmCc +KlPpb/ZBrdqawlYKQo/SJ39JhDbq5VxDUrjXzql1twIDAQABo4GMMIGJMB0GA1Ud +DgQWBBSkvI0HWkRUDRxbocwZBsU9vRMCSjAfBgNVHSMEGDAWgBSkvI0HWkRUDRxb +ocwZBsU9vRMCSjAUBgNVHREEDTALgglsb2NhbGhvc3QwDAYDVR0TAQH/BAIwADAO +BgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQEL +BQADggEBAJXIuHZwvk/pUWungm3qDybLp1yc1KOuIaqf4qAqFfAhe4cbShVcjY6C +VuOhAU33j8lEEDYq8+LCaOqp/l3NqXbtaWBK9tu6IuacMD5igOtlIt9Ha0XG30DU +Me5UFzXmphWbH9/2ybXLNlH+NiCC1/xx8Ew+eciGrar/iocHZMcXRvnXgoKOe+U3 +8WPoBo0y4IgI0K4PTYiPG2MDNxsJpnmXUWwiVzDAtBotoHI+MaUnIbVmSQ0Eiisp +sbtUybB1tSNhFB7KkQSVmkmEdVtPbX+8Bs7RXr22dHHRB81BIfbj7j8Mem/dXaAL +/UL9ZS4yvxwgAgaxgf9y/fyG/Jv5CVE= +-----END CERTIFICATE----- diff --git a/tests/tls/localhost-key.pem b/tests/tls/localhost-key.pem new file mode 100644 index 00000000..11e0ca90 --- /dev/null +++ b/tests/tls/localhost-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDB+xaMAvFMuLBI +FV1kmQ+xnkZNovh0/2et1PgKsApRfLdJ3Ol0OSqjB6U/KGaThjeUcLx8hGhJaeOz +bPb4NFGaAL6Ud1rThvIaXTiZa7+7NIMkX8Glzr9o3YfDfb2RfHCuqcANIy40flAE +jwE7MlgLqFnZBbzhfsZM0FC/qRpg4BJk7egMkVH19jocXO+x31RbQfpEB+gS+q2A +Nt2FQwzd5+yW7ylYJLRmKpEtd8kClkMlzHs/Asoj/oe7R52TlwrbW6d+HNB8iK4y +THG9dw0EUiMb8fd4FiiKWkMWN4WuYJwqU+lv9kGt2prCVgpCj9Inf0mENurlXENS +uNfOqXW3AgMBAAECggEAPXDgRVyxPVHKVCqgODra47F0JycVd0lmd4LbRddYjVLp +YzXbo/pvbNABIS4Uv2lltVi3+uCdf5NQkgZTe25m4laWBsR/Ojyl9n/Ma5vbUNpj +J2RFG9WOfxKb6+mp36kAr/IIaKDaA7p2cdsx0qQxiw+51amhl/LlsHLTAMH5HvGM +gH+DuvN+/kwT93QqREyRV0BsuXcFbO11G+QUIJsdcsJ9eaUsnnoNmWGycCC54k7e +UJQD9sY0ph9LdjdjP1T6/tm8KE78vEN8nNG3Pid1LdE6eglE5uc/K95kxiUM7Xza +uGER/JE1A/rMjB0kJ0WUUQRY/K9ZJJA26ESgeR/xgQKBgQDxpASKfiymZ1q3xHi3 +Wy/qmkoJ4JdFr1jBv+0y3wd0HZPubAuRKFLvgNX6d4gbmHRd5s+pwp9w2Ue44JNS +2nBblWOHUgjAg922sXFEaF/B54rhl84ZnSGanY/vnrNLiCnZs0w1Un7aA7OTGrCZ +26ozGqWMRNIqrGo2wHNOdVS8FwKBgQDNggoxJzQhNmqLpdtFIlXbtY0p6LWJszEH +IzSHPxFR48XR3f/oJ9ghvhjHIW/8TkawMrNCkOhNILwRHG0wUpcnZRZzkIzcD3Sq +/Pej01fsp1ekbsIuT56rVjb34N0bdYozC9zIGS9DsCxNP6+yTV8tKeXf3WUGOSzo +IotH0Qz3YQKBgB9r3pNLuO6ife9rgu8E0oRFL9987u7G0FnRZTy6N7PnrLbSIebs +QWyXaju5k19TQb58JwDCKA16k5L80OtzMC0Yjeqoxi6SdyAsjrgAgoMqYqrXHJvE +DPyYBIF6N4JY4GhIol5Ere4XkSBN1i0LnWQr0N8ei920sdMQz22iYDRdAoGBAI0Q +qGJZIMVzjutQ7BSmnvzzwHR2Erb008Csf9AekIr/qLiIhLMdNpTi4is+Xpp4OLmV +88rLoUngIHg2ZRT4fF1EXIbzQF2GFnWQs4Q2/HGr5NMwXAZiDO5bfdvk9KF2q2ii +yehosTLraV2VS8s+pQhhbDOgy0ncUWD+5VBkxVvBAoGBANTZYLCUfdW2iKPLigDf +vMMnyGBYL+dZC+4Ib4nUCtGwv9nUTQC59C7sQAvs0E7Ceg1zrCL5LrWxVos9QPhI ++EHaD6gSBGQ6JbM3AOlQebe0fnWrVjvD6am2yv6Ei6+/5v39u1Vq3MhdNx0UKgy4 +az7IWOTq2YUDbPqYoqFjavJO +-----END PRIVATE KEY----- diff --git a/tests/tls/unrelated-key.der b/tests/tls/unrelated-key.der new file mode 100644 index 00000000..cedde384 Binary files /dev/null and b/tests/tls/unrelated-key.der differ