icoa-cli 2.19.13 → 2.19.14
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/dist/commands/ctf.js +21 -6
- package/dist/commands/exam.js +17 -11
- package/dist/lib/ctfd-client.d.ts +1 -0
- package/dist/lib/ctfd-client.js +31 -1
- package/package.json +2 -2
package/dist/commands/ctf.js
CHANGED
|
@@ -176,12 +176,27 @@ export function registerCtfCommands(program) {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
catch (err) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
179
|
+
if (sessionCookie) {
|
|
180
|
+
// Session login succeeded but API test failed — save anyway
|
|
181
|
+
spinner2.succeed('Connected (session mode — limited API)');
|
|
182
|
+
saveConfig({
|
|
183
|
+
ctfdUrl: url,
|
|
184
|
+
token: sessionCookie,
|
|
185
|
+
userName: username,
|
|
186
|
+
sessionCookie: sessionCookie,
|
|
187
|
+
});
|
|
188
|
+
console.log();
|
|
189
|
+
printKeyValue('User', username);
|
|
190
|
+
printWarning('API access limited. Some features may not work.');
|
|
191
|
+
printSuccess('Connection saved.');
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
spinner2.fail('Connection test failed');
|
|
195
|
+
printError(err.message);
|
|
196
|
+
saveConfig({ ctfdUrl: url });
|
|
197
|
+
console.log();
|
|
198
|
+
printInfo('Connection saved. Try: join ' + url);
|
|
199
|
+
}
|
|
185
200
|
}
|
|
186
201
|
});
|
|
187
202
|
// ─── icoa ctf logout ───
|
package/dist/commands/exam.js
CHANGED
|
@@ -29,14 +29,17 @@ function checkTimeRemaining() {
|
|
|
29
29
|
return true;
|
|
30
30
|
if (new Date() >= deadline) {
|
|
31
31
|
const state = getExamState();
|
|
32
|
-
if (state
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
if (state) {
|
|
33
|
+
const answered = Object.keys(state.answers).length;
|
|
34
|
+
if (state.session.examId === 'demo-free' || answered === 0) {
|
|
35
|
+
// Demo or unanswered expired exam — auto-clear to avoid deadlock
|
|
36
|
+
clearExamState();
|
|
37
|
+
printWarning('Exam expired and cleared.');
|
|
38
|
+
printInfo('Type: demo to start again.');
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
printError('Time expired! Use "exam submit" to submit your answers.');
|
|
42
|
+
}
|
|
40
43
|
}
|
|
41
44
|
return false;
|
|
42
45
|
}
|
|
@@ -639,12 +642,15 @@ export function registerExamCommand(program) {
|
|
|
639
642
|
const total = state.session.questionCount;
|
|
640
643
|
const unanswered = total - answered;
|
|
641
644
|
if (answered === 0) {
|
|
642
|
-
|
|
643
|
-
|
|
645
|
+
const deadline = getExamDeadline();
|
|
646
|
+
const expired = deadline && new Date() >= deadline;
|
|
647
|
+
if (state.session.examId === 'demo-free' || expired) {
|
|
644
648
|
clearExamState();
|
|
645
|
-
|
|
649
|
+
printWarning('No answers to submit. Exam cleared.');
|
|
650
|
+
printInfo('Type: demo to start again.');
|
|
646
651
|
}
|
|
647
652
|
else {
|
|
653
|
+
printWarning('No answers to submit.');
|
|
648
654
|
printInfo('Answer some questions first: exam q 1');
|
|
649
655
|
}
|
|
650
656
|
return;
|
|
@@ -9,6 +9,7 @@ export declare class CTFdClient {
|
|
|
9
9
|
fetchCsrfNonce(): Promise<string>;
|
|
10
10
|
private request;
|
|
11
11
|
testConnection(): Promise<CTFdUser>;
|
|
12
|
+
private testConnectionViaProfile;
|
|
12
13
|
getChallenges(): Promise<CTFdChallengeListItem[]>;
|
|
13
14
|
getChallenge(id: number): Promise<CTFdChallenge>;
|
|
14
15
|
submitFlag(challengeId: number, submission: string): Promise<CTFdAttemptResponse['data']>;
|
package/dist/lib/ctfd-client.js
CHANGED
|
@@ -69,7 +69,37 @@ export class CTFdClient {
|
|
|
69
69
|
return json.data;
|
|
70
70
|
}
|
|
71
71
|
async testConnection() {
|
|
72
|
-
|
|
72
|
+
try {
|
|
73
|
+
return await this.request('GET', '/users/me');
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
// Session mode fallback: API may return 403, try scraping profile page
|
|
77
|
+
if (this.sessionCookie && err.message?.includes('403')) {
|
|
78
|
+
return this.testConnectionViaProfile();
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async testConnectionViaProfile() {
|
|
84
|
+
const res = await fetch(`${this.baseUrl}/settings`, {
|
|
85
|
+
headers: { Cookie: this.sessionCookie },
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok)
|
|
88
|
+
throw new Error('Session expired or invalid.');
|
|
89
|
+
const html = await res.text();
|
|
90
|
+
// Extract user name from settings page
|
|
91
|
+
const nameMatch = html.match(/name="name"[^>]*value="([^"]+)"/) ||
|
|
92
|
+
html.match(/<input[^>]*id="name"[^>]*value="([^"]+)"/);
|
|
93
|
+
const name = nameMatch?.[1] || 'User';
|
|
94
|
+
// Extract user ID from page
|
|
95
|
+
const idMatch = html.match(/user_id['":\s]+(\d+)/) ||
|
|
96
|
+
html.match(/userId['":\s]+(\d+)/);
|
|
97
|
+
const id = idMatch ? parseInt(idMatch[1], 10) : 0;
|
|
98
|
+
// Update CSRF nonce from settings page
|
|
99
|
+
const csrfMatch = html.match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);
|
|
100
|
+
if (csrfMatch)
|
|
101
|
+
this.csrfNonce = csrfMatch[1];
|
|
102
|
+
return { id, name, score: 0, team_id: 0, country: '' };
|
|
73
103
|
}
|
|
74
104
|
async getChallenges() {
|
|
75
105
|
return this.request('GET', '/challenges');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "icoa-cli",
|
|
3
|
-
"version": "2.19.
|
|
3
|
+
"version": "2.19.14",
|
|
4
4
|
"description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"build": "tsc",
|
|
16
16
|
"dev": "tsc --watch",
|
|
17
17
|
"start": "node dist/index.js",
|
|
18
|
-
"postinstall": "node dist/postinstall.js
|
|
18
|
+
"postinstall": "node -e \"try{require('./dist/postinstall.js')}catch(e){}\""
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
21
21
|
"ctf",
|