Skip to main content

htb_cli/api/
challenges.rs

1use serde_json::json;
2
3use crate::error::HtbError;
4use crate::models::challenge::{
5    Challenge, ChallengeCategoriesResponse, ChallengeCategory, ChallengeDetail,
6    ChallengeDetailResponse, ChallengeDownloadResponse, ChallengeOwnResponse,
7    ChallengeWriteupResponse,
8};
9use crate::models::{ActionResponse, Paginated};
10
11use super::HtbClient;
12
13pub struct ChallengeApi<'a>(pub(crate) &'a HtbClient);
14
15impl ChallengeApi<'_> {
16    pub async fn list(
17        &self,
18        page: u32,
19        per_page: u32,
20        keyword: Option<&str>,
21    ) -> Result<Paginated<Challenge>, HtbError> {
22        let mut url = format!("/api/v4/challenges?per_page={per_page}&page={page}");
23        if let Some(kw) = keyword {
24            url.push_str(&format!("&keyword={}", super::encode_path(kw)));
25        }
26        self.0.get(&url).await
27    }
28
29    pub async fn categories(&self) -> Result<Vec<ChallengeCategory>, HtbError> {
30        let resp: ChallengeCategoriesResponse =
31            self.0.get("/api/v4/challenge/categories/list").await?;
32        Ok(resp.info)
33    }
34
35    pub async fn info(&self, slug: &str) -> Result<ChallengeDetail, HtbError> {
36        let encoded = super::encode_path(slug);
37        let resp: ChallengeDetailResponse = self
38            .0
39            .get(&format!("/api/v4/challenge/info/{encoded}"))
40            .await?;
41        Ok(resp.challenge)
42    }
43
44    pub async fn start(&self, challenge_id: u64) -> Result<ActionResponse, HtbError> {
45        self.0
46            .post(
47                "/api/v4/container/start",
48                &json!({"containerable_id": challenge_id}),
49            )
50            .await
51    }
52
53    pub async fn stop(&self, challenge_id: u64) -> Result<ActionResponse, HtbError> {
54        self.0
55            .post(
56                "/api/v4/container/stop",
57                &json!({"containerable_id": challenge_id}),
58            )
59            .await
60    }
61
62    pub async fn submit_flag(
63        &self,
64        challenge_id: u64,
65        flag: &str,
66    ) -> Result<ChallengeOwnResponse, HtbError> {
67        self.0
68            .post(
69                "/api/v4/challenge/own",
70                &json!({"challenge_id": challenge_id, "flag": flag}),
71            )
72            .await
73    }
74
75    pub async fn download_link(&self, challenge_id: u64) -> Result<String, HtbError> {
76        let resp: ChallengeDownloadResponse = self
77            .0
78            .get(&format!("/api/v4/challenges/{challenge_id}/download_link"))
79            .await?;
80        Ok(resp.url)
81    }
82
83    pub async fn download_file(&self, url: &str) -> Result<Vec<u8>, HtbError> {
84        self.0.get_bytes(url).await
85    }
86
87    pub async fn writeup_info(
88        &self,
89        challenge_id: u64,
90    ) -> Result<ChallengeWriteupResponse, HtbError> {
91        self.0
92            .get(&format!("/api/v4/challenge/{challenge_id}/writeup"))
93            .await
94    }
95
96    pub async fn writeup_download(&self, challenge_id: u64) -> Result<Vec<u8>, HtbError> {
97        self.0
98            .get_bytes(&format!(
99                "/api/v4/challenge/{challenge_id}/writeup/official"
100            ))
101            .await
102    }
103}