open-vtop 1.0.3 → 1.0.4
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/captcha-solver.js +68 -16
- package/dist/index.js +19 -41
- package/dist/session-manager.js +8 -20
- package/package.json +9 -7
package/dist/captcha-solver.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CAPTCHA Solver for VTOP
|
|
3
|
-
* Uses
|
|
3
|
+
* Uses pure JS for image processing and bitmap matching / neural network for character recognition
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
5
|
+
import * as jpeg from "jpeg-js";
|
|
6
|
+
import { PNG } from "pngjs";
|
|
6
7
|
import { bitmaps } from "./bitmaps.js";
|
|
7
8
|
// ============ Bitmap-based solver (original method) ============
|
|
8
9
|
function captchaParse(imgarr) {
|
|
@@ -134,6 +135,53 @@ function maxSoft(a) {
|
|
|
134
135
|
}
|
|
135
136
|
return n;
|
|
136
137
|
}
|
|
138
|
+
async function decodeImage(dataUri) {
|
|
139
|
+
const parts = extractDataUriParts(dataUri);
|
|
140
|
+
if (!parts)
|
|
141
|
+
throw new Error("Invalid Data URI");
|
|
142
|
+
const buffer = Buffer.from(parts.base64, "base64");
|
|
143
|
+
if (parts.mimeType === "image/png") {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
const png = new PNG();
|
|
146
|
+
png.parse(buffer, (err, data) => {
|
|
147
|
+
if (err)
|
|
148
|
+
reject(err);
|
|
149
|
+
else
|
|
150
|
+
resolve({
|
|
151
|
+
width: data.width,
|
|
152
|
+
height: data.height,
|
|
153
|
+
data: data.data,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
else if (parts.mimeType === "image/jpeg") {
|
|
159
|
+
const decoded = jpeg.decode(buffer, { useTArray: true });
|
|
160
|
+
return {
|
|
161
|
+
width: decoded.width,
|
|
162
|
+
height: decoded.height,
|
|
163
|
+
data: decoded.data,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
throw new Error("Unsupported image type: " + parts.mimeType);
|
|
167
|
+
}
|
|
168
|
+
function resizeImage(src, targetW, targetH) {
|
|
169
|
+
// Nearest neighbor resize
|
|
170
|
+
const target = new Uint8ClampedArray(targetW * targetH * 4);
|
|
171
|
+
for (let y = 0; y < targetH; y++) {
|
|
172
|
+
for (let x = 0; x < targetW; x++) {
|
|
173
|
+
const srcX = Math.floor((x * src.width) / targetW);
|
|
174
|
+
const srcY = Math.floor((y * src.height) / targetH);
|
|
175
|
+
const srcIdx = (srcY * src.width + srcX) * 4;
|
|
176
|
+
const targetIdx = (y * targetW + x) * 4;
|
|
177
|
+
target[targetIdx] = src.data[srcIdx];
|
|
178
|
+
target[targetIdx + 1] = src.data[srcIdx + 1];
|
|
179
|
+
target[targetIdx + 2] = src.data[srcIdx + 2];
|
|
180
|
+
target[targetIdx + 3] = src.data[srcIdx + 3];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return target;
|
|
184
|
+
}
|
|
137
185
|
/**
|
|
138
186
|
* Solve a CAPTCHA image using the saturation-based neural network method
|
|
139
187
|
* @param imgDataUri - Base64 data URI of the captcha image
|
|
@@ -147,12 +195,15 @@ export async function solve(imgDataUri) {
|
|
|
147
195
|
return solveBitmap(imgDataUri);
|
|
148
196
|
}
|
|
149
197
|
const labelTxt = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
198
|
+
const img = await decodeImage(imgDataUri);
|
|
199
|
+
// Target size: 200x40
|
|
200
|
+
let dataToUse = img.data;
|
|
201
|
+
if (img.width !== 200 || img.height !== 40) {
|
|
202
|
+
dataToUse = resizeImage(img, 200, 40);
|
|
203
|
+
}
|
|
204
|
+
// dataToUse is now RGBA array of 200x40 image
|
|
205
|
+
// saturation expects Uint8ClampedArray or similar
|
|
206
|
+
let bls = saturation(dataToUse);
|
|
156
207
|
let out = "";
|
|
157
208
|
for (let i = 0; i < 6; i++) {
|
|
158
209
|
let block = preImg(bls[i]);
|
|
@@ -171,16 +222,17 @@ export async function solve(imgDataUri) {
|
|
|
171
222
|
* @returns Solved captcha string
|
|
172
223
|
*/
|
|
173
224
|
export async function solveBitmap(imgDataUri) {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
const data = imageData.data;
|
|
225
|
+
const img = await decodeImage(imgDataUri);
|
|
226
|
+
// For bitmap solver, we need 180x45
|
|
227
|
+
// Original code: canvas 180x45, drawImage(image, 0, 0, 180, 45)
|
|
228
|
+
// So we definitely need to resize
|
|
229
|
+
const resizedData = resizeImage(img, 180, 45);
|
|
180
230
|
// Convert to grayscale 2D array
|
|
181
231
|
const arr = [];
|
|
182
|
-
for (let i = 0; i <
|
|
183
|
-
const gval =
|
|
232
|
+
for (let i = 0; i < resizedData.length; i += 4) {
|
|
233
|
+
const gval = resizedData[i] * 0.299 +
|
|
234
|
+
resizedData[i + 1] * 0.587 +
|
|
235
|
+
resizedData[i + 2] * 0.114;
|
|
184
236
|
arr.push(Math.round(gval));
|
|
185
237
|
}
|
|
186
238
|
const newArr = [];
|
package/dist/index.js
CHANGED
|
@@ -6,25 +6,12 @@ import { serveStatic } from "@hono/node-server/serve-static";
|
|
|
6
6
|
import { Home } from "./views/Home.js";
|
|
7
7
|
import { SuccessMessage } from "./views/partials.js";
|
|
8
8
|
import { sessionManager } from "./session-manager.js";
|
|
9
|
-
import { readFile } from "fs/promises";
|
|
10
|
-
import { dirname, join } from "path";
|
|
11
|
-
import { fileURLToPath } from "url";
|
|
12
9
|
const app = new Hono();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const htmxPath =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const content = await readFile(htmxPath);
|
|
19
|
-
return c.body(content, 200, {
|
|
20
|
-
"Content-Type": "application/javascript",
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
catch (e) {
|
|
24
|
-
console.error("Failed to serve HTMX:", e);
|
|
25
|
-
return c.text("Failed to load HTMX", 500);
|
|
26
|
-
}
|
|
27
|
-
});
|
|
10
|
+
import { createRequire } from "module";
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const htmxPath = require.resolve("htmx.org/dist/htmx.min.js");
|
|
13
|
+
// Serve htmx from node_modules
|
|
14
|
+
app.use("/static/htmx.js", serveStatic({ path: htmxPath }));
|
|
28
15
|
// Main page
|
|
29
16
|
app.get("/", (c) => {
|
|
30
17
|
return c.html(_jsx(Home, {}));
|
|
@@ -40,11 +27,10 @@ app.get("/api/session/status", (c) => {
|
|
|
40
27
|
const lastInit = state.lastInitialized
|
|
41
28
|
? new Date(state.lastInitialized).toLocaleString()
|
|
42
29
|
: "Never";
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
return c.html(_jsxs("div", { style: "font-family: monospace; padding: 1rem; background: #f5f5f5; border-radius: 4px; color: #000;", children: [_jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "Status:" }), " ", status] }), _jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "Last Initialized:" }), " ", lastInit] }), _jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "Total Cookies:" }), " ", state.cookies.size] }), _jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "CSRF Token:" }), " ", hasCsrf ? "✅ Present" : "❌ Missing"] }), _jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "JSESSIONID:" }), " ", hasJsession ? "✅ Present" : "❌ Missing"] }), _jsxs("div", { children: [_jsx("strong", { children: "SERVERID:" }), " ", hasServerId ? "✅ Present" : "❌ Missing"] })] }));
|
|
30
|
+
const csrfToken = state.csrf || "None";
|
|
31
|
+
const jsessionid = state.cookies.get("JSESSIONID") || "None";
|
|
32
|
+
const serverid = state.cookies.get("SERVERID") || "None";
|
|
33
|
+
return c.html(_jsxs("div", { style: "font-family: monospace; padding: 1rem; background: #f5f5f5; border-radius: 4px; color: #000;", children: [_jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "Status:" }), " ", status] }), _jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "Last Initialized:" }), " ", lastInit] }), _jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "Total Cookies:" }), " ", state.cookies.size] }), _jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "CSRF Token:" }), " ", csrfToken] }), _jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "JSESSIONID:" }), " ", jsessionid] }), _jsxs("div", { children: [_jsx("strong", { children: "SERVERID:" }), " ", serverid] })] }));
|
|
48
34
|
});
|
|
49
35
|
// Manual session refresh endpoint
|
|
50
36
|
app.post("/api/session/refresh", async (c) => {
|
|
@@ -109,16 +95,13 @@ app.get("/api/login/status/html", (c) => {
|
|
|
109
95
|
// Debug logs storage
|
|
110
96
|
const debugLogs = [];
|
|
111
97
|
function addDebugLog(level, message) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (debugLogs.length > 100) {
|
|
118
|
-
debugLogs.shift();
|
|
119
|
-
}
|
|
120
|
-
console.log(`[${level}] ${message}`);
|
|
98
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
99
|
+
debugLogs.push({ timestamp, level, message });
|
|
100
|
+
// Keep only last 100 logs
|
|
101
|
+
if (debugLogs.length > 100) {
|
|
102
|
+
debugLogs.shift();
|
|
121
103
|
}
|
|
104
|
+
console.log(`[${level}] ${message}`);
|
|
122
105
|
}
|
|
123
106
|
// Form-based login endpoint (for HTMX)
|
|
124
107
|
app.post("/api/login/form", async (c) => {
|
|
@@ -131,19 +114,18 @@ app.post("/api/login/form", async (c) => {
|
|
|
131
114
|
addDebugLog("ERROR", "Login attempt with missing credentials");
|
|
132
115
|
return c.html(_jsxs("div", { style: "padding: 1rem; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24;", children: [_jsx("strong", { children: "\u274C Error:" }), " Username, password, and registration number are required."] }));
|
|
133
116
|
}
|
|
134
|
-
|
|
135
|
-
addDebugLog("INFO", "Login requested");
|
|
117
|
+
addDebugLog("INFO", `Login requested for user: ${username} (${regNo})`);
|
|
136
118
|
// Show that login is in progress
|
|
137
119
|
const startTime = Date.now();
|
|
138
120
|
addDebugLog("INFO", "Starting login process - polling for text CAPTCHA...");
|
|
139
121
|
const success = await sessionManager.login(username, password, regNo);
|
|
140
122
|
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
141
123
|
if (success) {
|
|
142
|
-
addDebugLog("SUCCESS", `Login successful (took ${duration}s)`);
|
|
124
|
+
addDebugLog("SUCCESS", `Login successful for ${username} (took ${duration}s)`);
|
|
143
125
|
return c.html(_jsxs("div", { style: "padding: 1rem; background: #d4edda; border: 1px solid #c3e6cb; border-radius: 4px; color: #155724;", children: [_jsx("strong", { children: "\uD83C\uDF89 Login Successful!" }), _jsxs("div", { style: "margin-top: 0.5rem;", children: ["Welcome, ", _jsx("strong", { children: username })] }), _jsxs("div", { style: "margin-top: 0.25rem; font-size: 0.85rem;", children: ["Login took ", duration, " seconds"] })] }));
|
|
144
126
|
}
|
|
145
127
|
else {
|
|
146
|
-
addDebugLog("ERROR", `Login failed (took ${duration}s)`);
|
|
128
|
+
addDebugLog("ERROR", `Login failed for ${username} (took ${duration}s)`);
|
|
147
129
|
return c.html(_jsxs("div", { style: "padding: 1rem; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24;", children: [_jsx("strong", { children: "\u274C Login Failed" }), _jsx("div", { style: "margin-top: 0.5rem;", children: "Check your credentials or try again. VTOP might be showing reCAPTCHA." }), _jsxs("div", { style: "margin-top: 0.25rem; font-size: 0.85rem;", children: ["Attempt took ", duration, " seconds"] })] }));
|
|
148
130
|
}
|
|
149
131
|
}
|
|
@@ -154,9 +136,6 @@ app.post("/api/login/form", async (c) => {
|
|
|
154
136
|
});
|
|
155
137
|
// Debug logs endpoint
|
|
156
138
|
app.get("/api/debug/logs", (c) => {
|
|
157
|
-
if (process.env.DEBUG_LOGS !== "true") {
|
|
158
|
-
return c.html(_jsx("div", { style: "color: #888;", children: "Debug logs are disabled. Set DEBUG_LOGS=true to enable." }));
|
|
159
|
-
}
|
|
160
139
|
if (debugLogs.length === 0) {
|
|
161
140
|
return c.html(_jsx("div", { style: "color: #888;", children: "No logs yet. Try logging in to see debug output." }));
|
|
162
141
|
}
|
|
@@ -220,10 +199,9 @@ app.get("/api/assignments/html", async (c) => {
|
|
|
220
199
|
return c.html(_jsxs("div", { style: "padding: 1rem; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24;", children: [_jsx("strong", { children: "\u274C Error:" }), " ", String(error)] }));
|
|
221
200
|
}
|
|
222
201
|
});
|
|
223
|
-
const port = Number(process.env.PORT) || 6767;
|
|
224
202
|
serve({
|
|
225
203
|
fetch: app.fetch,
|
|
226
|
-
port,
|
|
204
|
+
port: 6767,
|
|
227
205
|
}, (info) => {
|
|
228
206
|
console.log(`🚀 Server is running on http://localhost:${info.port}`);
|
|
229
207
|
console.log(`📊 Session status: http://localhost:${info.port}/api/session/status`);
|
package/dist/session-manager.js
CHANGED
|
@@ -276,7 +276,7 @@ class VTOPSessionManager {
|
|
|
276
276
|
solvedCaptcha = await solve(cleanDataUri);
|
|
277
277
|
console.log("✅ Solved CAPTCHA:", solvedCaptcha);
|
|
278
278
|
// Save captcha image for debugging
|
|
279
|
-
if (parts?.base64
|
|
279
|
+
if (parts?.base64) {
|
|
280
280
|
const out = path.resolve(process.cwd(), "captcha.jpg");
|
|
281
281
|
await saveCaptchaImage(parts.base64, out);
|
|
282
282
|
// console.log(`📷 Saved CAPTCHA image to ${out}`);
|
|
@@ -349,25 +349,13 @@ class VTOPSessionManager {
|
|
|
349
349
|
: new URL(location, LOGIN_PAGE).toString();
|
|
350
350
|
await this.fetchWithCookies(redirectUrl);
|
|
351
351
|
}
|
|
352
|
-
// After successfully submitting the captcha,
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
this.state.loggedIn = true;
|
|
360
|
-
this.state.username = username;
|
|
361
|
-
this.state.regNo = regNo;
|
|
362
|
-
return true;
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
console.error("❌ Login verification failed - cleaning up session");
|
|
366
|
-
this.state.loggedIn = false;
|
|
367
|
-
this.state.username = null;
|
|
368
|
-
this.state.regNo = null;
|
|
369
|
-
return false;
|
|
370
|
-
}
|
|
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;
|
|
371
359
|
}
|
|
372
360
|
catch (e) {
|
|
373
361
|
console.error("Login POST failed:", e);
|
package/package.json
CHANGED
|
@@ -4,26 +4,28 @@
|
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "tsx watch src/index.tsx",
|
|
6
6
|
"build": "tsc",
|
|
7
|
-
"start": "node dist/index.js"
|
|
7
|
+
"start": "node dist/index.js",
|
|
8
|
+
"prepare": "npm run build"
|
|
8
9
|
},
|
|
9
10
|
"dependencies": {
|
|
10
11
|
"@hono/node-server": "^1.19.9",
|
|
11
|
-
"canvas": "^3.2.1",
|
|
12
12
|
"hono": "^4.11.4",
|
|
13
|
-
"htmx.org": "^2.0.8"
|
|
13
|
+
"htmx.org": "^2.0.8",
|
|
14
|
+
"jpeg-js": "^0.4.4",
|
|
15
|
+
"pngjs": "^7.0.0"
|
|
14
16
|
},
|
|
15
17
|
"bin": {
|
|
16
18
|
"open-vtop": "dist/index.js"
|
|
17
19
|
},
|
|
18
20
|
"files": [
|
|
19
|
-
"dist"
|
|
20
|
-
"README.md",
|
|
21
|
-
"LICENSE"
|
|
21
|
+
"dist"
|
|
22
22
|
],
|
|
23
23
|
"devDependencies": {
|
|
24
|
+
"@types/jpeg-js": "^0.3.0",
|
|
24
25
|
"@types/node": "^20.11.17",
|
|
26
|
+
"@types/pngjs": "^6.0.5",
|
|
25
27
|
"tsx": "^4.7.1",
|
|
26
28
|
"typescript": "^5.8.3"
|
|
27
29
|
},
|
|
28
|
-
"version": "1.0.
|
|
30
|
+
"version": "1.0.4"
|
|
29
31
|
}
|