Skip to content

Getting Started

This guide introduces the primary components of Wasat and demonstrates how to perform requests, stream response bodies, and handle certificate verification.

All of the public classes, protocols, and exceptions are exposed at the top level of the package. You can import them directly from wasat.

Core Components

The following classes and protocols form the core interface of the library:

  • Client: The asynchronous client used to configure and dispatch requests.
  • Response: Represents the server's response, exposing the target URI (uri), the originally requested URI (requested_uri), redirect history (history), the path to any client certificate used (client_cert_path), whether a client certificate was used (client_cert_used), server TLS certificate details (server_cert, server_cert_der, server_cert_fingerprint), certificate verification method (verification_method), text decoding, and chunked body streaming.
  • ServerCertificate: A high-level representation of a server's TLS certificate providing parsed attributes (e.g. subject, issuer, validity dates, SANs, fingerprint). Access it lazily via response.server_cert.
  • GeminiURI: A utility class to parse, validate, and resolve Gemini URIs safely.
  • StatusCode: An integer enumeration representing the official status codes of the Gemini Protocol, featuring helper properties to categorise statuses.
  • TrustStore: A protocol defining the trust verification interface.
  • FileTrustStore: The default file-based Trust-On-First-Use (TOFU) backend that stores trusted certificate fingerprints.

Basic Request

To execute a request, initialise a Client and use its request method. The client automatically manages connections, TLS negotiation, and redirects.

import asyncio
from wasat import Client, WasatError

async def main():
    # Use "tofu" verification mode for standard self-signed Gemini certificates
    client = Client(verify_mode="tofu")

    try:
        # Perform the request (resolves host, port, TLS, and redirects)
        async with await client.request("gemini://geminiprotocol.net/") as response:
            print(f"Status: {response.status.value} ({response.status.name})")
            print(f"MIME type: {response.mime_type}")

            # Fetch and decode the response body text
            body = await response.text()
            print(body)

    except WasatError as e:
        print(f"Request failed: {e}")

if __name__ == "__main__":
    asyncio.run(main())

Streaming Responses

For large responses or continuous streams, you can read the body incrementally to minimise memory usage. Use the iter_chunks method on the Response object:

import sys
from wasat import Client

async def download_file():
    client = Client(verify_mode="tofu")
    async with await client.request("gemini://example.com/large-file.bin") as response:
        if response.status.is_success:
            async for chunk in response.iter_chunks(chunk_size=1024):
                sys.stdout.buffer.write(chunk)

Custom Trust Verification, TOFU, and Hybrid Mode

Wasat supports multiple certificate verification modes via verify_mode:

  • "hybrid": Combines system CA validation with TOFU fallback. Wasat first attempts to validate certificates using system CAs (e.g. Let's Encrypt). If CA verification fails because the certificate is untrusted or self-signed, it falls back to TOFU validation. If CA verification fails due to certificate expiration, hostname mismatch, or revocation, fallback is denied and a SecurityError is raised immediately.
  • "tofu": Strict Trust-On-First-Use validation using FileTrustStore.
  • "ca": Validates certificates strictly against system CAs.
  • "off": Disables certificate verification.

When a new self-signed certificate is encountered in "tofu" or "hybrid" mode, you can customise the behaviour by providing an asynchronous on_new_certificate callback to the Client:

async def confirm_cert(host: str, port: int, fingerprint: str) -> bool:
    print(f"Encountered a new certificate for {host}:{port}")
    print(f"Fingerprint: sha256:{fingerprint}")
    response = input("Trust this certificate? [y/N]: ").strip().lower()
    return response == "y"

client = Client(
    verify_mode="hybrid",
    on_new_certificate=confirm_cert
)

Client Certificate Authentication

Gemini supports client certificates as a method of user authentication (e.g. for session tracking or user identities). When a server requires a client certificate, it returns a 60 status code (CLIENT_CERTIFICATE_REQUIRED).

Wasat provides a FileClientCertificateStore to manage, scope, and persist generated client certificates.

Automatic Certificate Handling

You can configure the client to automatically prompt your application, generate self-signed certificates, and retry requests when faced with a client certificate requirement:

from typing import Literal
from wasat import Client, GeminiURI, ClientCertificateStore

async def handle_cert_request(
    uri: GeminiURI,
    store: ClientCertificateStore
) -> Literal["transient", "persistent", "ignore"]:
    # Determine whether to generate a transient/persistent certificate, or ignore.
    # Transient certificates are stored in a temporary folder and cleaned up at exit.
    print(f"Server at {uri.host} requested a client certificate.")
    return "transient"

client = Client(
    verify_mode="tofu",
    on_client_certificate_required=handle_cert_request
)

Shared Hosts and Certificate Mapping

On a shared host (like station.martinrue.com), where multiple independent users have their own directories (e.g. /davep and /otheruser), scoping the certificate to the host root / is a privacy risk because other users could request your client certificate. Instead, you should keep the certificate scoped to the specific path.

To reuse the certificate you generated on the sign-up page (e.g., /join) for your home page (e.g., /davep), you can retrieve the existing certificate from the store inside the callback and associate it with the new path using register_credentials:

async def handle_cert_request(uri: GeminiURI, store: ClientCertificateStore) -> str:
    # If visiting your page and the cert is not yet registered, map it from /join
    if uri.host == "station.martinrue.com" and uri.path.startswith("/davep"):
        join_uri = GeminiURI("gemini://station.martinrue.com/join")
        join_creds = await store.get_credentials(join_uri)
        if join_creds is not None:
            # Register the existing /join cert files for /davep
            await store.register_credentials(uri, join_creds[0], join_creds[1])
            return "persistent"

    # Default to generating a new transient cert
    return "transient"

Redirection and Certificate Reuse

When a request is redirected (e.g. from gemini://example.com/join to gemini://example.com/dashboard), and a client certificate was successfully used to authenticate a prior request in the redirect chain, Wasat will automatically retrieve and reuse the same certificate for any subsequent redirect targets on the same host and port.

Additionally, to ensure user session continuity across subsequent visits, once the redirect chain succeeds (returning a success or redirect status code), Wasat will automatically register/re-bind the certificate to the landed URI's scope. This ensures that future direct requests to the target URI will automatically present the certificate without needing to go through the initial redirect flow again.

Manual Certificate Handling

If you do not register the callback, you can manually generate, store, and present certificates inside your application flow:

from wasat import Client, StatusCode

client = Client(verify_mode="tofu")

response = await client.request("gemini://example.com/protected")
if response.status == StatusCode.CLIENT_CERTIFICATE_REQUIRED:
    # Generate a certificate for the host/path scope and save it in the store.
    # Set valid_days=None to create a certificate that expires on 9999-12-31.
    await client.client_cert_store.create_credentials(
        response.uri,
        transient=True,
        common_name="my_identity",
        email="user@example.com",
        valid_days=None
    )

    # Retry the request; the client automatically detects and loads the new cert
    response = await client.request("gemini://example.com/protected")

Exception Hierarchy

All exceptions raised by the library inherit from the base class WasatError. When managing errors, you can catch specific sub-classes for finer control:

  • URIError: Raised when a given URI cannot be parsed or resolved.
  • ProtocolError: Raised when server response headers violate the Gemini protocol specification.
  • ConnectionError: Raised when network connections fail, drop, or time out.
  • SecurityError: Raised when TLS verification fails or a TOFU fingerprint does not match the trust store.
  • RedirectError: Raised when a redirect loop is detected or the maximum redirect limit is exceeded.