use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::Arc;
use std::time::Duration;
const IO_TIMEOUT: Duration = Duration::from_secs(60);
static TRUNCATED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
pub fn relay_addr() -> Option<String> {
let p = crate::identity::home().join("relay");
std::fs::read_to_string(p)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
pub fn set_relay(addr: &str) -> std::io::Result<()> {
let dir = crate::identity::home();
std::fs::create_dir_all(&dir)?;
std::fs::write(dir.join("relay"), addr.trim())
}
pub fn clear_relay() -> std::io::Result<()> {
let p = crate::identity::home().join("relay");
match std::fs::remove_file(p) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
}
pub fn target(addr: &str) -> (String, String, Option<String>) {
let dial = |host: &str, port: u16| {
if host.contains(':') {
format!("[{host}]:{port}")
} else {
format!("{host}:{port}")
}
};
let a = addr.trim();
if let Some(rest) = a.strip_prefix("https://") {
let rest = rest.trim_end_matches('/');
let (host, port) = split_host_port(rest, 443);
(dial(host, port), host.to_string(), Some(host.to_string()))
} else {
let rest = a.strip_prefix("http://").unwrap_or(a).trim_end_matches('/');
let (host, port) = split_host_port(rest, 7788);
let d = dial(host, port);
(d.clone(), d, None)
}
}
fn split_host_port(s: &str, default_port: u16) -> (&str, u16) {
if let Some(end) = s.strip_prefix('[').and_then(|_| s.find(']')) {
let host = &s[1..end];
let port = s[end + 1..]
.strip_prefix(':')
.and_then(|p| p.parse().ok())
.unwrap_or(default_port);
return (host, port);
}
match s.rsplit_once(':') {
Some((h, p)) if !h.is_empty() && p.chars().all(|c| c.is_ascii_digit()) && !p.is_empty() => {
(h, p.parse().unwrap_or(default_port))
}
_ => (s, default_port),
}
}
fn tls_config() -> Arc<rustls::ClientConfig> {
use std::sync::OnceLock;
static CFG: OnceLock<Arc<rustls::ClientConfig>> = OnceLock::new();
CFG.get_or_init(|| {
let roots = rustls::RootCertStore {
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
};
Arc::new(
rustls::ClientConfig::builder()
.with_root_certificates(roots)
.with_no_client_auth(),
)
})
.clone()
}
pub fn post(addr: &str, path: &str, body: &str) -> std::io::Result<String> {
let (dial, host_header, tls_name) = target(addr);
let mut sock = TcpStream::connect(&dial)?;
sock.set_read_timeout(Some(IO_TIMEOUT))?;
sock.set_write_timeout(Some(IO_TIMEOUT))?;
let req = format!(
"POST {path} HTTP/1.1\r\nHost: {host_header}\r\nContent-Type: application/json\r\n\
Content-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
);
let raw = match tls_name {
None => {
sock.write_all(req.as_bytes())?;
sock.write_all(body.as_bytes())?;
sock.flush()?;
let mut raw = Vec::new();
sock.read_to_end(&mut raw)?;
raw
}
Some(name) => {
let server = rustls_pki_types::ServerName::try_from(name.clone())
.map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("{name} is not a name a certificate can be checked against"),
)
})?
.to_owned();
let conn = rustls::ClientConnection::new(tls_config(), server).map_err(|e| {
std::io::Error::other(format!("could not start TLS with {name}: {e}"))
})?;
let mut tls = rustls::StreamOwned::new(conn, sock);
tls.write_all(req.as_bytes())?;
tls.write_all(body.as_bytes())?;
tls.flush()?;
let mut raw = Vec::new();
let mut truncated = false;
match tls.read_to_end(&mut raw) {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => truncated = true,
Err(e) => return Err(e),
}
if truncated {
TRUNCATED.store(true, std::sync::atomic::Ordering::Relaxed);
}
raw
}
};
let text = String::from_utf8_lossy(&raw).to_string();
let (head, rest) = text
.split_once("\r\n\r\n")
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "no http body"))?;
if let Some(want) = head
.lines()
.find_map(|l| {
l.to_ascii_lowercase()
.strip_prefix("content-length:")
.map(|v| v.trim().parse::<usize>())
})
.and_then(|r| r.ok())
&& rest.len() < want
{
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!(
"the relay's answer was cut short: {} of {want} bytes{}",
rest.len(),
if TRUNCATED.swap(false, std::sync::atomic::Ordering::Relaxed) {
" (the connection closed without a TLS close_notify)"
} else {
""
}
),
));
}
TRUNCATED.store(false, std::sync::atomic::Ordering::Relaxed);
let status = head.lines().next().unwrap_or("");
if !status.contains(" 200") {
return Err(std::io::Error::other(format!(
"relay said: {} {}",
status.trim(),
rest.trim()
)));
}
Ok(rest.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_address_says_whether_it_is_verified() {
let (dial, host, tls) = target("https://relay.example.com");
assert_eq!(dial, "relay.example.com:443");
assert_eq!(host, "relay.example.com");
assert_eq!(tls.as_deref(), Some("relay.example.com"));
let (dial, host, tls) = target("https://relay.example.com:9443/");
assert_eq!(dial, "relay.example.com:9443");
assert_eq!(host, "relay.example.com");
assert_eq!(tls.as_deref(), Some("relay.example.com"));
let (dial, host, tls) = target("100.64.0.1:7788");
assert_eq!(dial, "100.64.0.1:7788");
assert_eq!(host, "100.64.0.1:7788");
assert!(tls.is_none());
let (dial, _, tls) = target("127.0.0.1");
assert_eq!(dial, "127.0.0.1:7788");
assert!(tls.is_none());
}
#[test]
fn an_ipv6_literal_stays_dialable() {
for a in ["[::1]:7788", "http://[2001:db8::5]:9000", "[::1]"] {
let (dial, _, tls) = target(a);
assert!(
dial.parse::<std::net::SocketAddr>().is_ok(),
"{a} produced {dial}, which is not a socket address"
);
assert!(tls.is_none());
}
assert_eq!(target("[::1]:7788").0, "[::1]:7788");
assert_eq!(target("[::1]").0, "[::1]:7788");
}
}