Skip to main content

The client

This page shows the HTTP/3 client in Rust, the scion-http3 crate, as the terminal client of the chat uses it. The SCION code of the Rust client is in one file, transport/scion.rs. The mobile clients shows the Android and iOS clients of the chat.

Build the client and send a request​

A Config names the endhost API and the credential. Client::new builds the client from it. The client sends a Request and returns the response. The SDK example http3_get_post.rs shows these steps in one short program.

The chat builds one Client in ScionTransport::new, each time the user connects to a server. The credential is an API key or a SNAP token. An API key goes into an AnapayaAaTokenSource.

A SNAP token expires. A fixed token is enough for a test, but a running app needs new tokens. AnapayaAaTokenSource sends the API key to the Anapaya AA (Authentication and Authorization service) and gets a new token before the current token expires. Access and authentication explains token sources.

To send a request, the transport converts an http::Request into a scion_http3::Request and sends it with Client::request. It collects the body of the response up to a limit.

Trust the server​

The client verifies the certificate of the server. The chat offers three choices, and the transport turns each choice into a QuicConfig:

transport/scion.rs
let mut quic = QuicConfig::builder();

match &config.trust {
Trust::SystemRoots => {}
Trust::Pinned(path) => {
let path = path.to_str().ok_or_else(|| {
ChatError::Config(format!("the certificate path is not utf-8: {path:?}"))
})?;

quic = quic.ca_certs_file(path);
}
Trust::Insecure => {
tracing::warn!(
"certificate verification is off: any server on the path can answer as this \
one"
);
quic = quic.verify_peer(false);
}
}
settings = settings.with_quic_config(quic.build());
  • SystemRoots keeps the default: the root certificates of the system.
  • Pinned trusts one certificate file, for example the self-signed certificate of the chat server.
  • Insecure accepts any certificate. Use it for tests only, because anyone on the path can then answer as the server.

Reach a server without a TSAR record​

The client resolves the host of a URL to a SCION address. It reads a DNS TXT record in the TSAR format (TXT-based SCION address resolution). A server in a local SCION network has no such record. The chat gives the address of such a server as target, and the resolver answers for the host of the server from the configuration:

transport/scion.rs
/// A resolver that answers for the server's host from configuration, and every other host normally.
fn resolver(server_url: &Url, target: &str) -> Result<ScionTxtDnsResolver, ChatError> {
let host = server_url
.host_str()
.ok_or_else(|| ChatError::Config(format!("the server url has no host: {server_url}")))?;
let address = ScionIpAddr::from_str(target).map_err(|error| {
ChatError::Config(format!("the target is not a scion address: {error}"))
})?;

ScionTxtDnsResolver::new()
.map(|resolver| resolver.with_override(host, vec![address]))
.map_err(|error| ChatError::Config(format!("building a resolver failed: {error}")))
}

with_override maps one host to fixed SCION addresses. The resolver looks up every other host in DNS. The address has no port: the port and the name that the certificate must match come from the URL.

Map the errors​

Every failure of the client is a scion_http3::Error. The chat sorts the variants into its own error kinds:

transport/scion.rs
/// Sorts an HTTP/3 failure into the taxonomy every transport reports.
fn failure(error: Error) -> TransportError {
let detail = describe(&error);

match error {
Error::Resolution { .. } => TransportError::Resolution(detail),
Error::Connect { .. } | Error::StackBuild { .. } => TransportError::Connect(detail),
Error::Tls { .. } => TransportError::Tls(detail),
Error::StreamReset { .. } | Error::ConnectionLimit => TransportError::StreamReset(detail),
Error::BodyTooLarge { limit, .. } => TransportError::BodyTooLarge { limit },
Error::Timeout { .. } => TransportError::Timeout,
_ => TransportError::Protocol(detail),
}
}

The TCP transport of the chat reports the same error kinds. Thus the rest of the client handles one set of errors for all transports.

Where to go next​