1use anyhow::{Context, Result, bail};
4use std::io::{Read, Write};
5use std::net::{TcpStream, ToSocketAddrs};
6use std::sync::Arc;
7use std::time::Duration;
8
9#[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
21pub(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
30pub(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
41pub(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 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 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
201pub(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}