@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 ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
package/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # CTFd API
2
+ A simple, typed API client for the CTFd API.
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;
@@ -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
@@ -0,0 +1,2 @@
1
+ export { CTFdClient } from './client';
2
+ export type { ScoreboardData, ChallengeData } from './types';
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
@@ -0,0 +1,3 @@
1
+ export function extractNonce(raw: string) {
2
+ return raw.match(/'csrfNonce': "(.+?)"/)![1];
3
+ }
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
+ }