1use crate::errors::*;
2use chrono::{DateTime, offset::Utc};
3use serde::{Deserialize, Serialize};
4use std::time::Duration;
5
6const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
7
8const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
9const READ_TIMEOUT: Duration = Duration::from_secs(30);
10
11#[derive(Debug, Clone)]
12pub struct Client {
13 http: reqwest::Client,
14}
15
16impl Client {
17 pub fn new() -> Result<Self> {
18 let http = reqwest::Client::builder()
19 .user_agent(APP_USER_AGENT)
20 .connect_timeout(CONNECT_TIMEOUT)
21 .read_timeout(READ_TIMEOUT)
22 .build()
23 .context("Failed to setup http client")?;
24 Ok(Self { http })
25 }
26
27 pub async fn get(&self, url: &str) -> Result<reqwest::Response> {
28 let req = self
29 .http
30 .get(url)
31 .send()
32 .await
33 .context("Failed to send HTTP request")?;
34
35 let status = req.status();
36 let headers = req.headers();
37 trace!("Received response from server: status={status:?}, headers={headers:?}");
38
39 let req = req.error_for_status()?;
40 Ok(req)
41 }
42
43 pub async fn github_branch_metadata(&self, base_url: &str, branch: &str) -> Result<GithubRef> {
44 let url = format!("{}/{}", base_url, branch);
45
46 info!("Fetching git repository meta data: {url:?}...");
47 let metadata = self
48 .get(&url)
49 .await?
50 .json::<GithubBranch>()
51 .await
52 .context("Failed to receive http response")?;
53
54 let commit = metadata.commit;
55 debug!("Found github commit for branch {branch:?}: {commit:?}");
56 Ok(commit)
57 }
58
59 pub async fn github_download_file(
60 &self,
61 base_url: &str,
62 commit: &GithubRef,
63 filename: &str,
64 ) -> Result<String> {
65 let url = base_url
66 .replace("{{commit}}", &commit.sha)
67 .replace("{{filename}}", filename);
68 info!("Downloading IOC file from {url:?}...");
69 let body = self
70 .get(&url)
71 .await?
72 .text()
73 .await
74 .context("Failed to download HTTP response")?;
75 Ok(body)
76 }
77}
78
79#[derive(Debug, Deserialize, Serialize)]
80pub struct GithubBranch {
81 pub name: String,
82 pub commit: GithubRef,
83}
84
85#[derive(Debug, Deserialize, Serialize)]
86pub struct GithubRef {
87 pub sha: String,
88 pub commit: GithubCommit,
89}
90
91#[derive(Debug, Deserialize, Serialize)]
92pub struct GithubCommit {
93 pub committer: GithubGitAuthor,
94}
95
96impl GithubCommit {
97 pub fn release_timestamp(&self) -> Result<i64> {
98 let datetime = &self.committer.date;
99 let timestamp = parse_datetime(datetime)
100 .with_context(|| anyhow!("Failed to parse datetime from github: {datetime:?}"))?;
101 Ok(timestamp)
102 }
103}
104
105#[derive(Debug, Deserialize, Serialize)]
106pub struct GithubGitAuthor {
107 pub name: String,
108 pub email: String,
109 pub date: String,
110}
111
112fn parse_datetime(datetime: &str) -> Result<i64> {
113 let dt = DateTime::parse_from_rfc3339(datetime)?;
114 let dt = DateTime::<Utc>::from(dt);
115 Ok(dt.timestamp())
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn test_parse_datetime() {
124 let timestamp = parse_datetime("2024-07-02T23:34:14Z").unwrap();
125 assert_eq!(timestamp, 1719963254);
126 }
127}