use std::{
collections::BTreeMap,
fs::File,
io::Error,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
use time::{macros::format_description, OffsetDateTime};
use tracing::error;
use crate::{deobfuscate::DeobfData, param::Language, util};
pub(crate) const DEFAULT_REPORT_DIR: &str = "rustypipe_reports";
const FILENAME_FORMAT: &[time::format_description::FormatItem] =
format_description!("[year]-[month]-[day]_[hour]-[minute]-[second]");
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Report<'a> {
pub info: RustyPipeInfo<'a>,
pub level: Level,
pub operation: &'a str,
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub msgs: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deobf_data: Option<DeobfData>,
pub http_request: HTTPRequest<'a>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RustyPipeInfo<'a> {
pub package: &'a str,
pub version: &'a str,
#[serde(with = "time::serde::rfc3339")]
pub date: OffsetDateTime,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<Language>,
pub botguard_version: Option<&'a str>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HTTPRequest<'a> {
pub url: &'a str,
pub method: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub req_header: Option<BTreeMap<&'a str, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub req_body: Option<String>,
pub status: u16,
pub resp_body: String,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Level {
DBG,
WRN,
ERR,
}
impl<'a> RustyPipeInfo<'a> {
pub(crate) fn new(language: Option<Language>, botguard_version: Option<&'a str>) -> Self {
Self {
package: env!("CARGO_PKG_NAME"),
version: crate::VERSION,
date: util::now_sec(),
language,
botguard_version,
}
}
}
pub trait Reporter: Sync + Send {
fn report(&self, report: &Report);
}
pub struct FileReporter {
path: PathBuf,
}
impl FileReporter {
pub fn new<P: AsRef<Path>>(path: P) -> Self {
Self {
path: path.as_ref().to_path_buf(),
}
}
fn _report(&self, report: &Report) -> Result<(), String> {
let report_path = get_report_path(&self.path, report, "json").map_err(|e| e.to_string())?;
let file = File::create(&report_path).map_err(|e| e.to_string())?;
serde_json::to_writer_pretty(&file, &report).map_err(|e| e.to_string())?;
tracing::warn!(
"created report: {}",
report_path.to_str().unwrap_or_default()
);
Ok(())
}
}
impl Default for FileReporter {
fn default() -> Self {
Self {
path: Path::new(DEFAULT_REPORT_DIR).to_path_buf(),
}
}
}
impl Reporter for FileReporter {
fn report(&self, report: &Report) {
self._report(report)
.unwrap_or_else(|e| error!("Could not store report file. Err: {}", e));
}
}
fn get_report_path(root: &Path, report: &Report, ext: &str) -> Result<PathBuf, Error> {
if !root.is_dir() {
std::fs::create_dir_all(root)?;
}
let filename_prefix = format!(
"{}_{:?}",
report.info.date.format(FILENAME_FORMAT).unwrap_or_default(),
report.level
);
let mut report_path = root.to_path_buf();
report_path.push(format!("{filename_prefix}.{ext}"));
for i in 1..u32::MAX {
if report_path.exists() {
report_path = root.to_path_buf();
report_path.push(format!("{filename_prefix}_{i}.{ext}"));
} else {
break;
}
}
Ok(report_path)
}