seekmux 0.0.1 → 0.0.2
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/README.md +36 -0
- package/dist/diagnose.js +54 -0
- package/dist/worker.js +16 -3
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# seekmux
|
|
2
|
+
|
|
3
|
+
The worker of a [SeekMux](https://github.com/fl0w1nd/seekmux) self-hosted
|
|
4
|
+
fetcher. It reads pages for the gateway in a Chrome where you are signed in,
|
|
5
|
+
through [OpenCLI](https://github.com/jackwener/opencli): X and Reddit posts
|
|
6
|
+
through OpenCLI's site adapters, other pages as they render.
|
|
7
|
+
|
|
8
|
+
## Use
|
|
9
|
+
|
|
10
|
+
1. On the gateway's console, open **自托管抓取器** (Self-hosted fetchers),
|
|
11
|
+
create a fetcher or add a worker to one, and copy the worker's token.
|
|
12
|
+
2. Install the OpenCLI
|
|
13
|
+
[Browser Bridge extension](https://github.com/jackwener/opencli#2-install-the-browser-bridge-extension)
|
|
14
|
+
in the Chrome profile to read with, and keep that Chrome open.
|
|
15
|
+
3. Run the worker (Node.js 22.4 or later):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx seekmux --server https://your-gateway --token <worker token>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The worker connects to the gateway's `/worker` endpoint over a WebSocket and
|
|
22
|
+
reconnects when the connection drops, so the machine needs no open port. It
|
|
23
|
+
stops when the gateway refuses its token (the token was changed or the worker
|
|
24
|
+
removed), or when another worker connects with the same token.
|
|
25
|
+
|
|
26
|
+
| Option | Environment | |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| `--server` | `SEEKMUX_SERVER` | The gateway's address |
|
|
29
|
+
| `--token` | `SEEKMUX_WORKER_TOKEN` | The worker's token |
|
|
30
|
+
| `--slots` | | Pages read at once, 1 or 2 (default 1) |
|
|
31
|
+
|
|
32
|
+
OpenCLI is a dependency of this package, so nothing else needs installing.
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
|
|
36
|
+
MIT
|
package/dist/diagnose.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Says why a connection to the gateway could not be opened. The WebSocket
|
|
2
|
+
// API reports a failed handshake only as code 1006, so the handshake is
|
|
3
|
+
// tried again over plain HTTP to see what answered it.
|
|
4
|
+
import { request as httpRequest } from "node:http";
|
|
5
|
+
import { request as httpsRequest } from "node:https";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
/** Returns why the handshake fails, or undefined when it succeeds now. */
|
|
8
|
+
export function diagnose(wsURL, timeoutMs = 10_000) {
|
|
9
|
+
const url = new URL(wsURL);
|
|
10
|
+
url.protocol = url.protocol === "ws:" ? "http:" : "https:";
|
|
11
|
+
const request = url.protocol === "http:" ? httpRequest : httpsRequest;
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const req = request(url, {
|
|
14
|
+
timeout: timeoutMs,
|
|
15
|
+
headers: {
|
|
16
|
+
Connection: "Upgrade",
|
|
17
|
+
Upgrade: "websocket",
|
|
18
|
+
"Sec-WebSocket-Version": "13",
|
|
19
|
+
"Sec-WebSocket-Key": randomBytes(16).toString("base64"),
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
req.on("upgrade", (_res, socket) => {
|
|
23
|
+
socket.destroy();
|
|
24
|
+
resolve(undefined);
|
|
25
|
+
});
|
|
26
|
+
req.on("response", (res) => {
|
|
27
|
+
let body = "";
|
|
28
|
+
res.setEncoding("utf8");
|
|
29
|
+
res.on("data", (chunk) => {
|
|
30
|
+
if (body.length < 2048)
|
|
31
|
+
body += chunk;
|
|
32
|
+
});
|
|
33
|
+
res.on("end", () => resolve(explain(url, res.statusCode ?? 0, String(res.headers["content-type"] ?? ""), body)));
|
|
34
|
+
res.on("error", () => resolve(`the gateway answered HTTP ${res.statusCode}`));
|
|
35
|
+
});
|
|
36
|
+
req.on("timeout", () => req.destroy(new Error(`no answer within ${timeoutMs / 1000}s`)));
|
|
37
|
+
req.on("error", (err) => resolve(`could not reach ${url.host}: ${err.message}`));
|
|
38
|
+
req.end();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function explain(url, status, type, body) {
|
|
42
|
+
if (status === 426 && /upgrade/i.test(body)) {
|
|
43
|
+
return `the request reached the gateway without its WebSocket upgrade: the reverse proxy in front of it must pass the Upgrade and Connection headers on ${url.pathname}`;
|
|
44
|
+
}
|
|
45
|
+
if (status === 200 && type.includes("text/html")) {
|
|
46
|
+
return `${url.host} answered ${url.pathname} with a web page: the gateway is older than 0.7.0, or the address is not the gateway's`;
|
|
47
|
+
}
|
|
48
|
+
if (status === 404)
|
|
49
|
+
return `${url.host} has no ${url.pathname}: the gateway is older than 0.7.0, or the address is not the gateway's`;
|
|
50
|
+
if (status === 502 || status === 503 || status === 504)
|
|
51
|
+
return `the reverse proxy answered HTTP ${status}: the gateway behind it is not reachable`;
|
|
52
|
+
const said = body.trim().split("\n")[0]?.slice(0, 200);
|
|
53
|
+
return `the gateway answered HTTP ${status}${said ? `: ${said}` : ""}`;
|
|
54
|
+
}
|
package/dist/worker.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The connection to the gateway: says hello, reports the browser's state,
|
|
2
2
|
// runs the fetches it is handed and reconnects when the connection drops.
|
|
3
|
+
import { diagnose } from "./diagnose.js";
|
|
3
4
|
import { Canceled, readState } from "./opencli.js";
|
|
4
5
|
import { CloseCode, FetchError, PROTOCOL_VERSION } from "./protocol.js";
|
|
5
6
|
import { read } from "./sites.js";
|
|
@@ -20,8 +21,10 @@ export async function run(opts) {
|
|
|
20
21
|
let state = await readState();
|
|
21
22
|
logState(opts, state);
|
|
22
23
|
let delay = 1000;
|
|
24
|
+
// Why the last connection could not be opened, said again only when it changes.
|
|
25
|
+
let problem;
|
|
23
26
|
for (;;) {
|
|
24
|
-
const { code, reason, welcomed } = await session(opts, state, (next) => (state = next));
|
|
27
|
+
const { code, reason, opened, welcomed } = await session(opts, state, (next) => (state = next));
|
|
25
28
|
if (code === CloseCode.Unauthorized || code === CloseCode.Protocol) {
|
|
26
29
|
throw new Error(`the gateway refused this worker: ${reason || code}`);
|
|
27
30
|
}
|
|
@@ -30,7 +33,15 @@ export async function run(opts) {
|
|
|
30
33
|
}
|
|
31
34
|
if (welcomed)
|
|
32
35
|
delay = 1000;
|
|
33
|
-
|
|
36
|
+
if (opened) {
|
|
37
|
+
problem = undefined;
|
|
38
|
+
opts.log(`disconnected (${reason || code}); reconnecting in ${delay / 1000}s`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const why = await diagnose(opts.url);
|
|
42
|
+
opts.log(why && why === problem ? `could not connect, as before; retrying in ${delay / 1000}s` : `could not connect to ${opts.url}${why ? `: ${why}` : ""}; retrying in ${delay / 1000}s`);
|
|
43
|
+
problem = why;
|
|
44
|
+
}
|
|
34
45
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
35
46
|
delay = Math.min(delay * 2, 30_000);
|
|
36
47
|
}
|
|
@@ -42,6 +53,7 @@ function logState(opts, state) {
|
|
|
42
53
|
function session(opts, initial, setState) {
|
|
43
54
|
return new Promise((resolve) => {
|
|
44
55
|
let state = initial;
|
|
56
|
+
let opened = false;
|
|
45
57
|
let welcomed = false;
|
|
46
58
|
const running = new Map();
|
|
47
59
|
const ws = new WebSocket(opts.url);
|
|
@@ -72,6 +84,7 @@ function session(opts, initial, setState) {
|
|
|
72
84
|
};
|
|
73
85
|
const timer = setInterval(check, STATE_INTERVAL);
|
|
74
86
|
ws.addEventListener("open", () => {
|
|
87
|
+
opened = true;
|
|
75
88
|
send({ type: "hello", protocol: PROTOCOL_VERSION, token: opts.token, version: opts.version, slots: opts.slots, state });
|
|
76
89
|
});
|
|
77
90
|
ws.addEventListener("message", async (event) => {
|
|
@@ -128,7 +141,7 @@ function session(opts, initial, setState) {
|
|
|
128
141
|
clearInterval(timer);
|
|
129
142
|
for (const controller of running.values())
|
|
130
143
|
controller.abort();
|
|
131
|
-
resolve({ code: event.code, reason: event.reason, welcomed });
|
|
144
|
+
resolve({ code: event.code, reason: event.reason, opened, welcomed });
|
|
132
145
|
});
|
|
133
146
|
// An error is followed by a close event, which ends the session.
|
|
134
147
|
ws.addEventListener("error", () => { });
|