@b01lers/ctfd-api 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -1
- package/dist/index.js +14 -2
- package/dist/types.d.ts +22 -15
- package/package.json +1 -1
- package/src/client.ts +20 -9
- package/src/index.ts +1 -1
- package/src/types.ts +20 -3
- package/tests/build.js +5 -2
- package/tests/demo.ts +5 -2
package/README.md
CHANGED
|
@@ -1,2 +1,31 @@
|
|
|
1
1
|
# CTFd API
|
|
2
|
-
A simple, typed
|
|
2
|
+
A simple, typed client for the CTFd API.
|
|
3
|
+
|
|
4
|
+
### Installing
|
|
5
|
+
```bash
|
|
6
|
+
npm i @b01lers/ctfd-api
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
### Usage
|
|
10
|
+
<!-- TODO: docs -->
|
|
11
|
+
Example usage that fetches challenges from the CTFd demo instance and attempts to submit a flag for a challenge:
|
|
12
|
+
```ts
|
|
13
|
+
import { CTFdClient } from '@b01lers/ctfd-api';
|
|
14
|
+
|
|
15
|
+
const client = new CTFdClient({
|
|
16
|
+
url: 'https://demo.ctfd.io/',
|
|
17
|
+
username: 'user',
|
|
18
|
+
password: 'password',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Fetch challenges list
|
|
22
|
+
const challs = await client.getChallenges();
|
|
23
|
+
|
|
24
|
+
// Fetch scoreboard data
|
|
25
|
+
const scoreboard = await client.getScoreboard();
|
|
26
|
+
console.log(scoreboard.slice(0, 5));
|
|
27
|
+
|
|
28
|
+
// Submit a flag for a challenge
|
|
29
|
+
const chall = challs.find((c) => c.name === 'The Lost Park')!;
|
|
30
|
+
await client.submitFlag(chall.id, 'cftd{test_flag}');
|
|
31
|
+
```
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ class CTFdClient {
|
|
|
21
21
|
}
|
|
22
22
|
async submitFlag(id, flag) {
|
|
23
23
|
const { session, nonce } = await this.getAuthedSessionNonce();
|
|
24
|
-
|
|
24
|
+
const res = await (await fetch(`${this.url}/api/v1/challenges/attempt`, {
|
|
25
25
|
method: "POST",
|
|
26
26
|
headers: {
|
|
27
27
|
"Content-Type": "application/json",
|
|
@@ -30,12 +30,24 @@ class CTFdClient {
|
|
|
30
30
|
},
|
|
31
31
|
body: JSON.stringify({ challenge_id: id, submission: flag })
|
|
32
32
|
})).json();
|
|
33
|
+
return res.data;
|
|
33
34
|
}
|
|
34
35
|
async getChallenges() {
|
|
35
36
|
const { session } = await this.getAuthedSessionNonce();
|
|
36
|
-
|
|
37
|
+
const res = await (await fetch(`${this.url}/api/v1/challenges`, {
|
|
37
38
|
headers: { cookie: session }
|
|
38
39
|
})).json();
|
|
40
|
+
return res.data;
|
|
41
|
+
}
|
|
42
|
+
async getScoreboard() {
|
|
43
|
+
const { session, nonce } = await this.getAuthedSessionNonce();
|
|
44
|
+
const res = await (await fetch(`${this.url}/api/v1/scoreboard`, {
|
|
45
|
+
headers: {
|
|
46
|
+
"Csrf-Token": nonce,
|
|
47
|
+
cookie: session
|
|
48
|
+
}
|
|
49
|
+
})).json();
|
|
50
|
+
return res.data;
|
|
39
51
|
}
|
|
40
52
|
async getAuthedSessionNonce() {
|
|
41
53
|
if (/* @__PURE__ */ new Date() < this.sessionExpiry && this.cachedSession && this.cachedNonce)
|
package/dist/types.d.ts
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
|
-
type
|
|
1
|
+
type BaseScoreboardEntry = {
|
|
2
2
|
pos: number;
|
|
3
3
|
account_id: number;
|
|
4
4
|
account_url: string;
|
|
5
|
-
account_type: "user";
|
|
6
5
|
oauth_id: null;
|
|
7
6
|
name: string;
|
|
8
7
|
score: number;
|
|
9
8
|
bracket_id: null;
|
|
10
9
|
bracket_name: null;
|
|
11
10
|
};
|
|
12
|
-
type
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
type ScoreboardUserEntry = BaseScoreboardEntry & {
|
|
12
|
+
account_type: "user";
|
|
13
|
+
};
|
|
14
|
+
type ScoreboardTeamEntry = BaseScoreboardEntry & {
|
|
15
|
+
account_type: "team";
|
|
16
|
+
members: {
|
|
17
|
+
id: number;
|
|
18
|
+
oauth_id: null;
|
|
19
|
+
name: string;
|
|
20
|
+
score: number;
|
|
21
|
+
bracket_id: null;
|
|
22
|
+
bracket_name: null;
|
|
23
|
+
}[];
|
|
15
24
|
};
|
|
25
|
+
type ScoreboardEntry = ScoreboardUserEntry | ScoreboardTeamEntry;
|
|
16
26
|
type ChallengeData = {
|
|
17
27
|
id: number;
|
|
18
28
|
type: 'standard' | 'multiple_choice' | 'code';
|
|
@@ -25,13 +35,6 @@ type ChallengeData = {
|
|
|
25
35
|
template: string;
|
|
26
36
|
value: number;
|
|
27
37
|
};
|
|
28
|
-
type FlagSubmissionResponse = {
|
|
29
|
-
success: true;
|
|
30
|
-
data: {
|
|
31
|
-
status: "incorrect";
|
|
32
|
-
message: "Incorrect";
|
|
33
|
-
};
|
|
34
|
-
};
|
|
35
38
|
|
|
36
39
|
type ClientOptions = {
|
|
37
40
|
url: string;
|
|
@@ -46,9 +49,13 @@ declare class CTFdClient {
|
|
|
46
49
|
private cachedNonce;
|
|
47
50
|
private sessionExpiry;
|
|
48
51
|
constructor(options: ClientOptions);
|
|
49
|
-
submitFlag(id: number, flag: string): Promise<
|
|
50
|
-
|
|
52
|
+
submitFlag(id: number, flag: string): Promise<{
|
|
53
|
+
status: "incorrect";
|
|
54
|
+
message: "Incorrect";
|
|
55
|
+
}>;
|
|
56
|
+
getChallenges(): Promise<ChallengeData[]>;
|
|
57
|
+
getScoreboard(): Promise<ScoreboardEntry[]>;
|
|
51
58
|
private getAuthedSessionNonce;
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
export { CTFdClient, type ChallengeData, type
|
|
61
|
+
export { CTFdClient, type ChallengeData, type ScoreboardEntry };
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { extractNonce } from './util';
|
|
2
|
-
import type { ChallengesResponse, FlagSubmissionResponse } from './types';
|
|
2
|
+
import type { ChallengesResponse, FlagSubmissionResponse, ScoreboardResponse } from './types';
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
type ClientOptions = {
|
|
@@ -26,7 +26,7 @@ export class CTFdClient {
|
|
|
26
26
|
public async submitFlag(id: number, flag: string) {
|
|
27
27
|
const { session, nonce } = await this.getAuthedSessionNonce();
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
const res = await (await fetch(`${this.url}/api/v1/challenges/attempt`, {
|
|
30
30
|
method: 'POST',
|
|
31
31
|
headers: {
|
|
32
32
|
'Content-Type': 'application/json',
|
|
@@ -35,14 +35,31 @@ export class CTFdClient {
|
|
|
35
35
|
},
|
|
36
36
|
body: JSON.stringify({ challenge_id: id, submission: flag }),
|
|
37
37
|
})).json() as FlagSubmissionResponse;
|
|
38
|
+
|
|
39
|
+
return res.data;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
public async getChallenges() {
|
|
41
43
|
const { session } = await this.getAuthedSessionNonce();
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
const res = await (await fetch(`${this.url}/api/v1/challenges`, {
|
|
44
46
|
headers: { cookie: session }
|
|
45
47
|
})).json() as ChallengesResponse;
|
|
48
|
+
|
|
49
|
+
return res.data;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public async getScoreboard() {
|
|
53
|
+
const { session, nonce } = await this.getAuthedSessionNonce();
|
|
54
|
+
|
|
55
|
+
const res = await (await fetch(`${this.url}/api/v1/scoreboard`, {
|
|
56
|
+
headers: {
|
|
57
|
+
'Csrf-Token': nonce,
|
|
58
|
+
cookie: session,
|
|
59
|
+
},
|
|
60
|
+
})).json() as ScoreboardResponse;
|
|
61
|
+
|
|
62
|
+
return res.data;
|
|
46
63
|
}
|
|
47
64
|
|
|
48
65
|
private async getAuthedSessionNonce() {
|
|
@@ -84,9 +101,3 @@ export class CTFdClient {
|
|
|
84
101
|
};
|
|
85
102
|
}
|
|
86
103
|
}
|
|
87
|
-
|
|
88
|
-
// export async function getScoreboard() {
|
|
89
|
-
// return await (await fetch('https://ectf.ctfd.io/api/v1/scoreboard', {
|
|
90
|
-
// headers: { 'Authorization': CTFD_API_KEY }
|
|
91
|
-
// })).json() as ScoreboardResponse;
|
|
92
|
-
// }
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { CTFdClient } from './client';
|
|
2
|
-
export type {
|
|
2
|
+
export type { ScoreboardEntry, ChallengeData } from './types';
|
package/src/types.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
export type ScoreboardResponse = {
|
|
2
2
|
success: true,
|
|
3
|
-
data:
|
|
3
|
+
data: ScoreboardEntry[],
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
type BaseScoreboardEntry = {
|
|
7
7
|
pos: number,
|
|
8
8
|
account_id: number,
|
|
9
9
|
account_url: string,
|
|
10
|
-
account_type: "user",
|
|
11
10
|
oauth_id: null,
|
|
12
11
|
name: string,
|
|
13
12
|
score: number,
|
|
@@ -15,6 +14,24 @@ export type ScoreboardData = {
|
|
|
15
14
|
bracket_name: null
|
|
16
15
|
}
|
|
17
16
|
|
|
17
|
+
type ScoreboardUserEntry = BaseScoreboardEntry & {
|
|
18
|
+
account_type: "user",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type ScoreboardTeamEntry = BaseScoreboardEntry & {
|
|
22
|
+
account_type: "team",
|
|
23
|
+
members: {
|
|
24
|
+
id: number,
|
|
25
|
+
oauth_id: null,
|
|
26
|
+
name: string,
|
|
27
|
+
score: number,
|
|
28
|
+
bracket_id: null,
|
|
29
|
+
bracket_name: null
|
|
30
|
+
}[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ScoreboardEntry = ScoreboardUserEntry | ScoreboardTeamEntry;
|
|
34
|
+
|
|
18
35
|
export type ChallengesResponse = {
|
|
19
36
|
success: true,
|
|
20
37
|
data: ChallengeData[]
|
package/tests/build.js
CHANGED
|
@@ -8,9 +8,12 @@ const { CTFdClient } = require('../dist/index.js');
|
|
|
8
8
|
});
|
|
9
9
|
|
|
10
10
|
const challs = await client.getChallenges();
|
|
11
|
-
console.log(challs
|
|
11
|
+
console.log(challs);
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const scoreboard = await client.getScoreboard();
|
|
14
|
+
console.log(scoreboard.slice(0, 5));
|
|
15
|
+
|
|
16
|
+
const chall = challs.find((c) => c.name === 'The Lost Park');
|
|
14
17
|
const res = await client.submitFlag(chall.id, 'cftd{test_flag}');
|
|
15
18
|
console.log(res);
|
|
16
19
|
})()
|
package/tests/demo.ts
CHANGED
|
@@ -9,9 +9,12 @@ import { CTFdClient } from '../src/client';
|
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
const challs = await client.getChallenges();
|
|
12
|
-
console.log(challs
|
|
12
|
+
console.log(challs);
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const scoreboard = await client.getScoreboard();
|
|
15
|
+
console.log(scoreboard.slice(0, 5));
|
|
16
|
+
|
|
17
|
+
const chall = challs.find((c) => c.name === 'The Lost Park')!;
|
|
15
18
|
const res = await client.submitFlag(chall.id, 'cftd{test_flag}');
|
|
16
19
|
console.log(res);
|
|
17
20
|
})()
|