Codex (gpt-5.5) implementer branch, senior-reviewed. - S10 (High): redact capabilities/chat from logs; create log 0600 + chmod existing; rotate at 5 MiB. New short_id/short_bytes_hex/redact_for_log seams. - T1 (P2): friends-ALPN authorizes (handler(from)) before reading any peer bytes; unauthorized conns closed pre-read (DoS relief). - T2 (P2): per-(author,kind) replay gate on state-changing gossip (Announce/ Leave) only; Chat bypasses it, preserving the S2 no-monotonic-ts decision. - T5 (P2): cap inbound Opus datagrams at 4 + MAX_OPUS_PAYLOAD (4000). - T6 (P2): bind friend-Pong room ticket host to the authenticated responder (interpret_pong/probe now thread the remote id) — blocks Join-button redirect/phishing. Non-regressive given the W7 P3 restamp design. - T7 (P3): sanitize_ticket caps/validates PeerState.sharing at gossip ingest so invalid offers never render a Watch button. 302 lib tests pass (was 291), clippy --all-targets clean, release builds. Tests-green only; DoS relief + 2-machine replay/redirect behavior want a field test. W7 P7 (n0 DNS privacy) reviewed read-only — findings to triage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
176 lines
5.5 KiB
Rust
176 lines
5.5 KiB
Rust
pub mod audio;
|
|
pub mod codec;
|
|
pub mod dsp;
|
|
pub mod network;
|
|
pub mod core;
|
|
pub mod app;
|
|
pub mod config;
|
|
pub mod identity;
|
|
pub mod friends;
|
|
pub mod presence;
|
|
pub mod presence_net;
|
|
pub mod theme;
|
|
pub mod notify;
|
|
pub mod screenshare;
|
|
pub mod sanitize;
|
|
pub mod avatar;
|
|
pub mod recents;
|
|
pub mod discovery;
|
|
pub mod hotkeys;
|
|
|
|
use std::fs::File;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::OnceLock;
|
|
|
|
const LOG_MAX_BYTES: u64 = 5 * 1024 * 1024;
|
|
const LOG_MODE: u32 = 0o600;
|
|
|
|
/// Resolves the log file path once: `$XDG_STATE_HOME/peerspeak/peerspeak.log`
|
|
/// (via `dirs::state_dir`), falling back to the system temp dir. Computed lazily
|
|
/// so we never hardcode a per-user path.
|
|
fn log_path() -> &'static PathBuf {
|
|
static LOG_PATH: OnceLock<PathBuf> = OnceLock::new();
|
|
LOG_PATH.get_or_init(|| {
|
|
let mut dir = dirs::state_dir()
|
|
.or_else(dirs::cache_dir)
|
|
.unwrap_or_else(std::env::temp_dir);
|
|
dir.push("peerspeak");
|
|
let _ = std::fs::create_dir_all(&dir);
|
|
dir.push("peerspeak.log");
|
|
dir
|
|
})
|
|
}
|
|
|
|
/// The resolved log file path. Exposed so diagnostic tools (e.g. the audio
|
|
/// probe) can tail the same log the app writes to.
|
|
pub fn log_file_path() -> PathBuf {
|
|
log_path().clone()
|
|
}
|
|
|
|
/// Short, human-matchable id prefix for diagnostics. Never use this where the
|
|
/// full value is needed for protocol behavior.
|
|
pub fn short_id(id: &str) -> String {
|
|
id.chars().take(8).collect()
|
|
}
|
|
|
|
/// Redact a capability-bearing value for logs while keeping a tiny prefix for
|
|
/// support correlation. Tickets and endpoint addresses are bearer capabilities:
|
|
/// logging the full string is equivalent to leaking the room/share.
|
|
pub fn redact_for_log(value: &str) -> String {
|
|
let value = value.trim();
|
|
if value.is_empty() {
|
|
"<redacted:empty>".to_string()
|
|
} else {
|
|
format!("<redacted:{}...>", short_id(value))
|
|
}
|
|
}
|
|
|
|
pub fn short_bytes_hex(bytes: &[u8]) -> String {
|
|
bytes.iter()
|
|
.take(6)
|
|
.map(|b| format!("{b:02x}"))
|
|
.collect::<Vec<_>>()
|
|
.join("")
|
|
}
|
|
|
|
fn rotated_log_path(path: &Path) -> PathBuf {
|
|
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("peerspeak.log");
|
|
path.with_file_name(format!("{file_name}.1"))
|
|
}
|
|
|
|
fn prepare_log_file(path: &Path) -> std::io::Result<File> {
|
|
prepare_log_file_with_limit(path, LOG_MAX_BYTES)
|
|
}
|
|
|
|
fn prepare_log_file_with_limit(path: &Path, max_bytes: u64) -> std::io::Result<File> {
|
|
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
|
|
|
if let Some(parent) = path.parent() {
|
|
let _ = std::fs::create_dir_all(parent);
|
|
}
|
|
|
|
if std::fs::metadata(path).is_ok_and(|m| m.len() > max_bytes) {
|
|
let rotated = rotated_log_path(path);
|
|
let _ = std::fs::remove_file(&rotated);
|
|
if std::fs::rename(path, &rotated).is_err() {
|
|
let _ = std::fs::OpenOptions::new().write(true).truncate(true).open(path);
|
|
}
|
|
}
|
|
|
|
let file = std::fs::OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.mode(LOG_MODE)
|
|
.open(path)?;
|
|
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(LOG_MODE));
|
|
Ok(file)
|
|
}
|
|
|
|
pub fn log_msg(msg: &str) {
|
|
// Format the whole line into one buffer first, then emit it with a single
|
|
// `write_all`. The file is opened with `O_APPEND`, so a lone `write()` is
|
|
// atomic w.r.t. concurrent writers; building the line up front avoids the
|
|
// multi-syscall `writeln!` path that would let threads interleave fragments.
|
|
let line = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
|
|
Ok(time) => format!("[{}.{:03}] {}\n", time.as_secs(), time.subsec_millis(), msg),
|
|
Err(_) => format!("{}\n", msg),
|
|
};
|
|
if let Ok(mut file) = prepare_log_file(log_path()) {
|
|
use std::io::Write;
|
|
let _ = file.write_all(line.as_bytes());
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::io::Write;
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
fn temp_log_dir() -> PathBuf {
|
|
let stamp = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos();
|
|
std::env::temp_dir().join(format!("peerspeak-log-test-{}-{stamp}", std::process::id()))
|
|
}
|
|
|
|
#[test]
|
|
fn redaction_keeps_only_a_short_prefix() {
|
|
let secret = "abcdefghijklmnopqrstuvwxyz";
|
|
let redacted = redact_for_log(secret);
|
|
assert!(redacted.contains("abcdefgh"));
|
|
assert!(!redacted.contains("ijklmnopqrstuvwxyz"));
|
|
assert_eq!(redact_for_log(" "), "<redacted:empty>");
|
|
}
|
|
|
|
#[test]
|
|
fn log_file_is_created_private() {
|
|
let dir = temp_log_dir();
|
|
let path = dir.join("peerspeak.log");
|
|
let _file = prepare_log_file(&path).unwrap();
|
|
|
|
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
|
|
assert_eq!(mode, LOG_MODE);
|
|
let _ = std::fs::remove_dir_all(dir);
|
|
}
|
|
|
|
#[test]
|
|
fn oversized_log_is_rotated_on_open() {
|
|
let dir = temp_log_dir();
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let path = dir.join("peerspeak.log");
|
|
{
|
|
let mut file = std::fs::File::create(&path).unwrap();
|
|
file.write_all(b"oversized").unwrap();
|
|
}
|
|
|
|
let _file = prepare_log_file_with_limit(&path, 4).unwrap();
|
|
let rotated = rotated_log_path(&path);
|
|
|
|
assert_eq!(std::fs::read_to_string(rotated).unwrap(), "oversized");
|
|
assert_eq!(std::fs::metadata(&path).unwrap().len(), 0);
|
|
let _ = std::fs::remove_dir_all(dir);
|
|
}
|
|
}
|