@b01lers/ctfd-api 1.0.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/.gitattributes +2 -0
- package/README.md +2 -0
- package/dist/index.js +71 -0
- package/dist/types.d.ts +54 -0
- package/package.json +20 -0
- package/rollup.config.mjs +20 -0
- package/src/client.ts +92 -0
- package/src/index.ts +2 -0
- package/src/types.ts +42 -0
- package/src/util.ts +3 -0
- package/tests/build.js +16 -0
- package/tests/demo.ts +17 -0
- package/tsconfig.json +18 -0
package/.gitattributes
ADDED
package/README.md
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function extractNonce(raw) {
|
|
4
|
+
return raw.match(/'csrfNonce': "(.+?)"/)[1];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
9
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
10
|
+
class CTFdClient {
|
|
11
|
+
constructor(options) {
|
|
12
|
+
__publicField(this, "url");
|
|
13
|
+
__publicField(this, "username");
|
|
14
|
+
__publicField(this, "password");
|
|
15
|
+
__publicField(this, "cachedSession", null);
|
|
16
|
+
__publicField(this, "cachedNonce", null);
|
|
17
|
+
__publicField(this, "sessionExpiry", /* @__PURE__ */ new Date());
|
|
18
|
+
this.url = options.url.endsWith("/") ? options.url.slice(0, -1) : options.url;
|
|
19
|
+
this.username = options.username;
|
|
20
|
+
this.password = options.password;
|
|
21
|
+
}
|
|
22
|
+
async submitFlag(id, flag) {
|
|
23
|
+
const { session, nonce } = await this.getAuthedSessionNonce();
|
|
24
|
+
return await (await fetch(`${this.url}/api/v1/challenges/attempt`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
"Csrf-Token": nonce,
|
|
29
|
+
cookie: session
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({ challenge_id: id, submission: flag })
|
|
32
|
+
})).json();
|
|
33
|
+
}
|
|
34
|
+
async getChallenges() {
|
|
35
|
+
const { session } = await this.getAuthedSessionNonce();
|
|
36
|
+
return await (await fetch(`${this.url}/api/v1/challenges`, {
|
|
37
|
+
headers: { cookie: session }
|
|
38
|
+
})).json();
|
|
39
|
+
}
|
|
40
|
+
async getAuthedSessionNonce() {
|
|
41
|
+
if (/* @__PURE__ */ new Date() < this.sessionExpiry && this.cachedSession && this.cachedNonce)
|
|
42
|
+
return { session: this.cachedSession, nonce: this.cachedNonce };
|
|
43
|
+
const res = await fetch(`${this.url}/login`);
|
|
44
|
+
const [session] = res.headers.getSetCookie()[0].split("; ");
|
|
45
|
+
const nonce = extractNonce(await res.text());
|
|
46
|
+
const formData = new FormData();
|
|
47
|
+
formData.append("name", this.username);
|
|
48
|
+
formData.append("password", this.password);
|
|
49
|
+
formData.append("_submit", "Submit");
|
|
50
|
+
formData.append("nonce", nonce);
|
|
51
|
+
const loginRes = await fetch(`${this.url}/login`, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: { cookie: session },
|
|
54
|
+
redirect: "manual",
|
|
55
|
+
body: formData
|
|
56
|
+
});
|
|
57
|
+
const [authedSession, expiresGmt] = loginRes.headers.getSetCookie()[0].split("; ");
|
|
58
|
+
const authedRaw = await (await fetch(`${this.url}/challenges`, {
|
|
59
|
+
headers: { cookie: authedSession }
|
|
60
|
+
})).text();
|
|
61
|
+
this.cachedSession = authedSession;
|
|
62
|
+
this.cachedNonce = extractNonce(authedRaw);
|
|
63
|
+
this.sessionExpiry = new Date(expiresGmt.slice(8));
|
|
64
|
+
return {
|
|
65
|
+
nonce: this.cachedNonce,
|
|
66
|
+
session: this.cachedSession
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
exports.CTFdClient = CTFdClient;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
type ScoreboardData = {
|
|
2
|
+
pos: number;
|
|
3
|
+
account_id: number;
|
|
4
|
+
account_url: string;
|
|
5
|
+
account_type: "user";
|
|
6
|
+
oauth_id: null;
|
|
7
|
+
name: string;
|
|
8
|
+
score: number;
|
|
9
|
+
bracket_id: null;
|
|
10
|
+
bracket_name: null;
|
|
11
|
+
};
|
|
12
|
+
type ChallengesResponse = {
|
|
13
|
+
success: true;
|
|
14
|
+
data: ChallengeData[];
|
|
15
|
+
};
|
|
16
|
+
type ChallengeData = {
|
|
17
|
+
id: number;
|
|
18
|
+
type: 'standard' | 'multiple_choice' | 'code';
|
|
19
|
+
name: string;
|
|
20
|
+
category: string;
|
|
21
|
+
script: string;
|
|
22
|
+
solved_by_me: boolean;
|
|
23
|
+
solves: number;
|
|
24
|
+
tags: string[];
|
|
25
|
+
template: string;
|
|
26
|
+
value: number;
|
|
27
|
+
};
|
|
28
|
+
type FlagSubmissionResponse = {
|
|
29
|
+
success: true;
|
|
30
|
+
data: {
|
|
31
|
+
status: "incorrect";
|
|
32
|
+
message: "Incorrect";
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type ClientOptions = {
|
|
37
|
+
url: string;
|
|
38
|
+
username: string;
|
|
39
|
+
password: string;
|
|
40
|
+
};
|
|
41
|
+
declare class CTFdClient {
|
|
42
|
+
private readonly url;
|
|
43
|
+
private readonly username;
|
|
44
|
+
private readonly password;
|
|
45
|
+
private cachedSession;
|
|
46
|
+
private cachedNonce;
|
|
47
|
+
private sessionExpiry;
|
|
48
|
+
constructor(options: ClientOptions);
|
|
49
|
+
submitFlag(id: number, flag: string): Promise<FlagSubmissionResponse>;
|
|
50
|
+
getChallenges(): Promise<ChallengesResponse>;
|
|
51
|
+
private getAuthedSessionNonce;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { CTFdClient, type ChallengeData, type ScoreboardData };
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@b01lers/ctfd-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "An API client for CTFd.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/types.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "rollup --config"
|
|
9
|
+
},
|
|
10
|
+
"author": "ky28059",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@types/node": "^22.10.10",
|
|
14
|
+
"rollup": "^4.32.0",
|
|
15
|
+
"rollup-plugin-dts": "^6.1.1",
|
|
16
|
+
"rollup-plugin-esbuild": "^6.1.1",
|
|
17
|
+
"tslib": "^2.8.1",
|
|
18
|
+
"typescript": "^5.7.3"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import esbuild from 'rollup-plugin-esbuild';
|
|
2
|
+
import { dts } from 'rollup-plugin-dts';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export default [{
|
|
6
|
+
input: 'src/index.ts',
|
|
7
|
+
output: {
|
|
8
|
+
file: 'dist/index.js',
|
|
9
|
+
format: 'cjs'
|
|
10
|
+
},
|
|
11
|
+
plugins: [
|
|
12
|
+
esbuild(),
|
|
13
|
+
]
|
|
14
|
+
}, {
|
|
15
|
+
input: "./src/index.ts",
|
|
16
|
+
output: [{ file: "dist/types.d.ts", format: "es" }],
|
|
17
|
+
plugins: [
|
|
18
|
+
dts()
|
|
19
|
+
],
|
|
20
|
+
}];
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { extractNonce } from './util';
|
|
2
|
+
import type { ChallengesResponse, FlagSubmissionResponse } from './types';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
type ClientOptions = {
|
|
6
|
+
url: string,
|
|
7
|
+
username: string,
|
|
8
|
+
password: string,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class CTFdClient {
|
|
12
|
+
private readonly url: string;
|
|
13
|
+
private readonly username: string;
|
|
14
|
+
private readonly password: string;
|
|
15
|
+
|
|
16
|
+
private cachedSession: string | null = null;
|
|
17
|
+
private cachedNonce: string | null = null;
|
|
18
|
+
private sessionExpiry = new Date();
|
|
19
|
+
|
|
20
|
+
constructor(options: ClientOptions) {
|
|
21
|
+
this.url = options.url.endsWith('/') ? options.url.slice(0, -1) : options.url;
|
|
22
|
+
this.username = options.username;
|
|
23
|
+
this.password = options.password;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public async submitFlag(id: number, flag: string) {
|
|
27
|
+
const { session, nonce } = await this.getAuthedSessionNonce();
|
|
28
|
+
|
|
29
|
+
return await (await fetch(`${this.url}/api/v1/challenges/attempt`, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
'Csrf-Token': nonce,
|
|
34
|
+
cookie: session,
|
|
35
|
+
},
|
|
36
|
+
body: JSON.stringify({ challenge_id: id, submission: flag }),
|
|
37
|
+
})).json() as FlagSubmissionResponse;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public async getChallenges() {
|
|
41
|
+
const { session } = await this.getAuthedSessionNonce();
|
|
42
|
+
|
|
43
|
+
return await (await fetch(`${this.url}/api/v1/challenges`, {
|
|
44
|
+
headers: { cookie: session }
|
|
45
|
+
})).json() as ChallengesResponse;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private async getAuthedSessionNonce() {
|
|
49
|
+
// If we have a cached, non-expired session, use it
|
|
50
|
+
if (new Date() < this.sessionExpiry && this.cachedSession && this.cachedNonce)
|
|
51
|
+
return { session: this.cachedSession, nonce: this.cachedNonce };
|
|
52
|
+
|
|
53
|
+
const res = await fetch(`${this.url}/login`);
|
|
54
|
+
|
|
55
|
+
const [session] = res.headers.getSetCookie()[0].split('; ');
|
|
56
|
+
const nonce = extractNonce(await res.text());
|
|
57
|
+
|
|
58
|
+
const formData = new FormData();
|
|
59
|
+
formData.append('name', this.username);
|
|
60
|
+
formData.append('password', this.password);
|
|
61
|
+
formData.append('_submit', 'Submit');
|
|
62
|
+
formData.append('nonce', nonce);
|
|
63
|
+
|
|
64
|
+
const loginRes = await fetch(`${this.url}/login`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { cookie: session },
|
|
67
|
+
redirect: 'manual',
|
|
68
|
+
body: formData,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const [authedSession, expiresGmt] = loginRes.headers.getSetCookie()[0].split('; ');
|
|
72
|
+
const authedRaw = await (await fetch(`${this.url}/challenges`, {
|
|
73
|
+
headers: { cookie: authedSession }
|
|
74
|
+
})).text();
|
|
75
|
+
|
|
76
|
+
// Cache session data and expiry date
|
|
77
|
+
this.cachedSession = authedSession;
|
|
78
|
+
this.cachedNonce = extractNonce(authedRaw);
|
|
79
|
+
this.sessionExpiry = new Date(expiresGmt.slice(8));
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
nonce: this.cachedNonce,
|
|
83
|
+
session: this.cachedSession,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
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
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type ScoreboardResponse = {
|
|
2
|
+
success: true,
|
|
3
|
+
data: ScoreboardData[],
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type ScoreboardData = {
|
|
7
|
+
pos: number,
|
|
8
|
+
account_id: number,
|
|
9
|
+
account_url: string,
|
|
10
|
+
account_type: "user",
|
|
11
|
+
oauth_id: null,
|
|
12
|
+
name: string,
|
|
13
|
+
score: number,
|
|
14
|
+
bracket_id: null,
|
|
15
|
+
bracket_name: null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type ChallengesResponse = {
|
|
19
|
+
success: true,
|
|
20
|
+
data: ChallengeData[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ChallengeData = {
|
|
24
|
+
id: number,
|
|
25
|
+
type: 'standard' | 'multiple_choice' | 'code',
|
|
26
|
+
name: string,
|
|
27
|
+
category: string,
|
|
28
|
+
script: string,
|
|
29
|
+
solved_by_me: boolean,
|
|
30
|
+
solves: number,
|
|
31
|
+
tags: string[],
|
|
32
|
+
template: string,
|
|
33
|
+
value: number,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type FlagSubmissionResponse = {
|
|
37
|
+
success: true,
|
|
38
|
+
data: {
|
|
39
|
+
status: "incorrect", // TODO
|
|
40
|
+
message: "Incorrect"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/util.ts
ADDED
package/tests/build.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { CTFdClient } = require('../dist/index.js');
|
|
2
|
+
|
|
3
|
+
;(async () => {
|
|
4
|
+
const client = new CTFdClient({
|
|
5
|
+
url: 'https://demo.ctfd.io/',
|
|
6
|
+
username: 'user',
|
|
7
|
+
password: 'password',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const challs = await client.getChallenges();
|
|
11
|
+
console.log(challs.data);
|
|
12
|
+
|
|
13
|
+
const chall = challs.data.find((c) => c.name === 'The Lost Park');
|
|
14
|
+
const res = await client.submitFlag(chall.id, 'cftd{test_flag}');
|
|
15
|
+
console.log(res);
|
|
16
|
+
})()
|
package/tests/demo.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CTFdClient } from '../src/client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
;(async () => {
|
|
5
|
+
const client = new CTFdClient({
|
|
6
|
+
url: 'https://demo.ctfd.io/',
|
|
7
|
+
username: 'user',
|
|
8
|
+
password: 'password',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const challs = await client.getChallenges();
|
|
12
|
+
console.log(challs.data);
|
|
13
|
+
|
|
14
|
+
const chall = challs.data.find((c) => c.name === 'The Lost Park')!;
|
|
15
|
+
const res = await client.submitFlag(chall.id, 'cftd{test_flag}');
|
|
16
|
+
console.log(res);
|
|
17
|
+
})()
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"display": "Node 20",
|
|
4
|
+
"_version": "20.1.0",
|
|
5
|
+
"include": ["src"],
|
|
6
|
+
"compilerOptions": {
|
|
7
|
+
"lib": ["es2023"],
|
|
8
|
+
"module": "node16",
|
|
9
|
+
"target": "es2022",
|
|
10
|
+
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"downlevelIteration": true,
|
|
16
|
+
"moduleResolution": "node16"
|
|
17
|
+
}
|
|
18
|
+
}
|