Skip to main content

htb_cli/api/
ctf.rs

1use serde_json::json;
2
3use crate::error::HtbError;
4use crate::models::ctf::{
5    CtfChallengeSolve, CtfEvent, CtfEventData, CtfEventDetail, CtfFlagResult, CtfMenu,
6    CtfScoreboard, CtfSolve, CtfTeamOverview, CtfUserProfile,
7};
8use crate::models::ActionResponse;
9
10#[derive(serde::Serialize)]
11struct ProgressBody {
12    status: Option<String>,
13}
14
15use super::HtbClient;
16
17pub struct CtfApi<'a>(pub(crate) &'a HtbClient);
18
19impl CtfApi<'_> {
20    pub async fn profile(&self) -> Result<CtfUserProfile, HtbError> {
21        self.0.get("/api/users/profile").await
22    }
23
24    pub async fn events(&self) -> Result<Vec<CtfEvent>, HtbError> {
25        self.0.get("/api/ctfs").await
26    }
27
28    pub async fn event_details(&self, slug: &str) -> Result<CtfEventDetail, HtbError> {
29        let encoded = super::encode_path(slug);
30        self.0.get(&format!("/api/ctfs/details/{encoded}")).await
31    }
32
33    pub async fn event_data(&self, event_id: u64) -> Result<CtfEventData, HtbError> {
34        self.0.get(&format!("/api/ctfs/{event_id}")).await
35    }
36
37    pub async fn menu(&self, event_id: u64) -> Result<CtfMenu, HtbError> {
38        self.0.get(&format!("/api/ctfs/{event_id}/menu")).await
39    }
40
41    pub async fn submit_flag(
42        &self,
43        challenge_id: u64,
44        flag: &str,
45    ) -> Result<CtfFlagResult, HtbError> {
46        self.0
47            .post(
48                "/api/flags/own",
49                &json!({"challenge_id": challenge_id, "flag": flag}),
50            )
51            .await
52    }
53
54    pub async fn download_file(&self, challenge_id: u64) -> Result<Vec<u8>, HtbError> {
55        self.0
56            .get_bytes(&format!("/api/challenges/{challenge_id}/download"))
57            .await
58    }
59
60    pub async fn container_start(&self, challenge_id: u64) -> Result<ActionResponse, HtbError> {
61        self.0
62            .post(
63                "/api/challenges/containers/start",
64                &json!({"id": challenge_id}),
65            )
66            .await
67    }
68
69    pub async fn container_stop(&self, challenge_id: u64) -> Result<ActionResponse, HtbError> {
70        self.0
71            .post(
72                "/api/challenges/containers/stop",
73                &json!({"id": challenge_id}),
74            )
75            .await
76    }
77
78    pub async fn scoreboard(&self, event_id: u64) -> Result<CtfScoreboard, HtbError> {
79        self.0.get(&format!("/api/ctfs/scores/{event_id}")).await
80    }
81
82    pub async fn solves(&self, event_id: u64) -> Result<Vec<CtfSolve>, HtbError> {
83        self.0.get(&format!("/api/ctfs/solves/{event_id}")).await
84    }
85
86    pub async fn challenge_solves(
87        &self,
88        challenge_id: u64,
89    ) -> Result<Vec<CtfChallengeSolve>, HtbError> {
90        self.0
91            .get(&format!("/api/challenges/{challenge_id}/solves"))
92            .await
93    }
94
95    pub async fn team_overview(&self, team_id: u64) -> Result<CtfTeamOverview, HtbError> {
96        self.0.get(&format!("/api/teams/{team_id}/overview")).await
97    }
98
99    pub async fn associate_challenge(
100        &self,
101        challenge_id: u64,
102        user_id: u64,
103    ) -> Result<ActionResponse, HtbError> {
104        self.0
105            .get(&format!(
106                "/api/challenges/{challenge_id}/associate/{user_id}"
107            ))
108            .await
109    }
110
111    pub async fn disassociate_challenge(
112        &self,
113        challenge_id: u64,
114        user_id: u64,
115    ) -> Result<ActionResponse, HtbError> {
116        self.0
117            .get(&format!(
118                "/api/challenges/{challenge_id}/disassociate/{user_id}"
119            ))
120            .await
121    }
122
123    pub async fn set_challenge_progress(
124        &self,
125        challenge_id: u64,
126        status: Option<&str>,
127    ) -> Result<(), HtbError> {
128        self.0
129            .post_no_content(
130                &format!("/api/challenges/{challenge_id}/progress"),
131                &ProgressBody {
132                    status: status.map(|s| s.to_string()),
133                },
134            )
135            .await
136    }
137}