-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
1,204 additions
and
5 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
## HTTP/0.9 File Serving Example | ||
|
||
The `server` and `client` examples demonstrate fetching files using a HTTP-like toy protocol. | ||
|
||
1. Server (`server.rs`) | ||
|
||
The server listens for any client requesting a file. | ||
If the file path is valid and allowed, it returns the contents. | ||
|
||
Open up a terminal and execute: | ||
|
||
```text | ||
$ cargo run --example server ./ | ||
``` | ||
|
||
2. Client (`client.rs`) | ||
|
||
The client requests a file and prints it to the console. | ||
If the file is on the server, it will receive the response. | ||
|
||
In a new terminal execute: | ||
|
||
```test | ||
$ cargo run --example client https://localhost:4433/Cargo.toml | ||
``` | ||
|
||
where `Cargo.toml` is any file in the directory passed to the server. | ||
|
||
**Result:** | ||
|
||
The output will be the contents of this README. | ||
|
||
**Troubleshooting:** | ||
|
||
If the client times out with no activity on the server, try forcing the server to run on IPv4 by | ||
running it with `cargo run --example server -- ./ --listen 127.0.0.1:4433`. The server listens on | ||
IPv6 by default, `localhost` tends to resolve to IPv4, and support for accepting IPv4 packets on | ||
IPv6 sockets varies between platforms. | ||
|
||
If the client prints `failed to process request: failed reading file`, the request was processed | ||
successfully but the path segment of the URL did not correspond to a file in the directory being | ||
served. | ||
|
||
## Minimal Example | ||
The `connection.rs` example intends to use the smallest amount of code to make a simple QUIC connection. | ||
The server issues it's own certificate and passes it to the client to trust. | ||
|
||
```text | ||
$ cargo run --example connection | ||
``` | ||
|
||
This example will make a QUIC connection on localhost, and you should see output like: | ||
|
||
```text | ||
[client] connected: addr=127.0.0.1:5000 | ||
[server] connection accepted: addr=127.0.0.1:53712 | ||
``` | ||
|
||
## Insecure Connection Example | ||
|
||
The `insecure_connection.rs` example demonstrates how to make a QUIC connection that ignores the server certificate. | ||
|
||
```text | ||
$ cargo run --example insecure_connection --features="rustls/dangerous_configuration" | ||
``` | ||
|
||
## Single Socket Example | ||
|
||
You can have multiple QUIC connections over a single UDP socket. This is especially | ||
useful, if you are building a peer-to-peer system where you potentially need to communicate with | ||
thousands of peers or if you have a | ||
[hole punched](https://en.wikipedia.org/wiki/UDP_hole_punching) UDP socket. | ||
Additionally, QUIC servers and clients can both operate on the same UDP socket. | ||
This example demonstrates how to make multiple outgoing connections on a single UDP socket. | ||
|
||
```text | ||
$ cargo run --example single_socket | ||
``` | ||
|
||
The expected output should be something like: | ||
|
||
```text | ||
[client] connected: addr=127.0.0.1:5000 | ||
[server] incoming connection: addr=127.0.0.1:48930 | ||
[client] connected: addr=127.0.0.1:5001 | ||
[client] connected: addr=127.0.0.1:5002 | ||
[server] incoming connection: addr=127.0.0.1:48930 | ||
[server] incoming connection: addr=127.0.0.1:48930 | ||
``` | ||
|
||
Notice how the server sees multiple incoming connections with different IDs coming from the same | ||
endpoint. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
//! This example demonstrates an HTTP client that requests files from a server. | ||
//! | ||
//! Checkout the `README.md` for guidance. | ||
use std::{ | ||
fs, | ||
io::{self, Write}, | ||
net::{SocketAddr, ToSocketAddrs}, | ||
path::PathBuf, | ||
sync::Arc, | ||
time::{Duration, Instant}, | ||
}; | ||
|
||
use anyhow::{anyhow, Result}; | ||
use clap::Parser; | ||
use proto::crypto::rustls::QuicClientConfig; | ||
use rustls::pki_types::CertificateDer; | ||
use tracing::{error, info}; | ||
use url::Url; | ||
|
||
mod common; | ||
|
||
/// HTTP/0.9 over QUIC client | ||
#[derive(Parser, Debug)] | ||
#[clap(name = "client")] | ||
struct Opt { | ||
/// Perform NSS-compatible TLS key logging to the file specified in `SSLKEYLOGFILE`. | ||
#[clap(long = "keylog")] | ||
keylog: bool, | ||
|
||
url: Url, | ||
|
||
/// Override hostname used for certificate verification | ||
#[clap(long = "host")] | ||
host: Option<String>, | ||
|
||
/// Custom certificate authority to trust, in DER format | ||
#[clap(long = "ca")] | ||
ca: Option<PathBuf>, | ||
|
||
/// Simulate NAT rebinding after connecting | ||
#[clap(long = "rebind")] | ||
rebind: bool, | ||
|
||
/// Address to bind on | ||
#[clap(long = "bind", default_value = "[::]:0")] | ||
bind: SocketAddr, | ||
} | ||
|
||
fn main() { | ||
tracing::subscriber::set_global_default( | ||
tracing_subscriber::FmtSubscriber::builder() | ||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) | ||
.finish(), | ||
) | ||
.unwrap(); | ||
let opt = Opt::parse(); | ||
let code = { | ||
if let Err(e) = run(opt) { | ||
eprintln!("ERROR: {e}"); | ||
1 | ||
} else { | ||
0 | ||
} | ||
}; | ||
::std::process::exit(code); | ||
} | ||
|
||
#[tokio::main] | ||
async fn run(options: Opt) -> Result<()> { | ||
let url = options.url; | ||
let url_host = strip_ipv6_brackets(url.host_str().unwrap()); | ||
let remote = (url_host, url.port().unwrap_or(4433)) | ||
.to_socket_addrs()? | ||
.next() | ||
.ok_or_else(|| anyhow!("couldn't resolve to an address"))?; | ||
|
||
let mut roots = rustls::RootCertStore::empty(); | ||
if let Some(ca_path) = options.ca { | ||
roots.add(CertificateDer::from(fs::read(ca_path)?))?; | ||
} else { | ||
let dirs = directories_next::ProjectDirs::from("org", "quinn", "quinn-examples").unwrap(); | ||
match fs::read(dirs.data_local_dir().join("cert.der")) { | ||
Ok(cert) => { | ||
roots.add(CertificateDer::from(cert))?; | ||
} | ||
Err(ref e) if e.kind() == io::ErrorKind::NotFound => { | ||
info!("local server certificate not found"); | ||
} | ||
Err(e) => { | ||
error!("failed to open local server certificate: {}", e); | ||
} | ||
} | ||
} | ||
let mut client_crypto = rustls::ClientConfig::builder() | ||
.with_root_certificates(roots) | ||
.with_no_client_auth(); | ||
|
||
client_crypto.alpn_protocols = common::ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect(); | ||
if options.keylog { | ||
client_crypto.key_log = Arc::new(rustls::KeyLogFile::new()); | ||
} | ||
|
||
let client_config = | ||
quinn::ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?)); | ||
let mut endpoint = quinn::Endpoint::client(options.bind)?; | ||
endpoint.set_default_client_config(client_config); | ||
|
||
let request = format!("GET {}\r\n", url.path()); | ||
let start = Instant::now(); | ||
let rebind = options.rebind; | ||
let host = options.host.as_deref().unwrap_or(url_host); | ||
|
||
eprintln!("connecting to {host} at {remote}"); | ||
let conn = endpoint | ||
.connect(remote, host)? | ||
.await | ||
.map_err(|e| anyhow!("failed to connect: {}", e))?; | ||
eprintln!("connected at {:?}", start.elapsed()); | ||
let (mut send, mut recv) = conn | ||
.open_bi() | ||
.await | ||
.map_err(|e| anyhow!("failed to open stream: {}", e))?; | ||
if rebind { | ||
let socket = std::net::UdpSocket::bind("[::]:0").unwrap(); | ||
let addr = socket.local_addr().unwrap(); | ||
eprintln!("rebinding to {addr}"); | ||
endpoint.rebind(socket).expect("rebind failed"); | ||
} | ||
|
||
send.write_all(request.as_bytes()) | ||
.await | ||
.map_err(|e| anyhow!("failed to send request: {}", e))?; | ||
send.finish().unwrap(); | ||
let response_start = Instant::now(); | ||
eprintln!("request sent at {:?}", response_start - start); | ||
let resp = recv | ||
.read_to_end(usize::max_value()) | ||
.await | ||
.map_err(|e| anyhow!("failed to read response: {}", e))?; | ||
let duration = response_start.elapsed(); | ||
eprintln!( | ||
"response received in {:?} - {} KiB/s", | ||
duration, | ||
resp.len() as f32 / (duration_secs(&duration) * 1024.0) | ||
); | ||
io::stdout().write_all(&resp).unwrap(); | ||
io::stdout().flush().unwrap(); | ||
conn.close(0u32.into(), b"done"); | ||
|
||
// Give the server a fair chance to receive the close packet | ||
endpoint.wait_idle().await; | ||
|
||
Ok(()) | ||
} | ||
|
||
fn strip_ipv6_brackets(host: &str) -> &str { | ||
// An ipv6 url looks like eg https://[::1]:4433/Cargo.toml, wherein the host [::1] is the | ||
// ipv6 address ::1 wrapped in brackets, per RFC 2732. This strips those. | ||
if host.starts_with('[') && host.ends_with(']') { | ||
&host[1..host.len() - 1] | ||
} else { | ||
host | ||
} | ||
} | ||
|
||
fn duration_secs(x: &Duration) -> f32 { | ||
x.as_secs() as f32 + x.subsec_nanos() as f32 * 1e-9 | ||
} |
Oops, something went wrong.