trace_share_core/
security.rs1use 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}