open-vtop 1.0.9 → 1.0.10

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.
@@ -139,118 +139,163 @@ class VTOPSessionManager {
139
139
  this.state.username = null;
140
140
  await this.initialize();
141
141
  }
142
- async login(username, password, maxAttempts = 10) {
142
+ async login(username, password, maxAttempts = 30, // Increased default for parallel attempts total buffer
143
+ concurrency = 3) {
144
+ const startTime = Date.now();
143
145
  if (!this.state.initialized) {
144
146
  console.log("Session not initialized, initializing first...");
145
147
  await this.initialize();
146
148
  }
147
- console.log("Starting VTOP login process...");
148
- console.log("Polling /vtop/login until text CAPTCHA appears...");
149
- let attempt = 0;
150
- while (attempt < maxAttempts) {
151
- attempt++;
149
+ console.log(`Starting parallel VTOP login (concurrency=${concurrency})...`);
150
+ this.events.emit("log", `Starting parallel login with ${concurrency} workers...`);
151
+ const abortController = new AbortController();
152
+ const signal = abortController.signal;
153
+ let winnerDetected = false;
154
+ let loginSuccess = false;
155
+ // We'll trust the first worker that finds a text captcha to handle the login.
156
+ // If it fails the POST, we fail the whole batch for simplicity,
157
+ // or we could retry, but let's stick to "first valid captcha wins" for now.
158
+ const pollWorker = async (workerId) => {
159
+ let attempts = 0;
160
+ const logPrefix = `[Worker ${workerId}]`;
152
161
  try {
153
- const res = await this.fetchWithCookies(LOGIN_PAGE);
154
- const body = await res.text();
155
- const { isTextCaptcha, isRecaptcha, csrf, imgDataUri } = detectCaptcha(body);
156
- const curJsession = this.state.cookies.get("JSESSIONID") || "(?)";
157
- const curServerID = this.state.cookies.get("SERVERID") || "(?)";
158
- // const logMsg = `Attempt ${attempt}: status ${res.status}\ntext-captcha=${isTextCaptcha ? "YES" : "no"} | recaptcha=${isRecaptcha ? "YES" : "no"} | JSESSIONID=${curJsession}`;
159
- const logMsg = `Attempt ${attempt} text-captcha=${isTextCaptcha ? "YES" : "no"} recaptcha=${isRecaptcha ? "YES" : "no"} `;
160
- console.log(logMsg);
161
- this.events.emit("log", logMsg);
162
- if (isTextCaptcha) {
163
- let solvedCaptcha = "";
164
- if (imgDataUri) {
165
- const parts = extractDataUriParts(imgDataUri);
166
- try {
167
- const cleanDataUri = imgDataUri.trim();
168
- if (cleanDataUri !== imgDataUri) {
169
- console.log("Trimmed whitespace from captcha data URI");
162
+ while (!signal.aborted && !winnerDetected) {
163
+ attempts++;
164
+ if (attempts > maxAttempts) {
165
+ console.log(`${logPrefix} Max attempts reached.`);
166
+ return;
167
+ }
168
+ try {
169
+ // Check signal before fetch
170
+ if (signal.aborted)
171
+ break;
172
+ // console.log(`${logPrefix} Fetching login page...`); // verbose
173
+ const res = await this.fetchWithCookies(LOGIN_PAGE, {
174
+ signal,
175
+ });
176
+ // Check signal after fetch (in case it aborted during fetch)
177
+ if (signal.aborted)
178
+ break;
179
+ const body = await res.text();
180
+ // Re-check detection
181
+ if (winnerDetected || signal.aborted)
182
+ break;
183
+ const { isTextCaptcha, isRecaptcha, csrf, imgDataUri } = detectCaptcha(body);
184
+ this.events.emit("log", `${logPrefix} text=${isTextCaptcha} recap=${isRecaptcha}`);
185
+ if (isTextCaptcha && !winnerDetected) {
186
+ // ATOMIC CLAIM
187
+ if (winnerDetected)
188
+ break; // Double check
189
+ winnerDetected = true;
190
+ console.log(`${logPrefix} !!! WINNER detected Text CAPTCHA !!!`);
191
+ this.events.emit("log", `${logPrefix} WINNER! Claiming login task.`);
192
+ // Cancel other workers immediately
193
+ abortController.abort();
194
+ // Proceed with solving and login
195
+ const curJsession = this.state.cookies.get("JSESSIONID") || "(?)";
196
+ const curServerID = this.state.cookies.get("SERVERID") || "(?)";
197
+ let solvedCaptcha = "";
198
+ if (imgDataUri) {
199
+ const cleanDataUri = imgDataUri.trim();
200
+ try {
201
+ solvedCaptcha = await solve(cleanDataUri);
202
+ console.log(`${logPrefix} Solved: ${solvedCaptcha}`);
203
+ this.events.emit("log", `${logPrefix} Solved CAPTCHA: ${solvedCaptcha}`);
204
+ }
205
+ catch (e) {
206
+ console.error(`${logPrefix} Solve failed:`, e);
207
+ this.events.emit("log", `${logPrefix} Solve failed.`);
208
+ }
170
209
  }
171
- solvedCaptcha = await solve(cleanDataUri);
172
- console.log("Solved CAPTCHA:", solvedCaptcha);
173
- if (parts?.base64) {
174
- const out = path.resolve(process.cwd(), "captcha.jpg");
175
- await saveCaptchaImage(parts.base64, out);
210
+ console.log(`${logPrefix} Submitting Login POST...`);
211
+ this.events.emit("log", `${logPrefix} Submitting credentials...`);
212
+ const loginForm = new URLSearchParams();
213
+ if (csrf)
214
+ loginForm.set("_csrf", csrf);
215
+ loginForm.set("username", username);
216
+ loginForm.set("password", password);
217
+ loginForm.set("captchaStr", solvedCaptcha);
218
+ const _cookies = `JSESSIONID=${curJsession}; SERVERID=${curServerID}`;
219
+ const postHeaders = {
220
+ ...LOGIN_POST_HEADERS,
221
+ Cookie: _cookies,
222
+ };
223
+ const postRes = await fetch(LOGIN_PAGE, {
224
+ method: "POST",
225
+ headers: postHeaders,
226
+ body: loginForm.toString(),
227
+ redirect: "manual",
228
+ });
229
+ this.storeCookies(postRes);
230
+ // Follow redirect if existing
231
+ const location = postRes.headers.get("Location");
232
+ if (location) {
233
+ const redirectUrl = location.startsWith("http")
234
+ ? location
235
+ : new URL(location, LOGIN_PAGE).toString();
236
+ await this.fetchWithCookies(redirectUrl);
176
237
  }
177
- }
178
- catch (e) {
179
- console.warn("Failed to solve captcha:", e);
180
- solvedCaptcha = "";
238
+ this.state.loggedIn = true;
239
+ this.state.username = username;
240
+ // Save credentials asynchronously
241
+ this.saveCredentials(username, password).catch((err) => console.error("Save creds failed", err));
242
+ const duration = (Date.now() - startTime) / 1000;
243
+ console.log(`${logPrefix} Login success in ${duration}s`);
244
+ this.events.emit("log", `${logPrefix} Login SUCCESS in ${duration}s`);
245
+ const logEntry = `[${new Date().toISOString()}] Login took ${duration} seconds (Workers: ${concurrency})\n`;
181
246
  try {
182
- const fs = await import("fs/promises");
183
- const failPath = path.resolve(process.cwd(), "failed_captcha_data.txt");
184
- await fs.writeFile(failPath, imgDataUri);
185
- console.log(`Saved failed captcha data URI to ${failPath}`);
247
+ await fs.appendFile("login_times.txt", logEntry);
248
+ console.log(`${logPrefix} Login time recorded to file.`);
186
249
  }
187
- catch (writeErr) {
188
- console.error("Failed to save failed captcha data:", writeErr);
250
+ catch (err) {
251
+ console.error(`${logPrefix} Failed to write login time:`, err);
189
252
  }
253
+ loginSuccess = true;
254
+ return;
190
255
  }
191
- }
192
- else {
193
- console.log("Text CAPTCHA detected but no data URI image found.");
194
- }
195
- console.log("\nSubmitting POST /vtop/login ...");
196
- const loginForm = new URLSearchParams();
197
- if (csrf)
198
- loginForm.set("_csrf", csrf);
199
- loginForm.set("username", username);
200
- loginForm.set("password", password);
201
- loginForm.set("captchaStr", solvedCaptcha);
202
- const _cookies = `JSESSIONID=${curJsession}; SERVERID=${curServerID}`;
203
- const postHeaders = {
204
- ...LOGIN_POST_HEADERS,
205
- Cookie: _cookies,
206
- };
207
- try {
208
- const postRes = await fetch(LOGIN_PAGE, {
209
- method: "POST",
210
- headers: postHeaders,
211
- body: loginForm.toString(),
212
- redirect: "manual",
213
- });
214
- console.log(` -> Login POST status: ${postRes.status}`);
215
- this.storeCookies(postRes);
216
- const setCookie = postRes.headers.getSetCookie?.() || [];
217
- if (setCookie.length > 0) {
218
- console.log(" -> New cookies received");
219
- }
220
- const location = postRes.headers.get("Location");
221
- if (location) {
222
- console.log(` -> Redirect to: ${location}`);
223
- const redirectUrl = location.startsWith("http")
224
- ? location
225
- : new URL(location, LOGIN_PAGE).toString();
226
- await this.fetchWithCookies(redirectUrl);
256
+ else {
257
+ // Not text captcha, retry
258
+ if (isRecaptcha) {
259
+ this.events.emit("log", `${logPrefix} Saw reCAPTCHA, skipping...`);
260
+ }
261
+ // Random delay/jitter to desynchronize workers
262
+ const delay = 500 + Math.random() * 500;
263
+ await new Promise((r) => setTimeout(r, delay));
227
264
  }
228
- console.log("Login POST submitted successfully!");
229
- this.state.loggedIn = true;
230
- this.state.username = username;
231
- await this.saveCredentials(username, password);
232
- this.events.emit("login-complete");
233
- return true;
234
265
  }
235
266
  catch (e) {
236
- console.error("Login POST failed:", e);
267
+ if (e.name === "AbortError") {
268
+ // Ignore aborts
269
+ break;
270
+ }
271
+ console.error(`${logPrefix} error:`, e);
272
+ this.events.emit("log", `${logPrefix} error: ${e.message}`);
273
+ // Short delay on error
274
+ await new Promise((r) => setTimeout(r, 1000));
237
275
  }
238
276
  }
239
- if (isRecaptcha) {
240
- console.log("reCAPTCHA detected - cannot solve automatically");
241
- await new Promise((r) => setTimeout(r, 2000));
242
- continue;
243
- }
244
- await new Promise((r) => setTimeout(r, 500));
245
277
  }
246
- catch (e) {
247
- console.error(`Attempt ${attempt} failed:`, e);
248
- await new Promise((r) => setTimeout(r, 1000));
278
+ catch (err) {
279
+ if (err.name !== "AbortError") {
280
+ console.error(`${logPrefix} unexpected fatal error:`, err);
281
+ }
249
282
  }
283
+ };
284
+ const workers = [];
285
+ for (let i = 1; i <= concurrency; i++) {
286
+ workers.push(pollWorker(i));
287
+ }
288
+ await Promise.all(workers);
289
+ if (loginSuccess) {
290
+ this.events.emit("login-complete");
291
+ return true;
292
+ }
293
+ else {
294
+ console.log("All workers finished without success.");
295
+ this.events.emit("log", "All workers failed/exhausted.");
296
+ this.events.emit("login-complete");
297
+ return false;
250
298
  }
251
- console.log(`Login failed after ${maxAttempts} attempts`);
252
- this.events.emit("login-complete");
253
- return false;
254
299
  }
255
300
  isLoggedIn() {
256
301
  return this.state.loggedIn;
package/package.json CHANGED
@@ -30,5 +30,5 @@
30
30
  "tsx": "^4.7.1",
31
31
  "typescript": "^5.8.3"
32
32
  },
33
- "version": "1.0.9"
33
+ "version": "1.0.10"
34
34
  }