⛳ alerts for any ctfd instance via ntfy
1package clients
2
3import (
4 "crypto/tls"
5 "encoding/json"
6 "fmt"
7 "io"
8 "net/http"
9 "sort"
10 "strings"
11 "time"
12)
13
14// CTFdClient interface defines the methods required for interacting with CTFd
15type CTFdClient interface {
16 GetScoreboard() (*ScoreboardResponse, error)
17 GetChallengeList() (*ChallengeListResponse, error)
18}
19
20// ctfdClient represents a CTFd API client implementation
21type ctfdClient struct {
22 baseURL string
23 apiToken string
24 httpClient *http.Client
25}
26
27// ScoreboardResponse represents the top-level response from the CTFd API for scoreboard
28type ScoreboardResponse struct {
29 Success bool `json:"success"`
30 Data []TeamStanding `json:"data"`
31}
32
33// TeamStanding represents a team's standing on the scoreboard
34type TeamStanding struct {
35 Position int `json:"pos"`
36 AccountID int `json:"account_id"`
37 AccountURL string `json:"account_url"`
38 AccountType string `json:"account_type"`
39 OAuthID *string `json:"oauth_id"`
40 Name string `json:"name"`
41 Score int `json:"score"`
42 BracketID *string `json:"bracket_id"`
43 BracketName *string `json:"bracket_name"`
44 Members []Member `json:"members"`
45}
46
47// Member represents a team member
48type Member struct {
49 ID int `json:"id"`
50 OAuthID *string `json:"oauth_id"`
51 Name string `json:"name"`
52 Score int `json:"score"`
53 BracketID *string `json:"bracket_id"`
54 BracketName *string `json:"bracket_name"`
55}
56
57// ChallengeListResponse represents the top-level response from the CTFd API for challenges
58type ChallengeListResponse struct {
59 Success bool `json:"success"`
60 Data []Challenge `json:"data"`
61}
62
63// Challenge represents a CTFd challenge
64type Challenge struct {
65 ID int `json:"id"`
66 Name string `json:"name"`
67 Description string `json:"description"`
68 Attribution string `json:"attribution"`
69 ConnectionInfo string `json:"connection_info"`
70 NextID int `json:"next_id"`
71 MaxAttempts int `json:"max_attempts"`
72 Value int `json:"value"`
73 Category string `json:"category"`
74 Type string `json:"type"`
75 State string `json:"state"`
76 Requirements map[string]any `json:"requirements"`
77 Solves int `json:"solves"`
78 SolvedByMe bool `json:"solved_by_me"`
79}
80
81// NewCTFdClient creates a new CTFd client with the specified base URL and API token.
82// It configures an HTTP client with a 10-second timeout and insecure TLS verification.
83func NewCTFdClient(baseURL, apiToken string) CTFdClient {
84 baseURL = strings.TrimSuffix(baseURL, "/")
85
86 return &ctfdClient{
87 baseURL: baseURL,
88 apiToken: apiToken,
89 httpClient: &http.Client{
90 Timeout: 10 * time.Second,
91 Transport: &http.Transport{
92 TLSClientConfig: &tls.Config{
93 InsecureSkipVerify: true,
94 },
95 },
96 },
97 }
98}
99
100// GetScoreboard fetches the CTFd scoreboard data from the API.
101// Returns a ScoreboardResponse containing team standings or an error if the request fails.
102func (c *ctfdClient) GetScoreboard() (*ScoreboardResponse, error) {
103 endpoint := "/scoreboard"
104
105 req, err := http.NewRequest("GET", c.baseURL+endpoint, nil)
106 if err != nil {
107 return nil, fmt.Errorf("error creating request: %v", err)
108 }
109
110 req.Header.Add("Accept", "application/json")
111 req.Header.Add("Authorization", "Token "+c.apiToken)
112 req.Header.Add("Content-Type", "application/json")
113
114 resp, err := c.httpClient.Do(req)
115 if err != nil {
116 return nil, fmt.Errorf("error executing request: %v", err)
117 }
118 defer resp.Body.Close()
119
120 body, err := io.ReadAll(resp.Body)
121 if err != nil {
122 return nil, fmt.Errorf("error reading response body: %v", err)
123 }
124
125 if resp.StatusCode != http.StatusOK {
126 return nil, fmt.Errorf("error response: %s", string(body))
127 }
128
129 var scoreboard ScoreboardResponse
130 if err := json.Unmarshal(body, &scoreboard); err != nil {
131 return nil, fmt.Errorf("error parsing JSON response: %v", err)
132 }
133
134 if !scoreboard.Success {
135 return nil, fmt.Errorf("API returned success=false")
136 }
137
138 return &scoreboard, nil
139}
140
141// GetChallengeList fetches the list of challenges from the CTFd API.
142// Returns a ChallengeListResponse containing all challenges sorted by ID or an error if the request fails.
143func (c *ctfdClient) GetChallengeList() (*ChallengeListResponse, error) {
144 endpoint := "/challenges"
145
146 req, err := http.NewRequest("GET", c.baseURL+endpoint, nil)
147 if err != nil {
148 return nil, fmt.Errorf("error creating request: %v", err)
149 }
150
151 req.Header.Add("Accept", "application/json")
152 req.Header.Add("Authorization", "Token "+c.apiToken)
153 req.Header.Add("Content-Type", "application/json")
154
155 resp, err := c.httpClient.Do(req)
156 if err != nil {
157 return nil, fmt.Errorf("error executing request: %v", err)
158 }
159 defer resp.Body.Close()
160
161 body, err := io.ReadAll(resp.Body)
162 if err != nil {
163 return nil, fmt.Errorf("error reading response body: %v", err)
164 }
165
166 if resp.StatusCode != http.StatusOK {
167 return nil, fmt.Errorf("error response: %s", string(body))
168 }
169
170 var challengeList ChallengeListResponse
171 if err := json.Unmarshal(body, &challengeList); err != nil {
172 return nil, fmt.Errorf("error parsing JSON response: %v", err)
173 }
174
175 sort.Slice(challengeList.Data, func(i, j int) bool {
176 return challengeList.Data[i].ID < challengeList.Data[j].ID
177 })
178
179 if !challengeList.Success {
180 return nil, fmt.Errorf("API returned success=false")
181 }
182
183 return &challengeList, nil
184}