///|
/// The certificate chain and private key an HTTPS server presents (← uvicorn's `ssl_certfile` and
/// `ssl_keyfile`), as PEM files. The chain file holds the leaf first, then any intermediates; the
/// key file holds a `PRIVATE KEY`, `EC PRIVATE KEY` or `RSA PRIVATE KEY`. The same files serve on
/// every platform: the TLS is moontls, not the system's.
pub(all) struct TlsCert {
certificate_file : String
private_key_file : String
}
///|
/// The application protocols a TLS listener offers, in preference order: HTTP/2 first, as
/// hypercorn and nginx do, falling back to HTTP/1.1.
pub let alpn : Array[String] = ["h2", "http/1.1"]
///|
/// Read the files into a TLS server configuration. A key that is not the leaf's is refused here,
/// at startup, rather than by every client's handshake.
pub async fn TlsCert::load(
self : TlsCert,
alpn~ : Array[String] = alpn,
) -> @tls.Server {
@tls.Server::new(
@x509.pems(@fs.read_file(self.certificate_file).text()),
@x509.PrivateKey::pem(@fs.read_file(self.private_key_file).text()),
alpn~,
)
}
///|
/// Serve a moonasgi ASGI application over native HTTP/1.1 **over TLS** (HTTPS,
/// ← uvicorn's `--ssl-certfile`/`--ssl-keyfile`). Convenience wrapper over
/// `serve_tls_config` that builds the `Config` from `host`/`port`/`backlog` and
/// the certificate paths.
///
/// Runs the full ASGI lifespan protocol around the accept loop (startup before
/// the listener binds, shutdown on exit — even under cancellation), then, for
/// every accepted connection, completes a TLS handshake and serves HTTP/2 when
/// ALPN settles on `h2`, HTTP/1.1 otherwise.
///
/// A WebSocket upgrade on an HTTP/1.1 connection is served too (`wss://`): the
/// 101, the subprotocol echo and the frames all travel over the encrypted stream.
pub async fn serve_tls(
app : @moonasgi.AsgiApp,
certificate_file~ : String,
private_key_file~ : String,
host~ : String = "127.0.0.1",
port~ : Int32 = @config.port,
backlog~ : Int32 = 2048,
) -> Unit {
serve_tls_config(app, @config.Config::new(host~, port~, backlog~), {
certificate_file,
private_key_file,
})
}
///|
/// Serve a moonasgi ASGI application over HTTPS under an explicit `Config` and
/// `TlsCert`. Mirrors `serve_config`: lifespan startup is driven before the
/// listener binds and shutdown on the way out (guarded by `protect_from_cancel`
/// so it still runs when the serving task is cancelled), and each accepted
/// connection completes a TLS handshake within `timeout_handshake` and then runs
/// the same loop as a plain one.
pub async fn serve_tls_config(
app : @moonasgi.AsgiApp,
config : @config.Config,
cert : TlsCert,
) -> Unit {
run(app, config, ShutdownHandle::new(), Some(cert.load()))
}
///|
/// Self-signed localhost material for the integration tests below, generated by
/// `scripts/gen-cert.sh`.
fn test_cert() -> TlsCert {
{ certificate_file: "certs/cert.pem", private_key_file: "certs/key.pem", }
}
///|
/// Real HTTPS round-trip: a genuine `@http` TLS client performs `GET /` over an
/// encrypted connection to a `serve_tls`-served app and observes `200` + the
/// exact body. The client is the system's TLS (OpenSSL or SChannel), so this is
/// moontls against another implementation, through the HTTP/1.1 codec to the app.
async test "https: real TLS client GET returns 200 with body" {
@async.with_task_group(g => {
let app : @moonasgi.AsgiApp = (_scope, _receive, send) => {
send(
@moonasgi.Event::HttpResponseStart(
status=200,
headers=[("content-type", "text/plain")],
trailers=false,
),
)
send(
@moonasgi.Event::HttpResponseBody(
body=b"secure hello over tls",
more_body=false,
),
)
}
let task = g.spawn(() => {
serve_tls_config(app, @config.Config::new(port=18443), test_cert())
})
@async.sleep(400)
let client = @http.Client::Client(
"https://127.0.0.1:18443",
trust=NoVerification,
)
let resp = client.get("/")
assert_eq(resp.code, 200)
assert_eq(client.read_all().text(), "secure hello over tls")
client.close()
task.cancel()
})
}