⛳ alerts for any ctfd instance via ntfy
at main 5.5 kB view raw
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}