htb_cli/api/
challenges.rs1use 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}