Skip to main content

trace_share_core/
security.rs

1use anyhow::{Context, Result, bail};
2use std::{
3    env, fs,
4    io::Write,
5    path::{Path, PathBuf},
6    time::{SystemTime, UNIX_EPOCH},
7};
8
9pub fn ensure_secure_url(url: &str, label: &str) -> Result<()> {
10    let parsed = reqwest::Url::parse(url).with_context(|| format!("invalid {label} URL: {url}"))?;
11    match parsed.scheme() {
12        "https" => Ok(()),
13        "http" if allow_insecure_http() => Ok(()),
14        scheme => bail!(
15            "{label} must use https (got {scheme}). Set TRACE_SHARE_ALLOW_INSECURE_HTTP=1 only for local testing."
16        ),
17    }
18}
19
20fn allow_insecure_http() -> bool {
21    env::var("TRACE_SHARE_ALLOW_INSECURE_HTTP")
22        .ok()
23        .map(|v| matches!(v.as_str(), "1" | "true" | "TRUE" | "yes"))
24        .unwrap_or(false)
25}
26
27pub fn write_private_file(path: &Path, bytes: &[u8]) -> Result<()> {
28    if let Some(parent) = path.parent() {
29        fs::create_dir_all(parent)?;
30    }
31
32    let nonce = SystemTime::now()
33        .duration_since(UNIX_EPOCH)
34        .unwrap_or_default()
35        .as_nanos();
36    let tmp_path = temp_path(path, nonce);
37
38    let mut file = new_private_file(&tmp_path)?;
39    file.write_all(bytes)?;
40    file.sync_all()?;
41    drop(file);
42
43    if path.exists() {
44        let _ = fs::remove_file(path);
45    }
46    fs::rename(&tmp_path, path)?;
47
48    #[cfg(unix)]
49    {
50        use std::os::unix::fs::PermissionsExt;
51        fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
52    }
53    Ok(())
54}
55
56fn temp_path(path: &Path, nonce: u128) -> PathBuf {
57    let mut p = path.as_os_str().to_os_string();
58    p.push(format!(".tmp-{nonce}"));
59    PathBuf::from(p)
60}
61
62fn new_private_file(path: &Path) -> Result<fs::File> {
63    let mut opts = fs::OpenOptions::new();
64    opts.create(true).truncate(true).write(true);
65
66    #[cfg(unix)]
67    {
68        use std::os::unix::fs::OpenOptionsExt;
69        opts.mode(0o600);
70    }
71
72    Ok(opts.open(path)?)
73}
74
75#[cfg(test)]
76mod tests {
77    use super::ensure_secure_url;
78
79    #[test]
80    fn enforces_https_by_default() {
81        assert!(ensure_secure_url("https://example.com", "test").is_ok());
82        assert!(ensure_secure_url("http://example.com", "test").is_err());
83    }
84}