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.
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * CAPTCHA Solver for VTOP
3
- * Uses canvas for image processing and bitmap matching / neural network for character recognition
3
+ * Uses pure JS for image processing and bitmap matching / neural network for character recognition
4
4
  */
5
- import { createCanvas, loadImage } from "canvas";
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 canvas = createCanvas(200, 40);
151
- const ctx = canvas.getContext("2d");
152
- const image = await loadImage(imgDataUri);
153
- ctx.drawImage(image, 0, 0, 200, 40);
154
- const pd = ctx.getImageData(0, 0, 200, 40);
155
- let bls = saturation(pd.data);
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 canvas = createCanvas(180, 45);
175
- const ctx = canvas.getContext("2d");
176
- const image = await loadImage(imgDataUri);
177
- ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
178
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
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 < data.length; i += 4) {
183
- const gval = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
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
- const __dirname = dirname(fileURLToPath(import.meta.url));
14
- // Serve htmx from node_modules - resolve absolute path
15
- const htmxPath = join(__dirname, "../node_modules/htmx.org/dist/htmx.min.js");
16
- app.get("/static/htmx.js", async (c) => {
17
- try {
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
- // Redact sensitive values for security
44
- const hasCsrf = !!state.csrf;
45
- const hasJsession = state.cookies.has("JSESSIONID");
46
- const hasServerId = state.cookies.has("SERVERID");
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
- // Only log if specifically enabled
113
- if (process.env.DEBUG_LOGS === "true") {
114
- const timestamp = new Date().toLocaleTimeString();
115
- debugLogs.push({ timestamp, level, message });
116
- // Keep only last 100 logs
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
- // Don't log credentials even in debug
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`);
@@ -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 && process.env.DEBUG_CAPTCHA === "true") {
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, we MUST verify login success
353
- console.log("POST successful, verifying login status...");
354
- // Try to navigate to post-login pages to confirm we are actually logged in
355
- // VTOP is tricky: a 200 OK on login POST doesn't guarantee success (could be wrong password)
356
- const navSuccess = await this.navigatePostLogin();
357
- if (navSuccess) {
358
- console.log("🎉 Login verified successfully!");
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.3"
30
+ "version": "1.0.4"
29
31
  }