use base64::{engine::general_purpose::STANDARD, Engine as _};
use color_eyre::Result;
use rand::RngCore;
use subtle::ConstantTimeEq;
use std::{
fs::{remove_file, File},
io::Write,
path::{Path, PathBuf},
};
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;
const FILE: &str = ".cookie";
const TEMPORARY_COOKIE_FILE_CREATE_RETRIES: usize = 10;
#[derive(Clone, Debug)]
pub struct Cookie(String);
impl Cookie {
pub fn authenticate(&self, passwd: String) -> bool {
if passwd.len() != self.0.len() {
return false;
}
passwd.as_bytes().ct_eq(self.0.as_bytes()).into()
}
}
impl Default for Cookie {
fn default() -> Self {
let mut bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut bytes);
Self(STANDARD.encode(bytes))
}
}
pub fn write_to_disk(cookie: &Cookie, dir: &Path, file_name: Option<&str>) -> Result<()> {
std::fs::create_dir_all(dir)?;
let cookie_path = dir.join(file_name.unwrap_or(FILE));
if cookie_path
.symlink_metadata()
.map(|m| m.file_type().is_symlink())
.unwrap_or(false)
{
return Err(color_eyre::eyre::eyre!(
"cookie path {cookie_path:?} is a symlink, refusing to write"
));
}
write_owner_only_file(&cookie_path, format!("__cookie__:{}", cookie.0).as_bytes())?;
tracing::info!("RPC auth cookie written to disk");
Ok(())
}
fn write_owner_only_file(path: &Path, contents: &[u8]) -> Result<()> {
for _ in 0..TEMPORARY_COOKIE_FILE_CREATE_RETRIES {
let temp_path = temporary_cookie_path(path);
let mut file = match create_owner_only_file(&temp_path) {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(error) => return Err(error.into()),
};
file.write_all(contents)?;
drop(file);
if let Err(error) = std::fs::rename(&temp_path, path) {
let _ = remove_file(temp_path);
return Err(error.into());
}
return Ok(());
}
Err(color_eyre::eyre::eyre!(
"failed to create a unique temporary cookie file for {path:?}"
))
}
fn temporary_cookie_path(path: &Path) -> PathBuf {
let file_name = path
.file_name()
.map(|file_name| file_name.to_string_lossy())
.unwrap_or_else(|| FILE.into());
let random_suffix = rand::thread_rng().next_u64();
path.with_file_name(format!("{file_name}.{random_suffix:x}.tmp"))
}
fn create_owner_only_file(path: &Path) -> std::io::Result<File> {
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create_new(true);
#[cfg(unix)]
opts.mode(0o600);
opts.open(path)
}
pub fn remove_from_disk(dir: &Path, file_name: Option<&str>) -> Result<()> {
remove_file(dir.join(file_name.unwrap_or(FILE)))?;
tracing::info!("RPC auth cookie removed from disk");
Ok(())
}