open-vtop 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/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/bitmaps.js +9081 -0
- package/dist/captcha-solver.js +213 -0
- package/dist/index.js +205 -0
- package/dist/session-manager.js +588 -0
- package/dist/views/Home.js +5 -0
- package/dist/views/components/Container.js +4 -0
- package/dist/views/components/Item.js +4 -0
- package/dist/views/layouts/Base.js +121 -0
- package/dist/views/partials.js +4 -0
- package/package.json +27 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager for VTOP
|
|
3
|
+
* Automatically initializes and maintains VTOP session cookies and CSRF tokens
|
|
4
|
+
*/
|
|
5
|
+
import { solve, extractDataUriParts, saveCaptchaImage, } from "./captcha-solver.js";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
const BASE = "https://vtop.vit.ac.in";
|
|
8
|
+
const VTOP = `${BASE}/vtop/`;
|
|
9
|
+
const OPEN_PAGE = `${BASE}/vtop/openPage`;
|
|
10
|
+
const OPEN_PAGE_ALT = `${BASE}/vtop/open/page`;
|
|
11
|
+
const PRELOGIN_SETUP = `${BASE}/vtop/prelogin/setup`;
|
|
12
|
+
const LOGIN_PAGE = `${BASE}/vtop/login`;
|
|
13
|
+
// Post-login endpoints
|
|
14
|
+
const INIT_PAGE = `${BASE}/vtop/init/page`;
|
|
15
|
+
const MAIN_PAGE = `${BASE}/vtop/main/page`;
|
|
16
|
+
const VTOP_OPEN = `${BASE}/vtop/open`;
|
|
17
|
+
const CONTENT = `${BASE}/vtop/content`;
|
|
18
|
+
const ACADEMICS_CHECK = `${BASE}/vtop/academics/common/AcademicsDefaultCheck`;
|
|
19
|
+
const UPCOMING_ASSIGNMENTS = `${BASE}/vtop/get/upcoming/digital/assignments`;
|
|
20
|
+
class VTOPSessionManager {
|
|
21
|
+
state = {
|
|
22
|
+
cookies: new Map(),
|
|
23
|
+
csrf: null,
|
|
24
|
+
initialized: false,
|
|
25
|
+
lastInitialized: null,
|
|
26
|
+
loggedIn: false,
|
|
27
|
+
username: null,
|
|
28
|
+
regNo: null,
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Extract CSRF token from HTML response
|
|
32
|
+
*/
|
|
33
|
+
extractCsrf(html) {
|
|
34
|
+
// Look for _csrf token in various forms
|
|
35
|
+
const patterns = [
|
|
36
|
+
/name="_csrf"\s+value="([^"]+)"/,
|
|
37
|
+
/name='_csrf'\s+value='([^']+)'/,
|
|
38
|
+
/<input[^>]*name="_csrf"[^>]*value="([^"]+)"/,
|
|
39
|
+
/<input[^>]*value="([^"]+)"[^>]*name="_csrf"/,
|
|
40
|
+
];
|
|
41
|
+
for (const pattern of patterns) {
|
|
42
|
+
const match = html.match(pattern);
|
|
43
|
+
if (match && match[1]) {
|
|
44
|
+
return match[1];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Parse Set-Cookie headers and store cookies
|
|
51
|
+
*/
|
|
52
|
+
storeCookies(response) {
|
|
53
|
+
const setCookieHeaders = response.headers.getSetCookie?.() || [];
|
|
54
|
+
for (const cookieStr of setCookieHeaders) {
|
|
55
|
+
const [nameValue] = cookieStr.split(";");
|
|
56
|
+
const [name, value] = nameValue.split("=");
|
|
57
|
+
if (name && value) {
|
|
58
|
+
this.state.cookies.set(name.trim(), value.trim());
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get cookie header string for requests
|
|
64
|
+
*/
|
|
65
|
+
getCookieHeader() {
|
|
66
|
+
return Array.from(this.state.cookies.entries())
|
|
67
|
+
.map(([name, value]) => `${name}=${value}`)
|
|
68
|
+
.join("; ");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Make a fetch request with cookie handling
|
|
72
|
+
*/
|
|
73
|
+
async fetchWithCookies(url, options = {}) {
|
|
74
|
+
const cookieHeader = this.getCookieHeader();
|
|
75
|
+
const headers = new Headers(options.headers);
|
|
76
|
+
if (cookieHeader) {
|
|
77
|
+
headers.set("Cookie", cookieHeader);
|
|
78
|
+
}
|
|
79
|
+
// Add common browser headers
|
|
80
|
+
headers.set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
|
81
|
+
headers.set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
|
|
82
|
+
headers.set("Accept-Language", "en-US,en;q=0.9");
|
|
83
|
+
headers.set("Accept-Encoding", "gzip, deflate, br");
|
|
84
|
+
headers.set("Connection", "keep-alive");
|
|
85
|
+
headers.set("Upgrade-Insecure-Requests", "1");
|
|
86
|
+
const response = await fetch(url, {
|
|
87
|
+
...options,
|
|
88
|
+
headers,
|
|
89
|
+
redirect: "manual", // Handle redirects manually to capture cookies
|
|
90
|
+
});
|
|
91
|
+
// Store any cookies from the response
|
|
92
|
+
this.storeCookies(response);
|
|
93
|
+
// Follow redirects manually if needed
|
|
94
|
+
if (response.status >= 300 && response.status < 400) {
|
|
95
|
+
const location = response.headers.get("Location");
|
|
96
|
+
if (location) {
|
|
97
|
+
const redirectUrl = location.startsWith("http")
|
|
98
|
+
? location
|
|
99
|
+
: new URL(location, url).toString();
|
|
100
|
+
// Create new options without body for GET redirect
|
|
101
|
+
const redirectOptions = {
|
|
102
|
+
headers: options.headers,
|
|
103
|
+
redirect: options.redirect,
|
|
104
|
+
};
|
|
105
|
+
return this.fetchWithCookies(redirectUrl, redirectOptions);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return response;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Initialize VTOP session
|
|
112
|
+
*/
|
|
113
|
+
async initialize() {
|
|
114
|
+
try {
|
|
115
|
+
console.log("🔄 Initializing VTOP session...");
|
|
116
|
+
// Step 1: GET / to get SERVERID cookie
|
|
117
|
+
console.log("Step 1: GET / to get SERVERID...");
|
|
118
|
+
await this.fetchWithCookies(BASE);
|
|
119
|
+
console.log(" ✓ OK");
|
|
120
|
+
// Step 2: GET /vtop/ to get JSESSIONID cookie
|
|
121
|
+
console.log("Step 2: GET /vtop/ to get JSESSIONID...");
|
|
122
|
+
await this.fetchWithCookies(VTOP);
|
|
123
|
+
console.log(" ✓ OK");
|
|
124
|
+
// Step 3: GET /vtop/openPage for CSRF token
|
|
125
|
+
console.log("Step 3: GET /vtop/openPage for CSRF...");
|
|
126
|
+
const openPageRes = await this.fetchWithCookies(OPEN_PAGE);
|
|
127
|
+
const openPageHtml = await openPageRes.text();
|
|
128
|
+
console.log(" ✓ OK");
|
|
129
|
+
const csrf = this.extractCsrf(openPageHtml);
|
|
130
|
+
if (!csrf) {
|
|
131
|
+
console.warn("⚠️ Warning: _csrf not found on /vtop/openPage. Continuing anyway...");
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
this.state.csrf = csrf;
|
|
135
|
+
console.log(` ✓ Found _csrf: ${csrf.substring(0, 20)}...`);
|
|
136
|
+
}
|
|
137
|
+
// Optional: Visit alternate open page (used as Referer in browser)
|
|
138
|
+
await this.fetchWithCookies(OPEN_PAGE_ALT).catch(() => {
|
|
139
|
+
// Ignore errors on this optional step
|
|
140
|
+
});
|
|
141
|
+
// Step 4: POST /vtop/prelogin/setup
|
|
142
|
+
console.log("Step 4: POST /vtop/prelogin/setup (flag=VTOP)...");
|
|
143
|
+
const formData = new URLSearchParams();
|
|
144
|
+
if (csrf) {
|
|
145
|
+
formData.set("_csrf", csrf);
|
|
146
|
+
}
|
|
147
|
+
formData.set("flag", "VTOP");
|
|
148
|
+
const preloginRes = await this.fetchWithCookies(PRELOGIN_SETUP, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: {
|
|
151
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
152
|
+
Referer: OPEN_PAGE,
|
|
153
|
+
Origin: BASE,
|
|
154
|
+
},
|
|
155
|
+
body: formData.toString(),
|
|
156
|
+
});
|
|
157
|
+
console.log(` ✓ Prelogin status: ${preloginRes.status}`);
|
|
158
|
+
// Mark as initialized
|
|
159
|
+
this.state.initialized = true;
|
|
160
|
+
this.state.lastInitialized = new Date();
|
|
161
|
+
console.log("✅ VTOP session initialized successfully!");
|
|
162
|
+
console.log(` Cookies stored: ${this.state.cookies.size}`);
|
|
163
|
+
console.log(` Cookie names: ${Array.from(this.state.cookies.keys()).join(", ")}`);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
console.error("❌ Failed to initialize VTOP session:", error);
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Get current session state
|
|
172
|
+
*/
|
|
173
|
+
getState() {
|
|
174
|
+
return {
|
|
175
|
+
...this.state,
|
|
176
|
+
cookies: new Map(this.state.cookies),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get CSRF token
|
|
181
|
+
*/
|
|
182
|
+
getCsrf() {
|
|
183
|
+
return this.state.csrf;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Get cookies as a header string
|
|
187
|
+
*/
|
|
188
|
+
getCookies() {
|
|
189
|
+
return this.getCookieHeader();
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Check if session is initialized
|
|
193
|
+
*/
|
|
194
|
+
isInitialized() {
|
|
195
|
+
return this.state.initialized;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Re-initialize session (useful for periodic refresh)
|
|
199
|
+
*/
|
|
200
|
+
async refresh() {
|
|
201
|
+
console.log("🔄 Refreshing VTOP session...");
|
|
202
|
+
this.state.cookies.clear();
|
|
203
|
+
this.state.csrf = null;
|
|
204
|
+
this.state.initialized = false;
|
|
205
|
+
this.state.loggedIn = false;
|
|
206
|
+
this.state.username = null;
|
|
207
|
+
await this.initialize();
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Detect captcha type from login page HTML using regex (no cheerio)
|
|
211
|
+
*/
|
|
212
|
+
detectCaptcha(html) {
|
|
213
|
+
// reCAPTCHA signals
|
|
214
|
+
const recaptchaDom = html.includes('id="recaptcha"') ||
|
|
215
|
+
html.includes('id="g-recaptcha"') ||
|
|
216
|
+
html.includes('class="g-recaptcha"');
|
|
217
|
+
const recaptchaJs = html.includes("var captchaType=2");
|
|
218
|
+
const isRecaptcha = recaptchaDom || recaptchaJs;
|
|
219
|
+
// Text CAPTCHA signals - check for captchaStr input
|
|
220
|
+
const hasCaptchaInput = html.includes('name="captchaStr"') || html.includes('id="captchaStr"');
|
|
221
|
+
// Look for data URI image in the HTML
|
|
222
|
+
// Support both single and double quotes
|
|
223
|
+
// VTOP sometimes sends "null" in base64, so we need to find the first valid one
|
|
224
|
+
const imgDataUriMatches = [
|
|
225
|
+
...html.matchAll(/src=["'](data:image\/[^"']+)["']/gi),
|
|
226
|
+
];
|
|
227
|
+
let imgDataUri = null;
|
|
228
|
+
for (const match of imgDataUriMatches) {
|
|
229
|
+
if (match[1] && !match[1].includes(";base64,null")) {
|
|
230
|
+
imgDataUri = match[1];
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// specific check for the warning if we found matches but none were valid
|
|
235
|
+
if (!imgDataUri && imgDataUriMatches.length > 0) {
|
|
236
|
+
console.warn("⚠️ Detected invalid captcha image (base64 is null)");
|
|
237
|
+
}
|
|
238
|
+
const isTextCaptcha = hasCaptchaInput && imgDataUri !== null;
|
|
239
|
+
// Extract CSRF token
|
|
240
|
+
const csrf = this.extractCsrf(html);
|
|
241
|
+
return { isTextCaptcha, isRecaptcha, csrf, imgDataUri };
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Login to VTOP with username, password, and registration number
|
|
245
|
+
* Polls the login page until a text CAPTCHA appears, solves it, and submits
|
|
246
|
+
*/
|
|
247
|
+
async login(username, password, regNo, maxAttempts = 10) {
|
|
248
|
+
if (!this.state.initialized) {
|
|
249
|
+
console.log("Session not initialized, initializing first...");
|
|
250
|
+
await this.initialize();
|
|
251
|
+
}
|
|
252
|
+
console.log("🔐 Starting VTOP login process...");
|
|
253
|
+
console.log("Polling /vtop/login until text CAPTCHA appears...");
|
|
254
|
+
let attempt = 0;
|
|
255
|
+
while (attempt < maxAttempts) {
|
|
256
|
+
attempt++;
|
|
257
|
+
try {
|
|
258
|
+
// Fetch login page
|
|
259
|
+
const res = await this.fetchWithCookies(LOGIN_PAGE);
|
|
260
|
+
const body = await res.text();
|
|
261
|
+
const { isTextCaptcha, isRecaptcha, csrf, imgDataUri } = this.detectCaptcha(body);
|
|
262
|
+
// Get current cookies for logging
|
|
263
|
+
const curJsession = this.state.cookies.get("JSESSIONID") || "(?)";
|
|
264
|
+
const curServerID = this.state.cookies.get("SERVERID") || "(?)";
|
|
265
|
+
console.log(`Attempt ${attempt}: status ${res.status} | text-captcha=${isTextCaptcha ? "YES" : "no"} | recaptcha=${isRecaptcha ? "YES" : "no"} | JSESSIONID=${curJsession}`);
|
|
266
|
+
if (isTextCaptcha) {
|
|
267
|
+
let solvedCaptcha = "";
|
|
268
|
+
if (imgDataUri) {
|
|
269
|
+
const parts = extractDataUriParts(imgDataUri);
|
|
270
|
+
try {
|
|
271
|
+
// Trim whitespace just in case
|
|
272
|
+
const cleanDataUri = imgDataUri.trim();
|
|
273
|
+
if (cleanDataUri !== imgDataUri) {
|
|
274
|
+
console.log("⚠️ Trimmed whitespace from captcha data URI");
|
|
275
|
+
}
|
|
276
|
+
solvedCaptcha = await solve(cleanDataUri);
|
|
277
|
+
console.log("✅ Solved CAPTCHA:", solvedCaptcha);
|
|
278
|
+
// Save captcha image for debugging
|
|
279
|
+
if (parts?.base64) {
|
|
280
|
+
const out = path.resolve(process.cwd(), "captcha.jpg");
|
|
281
|
+
await saveCaptchaImage(parts.base64, out);
|
|
282
|
+
// console.log(`📷 Saved CAPTCHA image to ${out}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (e) {
|
|
286
|
+
console.warn("⚠️ Failed to solve captcha:", e);
|
|
287
|
+
solvedCaptcha = "";
|
|
288
|
+
// Save the failed data URI for inspection
|
|
289
|
+
try {
|
|
290
|
+
const fs = await import("fs/promises");
|
|
291
|
+
const failPath = path.resolve(process.cwd(), "failed_captcha_data.txt");
|
|
292
|
+
await fs.writeFile(failPath, imgDataUri);
|
|
293
|
+
console.log(`📝 Saved failed captcha data URI to ${failPath}`);
|
|
294
|
+
}
|
|
295
|
+
catch (writeErr) {
|
|
296
|
+
console.error("Failed to save failed captcha data:", writeErr);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
console.log("Text CAPTCHA detected but no data URI image found.");
|
|
302
|
+
}
|
|
303
|
+
// Build and submit login form
|
|
304
|
+
console.log("\n📤 Submitting POST /vtop/login ...");
|
|
305
|
+
const loginForm = new URLSearchParams();
|
|
306
|
+
if (csrf)
|
|
307
|
+
loginForm.set("_csrf", csrf);
|
|
308
|
+
loginForm.set("username", username);
|
|
309
|
+
loginForm.set("password", password);
|
|
310
|
+
loginForm.set("captchaStr", solvedCaptcha);
|
|
311
|
+
const _cookies = `JSESSIONID=${curJsession}; SERVERID=${curServerID}`;
|
|
312
|
+
const postHeaders = {
|
|
313
|
+
"Cache-Control": "max-age=0",
|
|
314
|
+
Origin: BASE,
|
|
315
|
+
Referer: OPEN_PAGE_ALT,
|
|
316
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
317
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
|
318
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
319
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
320
|
+
"Sec-Fetch-Dest": "document",
|
|
321
|
+
"Sec-Fetch-Mode": "navigate",
|
|
322
|
+
"Sec-Fetch-Site": "same-origin",
|
|
323
|
+
"Sec-Fetch-User": "?1",
|
|
324
|
+
"Upgrade-Insecure-Requests": "1",
|
|
325
|
+
Priority: "u=0, i",
|
|
326
|
+
Cookie: _cookies,
|
|
327
|
+
};
|
|
328
|
+
try {
|
|
329
|
+
const postRes = await fetch(LOGIN_PAGE, {
|
|
330
|
+
method: "POST",
|
|
331
|
+
headers: postHeaders,
|
|
332
|
+
body: loginForm.toString(),
|
|
333
|
+
redirect: "manual",
|
|
334
|
+
});
|
|
335
|
+
console.log(` -> Login POST status: ${postRes.status}`);
|
|
336
|
+
// Store any new cookies
|
|
337
|
+
this.storeCookies(postRes);
|
|
338
|
+
const setCookie = postRes.headers.getSetCookie?.() || [];
|
|
339
|
+
if (setCookie.length > 0) {
|
|
340
|
+
console.log(" -> New cookies received");
|
|
341
|
+
}
|
|
342
|
+
// Check for redirect
|
|
343
|
+
const location = postRes.headers.get("Location");
|
|
344
|
+
if (location) {
|
|
345
|
+
console.log(` -> Redirect to: ${location}`);
|
|
346
|
+
// Follow redirect to complete the login flow
|
|
347
|
+
const redirectUrl = location.startsWith("http")
|
|
348
|
+
? location
|
|
349
|
+
: new URL(location, LOGIN_PAGE).toString();
|
|
350
|
+
await this.fetchWithCookies(redirectUrl);
|
|
351
|
+
}
|
|
352
|
+
// After successfully submitting the captcha, assume login worked
|
|
353
|
+
// The actual success will be determined when we try to fetch data
|
|
354
|
+
console.log("🎉 Login POST submitted successfully!");
|
|
355
|
+
this.state.loggedIn = true;
|
|
356
|
+
this.state.username = username;
|
|
357
|
+
this.state.regNo = regNo;
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
catch (e) {
|
|
361
|
+
console.error("Login POST failed:", e);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// If reCAPTCHA, we cannot solve it automatically
|
|
365
|
+
if (isRecaptcha) {
|
|
366
|
+
console.log("⚠️ reCAPTCHA detected - cannot solve automatically");
|
|
367
|
+
// Wait a bit and retry, hoping for text captcha
|
|
368
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
// Wait before next poll
|
|
372
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
373
|
+
}
|
|
374
|
+
catch (e) {
|
|
375
|
+
console.error(`Attempt ${attempt} failed:`, e);
|
|
376
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
console.log(`❌ Login failed after ${maxAttempts} attempts`);
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Check if logged in
|
|
384
|
+
*/
|
|
385
|
+
isLoggedIn() {
|
|
386
|
+
return this.state.loggedIn;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Get current username
|
|
390
|
+
*/
|
|
391
|
+
getUsername() {
|
|
392
|
+
return this.state.username;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Navigate through post-login pages to establish authenticated session
|
|
396
|
+
* This mimics what the browser does after successful login
|
|
397
|
+
*/
|
|
398
|
+
async navigatePostLogin() {
|
|
399
|
+
if (!this.state.loggedIn) {
|
|
400
|
+
console.error("❌ Cannot navigate post-login pages: not logged in");
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
const cookies = this.getCookieHeader();
|
|
404
|
+
const headers = {
|
|
405
|
+
Referer: LOGIN_PAGE,
|
|
406
|
+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
|
|
407
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
|
408
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
409
|
+
"Cache-Control": "max-age=0",
|
|
410
|
+
"Upgrade-Insecure-Requests": "1",
|
|
411
|
+
Origin: BASE,
|
|
412
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
413
|
+
Cookie: cookies,
|
|
414
|
+
};
|
|
415
|
+
try {
|
|
416
|
+
// Navigate through required pages
|
|
417
|
+
console.log("📄 Navigating post-login pages...");
|
|
418
|
+
// 1. /vtop/init/page
|
|
419
|
+
const initRes = await this.fetchWithCookies(INIT_PAGE, { headers });
|
|
420
|
+
console.log(` -> /vtop/init/page: ${initRes.status}`);
|
|
421
|
+
// 2. /vtop/main/page
|
|
422
|
+
const mainRes = await this.fetchWithCookies(MAIN_PAGE, { headers });
|
|
423
|
+
console.log(` -> /vtop/main/page: ${mainRes.status}`);
|
|
424
|
+
// 3. /vtop/open
|
|
425
|
+
const openRes = await this.fetchWithCookies(VTOP_OPEN, { headers });
|
|
426
|
+
console.log(` -> /vtop/open: ${openRes.status}`);
|
|
427
|
+
// 4. /vtop/content - extract new CSRF from here
|
|
428
|
+
const contentRes = await this.fetchWithCookies(CONTENT, { headers });
|
|
429
|
+
const contentHtml = await contentRes.text();
|
|
430
|
+
console.log(` -> /vtop/content: ${contentRes.status}`);
|
|
431
|
+
const newCsrf = this.extractCsrf(contentHtml);
|
|
432
|
+
if (newCsrf) {
|
|
433
|
+
this.state.csrf = newCsrf;
|
|
434
|
+
console.log(` ✓ Updated CSRF token from content page`);
|
|
435
|
+
}
|
|
436
|
+
console.log("✅ Post-login navigation complete");
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
catch (e) {
|
|
440
|
+
console.error("❌ Post-login navigation failed:", e);
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Parse upcoming assignments from VTOP HTML response
|
|
446
|
+
* Uses regex instead of cheerio
|
|
447
|
+
*/
|
|
448
|
+
parseAssignmentsHtml(html) {
|
|
449
|
+
const assignments = [];
|
|
450
|
+
// VTOP returns assignments in a table or as JSON-like structure
|
|
451
|
+
// Try to parse as JSON first (some endpoints return JSON)
|
|
452
|
+
try {
|
|
453
|
+
const jsonMatch = html.match(/\[[\s\S]*?\]/);
|
|
454
|
+
if (jsonMatch) {
|
|
455
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
456
|
+
if (Array.isArray(parsed)) {
|
|
457
|
+
return parsed.map((item) => ({
|
|
458
|
+
courseCode: String(item.courseCode || item.code || ""),
|
|
459
|
+
courseName: String(item.courseName || item.name || ""),
|
|
460
|
+
assignmentTitle: String(item.assignmentTitle || item.title || item.assignmentName || ""),
|
|
461
|
+
dueDate: String(item.dueDate || item.endDate || item.deadline || ""),
|
|
462
|
+
status: String(item.status || ""),
|
|
463
|
+
maxMarks: String(item.maxMarks || item.marks || ""),
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
// Not JSON, try HTML parsing
|
|
470
|
+
}
|
|
471
|
+
// Parse HTML table rows using regex
|
|
472
|
+
// VTOP table structure: #(th), Course Name(td), Title(td), Last Date(td), Uploaded(td)
|
|
473
|
+
// Note: VTOP HTML sometimes has malformed tags like <<td
|
|
474
|
+
const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
|
|
475
|
+
// Match both <th> and <td> cells, and handle malformed <<td
|
|
476
|
+
const cellRegex = /<?<t[hd][^>]*>([\s\S]*?)(?:<\/t[hd]>|<\/tr|<tr|$)/gi;
|
|
477
|
+
let rowMatch;
|
|
478
|
+
while ((rowMatch = rowRegex.exec(html)) !== null) {
|
|
479
|
+
const cells = [];
|
|
480
|
+
let cellMatch;
|
|
481
|
+
const rowContent = rowMatch[1];
|
|
482
|
+
// Reset lastIndex for cell regex
|
|
483
|
+
cellRegex.lastIndex = 0;
|
|
484
|
+
while ((cellMatch = cellRegex.exec(rowContent)) !== null) {
|
|
485
|
+
// Strip HTML tags from cell content
|
|
486
|
+
const cellContent = cellMatch[1].replace(/<[^>]+>/g, "").trim();
|
|
487
|
+
cells.push(cellContent);
|
|
488
|
+
}
|
|
489
|
+
// Skip header rows (first cell would be "#" or similar header text)
|
|
490
|
+
if (cells.length >= 4 && cells[0] !== "#" && !isNaN(Number(cells[0]))) {
|
|
491
|
+
// Table structure: # (row num), Course Name, Title, Last Date, Uploaded
|
|
492
|
+
// cells[0] = row number (skip)
|
|
493
|
+
// cells[1] = Course Name
|
|
494
|
+
// cells[2] = Title (assignment title)
|
|
495
|
+
// cells[3] = Last Date (due date)
|
|
496
|
+
// cells[4] = Uploaded (status)
|
|
497
|
+
assignments.push({
|
|
498
|
+
courseCode: "", // Not in this table format
|
|
499
|
+
courseName: cells[1] || "",
|
|
500
|
+
assignmentTitle: cells[2] || "",
|
|
501
|
+
dueDate: cells[3] || "",
|
|
502
|
+
status: cells[4] || "Pending",
|
|
503
|
+
maxMarks: "", // Not in this table format
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return assignments;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Fetch upcoming assignments from VTOP
|
|
511
|
+
*/
|
|
512
|
+
async fetchUpcomingAssignments() {
|
|
513
|
+
if (!this.state.loggedIn || !this.state.regNo) {
|
|
514
|
+
console.error("❌ Cannot fetch assignments: not logged in or no regNo");
|
|
515
|
+
return [];
|
|
516
|
+
}
|
|
517
|
+
// Ensure we've navigated post-login pages
|
|
518
|
+
await this.navigatePostLogin();
|
|
519
|
+
const cookies = this.getCookieHeader();
|
|
520
|
+
const now = new Date();
|
|
521
|
+
// First, do the academics default check
|
|
522
|
+
const accParams = new URLSearchParams();
|
|
523
|
+
accParams.set("authorizedID", this.state.regNo);
|
|
524
|
+
if (this.state.csrf)
|
|
525
|
+
accParams.set("_csrf", this.state.csrf);
|
|
526
|
+
accParams.set("x", now.toUTCString());
|
|
527
|
+
const apiHeaders = {
|
|
528
|
+
Accept: "*/*",
|
|
529
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
530
|
+
"Accept-Language": "en-US,en;q=0.7",
|
|
531
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
532
|
+
Cookie: cookies,
|
|
533
|
+
Origin: BASE,
|
|
534
|
+
Priority: "u=1, i",
|
|
535
|
+
Referer: CONTENT,
|
|
536
|
+
"Sec-Fetch-Dest": "empty",
|
|
537
|
+
"Sec-Fetch-Mode": "cors",
|
|
538
|
+
"Sec-Fetch-Site": "same-origin",
|
|
539
|
+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
|
|
540
|
+
};
|
|
541
|
+
try {
|
|
542
|
+
// AcademicsDefaultCheck
|
|
543
|
+
console.log("📚 Checking academics context...");
|
|
544
|
+
const accRes = await fetch(ACADEMICS_CHECK, {
|
|
545
|
+
method: "POST",
|
|
546
|
+
headers: apiHeaders,
|
|
547
|
+
body: accParams.toString(),
|
|
548
|
+
});
|
|
549
|
+
console.log(` -> AcademicsDefaultCheck: ${accRes.status}`);
|
|
550
|
+
}
|
|
551
|
+
catch (e) {
|
|
552
|
+
console.warn("⚠️ AcademicsDefaultCheck failed:", e);
|
|
553
|
+
}
|
|
554
|
+
// Now fetch the assignments
|
|
555
|
+
const assParams = new URLSearchParams();
|
|
556
|
+
assParams.set("authorizedID", this.state.regNo);
|
|
557
|
+
if (this.state.csrf)
|
|
558
|
+
assParams.set("_csrf", this.state.csrf);
|
|
559
|
+
assParams.set("x", now.toUTCString());
|
|
560
|
+
try {
|
|
561
|
+
console.log("📝 Fetching upcoming assignments...");
|
|
562
|
+
const assRes = await fetch(UPCOMING_ASSIGNMENTS, {
|
|
563
|
+
method: "POST",
|
|
564
|
+
headers: apiHeaders,
|
|
565
|
+
body: assParams.toString(),
|
|
566
|
+
});
|
|
567
|
+
console.log(` -> Upcoming assignments: ${assRes.status}`);
|
|
568
|
+
const assBody = await assRes.text();
|
|
569
|
+
console.log(` -> Response length: ${assBody.length} chars`);
|
|
570
|
+
console.log(` -> Raw response:\n${assBody}`);
|
|
571
|
+
if (assBody) {
|
|
572
|
+
const assignments = this.parseAssignmentsHtml(assBody);
|
|
573
|
+
console.log(`✅ Parsed ${assignments.length} assignments`);
|
|
574
|
+
return assignments;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
catch (e) {
|
|
578
|
+
console.error("❌ Failed to fetch assignments:", e);
|
|
579
|
+
}
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// Export singleton instance
|
|
584
|
+
export const sessionManager = new VTOPSessionManager();
|
|
585
|
+
// Auto-initialize on module load
|
|
586
|
+
sessionManager.initialize().catch((error) => {
|
|
587
|
+
console.error("Failed to auto-initialize session:", error);
|
|
588
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
|
+
import { BaseLayout } from "./layouts/Base.js";
|
|
3
|
+
export const Home = () => {
|
|
4
|
+
return (_jsxs(BaseLayout, { title: "Open-VTOP", children: [_jsx("h1", { children: "Open-VTOP" }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDD10 Login to VTOP" }), _jsxs("form", { id: "login-form", style: "display: flex; flex-direction: column; gap: 0.75rem; max-width: 400px;", children: [_jsxs("div", { children: [_jsx("label", { for: "username", style: "display: block; margin-bottom: 0.25rem; font-weight: bold;", children: "Username:" }), _jsx("input", { type: "text", id: "username", name: "username", placeholder: "Enter your VTOP username", style: "width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; font-size: 1rem;", required: true })] }), _jsxs("div", { children: [_jsx("label", { for: "password", style: "display: block; margin-bottom: 0.25rem; font-weight: bold;", children: "Password:" }), _jsx("input", { type: "password", id: "password", name: "password", placeholder: "Enter your password", style: "width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; font-size: 1rem;", required: true })] }), _jsxs("div", { children: [_jsx("label", { for: "regNo", style: "display: block; margin-bottom: 0.25rem; font-weight: bold;", children: "Registration Number:" }), _jsx("input", { type: "text", id: "regNo", name: "regNo", placeholder: "e.g. 24BCI0150", style: "width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; font-size: 1rem;", required: true })] }), _jsx("button", { type: "submit", "hx-post": "/api/login/form", "hx-target": "#login-result", "hx-swap": "innerHTML", "hx-include": "#login-form", style: "padding: 0.75rem; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; font-weight: bold;", children: "\uD83D\uDE80 Login" })] }), _jsx("div", { id: "login-result", style: "margin-top: 1rem;" })] }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDCCA Login Status" }), _jsx("button", { "hx-get": "/api/login/status/html", "hx-target": "#login-status", "hx-swap": "innerHTML", "hx-trigger": "load, click", children: "Refresh Login Status" }), _jsx("div", { id: "login-status", children: "Loading..." })] }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDCDA Upcoming Assignments" }), _jsx("button", { "hx-get": "/api/assignments/html", "hx-target": "#assignments-list", "hx-swap": "innerHTML", "hx-indicator": "#assignments-loading", style: "background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; color: white; padding: 0.75rem 1.5rem; border-radius: 8px; font-weight: bold; cursor: pointer;", children: "\uD83D\uDD04 Load Assignments" }), _jsx("span", { id: "assignments-loading", class: "htmx-indicator", style: "margin-left: 0.5rem; color: #aaa;", children: "Loading..." }), _jsx("div", { id: "assignments-list", style: "margin-top: 1rem;", children: _jsx("div", { style: "color: #888; padding: 1rem; background: #f5f5f5; border-radius: 8px;", children: "Click \"Load Assignments\" to fetch your upcoming assignments from VTOP." }) })] }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDD27 Session Status" }), _jsx("button", { "hx-get": "/api/session/status", "hx-target": "#session-status", "hx-swap": "innerHTML", "hx-trigger": "load, click", children: "Refresh Session Status" }), _jsx("div", { id: "session-status", children: "Loading session status..." })] }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDCDD Debug Logs" }), _jsx("button", { "hx-get": "/api/debug/logs", "hx-target": "#debug-logs", "hx-swap": "innerHTML", "hx-trigger": "click", children: "Load Debug Logs" }), _jsx("button", { "hx-post": "/api/debug/logs/clear", "hx-target": "#debug-logs", "hx-swap": "innerHTML", style: "margin-left: 0.5rem; background: #ff6b6b;", children: "Clear Logs" }), _jsx("div", { id: "debug-logs", style: "margin-top: 1rem; max-height: 400px; overflow-y: auto; background: #1e1e1e; color: #0f0; font-family: monospace; padding: 1rem; border-radius: 4px; font-size: 0.85rem;", children: "Click \"Load Debug Logs\" to see login attempts..." })] })] }));
|
|
5
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
|
+
export const Item = ({ id, name }) => {
|
|
3
|
+
return (_jsxs("div", { id: `item-${id}`, class: "item", children: [name, _jsx("button", { "hx-delete": `/api/items/${id}`, "hx-confirm": "Are you sure you want to delete this item?", "hx-target": `#item-${id}`, "hx-swap": "outerHTML", class: "danger", children: "Delete" })] }));
|
|
4
|
+
};
|