Skip to content

Commit c736700

Browse files
authored
Bump CTFd to v3.8.0 (#182)
* chore(deps): bump CTFd to 3.8.0 * feat(api): bump API support to 3.8.0, refacto tests * ci: fix sha256 sum for proper tag pin * ci: fix tests output * ci: fix condition * chore: remove Makefile We no longer run tests manually, and the Makefile had low value so I drop it
1 parent aa67ebe commit c736700

File tree

12 files changed

+264
-217
lines changed

12 files changed

+264
-217
lines changed

.github/workflows/ci.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
runs-on: ubuntu-latest
1515
services:
1616
ctfd:
17-
image: ctfd/ctfd:3.7.7@sha256:9847758cdafc5ab86bdc121353dcf5a27a29ce313587270ee90a71bfbda2b910
17+
image: ctfd/ctfd:3.8.0@sha256:61712c4af6e9cddccfafc4503484c8091f6325be286d407595b922dc9552b5ae
1818
ports:
1919
- 8000:8000
2020
steps:
@@ -52,15 +52,15 @@ jobs:
5252
fi
5353
5454
- name: Run go tests
55-
run: make tests
55+
run: go test ./api -run=^Test_F_ -coverprofile=functional.cov
5656
env:
5757
CTFD_URL: http://localhost:8000
5858

5959
- name: Upload coverage to Coveralls
6060
if: ${{ matrix.update-coverage }}
6161
uses: shogo82148/actions-goveralls@25f5320d970fb565100cf1993ada29be1bb196a1 # v1.10.0
6262
with:
63-
path-to-profile: functional.out
63+
path-to-profile: functional.cov
6464

6565
go-lint:
6666
runs-on: ubuntu-latest

Makefile

Lines changed: 0 additions & 11 deletions
This file was deleted.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@
1313

1414
Golang client for interacting with [CTFd](https://ctfd.io/).
1515

16-
Last version tested on: [3.7.7](https://github.com/CTFd/CTFd/releases/tag/3.7.7).
16+
Last version tested on: [3.8.0](https://github.com/CTFd/CTFd/releases/tag/3.8.0).

api/challenges.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type PostChallengesParams struct {
3333
Initial *int `json:"initial,omitempty"`
3434
Decay *int `json:"decay,omitempty"`
3535
Minimum *int `json:"minimum,omitempty"`
36+
Logic string `json:"logic"`
3637
MaxAttempts *int `json:"max_attempts,omitempty"`
3738
NextID *int `json:"next_id,omitempty"`
3839
Requirements *Requirements `json:"requirements,omitempty"`
@@ -89,6 +90,7 @@ type PatchChallengeParams struct {
8990
Initial *int `json:"initial,omitempty"`
9091
Decay *int `json:"decay,omitempty"`
9192
Minimum *int `json:"minimum,omitempty"`
93+
Logic *string `json:"logic,omitempty"`
9294
MaxAttempts *int `json:"max_attempts,omitempty"`
9395
NextID *int `json:"next_id,omitempty"`
9496
// Requirements can update the challenge's behavior and prerequisites i.e.
@@ -167,3 +169,24 @@ func (client *Client) GetChallengeTopics(id int, opts ...Option) ([]*Topic, erro
167169
}
168170
return ct, nil
169171
}
172+
173+
func (client *Client) GetChallengeRatings(id int, opts ...Option) ([]*Rating, error) {
174+
ratings := []*Rating{}
175+
if err := client.Get(fmt.Sprintf("/challenges/%d/ratings", id), nil, &ratings, opts...); err != nil {
176+
return nil, err
177+
}
178+
return ratings, nil
179+
}
180+
181+
type PutChallengeRatingsParams struct {
182+
Value int `json:"value"` // either 1 for thumbsup or -1 for thumbsdown
183+
Review string `json:"review"`
184+
}
185+
186+
func (client *Client) PutChallengeRatings(id int, params *PutChallengeRatingsParams, opts ...Option) (*Rating, error) {
187+
rat := &Rating{}
188+
if err := client.Put(fmt.Sprintf("/challenges/%c/ratings", id), params, rat, opts...); err != nil {
189+
return nil, err
190+
}
191+
return rat, nil
192+
}

api/client.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,16 @@ func (client *Client) Patch(edp string, params any, dst any, opts ...Option) err
216216
return client.Call(req, dst, opts...)
217217
}
218218

219+
func (client *Client) Put(edp string, params any, dst any, opts ...Option) error {
220+
body, err := json.Marshal(params)
221+
if err != nil {
222+
return err
223+
}
224+
req, _ := http.NewRequest(http.MethodPatch, edp, bytes.NewBuffer(body))
225+
226+
return client.Call(req, dst, opts...)
227+
}
228+
219229
func (client *Client) Delete(edp string, params any, dst any, opts ...Option) (err error) {
220230
var body []byte
221231
if params != nil {

api/configs.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,17 @@ type PatchConfigsParams struct {
4343
TeamCreation *bool `json:"team_creation,omitempty"`
4444
TeamDisbanding *string `json:"team_disbanding,omitempty"`
4545
TeamSize *int `json:"team_size,omitempty"`
46+
PasswordMinLength *int `json:"password_min_length,omitempty"`
4647
VerifyEmails *bool `json:"verify_emails,omitempty"`
4748

49+
// Challenges
50+
51+
ViewSelfSubmission bool `json:"view_self_submissions"`
52+
MaxAttemptsBehavior string `json:"max_attempts_behavior"`
53+
MaxAttemptsTimeout int `json:"max_attempts_timeout"`
54+
HintsFreePublicAccess bool `json:"hints_free_public_access"`
55+
ChallengeRatings string `json:"challenge_ratings"`
56+
4857
// Pages
4958

5059
RobotsTxt *string `json:"robots_txt,omitempty"`
@@ -103,7 +112,8 @@ type PatchConfigsParams struct {
103112

104113
// Social
105114

106-
SocialShares *bool `json:"social_shares,omitempty"`
115+
SocialShares *bool `json:"social_shares,omitempty"`
116+
SocialSharesTemplate string `json:"social_share_solve_template"`
107117

108118
// Legal
109119

api/model.go

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,48 @@ package api
22

33
type (
44
Challenge struct {
5-
ID int `json:"id"`
6-
Name string `json:"name"`
7-
Description string `json:"description"`
8-
Attribution *string `json:"attribution,omitempty"`
9-
ConnectionInfo *string `json:"connection_info,omitempty"`
10-
MaxAttempts *int `json:"max_attempts,omitempty"`
11-
Function *string `json:"function,omitempty"`
12-
Value int `json:"value"`
13-
Initial *int `json:"initial,omitempty"`
14-
Decay *int `json:"decay,omitempty"`
15-
Minimum *int `json:"minimum,omitempty"`
16-
Category string `json:"category"`
17-
Type string `json:"type"`
18-
TypeData *Type `json:"type_data,omitempty"`
19-
State string `json:"state"`
20-
NextID *int `json:"next_id"`
21-
Requirements *Requirements `json:"requirements"` // List of challenge IDs to complete before
22-
Solves int `json:"solves"`
23-
SolvedByMe bool `json:"solved_by_me"`
5+
ID int `json:"id"`
6+
Name string `json:"name"`
7+
Description string `json:"description"`
8+
Attribution *string `json:"attribution,omitempty"`
9+
ConnectionInfo *string `json:"connection_info,omitempty"`
10+
MaxAttempts *int `json:"max_attempts,omitempty"`
11+
Function *string `json:"function,omitempty"`
12+
Value int `json:"value"`
13+
Initial *int `json:"initial,omitempty"`
14+
Decay *int `json:"decay,omitempty"`
15+
Minimum *int `json:"minimum,omitempty"`
16+
Logic string `json:"logic"`
17+
Category string `json:"category"`
18+
SolutionID *int `json:"solution_id,omitempty"`
19+
Type string `json:"type"`
20+
TypeData *Type `json:"type_data,omitempty"`
21+
State string `json:"state"`
22+
NextID *int `json:"next_id"`
23+
Requirements *Requirements `json:"requirements"` // List of challenge IDs to complete before
24+
Solves int `json:"solves"`
25+
SolvedByMe bool `json:"solved_by_me"`
26+
Rating *Rating `json:"rating,omitempty"`
27+
Ratings *ChallengeRatings `json:"ratings,omitempty"`
28+
}
29+
30+
Solution struct {
31+
ID int `json:"id"`
32+
ChallengeID int `json:"challenge_id"`
33+
State string `json:"state"`
34+
Content string `json:"content"`
35+
HTML string `json:"html"`
36+
}
37+
38+
Rating struct {
39+
ID int `json:"id"`
40+
Date string `json:"date"`
41+
Value int `json:"value"`
42+
Review string `json:"review"`
43+
UserID int `json:"user_id"`
44+
User *User `json:"user"`
45+
ChallengeID int `json:"challenge_id"`
46+
Challenge *Challenge `json:"challenge"`
2447
}
2548

2649
Bracket struct {
@@ -85,6 +108,12 @@ type (
85108
Prerequisites []int `json:"prerequisites"`
86109
}
87110

111+
ChallengeRatings struct {
112+
Up int `json:"up"`
113+
Down int `json:"down"`
114+
Count int `json:"count"`
115+
}
116+
88117
Tag struct {
89118
ID int `json:"id"`
90119
Challenge *int `json:"challenge,omitempty"` // XXX This may be duplicated with ChallengeID ?
@@ -313,4 +342,8 @@ type (
313342
Status string `json:"status"`
314343
Message string `json:"message"`
315344
}
345+
346+
Shares struct {
347+
URL string `json:"url"`
348+
}
316349
)

api/run_test.go

Lines changed: 31 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/ctfer-io/go-ctfd/api"
99
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
1011
)
1112

1213
func Test_F_CTF(t *testing.T) {
@@ -17,13 +18,9 @@ func Test_F_CTF(t *testing.T) {
1718
// For the first blood the admin award the player, then extract the
1819
// statistics and pause the event.
1920

20-
assert := assert.New(t)
21-
2221
// 1a. Get nonce and session to mock a browser first
2322
nonce, session, err := api.GetNonceAndSession(CTFD_URL)
24-
if !assert.Nil(err, "got error: %s", err) {
25-
return
26-
}
23+
require.NoError(t, err)
2724
admin := api.NewClient(CTFD_URL, nonce, session, "")
2825

2926
t.Cleanup(func() {
@@ -63,18 +60,14 @@ func Test_F_CTF(t *testing.T) {
6360
Start: "",
6461
End: "",
6562
})
66-
if !assert.Nil(err, "got error: %s", err) {
67-
return
68-
}
63+
require.NoError(t, err)
6964

7065
// 1c. Create an API Key to avoid session/nonce+cookies dance
7166
token, err := admin.PostTokens(&api.PostTokensParams{
7267
Expiration: "2222-01-01",
7368
Description: "Example API token.",
7469
})
75-
if !assert.Nil(err, "got error: %s", err) {
76-
return
77-
}
70+
require.NoError(t, err)
7871
admin.SetAPIKey(*token.Value)
7972

8073
// 2. Add a page
@@ -88,9 +81,7 @@ func Test_F_CTF(t *testing.T) {
8881
Route: "/tutorials/test",
8982
Title: "Test",
9083
})
91-
if !assert.Nil(err, "got error: %s", err) {
92-
return
93-
}
84+
require.NoError(t, err)
9485

9586
// 3. Add a challenge with a flag
9687
chall, err := admin.PostChallenges(&api.PostChallengesParams{
@@ -106,20 +97,17 @@ func Test_F_CTF(t *testing.T) {
10697
State: "visible",
10798
Type: "dynamic",
10899
})
109-
assert.NotNil(chall)
110-
if !assert.Nil(err, "got error: %s", err) {
111-
return
112-
}
100+
require.NotNil(t, chall)
101+
require.NoError(t, err)
102+
113103
flag, err := admin.PostFlags(&api.PostFlagsParams{
114104
Challenge: chall.ID,
115105
Content: "24HIUT{IcmpExfiltrationIsEasy}",
116106
Data: "case_sensitive",
117107
Type: "static",
118108
})
119-
assert.NotNil(flag)
120-
if !assert.Nil(err, "got error: %s", err) {
121-
return
122-
}
109+
assert.NotNil(t, flag)
110+
require.NoError(t, err)
123111

124112
// 4. User register
125113
name := "ctfer-" + randHex()
@@ -130,33 +118,34 @@ func Test_F_CTF(t *testing.T) {
130118
Email: name + "@example.com",
131119
Password: "password",
132120
})
133-
if !assert.Nil(err, "got error: %s", err) {
134-
return
135-
}
121+
require.NoError(t, err)
122+
136123
usr, err := user.GetUsersMe()
137-
if !assert.Nil(err, "got error: %s", err) {
138-
return
139-
}
124+
require.NoError(t, err)
140125

141126
// 5a. User failed attempt
142127
att, err := user.PostChallengesAttempt(&api.PostChallengesAttemptParams{
143128
ChallengeID: chall.ID,
144129
Submission: "INVALID-FLAG",
145130
})
146-
if !assert.Nil(err, "got error: %s", err) {
147-
return
148-
}
149-
assert.Equal("incorrect", att.Status)
131+
assert.Equal(t, "incorrect", att.Status)
132+
require.NoError(t, err)
150133

151134
// 5b. User successfull attempt
152135
att, err = user.PostChallengesAttempt(&api.PostChallengesAttemptParams{
153136
ChallengeID: chall.ID,
154137
Submission: "24HIUT{IcmpExfiltrationIsEasy}",
155138
})
156-
if !assert.Nil(err, "got error: %s", err) {
157-
return
158-
}
159-
assert.Equal("correct", att.Status)
139+
assert.Equal(t, "correct", att.Status)
140+
require.NoError(t, err)
141+
142+
// 5c. User share its work
143+
sh, err := user.PostShares(&api.PostSharesParams{
144+
ChallengeID: chall.ID,
145+
Type: "solve",
146+
})
147+
require.Nil(t, err)
148+
assert.NotEmpty(t, sh.URL)
160149

161150
// 6. Admin gives an award for first blood
162151
_, err = admin.PostAwards(&api.PostAwardsParams{
@@ -167,28 +156,22 @@ func Test_F_CTF(t *testing.T) {
167156
UserID: usr.ID,
168157
Value: 50,
169158
})
170-
if !assert.Nil(err, "got error: %s", err) {
171-
return
172-
}
159+
require.NoError(t, err)
173160

174161
// 7. Admin gets some statistics
175162
_, err = admin.GetStatisticsChallengesSolves()
176-
if !assert.Nil(err, "got error: %s", err) {
177-
return
178-
}
163+
require.NoError(t, err)
164+
179165
_, err = admin.GetStatisticsUsers()
180-
if !assert.Nil(err, "got error: %s", err) {
181-
return
182-
}
166+
require.NoError(t, err)
167+
183168
// ...
184169

185170
// 8. Admin pause event
186171
err = admin.PatchConfigs(&api.PatchConfigsParams{
187172
Paused: ptr(true),
188173
})
189-
if !assert.Nil(err, "got error: %s", err) {
190-
return
191-
}
174+
require.NoError(t, err)
192175

193176
// Time to open-source your challenges :)
194177
}

0 commit comments

Comments
 (0)