Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Send HTTPS requests

std.http.https gives the HTTP client https URLs. It runs TLS on each connection, checking that the server’s certificate chains to a trusted root and names the host in the URL. Everything else is the same client: pooled connections, deadlines, redirects and replays.

This program serves HTTPS on a loopback port with std.http.tls and sends it two requests from another thread. The second streams its body from a reader instead of holding it in the request. Like the TLS lesson, it embeds a test CA so that it runs offline.

import std.bytes
import std.http
import std.http.client
import std.http.https
import std.http.server
import std.http.tls
import std.io
import std.net
import std.time
import std.tls
from std.http.server import Exchange
from std.http.tls import TlsTransport
from std.sync import CancellationToken

# Test-only certificates: a CA, and a leaf it signed for localhost and
# 127.0.0.1. Never use a published key outside an example.
def ca_pem() -> str:
    return "-----BEGIN CERTIFICATE-----\nMIIBmjCCAT+gAwIBAgIULOQ2Qxr8CVZY2bG0uRYKKEdMD3swCgYIKoZIzj0EAwIw\nGTEXMBUGA1UEAwwOUGxlbnR5IFRlc3QgQ0EwIBcNMjQwMTAxMDAwMDAwWhgPMjEy\nNDAxMDEwMDAwMDBaMBkxFzAVBgNVBAMMDlBsZW50eSBUZXN0IENBMFkwEwYHKoZI\nzj0CAQYIKoZIzj0DAQcDQgAEkiB3Ogehrrt6YbQghwDPv6tkhE+z9zTCVih8fvtR\nZ1/691Nfh7/J9IgaNgNSTMJjGTolzGnjaCUDqoARlFyM+6NjMGEwHQYDVR0OBBYE\nFOTFBDgWXHj/qmVJDMlAHYCEZFX5MB8GA1UdIwQYMBaAFOTFBDgWXHj/qmVJDMlA\nHYCEZFX5MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgIEMAoGCCqGSM49\nBAMCA0kAMEYCIQD41ATQnIjKkKMLQAb9AyoyOFmljYZyz348XJEIbqMgKwIhAL3m\nmWd8On5gvkfytspatH5mDWOcSyW+au7vuIC2gHhO\n-----END CERTIFICATE-----\n"

def leaf_pem() -> str:
    return "-----BEGIN CERTIFICATE-----\nMIIBnDCCAUKgAwIBAgIBATAKBggqhkjOPQQDAjAZMRcwFQYDVQQDDA5QbGVudHkg\nVGVzdCBDQTAgFw0yNDAxMDEwMDAwMDBaGA8yMTI0MDEwMTAwMDAwMFowFDESMBAG\nA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8THNQeTi\nJ6Eq0tSxtHW5Q4TCsbIqB3MOtM98ERyKgl6kHeHCeDZYKx8B+VRGT2jkWuTeN24Q\nljwUfh7uh7Iaz6N+MHwwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMAkGA1Ud\nEwQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHQYDVR0OBBYEFEIFC2Fyd4cfAKTx\nMiNCMdDkqDNbMB8GA1UdIwQYMBaAFOTFBDgWXHj/qmVJDMlAHYCEZFX5MAoGCCqG\nSM49BAMCA0gAMEUCIAUF3HH43lnirgSN1Vr4twZrFmXKBxlbYNQni/h4ZaxhAiEA\n5rD/yE1B4IsbEGCMlyAeX507leXfGY9+wrHaxikU/w0=\n-----END CERTIFICATE-----\n"

def leaf_key() -> str:
    return "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQglvSEE3H7v+mInxSF\nAyWFDKYpzpXr20wH8mfFY3bCaB2hRANCAATxMc1B5OInoSrS1LG0dblDhMKxsioH\ncw60z3wRHIqCXqQd4cJ4NlgrHwH5VEZPaORa5N43bhCWPBR+Hu6HshrP\n-----END PRIVATE KEY-----\n"

pub enum Route:
    Count

pub class Site:
    pub def handle(self, route: &Route, exchange: &mut Exchange[TlsTransport]) -> Result[(), AllocError | std.http.HttpError | std.bytes.BytesError]:
        mut body = std.bytes.empty()?
        exchange.read_all(&mut body)?
        exchange.send_text(200, &f"{body.len()} bytes"?)?
        Ok(())

type VisitError = AllocError | IoError | std.bytes.BytesError | std.http.client.ClientError | std.tls.TlsError | RangeError

def visit(port: u16, stop: CancellationToken) -> Result[(), VisitError]:
    with stop.cancel_on_exit():
        never = CancellationToken()?
        deadline = std.time.deadline_after(&std.time.monotonic_now(), &std.time.seconds(5u32))?
        base = f"https://localhost:{port}"?
        roots = std.tls.roots_from_pem(&ca_pem())?
        tls = std.tls.client_config(&roots)?
        client = std.http.https.client(std.http.client.config()?, tls)?

        home = client.get(&f"{base}/"?, &deadline, &never)?
        print(home.status, home.body.to_text()?)?

        mut source = std.io.cursor(std.bytes.from_text("a body read in pieces")?)
        request = std.http.client.request("PUT", &f"{base}/upload"?)?
        mut response = client.send_from(request, &mut source, &std.http.client.chunked(), &deadline, &never)?
        mut answer = std.bytes.empty()?
        response.read_all(&mut answer)?
        print(response.status, answer.to_text()?)?
        client.finish(response)

        stats = client.stats()
        print(stats.opened, stats.reused)?
    Ok(())

type HttpsSetupError = std.http.server.ServerError | std.net.NetError | std.tls.TlsError

def main() -> Result[(), SpawnError[()] | AllocError | HttpsSetupError | VisitError]:
    mut routes = std.http.server.router[Route]()?
    routes.add("GET", "/", Route.Count)?
    routes.add("PUT", "/upload", Route.Count)?
    listener = std.net.listen(&std.net.loopback_v4(0u16), 16)?
    port = listener.local_address()?.port
    stop = CancellationToken()?
    server = std.tls.server_config(&leaf_pem(), &leaf_key())?
    with spawn(visit, port, stop.share()).drop_job()? as task:
        drop(std.http.tls.serve(listener, server, &routes, &Site(), &std.http.server.config(), &stop)?)
        task.join()?
    Ok(())
200 0 bytes
200 21 bytes
1 1

std.http.https.client takes the client config and a TLS ClientConfig. The client uses the URL’s host, here localhost, as the name the certificate must carry. Verification cannot be turned off: to trust a private server, build the ClientConfig from its CA as this program does, and use std.http.https.system_client for public servers. A certificate from an unknown CA fails with Tls(UntrustedCertificate), and one for another host with Tls(WrongHost). Both requests used one TLS connection.

send_from reads the body from any std.io reader in 16 KiB pieces, so a large upload never sits in memory whole. chunked() sends it in chunks of unknown total length; sized(n) sends Content-Length: n and fails with WrongBodyLength if the reader yields a different amount. Setting expect_continue on the upload asks the server to approve the request before the body is sent.

A streamed body can be read only once. The client still replays a request on a fresh connection when a pooled one was closed before any of the body was read, but it returns a redirect that would need the body again rather than following it.

With config.proxy set, an https request asks the proxy to open a tunnel with CONNECT, then runs TLS to the server through it, so the proxy sees neither the request nor the response.

The HTTPS support is a separate module: a program that imports only std.http.client does not include the TLS library.