1use std::{
20 collections::BTreeMap,
21 fs::File,
22 io::Error,
23 path::{Path, PathBuf},
24};
25
26use serde::{Deserialize, Serialize};
27use time::{macros::format_description, OffsetDateTime};
28use tracing::error;
29
30use crate::{deobfuscate::DeobfData, param::Language, util};
31
32pub(crate) const DEFAULT_REPORT_DIR: &str = "rustypipe_reports";
33
34const FILENAME_FORMAT: &[time::format_description::FormatItem] =
35 format_description!("[year]-[month]-[day]_[hour]-[minute]-[second]");
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39#[non_exhaustive]
40pub struct Report<'a> {
41 pub info: RustyPipeInfo<'a>,
43 pub level: Level,
45 pub operation: &'a str,
47 pub error: Option<String>,
49 #[serde(default, skip_serializing_if = "Vec::is_empty")]
51 pub msgs: Vec<String>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub deobf_data: Option<DeobfData>,
55 pub http_request: HTTPRequest<'a>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61#[non_exhaustive]
62pub struct RustyPipeInfo<'a> {
63 pub package: &'a str,
65 pub version: &'a str,
67 #[serde(with = "time::serde::rfc3339")]
69 pub date: OffsetDateTime,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub language: Option<Language>,
73 pub botguard_version: Option<&'a str>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79#[non_exhaustive]
80pub struct HTTPRequest<'a> {
81 pub url: &'a str,
83 pub method: &'a str,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub req_header: Option<BTreeMap<&'a str, String>>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub req_body: Option<String>,
91 pub status: u16,
93 pub resp_body: String,
95}
96
97#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
99pub enum Level {
100 DBG,
103 WRN,
105 ERR,
107}
108
109impl<'a> RustyPipeInfo<'a> {
110 pub(crate) fn new(language: Option<Language>, botguard_version: Option<&'a str>) -> Self {
111 Self {
112 package: env!("CARGO_PKG_NAME"),
113 version: crate::VERSION,
114 date: util::now_sec(),
115 language,
116 botguard_version,
117 }
118 }
119}
120
121pub trait Reporter: Sync + Send {
124 fn report(&self, report: &Report);
126}
127
128pub struct FileReporter {
130 path: PathBuf,
131}
132
133impl FileReporter {
134 pub fn new<P: AsRef<Path>>(path: P) -> Self {
136 Self {
137 path: path.as_ref().to_path_buf(),
138 }
139 }
140
141 fn _report(&self, report: &Report) -> Result<(), String> {
142 let report_path = get_report_path(&self.path, report, "json").map_err(|e| e.to_string())?;
143 let file = File::create(&report_path).map_err(|e| e.to_string())?;
144 serde_json::to_writer_pretty(&file, &report).map_err(|e| e.to_string())?;
145 tracing::warn!(
146 "created report: {}",
147 report_path.to_str().unwrap_or_default()
148 );
149 Ok(())
150 }
151}
152
153impl Default for FileReporter {
154 fn default() -> Self {
155 Self {
156 path: Path::new(DEFAULT_REPORT_DIR).to_path_buf(),
157 }
158 }
159}
160
161impl Reporter for FileReporter {
162 fn report(&self, report: &Report) {
163 self._report(report)
164 .unwrap_or_else(|e| error!("Could not store report file. Err: {}", e));
165 }
166}
167
168fn get_report_path(root: &Path, report: &Report, ext: &str) -> Result<PathBuf, Error> {
169 if !root.is_dir() {
170 std::fs::create_dir_all(root)?;
171 }
172
173 let filename_prefix = format!(
174 "{}_{:?}",
175 report.info.date.format(FILENAME_FORMAT).unwrap_or_default(),
176 report.level
177 );
178
179 let mut report_path = root.to_path_buf();
180 report_path.push(format!("{filename_prefix}.{ext}"));
181
182 for i in 1..u32::MAX {
184 if report_path.exists() {
185 report_path = root.to_path_buf();
186 report_path.push(format!("{filename_prefix}_{i}.{ext}"));
187 } else {
188 break;
189 }
190 }
191
192 Ok(report_path)
193}