use core::clone::Clone;
use core::convert::AsRef;
use std::io::{self, Cursor, Read, Write};
use anyhow::{Result, anyhow};
use colored::Colorize;
use qrcode::{QrCode, render::unicode};
use url::Url;
use zeroize::{Zeroize, Zeroizing};
use zip::{ZipWriter, write::ExtendedFileOptions, write::FileOptions};
use hakanai_lib::client::Client;
use hakanai_lib::models::{Payload, SecretRestrictions};
use hakanai_lib::options::SecretSendOptions;
use hakanai_lib::utils::content_analysis;
use hakanai_lib::utils::timestamp;
use crate::args::SendArgs;
use crate::factory::Factory;
use crate::helper;
#[derive(Debug)]
struct Secret {
bytes: Zeroizing<Vec<u8>>,
filename: Option<String>,
}
pub async fn send<T: Factory>(factory: T, args: SendArgs) -> Result<()> {
args.validate()?;
if args.ttl.as_secs() == 0 {
return Err(anyhow!("TTL must be greater than zero seconds."));
}
let token = args.token()?.unwrap_or_default();
if token.is_empty() {
eprintln!("{}", "Warning: No token provided.".yellow());
}
let secret = read_secret(args.clone())?;
if secret.bytes.is_empty() {
return Err(anyhow!(
"No secret provided. Please input a secret to send."
));
}
let mut payload = Payload::from_bytes(secret.bytes.as_ref());
if let Some(filename) = get_filename(&secret, args.clone())? {
payload = payload.with_filename(&filename);
}
let user_agent = helper::get_user_agent_name();
let observer = factory.new_observer("Sending secret...")?;
let mut opts = SecretSendOptions::default()
.with_user_agent(user_agent)
.with_observer(observer);
let restrictions = args.get_restrictions();
if let Some(restrictions) = &restrictions {
opts = opts.with_restrictions(restrictions.clone());
}
let mut link = factory
.new_client()
.send_secret(args.server.clone(), payload, args.ttl, token, Some(opts))
.await?
.clone();
print_link(&mut link, args)?;
if let Some(restrictions) = restrictions {
print_restrictions(&restrictions);
}
Ok(())
}
fn read_secret(args: SendArgs) -> Result<Secret> {
if let Some(files) = args.files {
read_secret_from_files(files)
} else {
let mut bytes = Zeroizing::new(Vec::new());
io::stdin().read_to_end(&mut bytes)?;
Ok(Secret {
bytes,
filename: None,
})
}
}
fn read_secret_from_files(files: Vec<String>) -> Result<Secret> {
if files.len() != 1 {
return archive_files(files);
}
let file_path = files[0].clone();
let bytes = Zeroizing::new(std::fs::read(&file_path)?);
let filename = std::path::Path::new(&file_path)
.file_name()
.and_then(|name| name.to_str())
.map(|s| s.to_string());
Ok(Secret { bytes, filename })
}
fn archive_files(files: Vec<String>) -> Result<Secret> {
let mut buffer = Vec::new();
let cursor = Cursor::new(&mut buffer);
let mut zip = ZipWriter::new(cursor);
for file in files {
let bytes = Zeroizing::new(std::fs::read(&file)?);
let filename = std::path::Path::new(&file)
.file_name()
.unwrap_or_default()
.to_string_lossy();
zip.start_file(filename, FileOptions::<ExtendedFileOptions>::default())?;
zip.write_all(bytes.as_ref())?;
}
zip.finish()?;
let timestamp = timestamp::now_string()?;
let filename = format!("secret-{timestamp}.zip");
Ok(Secret {
bytes: Zeroizing::new(buffer),
filename: Some(filename),
})
}
fn get_filename(secret: &Secret, args: SendArgs) -> Result<Option<String>> {
let mut as_file = args.as_file;
if args.files.is_some() && !as_file && content_analysis::is_binary(secret.bytes.as_ref()) {
println!(
"{}",
"Sending binary files as text may lead to data corruption. Sending as file instead."
.yellow()
);
as_file = true;
}
if !as_file {
return Ok(None);
}
if let Some(filename) = args.filename {
return Ok(Some(filename));
}
if let Some(filename) = &secret.filename {
Ok(Some(filename.clone()))
} else {
Err(anyhow!("File name is required when sending as a file."))
}
}
fn print_link(link: &mut Url, args: SendArgs) -> Result<()> {
println!("Secret sent successfully!\n");
if args.separate_key {
print_link_separate_key(link);
} else {
println!("Secret link: {}", link.to_string().cyan());
}
if args.print_qr_code {
print_qr_code(link)?;
}
Ok(())
}
fn print_qr_code(link: &Url) -> Result<()> {
let code = QrCode::with_error_correction_level(link.to_string(), qrcode::EcLevel::L)?;
let ascii = code
.render::<unicode::Dense1x2>()
.dark_color(unicode::Dense1x2::Dark)
.light_color(unicode::Dense1x2::Light)
.build();
println!("{ascii}");
Ok(())
}
fn print_link_separate_key(link: &mut Url) {
let mut fragment = link.fragment().unwrap_or_default().to_string();
link.set_fragment(None);
println!("Secret link: {}", link.to_string().cyan());
println!("Key: {}", fragment.cyan());
fragment.zeroize();
}
fn print_restrictions(restrictions: &SecretRestrictions) {
eprintln!("\n{}", "Access to secret is restricted: ".yellow());
eprintln!(" {restrictions}");
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::time::Duration;
use tempfile::TempDir;
use hakanai_lib::client_mock::MockClient;
use hakanai_lib::utils::test::MustParse;
use crate::factory_mock::test_utils::MockFactory;
#[test]
fn test_read_secret_from_files_creates_archive() -> Result<()> {
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("test_secret.txt");
let test_content = b"test secret content";
fs::write(&file_path, test_content)?;
let file_path2 = temp_dir.path().join("test_secret2.txt");
let test_content2 = b"test secret content part 2";
fs::write(&file_path2, test_content2)?;
let args = SendArgs::builder().with_files(vec![
file_path.to_string_lossy().as_ref().to_string(),
file_path2.to_string_lossy().as_ref().to_string(),
]);
let result = read_secret(args)?;
assert_eq!(&result.bytes[0..4], b"PK\x03\x04", "Invalid ZIP signature");
if let Some(filename) = &result.filename {
assert!(filename.ends_with(".zip"), "Filename should end with .zip");
} else {
panic!("Filename should be set for archive");
}
Ok(())
}
#[test]
fn test_read_secret_file_not_found() {
let args = SendArgs::builder().with_file("/nonexistent/file.txt");
let result = read_secret(args);
assert!(
result.is_err(),
"Expected error for missing filename, got: {:?}",
result
);
}
#[tokio::test]
async fn test_send_zero_ttl() -> Result<()> {
let factory = MockFactory::new();
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("test.txt");
fs::write(&file_path, b"test content")?;
let args = SendArgs::builder()
.with_server("https://example.com")
.with_ttl(Duration::from_secs(0))
.with_token("token")
.with_file(file_path.to_string_lossy().as_ref());
let result = send(factory, args).await;
assert!(
result.is_err(),
"Expected error for zero TTL, got: {:?}",
result
);
assert!(
result
.unwrap_err()
.to_string()
.contains("TTL must be greater than zero")
);
Ok(())
}
#[tokio::test]
async fn test_send_empty_secret_from_file() -> Result<()> {
let factory = MockFactory::new();
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("empty.txt");
fs::write(&file_path, b"")?;
let args = SendArgs::builder()
.with_server("https://example.com")
.with_ttl(Duration::from_secs(3600))
.with_token("token")
.with_file(file_path.to_string_lossy().as_ref());
let result = send(factory, args).await;
assert!(
result.is_err(),
"Expected error for empty secret, got: {:?}",
result
);
assert!(
result
.unwrap_err()
.to_string()
.contains("No secret provided")
);
Ok(())
}
#[tokio::test]
async fn test_send_successful_text_file() -> Result<()> {
let expected_url: Url = "https://example.com/s/success123#key".must_parse();
let client = MockClient::new().with_send_success(expected_url.clone());
let factory = MockFactory::new().with_client(client);
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("test.txt");
fs::write(&file_path, b"test secret content")?;
let args = SendArgs::builder()
.with_server("https://example.com")
.with_ttl(Duration::from_secs(3600))
.with_token("token123")
.with_file(file_path.to_string_lossy().as_ref());
send(factory, args).await?;
Ok(())
}
#[tokio::test]
async fn test_send_successful_as_file() -> Result<()> {
let expected_url: Url = "https://example.com/s/file123#key".must_parse();
let client = MockClient::new().with_send_success(expected_url.clone());
let factory = MockFactory::new().with_client(client);
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("document.pdf");
fs::write(&file_path, b"fake pdf content")?;
let args = SendArgs::builder()
.with_server("https://example.com")
.with_ttl(Duration::from_secs(7200))
.with_token("token456")
.with_file(file_path.to_string_lossy().as_ref())
.with_as_file();
send(factory, args).await?;
Ok(())
}
#[tokio::test]
async fn test_send_successful_with_custom_filename() -> Result<()> {
let expected_url: url::Url = "https://example.com/s/custom123#key".must_parse();
let client = MockClient::new().with_send_success(expected_url.clone());
let factory = MockFactory::new().with_client(client);
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("original.txt");
fs::write(&file_path, b"file content")?;
let args = SendArgs::builder()
.with_server("https://example.com")
.with_ttl(Duration::from_secs(3600))
.with_token("token789")
.with_file(file_path.to_string_lossy().as_ref())
.with_as_file()
.with_filename("custom_name.txt");
send(factory, args).await?;
Ok(())
}
#[tokio::test]
async fn test_send_client_error() -> Result<()> {
let client = MockClient::new().with_send_failure("Network error".to_string());
let factory = MockFactory::new().with_client(client);
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("test.txt");
fs::write(&file_path, b"test content")?;
let args = SendArgs::builder()
.with_server("https://example.com")
.with_ttl(Duration::from_secs(3600))
.with_token("token")
.with_file(file_path.to_string_lossy().as_ref());
let result = send(factory, args).await;
assert!(
result.is_err(),
"Expected error for network failure, got: {:?}",
result
);
assert!(result.unwrap_err().to_string().contains("Network error"));
Ok(())
}
#[tokio::test]
async fn test_send_with_all_restrictions() -> Result<()> {
let expected_url: url::Url = "https://example.com/s/restricted123#key".must_parse();
let client = MockClient::new().with_send_success(expected_url.clone());
let factory = MockFactory::new().with_client(client);
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("restricted.txt");
fs::write(&file_path, b"restricted secret content")?;
let args = SendArgs::builder()
.with_server("https://example.com")
.with_ttl(Duration::from_secs(3600))
.with_token("token123")
.with_file(file_path.to_string_lossy().as_ref())
.with_allowed_ips(vec!["192.168.1.0/24".must_parse()])
.with_allowed_countries(vec!["US".must_parse()])
.with_allowed_asns(vec![13335])
.with_require_passphrase("test123456");
send(factory, args).await?;
Ok(())
}
}