use anyhow::{anyhow, Result};
use base64::Engine;
use clap::Parser;
use saorsa_gossip_types::PeerId;
use x0x::crdt::{forge_unattested_delta_bytes, TaskId};
use x0x::identity::AgentId;
#[derive(Parser, Debug)]
#[command(
name = "x0xd-forge-injector",
about = "Hostile first-seen TaskItem gossip injector"
)]
struct Cli {
#[arg(long)]
daemon: String,
#[arg(long)]
token: String,
#[arg(long)]
topic: String,
#[arg(long, default_value = "missing_att")]
variant: String,
#[arg(long)]
victim_agent: Option<String>,
#[arg(long)]
task_id: Option<String>,
#[arg(long)]
spoof_peer: Option<String>,
}
fn hex32(s: &str) -> Option<[u8; 32]> {
let b = hex::decode(s).ok()?;
b.try_into().ok()
}
fn rand32() -> [u8; 32] {
use rand::RngCore;
let mut b = [0u8; 32];
rand::thread_rng().fill_bytes(&mut b);
b
}
fn host_port(url: &str) -> Result<(String, u16, String)> {
let no_scheme = url
.strip_prefix("http://")
.or_else(|| url.strip_prefix("https://"))
.unwrap_or(url);
let authority = no_scheme.split('/').next().unwrap_or(no_scheme);
let (host, port_s) = authority
.split_once(':')
.ok_or_else(|| anyhow!("daemon url must be host:port (got {url})"))?;
let port: u16 = port_s
.parse()
.map_err(|e| anyhow!("daemon port parse ({port_s}): {e}"))?;
Ok((host.to_string(), port, "/publish".to_string()))
}
fn http_post_publish(
host: &str,
port: u16,
path: &str,
token: &str,
body: &str,
) -> Result<(u16, String)> {
use std::io::{Read, Write};
use std::net::TcpStream;
let req = format!(
"POST {path} HTTP/1.1\r\nHost: {host}:{port}\r\n\
Content-Type: application/json\r\nAuthorization: Bearer {token}\r\n\
Content-Length: {len}\r\nConnection: close\r\n\r\n{body}",
len = body.len(),
);
let mut stream =
TcpStream::connect((host, port)).map_err(|e| anyhow!("connect {host}:{port}: {e}"))?;
stream.write_all(req.as_bytes())?;
let mut resp = String::new();
stream.read_to_string(&mut resp)?;
let status = resp
.get(9..12)
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(0);
let body_off = resp.find("\r\n\r\n").map(|i| i + 4).unwrap_or(resp.len());
Ok((status, resp[body_off..].to_string()))
}
fn main() -> Result<()> {
let cli = Cli::parse();
let victim_bytes = cli
.victim_agent
.as_deref()
.and_then(hex32)
.unwrap_or_else(rand32);
let task_bytes = cli
.task_id
.as_deref()
.and_then(hex32)
.unwrap_or_else(rand32);
let peer_bytes = cli
.spoof_peer
.as_deref()
.and_then(hex32)
.unwrap_or_else(rand32);
let victim = AgentId(victim_bytes);
let spoof_peer = PeerId::new(peer_bytes);
let task_id = TaskId::from_bytes(task_bytes);
let delta_bytes = forge_unattested_delta_bytes(victim, task_id, spoof_peer);
let payload_b64 = base64::engine::general_purpose::STANDARD.encode(&delta_bytes);
let (host, port, path) = host_port(&cli.daemon)?;
let body = serde_json::json!({ "topic": cli.topic, "payload": payload_b64 }).to_string();
let (status, resp_body) = http_post_publish(&host, port, &path, &cli.token, &body)?;
let published = status == 200;
let out = serde_json::json!({
"ok": published,
"published": published,
"variant": cli.variant,
"topic": cli.topic,
"forged_task_id": hex::encode(task_bytes),
"victim_agent": hex::encode(victim_bytes),
"spoof_peer": hex::encode(peer_bytes),
"publish_status": status,
"publish_body": resp_body,
});
println!("{out}");
if !published {
std::process::exit(1);
}
Ok(())
}