windscout 0.5.8 → 0.6.1

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,6 +1,7 @@
1
1
  // The Windscribe desktop app, driven through the windscribe-cli it ships with.
2
- import { spawn } from "node:child_process";
3
- import { existsSync } from "node:fs";
2
+ import { execFileSync, spawn } from "node:child_process";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
4
5
  import { join } from "node:path";
5
6
  import { sleep } from "./time.js";
6
7
  export const CLI = "windscribe-cli";
@@ -69,6 +70,51 @@ export async function waitForLogin() {
69
70
  + `\nDon't have it? ${DOWNLOAD_URL}`);
70
71
  return status;
71
72
  }
73
+ // The app's settings (Qt's, as Windscribe/Windscribe2): this INI file on Linux, the user's defaults on macOS, the registry on Windows
74
+ export const SETTINGS = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "Windscribe", "Windscribe2.conf");
75
+ /** A top-level value in the text of the INI file: Qt puts those under [General] */
76
+ export function iniValue(ini, key) {
77
+ let general = false;
78
+ for (const line of ini.split(/\r?\n/)) {
79
+ if (line.startsWith("["))
80
+ general = line.trim() === "[General]";
81
+ else if (general && line.startsWith(`${key}=`))
82
+ return line.slice(key.length + 1);
83
+ }
84
+ }
85
+ /** The INI file's text with the app logging in with this session when it starts. Settings another account left start over:
86
+ * the WireGuard keys in them are registered with that account */
87
+ export function withSession(ini, { userId, authHash }) {
88
+ if (userId && iniValue(ini, "userId") !== userId)
89
+ ini = "";
90
+ let general = false;
91
+ const lines = ini.split("\n").filter((line) => {
92
+ if (line.startsWith("["))
93
+ general = line.trim() === "[General]";
94
+ return !(general && line.startsWith("authHash="));
95
+ });
96
+ const at = lines.findIndex((line) => line.trim() === "[General]");
97
+ lines.splice(at + 1, 0, ...(at < 0 ? ["[General]"] : []), `authHash=${authHash}`);
98
+ return lines.join("\n");
99
+ }
100
+ /** One of the app's top-level settings on this computer; "" when it isn't there, or doesn't look like an ID or a token */
101
+ function setting(key) {
102
+ const run = (cmd, args) => execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 10_000 });
103
+ try {
104
+ const value = process.platform === "darwin" ? run("defaults", ["read", "com.windscribe.Windscribe2", key])
105
+ : process.platform === "win32" ? run("reg", ["query", "HKCU\\Software\\Windscribe\\Windscribe2", "/v", key]).match(/REG_SZ\s+(.*)/)?.[1]
106
+ : iniValue(readFileSync(SETTINGS, "utf8"), key);
107
+ return value?.trim().match(/^[\w:.+/=-]+$/)?.[0] ?? "";
108
+ }
109
+ catch {
110
+ return ""; // no such setting, or no settings
111
+ }
112
+ }
113
+ /** The session this computer's app is logged in with; null when its settings don't have one */
114
+ export function session() {
115
+ const authHash = setting("authHash");
116
+ return authHash ? { userId: setting("userId"), authHash } : null;
117
+ }
72
118
  export const isFirewallOn = (status) => /^Firewall state: On/m.test(status);
73
119
  let firewallChanged = false; // blockInternet() turned the firewall on, so restoreFirewall() turns it back off
74
120
  /** Turns on the app's firewall, so nothing goes online outside the VPN. False if it was on already (it then stays on) */
package/dist/worker.js ADDED
@@ -0,0 +1,75 @@
1
+ // windscout --worker: runs in a parallel scan's docker container (docker.ts), beside the container's own Windscribe app.
2
+ // Takes the host's messages from stdin and answers on stdout, a JSON object per line.
3
+ import { spawn } from "node:child_process";
4
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { dirname } from "node:path";
6
+ import { createInterface } from "node:readline";
7
+ import { probe } from "./probe.js";
8
+ import { now, sleep } from "./time.js";
9
+ import { SETTINGS, windscribe, withSession } from "./windscribe.js";
10
+ const APP = "/opt/windscribe/Windscribe"; // as the image installs it
11
+ const send = (m) => process.stdout.write(JSON.stringify(m) + "\n");
12
+ export async function worker() {
13
+ const inbox = [];
14
+ let wake = () => { }, closed = false, stop = false;
15
+ createInterface({ input: process.stdin })
16
+ .on("line", (l) => {
17
+ const m = JSON.parse(l);
18
+ if (m.stop)
19
+ stop = true; // the scan is stopping: give up connecting
20
+ else
21
+ inbox.push(m), wake();
22
+ })
23
+ .on("close", () => ((closed = true), wake()));
24
+ /** The host's next message; null once it closed stdin: the scan is over */
25
+ const next = async () => {
26
+ while (!inbox.length && !closed)
27
+ await new Promise((resolve) => (wake = resolve));
28
+ return inbox.shift() ?? null;
29
+ };
30
+ // The app logs in with this computer's session, which goes in its settings before it starts: no login of its own, since
31
+ // Windscribe locks an account that logs in many times at once. Never log out here: that would end this computer's session too
32
+ const session = (await next())?.session;
33
+ if (!session)
34
+ return;
35
+ let ini = "";
36
+ try {
37
+ ini = readFileSync(SETTINGS, "utf8");
38
+ }
39
+ catch { } // a new volume: no settings yet
40
+ mkdirSync(dirname(SETTINGS), { recursive: true });
41
+ writeFileSync(SETTINGS, withSession(ini, session));
42
+ // no systemd in a container: start the app like its unit would, as this user (its CLI refuses root)
43
+ spawn(APP, { stdio: "ignore" }).on("error", () => { }); // not there: its CLI never answers below
44
+ // its CLI answers once it's up, and says logged in once Windscribe accepts the session, or the error it got instead
45
+ let status = "";
46
+ for (const t = now(); now() - t < 120 && !/Login state: (Logged in|Error)/.test(status = await windscribe("status"));)
47
+ await sleep(1000);
48
+ if (!status.includes("Logged in")) {
49
+ const state = status.match(/^Login state: (.*)$/m)?.[1].trim();
50
+ return void console.error(state ? `Windscribe's app didn't log in with this computer's session (${state})` : "Windscribe's app didn't start in the container");
51
+ }
52
+ send({ ready: true, wireguard: process.env.WS_WIREGUARD !== "0" }); // the image's CMD checked the kernel
53
+ for (let m; (m = await next());) {
54
+ if (!m.probe)
55
+ continue;
56
+ stop = false;
57
+ const { query, protocol, timeout, speedTest } = m.probe;
58
+ const result = await probe(query, protocol, {
59
+ timeout, speedTest,
60
+ stopped: () => stop,
61
+ onStep: (step) => send({ step }),
62
+ measure: async (fn) => {
63
+ send({ measure: true });
64
+ await next(); // go
65
+ try {
66
+ await fn();
67
+ }
68
+ finally {
69
+ send({ measured: true });
70
+ }
71
+ },
72
+ });
73
+ send({ result });
74
+ }
75
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "windscout",
3
- "version": "0.5.8",
3
+ "version": "0.6.1",
4
4
  "description": "Find which Windscribe locations and protocols work from your network, ranked by speed (unofficial)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,8 @@
11
11
  ],
12
12
  "scripts": {
13
13
  "build": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\" && tsc && node -e \"fs.chmodSync('dist/cli.js', 0o755)\"",
14
- "prepack": "npm run build"
14
+ "prepack": "npm run build",
15
+ "test": "npm run build && node --test test/"
15
16
  },
16
17
  "engines": {
17
18
  "node": ">=20"
@@ -32,7 +33,8 @@
32
33
  "firewall",
33
34
  "unblock",
34
35
  "vpn-speed-test",
35
- "server-finder"
36
+ "server-finder",
37
+ "docker"
36
38
  ],
37
39
  "author": "Dier Cohen <i.am.dier.cohen@gmail.com>",
38
40
  "license": "MIT",