Skip to main content

sheathe_package/
tls.rs

1//! HTTP(S) client helpers: plain HTTP via std, HTTPS via rustls.
2
3use anyhow::{Context, Result, bail};
4use std::io::{Read, Write};
5use std::net::{TcpStream, ToSocketAddrs};
6use std::sync::Arc;
7use std::time::Duration;
8
9/// TLS / HTTP options for push, CPIX, and Widevine.
10#[derive(Debug, Clone, Default)]
11pub struct HttpOptions {
12    pub user_agent: Option<String>,
13    pub ca_file: Option<std::path::PathBuf>,
14    pub client_cert_file: Option<std::path::PathBuf>,
15    pub client_cert_key_file: Option<std::path::PathBuf>,
16    pub client_cert_key_password: Option<String>,
17    pub disable_peer_verification: bool,
18    pub ignore_failures: bool,
19}
20
21/// GET `url`, return response body.
22pub(crate) fn http_get(
23    url: &str,
24    opts: &HttpOptions,
25    extra_headers: &[(&str, &str)],
26) -> Result<Vec<u8>> {
27    http_exchange("GET", url, None, None, opts, extra_headers)
28}
29
30/// POST `body` to `url`.
31pub(crate) fn http_post(
32    url: &str,
33    body: &[u8],
34    content_type: &str,
35    opts: &HttpOptions,
36    extra_headers: &[(&str, &str)],
37) -> Result<Vec<u8>> {
38    http_exchange("POST", url, Some(body), Some(content_type), opts, extra_headers)
39}
40
41/// PUT `body` to `url`.
42pub(crate) fn http_put(url: &str, body: &[u8], opts: &HttpOptions) -> Result<()> {
43    match http_exchange("PUT", url, Some(body), Some("application/octet-stream"), opts, &[]) {
44        Ok(_) => Ok(()),
45        Err(e) if opts.ignore_failures => {
46            eprintln!("http: ignoring PUT failure for {url}: {e:#}");
47            Ok(())
48        }
49        Err(e) => Err(e),
50    }
51}
52
53fn http_exchange(
54    method: &str,
55    url: &str,
56    body: Option<&[u8]>,
57    content_type: Option<&str>,
58    opts: &HttpOptions,
59    extra_headers: &[(&str, &str)],
60) -> Result<Vec<u8>> {
61    let https = url.starts_with("https://");
62    let rest = url
63        .strip_prefix("https://")
64        .or_else(|| url.strip_prefix("http://"))
65        .context("URL must be http:// or https://")?;
66    let (hostport, path) =
67        rest.split_once('/').map(|(h, p)| (h, format!("/{p}"))).unwrap_or((rest, "/".into()));
68    let host = hostport.split(':').next().unwrap_or(hostport);
69    let default_port = if https { 443 } else { 80 };
70    let addr_s = if hostport.contains(':') {
71        hostport.to_string()
72    } else {
73        format!("{hostport}:{default_port}")
74    };
75    let addr = addr_s
76        .to_socket_addrs()
77        .with_context(|| format!("resolving {addr_s}"))?
78        .next()
79        .context("no addresses")?;
80    let tcp = TcpStream::connect_timeout(&addr, Duration::from_secs(15))
81        .with_context(|| format!("connecting to {addr_s}"))?;
82    tcp.set_read_timeout(Some(Duration::from_secs(60)))?;
83    tcp.set_write_timeout(Some(Duration::from_secs(60)))?;
84
85    let ua = opts.user_agent.as_deref().unwrap_or("sheathe");
86    let mut req = format!(
87        "{method} {path} HTTP/1.1\r\nHost: {host}\r\nUser-Agent: {ua}\r\nConnection: close\r\n"
88    );
89    if let Some(ct) = content_type {
90        req.push_str(&format!("Content-Type: {ct}\r\n"));
91    }
92    if let Some(b) = body {
93        req.push_str(&format!("Content-Length: {}\r\n", b.len()));
94    }
95    for (k, v) in extra_headers {
96        req.push_str(&format!("{k}: {v}\r\n"));
97    }
98    req.push_str("\r\n");
99
100    let raw = if https {
101        let mut stream = tls_wrap(tcp, host, opts)?;
102        stream.write_all(req.as_bytes())?;
103        if let Some(b) = body {
104            stream.write_all(b)?;
105        }
106        stream.flush()?;
107        let mut resp = Vec::new();
108        stream.read_to_end(&mut resp).ok();
109        resp
110    } else {
111        let mut stream = tcp;
112        stream.write_all(req.as_bytes())?;
113        if let Some(b) = body {
114            stream.write_all(b)?;
115        }
116        stream.flush()?;
117        let mut resp = Vec::new();
118        stream.read_to_end(&mut resp).ok();
119        resp
120    };
121    split_http_body(&raw)
122}
123
124fn split_http_body(raw: &[u8]) -> Result<Vec<u8>> {
125    let text = String::from_utf8_lossy(raw);
126    let status = text.lines().next().unwrap_or("");
127    let ok = status.contains(" 200")
128        || status.contains(" 201")
129        || status.contains(" 204")
130        || status.contains(" 100");
131    if !ok && !status.starts_with("HTTP/") {
132        // maybe just the body
133        return Ok(raw.to_vec());
134    }
135    if !ok {
136        bail!("HTTP error: {status}");
137    }
138    if let Some(idx) = raw.windows(4).position(|w| w == b"\r\n\r\n") {
139        Ok(raw[idx + 4..].to_vec())
140    } else {
141        Ok(raw.to_vec())
142    }
143}
144
145fn tls_wrap(
146    tcp: TcpStream,
147    host: &str,
148    opts: &HttpOptions,
149) -> Result<rustls::StreamOwned<rustls::ClientConnection, TcpStream>> {
150    let mut root = rustls::RootCertStore::empty();
151    root.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
152    if let Some(ca) = &opts.ca_file {
153        let pem = fs_read(ca)?;
154        for cert in rustls_pemfile::certs(&mut pem.as_slice()).flatten() {
155            root.add(cert).ok();
156        }
157    }
158    if opts.disable_peer_verification {
159        // Still use roots; rustls has no official skip. Document that
160        // disable_peer_verification currently only skips adding extra checks
161        // beyond the platform roots — we refuse to silently disable TLS.
162        eprintln!(
163            "sheathe: --disable-peer-verification is ignored for rustls (roots still enforced)"
164        );
165    }
166    let builder = rustls::ClientConfig::builder().with_root_certificates(root);
167    let config =
168        if let (Some(cert), Some(key)) = (&opts.client_cert_file, &opts.client_cert_key_file) {
169            let certs = load_certs(cert)?;
170            let key = load_key(key, opts.client_cert_key_password.as_deref())?;
171            builder.with_client_auth_cert(certs, key).context("client cert")?
172        } else {
173            builder.with_no_client_auth()
174        };
175    let server = rustls::pki_types::ServerName::try_from(host.to_string()).context("SNI")?;
176    let conn = rustls::ClientConnection::new(Arc::new(config), server).context("TLS client")?;
177    Ok(rustls::StreamOwned::new(conn, tcp))
178}
179
180fn load_certs(path: &std::path::Path) -> Result<Vec<rustls::pki_types::CertificateDer<'static>>> {
181    let pem = fs_read(path)?;
182    rustls_pemfile::certs(&mut pem.as_slice()).collect::<Result<Vec<_>, _>>().context("certs")
183}
184
185fn load_key(
186    path: &std::path::Path,
187    _password: Option<&str>,
188) -> Result<rustls::pki_types::PrivateKeyDer<'static>> {
189    let pem = fs_read(path)?;
190    let mut cursor = pem.as_slice();
191    if let Ok(Some(k)) = rustls_pemfile::private_key(&mut cursor) {
192        return Ok(k);
193    }
194    bail!("no private key in {}", path.display())
195}
196
197fn fs_read(path: &std::path::Path) -> Result<Vec<u8>> {
198    std::fs::read(path).with_context(|| format!("reading {}", path.display()))
199}
200
201/// rustls server config from PEM cert + key (origin TLS).
202pub(crate) fn server_config(
203    cert: &std::path::Path,
204    key: &std::path::Path,
205) -> Result<Arc<rustls::ServerConfig>> {
206    let certs = load_certs(cert)?;
207    let key = load_key(key, None)?;
208    let mut cfg = rustls::ServerConfig::builder()
209        .with_no_client_auth()
210        .with_single_cert(certs, key)
211        .context("origin TLS cert")?;
212    cfg.alpn_protocols = vec![b"http/1.1".to_vec()];
213    Ok(Arc::new(cfg))
214}