spytrap_adb/
http.rs

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}