moshcode 0.68.0 → 0.69.0
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/bin/moshcode.mjs +5 -0
- package/package.json +1 -1
- package/src/cli-schema.mjs +37 -0
- package/src/completion.mjs +3 -3
- package/src/dns-filter-cli.mjs +407 -0
- package/src/dns-filter.mjs +500 -0
- package/src/dns.mjs +89 -2
- package/src/shorten.mjs +212 -0
- package/src/tui.mjs +7 -0
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ or miss one that does. A test fails the build when it drifts.
|
|
|
52
52
|
| `moshcode doh` | hosting | run the DNS-over-HTTPS resolver |
|
|
53
53
|
| `moshcode site` <br>`serve` | hosting | install web-server config for a Moshpit name |
|
|
54
54
|
| `moshcode template` <br>`templates` | hosting | scaffold a stack for a Moshpit-hosted service |
|
|
55
|
+
| `moshcode shorten` <br>`short` `link` | hosting | mint a short link on the pit — /f/<code> follows to your url |
|
|
55
56
|
| `moshcode games` <br>`game` `arcade` | arcade | the moshcode arcade — twenty-two games, no menus |
|
|
56
57
|
| `moshcode pwd` <br>`where` | system | show the current directory and git context |
|
|
57
58
|
| `moshcode engines` | engines | list engines and installation status |
|
|
@@ -1278,6 +1279,41 @@ Full walkthrough, including the layer-by-layer way to debug it and the limits
|
|
|
1278
1279
|
worth knowing before you build:
|
|
1279
1280
|
**[docs/hosting-a-moshpit-name.md](docs/hosting-a-moshpit-name.md)**.
|
|
1280
1281
|
|
|
1282
|
+
### Filtering what resolves
|
|
1283
|
+
|
|
1284
|
+
With `dns enable` on, the bridge already sees every lookup this machine makes.
|
|
1285
|
+
`dns filter` is the other thing a resolver in that position can do: refuse the
|
|
1286
|
+
names that exist only to advertise, track, mine or phish, before a connection is
|
|
1287
|
+
ever opened. Nothing is filtered until you ask for it.
|
|
1288
|
+
|
|
1289
|
+
```sh
|
|
1290
|
+
moshcode dns filter on # ads, malware, phishing, mining
|
|
1291
|
+
moshcode dns filter update # fetch the lists — nothing downloads on its own
|
|
1292
|
+
moshcode dns filter # what is on, and what it has blocked
|
|
1293
|
+
moshcode dns filter test ads.example.com # would this be blocked, and by which rule
|
|
1294
|
+
moshcode dns filter allow news.example # never block it, whatever any list says
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
| list | what it blocks |
|
|
1298
|
+
|---|---|
|
|
1299
|
+
| `ads` | ads and trackers — StevenBlack unified, on by default |
|
|
1300
|
+
| `malware` | hosts serving malware — URLhaus, on by default |
|
|
1301
|
+
| `phishing` | Phishing Army, on by default |
|
|
1302
|
+
| `mining` | in-browser cryptominers, on by default |
|
|
1303
|
+
| `adult` `gambling` `social` `fakenews` | opt in by name with `filter add <list>` |
|
|
1304
|
+
|
|
1305
|
+
Three things worth knowing. Blocking a name blocks everything under it, and an
|
|
1306
|
+
`allow` rule always wins — that is the escape hatch for the day a list someone
|
|
1307
|
+
else maintains takes down something you need. Changes reach a running bridge
|
|
1308
|
+
within about five seconds, so nothing has to be restarted. And a blocked name is
|
|
1309
|
+
answered `NXDOMAIN` by default; `--mode zero` answers `0.0.0.0` instead, and
|
|
1310
|
+
`--mode refuse` says `REFUSED`, which is the one a client can tell apart from a
|
|
1311
|
+
real absence while you work out whether the filter is what broke something.
|
|
1312
|
+
|
|
1313
|
+
`dns filter` never turns DNS routing on — it writes a config and nothing else,
|
|
1314
|
+
so on a machine whose resolver has never heard of the bridge it changes nothing.
|
|
1315
|
+
Its status says so rather than reporting `on` and leaving you to find out.
|
|
1316
|
+
|
|
1281
1317
|
## Shell completion
|
|
1282
1318
|
|
|
1283
1319
|
MoshCode can print context-aware completion scripts for its commands, engines,
|
package/bin/moshcode.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import { dnsCommand } from "../src/dns.mjs";
|
|
|
36
36
|
import { nameCommand } from "../src/name-link.mjs";
|
|
37
37
|
import { templateCommand } from "../src/templates.mjs";
|
|
38
38
|
import { serveCommand } from "../src/serve.mjs";
|
|
39
|
+
import { shortenCommand } from "../src/shorten.mjs";
|
|
39
40
|
import { createDohServer, nginxDohSite, parseDohPort, parseGuardArgs, DEFAULT_DOH_PORT, DOH_PATH } from "../src/doh-server.mjs";
|
|
40
41
|
import { completionScript } from "../src/completion.mjs";
|
|
41
42
|
import { CORE_CLI_COMMAND_NAMES } from "../src/cli-schema.mjs";
|
|
@@ -587,6 +588,10 @@ async function main() {
|
|
|
587
588
|
process.exitCode = (await templateCommand(rest)) || 0;
|
|
588
589
|
return;
|
|
589
590
|
}
|
|
591
|
+
if (cmd === "shorten" || cmd === "short" || cmd === "link") {
|
|
592
|
+
process.exitCode = (await shortenCommand(rest, { prefix: `moshcode ${cmd}` })) || 0;
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
590
595
|
|
|
591
596
|
if (cmd === "pwd" || cmd === "where") {
|
|
592
597
|
const location = locate();
|
package/package.json
CHANGED
package/src/cli-schema.mjs
CHANGED
|
@@ -455,6 +455,30 @@ export const CORE_CLI_COMMANDS = [
|
|
|
455
455
|
seeAlso: ["site"],
|
|
456
456
|
},
|
|
457
457
|
{ name: "templates", aliasOf: "template", description: "alias for template" },
|
|
458
|
+
{
|
|
459
|
+
name: "shorten",
|
|
460
|
+
group: "hosting",
|
|
461
|
+
description: "mint a short link on the pit — /f/<code> follows to your url",
|
|
462
|
+
synopsis: [
|
|
463
|
+
["moshcode shorten <url>", "mint one and print it"],
|
|
464
|
+
["moshcode shorten <url> --name <name>", "file it under a Moshpit name you hold"],
|
|
465
|
+
["moshcode shorten list", "every link you have minted, newest first"],
|
|
466
|
+
["moshcode shorten rm <code>", "take one down"],
|
|
467
|
+
],
|
|
468
|
+
flags: [
|
|
469
|
+
["--name <name>", "file the link under a Moshpit name you hold", "none"],
|
|
470
|
+
["--json", "print the link (or the list) as machine-readable JSON", ""],
|
|
471
|
+
],
|
|
472
|
+
examples: [
|
|
473
|
+
["moshcode shorten https://pit.moshcode.sh/n/blue.eggs", "→ pit.moshcode.sh/f/k7mq2xd"],
|
|
474
|
+
["moshcode shorten list --json", "pipe your links into a script"],
|
|
475
|
+
["moshcode shorten rm k7mq2xd", "the code stops resolving"],
|
|
476
|
+
],
|
|
477
|
+
seeAlso: ["login", "name", "site"],
|
|
478
|
+
note: "needs an account — run `moshcode login` first. Shortening the same url twice returns the same code.",
|
|
479
|
+
},
|
|
480
|
+
{ name: "short", aliasOf: "shorten", description: "alias for shorten" },
|
|
481
|
+
{ name: "link", aliasOf: "shorten", description: "alias for shorten" },
|
|
458
482
|
{
|
|
459
483
|
name: "games",
|
|
460
484
|
group: "arcade",
|
|
@@ -815,6 +839,17 @@ export const DNS_VERBS = [
|
|
|
815
839
|
{ name: "tlds", description: "list the endings claimed in the Pit" },
|
|
816
840
|
{ name: "resolve", description: "what a name resolves to, and why" },
|
|
817
841
|
{ name: "trust", description: "trust one name's certificate, after checking it against the registry pin" },
|
|
842
|
+
{
|
|
843
|
+
name: "filter",
|
|
844
|
+
description: "block ads, trackers, malware and phishing at the resolver",
|
|
845
|
+
synopsis: [
|
|
846
|
+
["moshcode dns filter", "what is on, and what it has blocked"],
|
|
847
|
+
["moshcode dns filter on [--mode nxdomain|zero|refuse] [--lists a,b]", ""],
|
|
848
|
+
["moshcode dns filter update", "fetch the lists — nothing downloads on its own"],
|
|
849
|
+
["moshcode dns filter allow <name>", "never block it, whatever any list says"],
|
|
850
|
+
["moshcode dns filter test <name>", "would this be blocked, and by which rule"],
|
|
851
|
+
],
|
|
852
|
+
},
|
|
818
853
|
];
|
|
819
854
|
|
|
820
855
|
/**
|
|
@@ -1166,6 +1201,8 @@ export const PIT_COMMANDS = [
|
|
|
1166
1201
|
description: "install moshcode's slash commands into Claude Code" },
|
|
1167
1202
|
{ name: "games", aliases: ["game", "arcade", "play"], args: "[game]", cli: "games",
|
|
1168
1203
|
description: "the arcade — tetris, invaders, pac-man, frogger, kong, outrun, chess and more" },
|
|
1204
|
+
{ name: "shorten", aliases: ["short", "link"], args: "<url> | list | rm <code>", cli: "shorten",
|
|
1205
|
+
description: "mint a short link on the pit — /f/<code> follows to your url" },
|
|
1169
1206
|
{ name: "socials", aliases: ["social"], pitOnly: true,
|
|
1170
1207
|
description: "list social networks available for posting" },
|
|
1171
1208
|
{ name: "post", args: '<social> "message"', pitOnly: true,
|
package/src/completion.mjs
CHANGED
|
@@ -336,7 +336,7 @@ _moshcode_completion() {
|
|
|
336
336
|
;;
|
|
337
337
|
dns)
|
|
338
338
|
if (( COMP_CWORD == 2 )); then
|
|
339
|
-
choices="enable disable status tlds resolve start install trust"
|
|
339
|
+
choices="enable disable status tlds resolve start install trust filter"
|
|
340
340
|
elif [[ "$nested" == "resolve" && "$cur" == -* ]]; then
|
|
341
341
|
choices="--json --open --registry"
|
|
342
342
|
fi
|
|
@@ -485,7 +485,7 @@ _moshcode() {
|
|
|
485
485
|
;;
|
|
486
486
|
dns)
|
|
487
487
|
if (( CURRENT == 3 )); then
|
|
488
|
-
_values "dns command" enable disable status tlds resolve start install trust
|
|
488
|
+
_values "dns command" enable disable status tlds resolve start install trust filter
|
|
489
489
|
elif [[ "\${words[3]}" == "resolve" && "$PREFIX" == -* ]]; then
|
|
490
490
|
_values "dns resolve option" --json --open --registry
|
|
491
491
|
else
|
|
@@ -566,7 +566,7 @@ complete -c moshcode -n '${atSecondToken("console")}' -a '--url' -d 'print a gat
|
|
|
566
566
|
complete -c moshcode -n '__moshcode_nested_is console serve' -l port -r -d 'local HTTP port'
|
|
567
567
|
complete -c moshcode -n '__moshcode_nested_is console serve' -l ttyd -r -d 'ttyd host and port'
|
|
568
568
|
complete -c moshcode -n '__moshcode_nested_is console serve' -l bind -r -d 'bind address'
|
|
569
|
-
complete -c moshcode -n '${atSecondToken("dns")}' -a 'enable disable status tlds resolve start install trust' -d 'dns command'
|
|
569
|
+
complete -c moshcode -n '${atSecondToken("dns")}' -a 'enable disable status tlds resolve start install trust filter' -d 'dns command'
|
|
570
570
|
complete -c moshcode -n '__moshcode_nested_is dns resolve' -l json -d 'print JSON'
|
|
571
571
|
complete -c moshcode -n '__moshcode_nested_is dns resolve' -l open -d 'open a parked name in the Pit'
|
|
572
572
|
complete -c moshcode -n '__moshcode_nested_is dns resolve' -l registry -r -d 'registry base URL'
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `moshcode dns filter` — the verb over the policy in `dns-filter.mjs`.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of `dns.mjs` for the same reason the policy is: that file is vendored
|
|
5
|
+
* from `@moshcoder/moshpit-dns` and is ported by hand, so it gets the hook and
|
|
6
|
+
* nothing else.
|
|
7
|
+
*
|
|
8
|
+
* The thing this command has to be honest about, in every subcommand that could
|
|
9
|
+
* mislead, is that a filter only filters what passes through the bridge. Writing
|
|
10
|
+
* `enabled: true` into a file on a machine whose resolver has never heard of the
|
|
11
|
+
* bridge changes nothing at all, and a status line that says `on` without saying
|
|
12
|
+
* that is the same lie as `bridge started` being printed by a run that never
|
|
13
|
+
* wrote the routing.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
BLOCK_MODES,
|
|
18
|
+
CATALOG_BY_ID,
|
|
19
|
+
DEFAULT_CATEGORIES,
|
|
20
|
+
DEFAULT_MODE,
|
|
21
|
+
FILTER_CATALOG,
|
|
22
|
+
configPath,
|
|
23
|
+
filterDir,
|
|
24
|
+
listPath,
|
|
25
|
+
listStatus,
|
|
26
|
+
matchSuffix,
|
|
27
|
+
normaliseName,
|
|
28
|
+
readCachedList,
|
|
29
|
+
readConfig,
|
|
30
|
+
readStats,
|
|
31
|
+
updateList,
|
|
32
|
+
writeConfig,
|
|
33
|
+
} from "./dns-filter.mjs";
|
|
34
|
+
import { DEFAULT_HOST, DEFAULT_PORT, bridgePresence, describeBridge, parseDnsPort } from "./dns.mjs";
|
|
35
|
+
import { daemonStatus } from "./dns-system.mjs";
|
|
36
|
+
|
|
37
|
+
const USAGE = `moshcode dns filter — block names before they are ever looked up
|
|
38
|
+
|
|
39
|
+
moshcode dns filter what is on, what it has blocked
|
|
40
|
+
moshcode dns filter on start filtering (${DEFAULT_CATEGORIES.join(", ")})
|
|
41
|
+
moshcode dns filter off stop filtering; keeps the lists and the rules
|
|
42
|
+
moshcode dns filter lists the catalogue, and what is cached here
|
|
43
|
+
moshcode dns filter add <list>... turn a category on
|
|
44
|
+
moshcode dns filter remove <list>... turn one off
|
|
45
|
+
moshcode dns filter update [<list>] fetch the lists — nothing downloads on its own
|
|
46
|
+
moshcode dns filter block <name>... always block this name and everything under it
|
|
47
|
+
moshcode dns filter allow <name>... never block it, whatever any list says
|
|
48
|
+
moshcode dns filter unblock <name>...
|
|
49
|
+
moshcode dns filter unallow <name>...
|
|
50
|
+
moshcode dns filter test <name> would this be blocked, and by which rule
|
|
51
|
+
|
|
52
|
+
--mode nxdomain|zero|refuse how a blocked name is answered (default ${DEFAULT_MODE})
|
|
53
|
+
--lists a,b with \`on\`: the categories to run, instead of the default
|
|
54
|
+
--json with status, lists or test: one document for scripts
|
|
55
|
+
|
|
56
|
+
Filtering happens in the bridge, so it applies to exactly the queries the bridge
|
|
57
|
+
sees: with \`dns enable\` on, that is every lookup this machine makes. Changes are
|
|
58
|
+
picked up by a running bridge within about five seconds — no restart, no reload.
|
|
59
|
+
This command never turns DNS routing on.`;
|
|
60
|
+
|
|
61
|
+
const flagValue = (args, name) => {
|
|
62
|
+
const index = args.indexOf(name);
|
|
63
|
+
if (index >= 0 && args[index + 1]) return args[index + 1];
|
|
64
|
+
const inline = args.find((a) => a.startsWith(`${name}=`));
|
|
65
|
+
return inline ? inline.slice(name.length + 1) : null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const positional = (args) => {
|
|
69
|
+
const out = [];
|
|
70
|
+
for (let i = 0; i < args.length; i++) {
|
|
71
|
+
const arg = args[i];
|
|
72
|
+
if (arg === "--mode" || arg === "--lists" || arg === "--port") { i += 1; continue; }
|
|
73
|
+
if (arg.startsWith("-")) continue;
|
|
74
|
+
out.push(arg);
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Is there a bridge that could be applying this policy?
|
|
83
|
+
*
|
|
84
|
+
* Probed rather than read from the pidfile. A bridge started by systemd, by
|
|
85
|
+
* hand, or by an escalated `dns enable` leaves no pidfile this process can see,
|
|
86
|
+
* and reporting "no bridge" for a machine that is filtering every lookup would
|
|
87
|
+
* send someone to fix the wrong thing.
|
|
88
|
+
*/
|
|
89
|
+
async function bridgeLine({ host, port, presence = bridgePresence, recorded = daemonStatus }) {
|
|
90
|
+
try {
|
|
91
|
+
const found = await presence({ host, port, recorded: await recorded().catch(() => undefined) });
|
|
92
|
+
return { found, text: describeBridge(found, { host, port }) };
|
|
93
|
+
} catch {
|
|
94
|
+
return { found: { kind: "unknown", answering: false }, text: "could not be determined" };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function filterCommand(args = [], out = console.log, deps = {}) {
|
|
99
|
+
const {
|
|
100
|
+
dir = filterDir(),
|
|
101
|
+
fetchImpl = fetch,
|
|
102
|
+
presence = bridgePresence,
|
|
103
|
+
recorded = daemonStatus,
|
|
104
|
+
now = () => new Date(),
|
|
105
|
+
} = deps;
|
|
106
|
+
|
|
107
|
+
const sub = positional(args)[0] || "status";
|
|
108
|
+
const rest = positional(args).slice(1);
|
|
109
|
+
const json = args.includes("--json");
|
|
110
|
+
const host = DEFAULT_HOST;
|
|
111
|
+
const port = parseDnsPort(flagValue(args, "--port")) || DEFAULT_PORT;
|
|
112
|
+
|
|
113
|
+
if (sub === "help" || args.includes("--help") || args.includes("-h")) {
|
|
114
|
+
out(USAGE);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let config;
|
|
119
|
+
try {
|
|
120
|
+
config = await readConfig(dir);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
out(`! ${err.message}`);
|
|
123
|
+
out(` fix or remove ${configPath(dir)} — filtering is off until it parses`);
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* ------------------------------------------------------------------ lists */
|
|
128
|
+
|
|
129
|
+
if (sub === "lists") {
|
|
130
|
+
const rows = [];
|
|
131
|
+
for (const entry of FILTER_CATALOG) {
|
|
132
|
+
const cached = await listStatus(dir, entry.id);
|
|
133
|
+
rows.push({ ...entry, ...cached, on: config.categories.includes(entry.id) });
|
|
134
|
+
}
|
|
135
|
+
if (json) {
|
|
136
|
+
out(JSON.stringify({ dir, categories: config.categories, lists: rows }, null, 2));
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
for (const row of rows) {
|
|
140
|
+
const state = row.on ? "on " : "off";
|
|
141
|
+
const cache = row.cached
|
|
142
|
+
? `cached ${row.bytes < 1024 ? "<1" : Math.round(row.bytes / 1024)}k, ${row.at.slice(0, 10)}`
|
|
143
|
+
: "not fetched";
|
|
144
|
+
out(` ${state} ${row.id.padEnd(9)} ${row.title.padEnd(22)} ${cache}`);
|
|
145
|
+
out(` ${row.note}`);
|
|
146
|
+
}
|
|
147
|
+
out("");
|
|
148
|
+
out("fetch what is on with: moshcode dns filter update");
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ------------------------------------------------------------------- test */
|
|
153
|
+
|
|
154
|
+
if (sub === "test") {
|
|
155
|
+
const name = rest[0];
|
|
156
|
+
if (!name) {
|
|
157
|
+
out("usage: moshcode dns filter test <name>");
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
const clean = normaliseName(name);
|
|
161
|
+
if (!clean) {
|
|
162
|
+
out(`! ${name} is not a name this can match`);
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
// Read straight from the cache rather than through a filter handle: this
|
|
166
|
+
// has to answer for a category that is cached but switched off, so that
|
|
167
|
+
// "why is this not blocked" has an answer other than silence.
|
|
168
|
+
const allowed = matchSuffix(clean, new Set(config.allow));
|
|
169
|
+
const blockedBy = matchSuffix(clean, new Set(config.block));
|
|
170
|
+
const hits = [];
|
|
171
|
+
if (blockedBy) hits.push({ list: "custom", rule: blockedBy, on: true });
|
|
172
|
+
for (const entry of FILTER_CATALOG) {
|
|
173
|
+
const set = await readCachedList(dir, entry.id);
|
|
174
|
+
if (!set) continue;
|
|
175
|
+
const rule = matchSuffix(clean, set);
|
|
176
|
+
if (rule) hits.push({ list: entry.id, rule, on: config.categories.includes(entry.id) });
|
|
177
|
+
}
|
|
178
|
+
const live = hits.filter((h) => h.on);
|
|
179
|
+
const blocked = config.enabled && !allowed && live.length > 0;
|
|
180
|
+
|
|
181
|
+
if (json) {
|
|
182
|
+
out(JSON.stringify({ name: clean, blocked, mode: config.mode, allowed, hits }, null, 2));
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
if (!config.enabled) out("filtering is off — this is what would happen with it on");
|
|
186
|
+
if (allowed) {
|
|
187
|
+
out(`${clean} — allowed by your rule \`${allowed}\``);
|
|
188
|
+
if (hits.length) out(` (${plural(hits.length, "list")} would otherwise block it: ${hits.map((h) => h.list).join(", ")})`);
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
if (!live.length) {
|
|
192
|
+
out(`${clean} — not blocked`);
|
|
193
|
+
const dormant = hits.filter((h) => !h.on);
|
|
194
|
+
if (dormant.length) {
|
|
195
|
+
out(` it is in ${dormant.map((h) => h.list).join(", ")}, which ${dormant.length === 1 ? "is" : "are"} not switched on`);
|
|
196
|
+
out(` turn one on with: moshcode dns filter add ${dormant[0].list}`);
|
|
197
|
+
}
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
out(`${clean} — blocked by ${live[0].list} (rule \`${live[0].rule}\`), answered as ${config.mode}`);
|
|
201
|
+
if (live.length > 1) out(` also in: ${live.slice(1).map((h) => h.list).join(", ")}`);
|
|
202
|
+
out(` keep it working with: moshcode dns filter allow ${clean}`);
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* ----------------------------------------------------------------- update */
|
|
207
|
+
|
|
208
|
+
if (sub === "update") {
|
|
209
|
+
const wanted = rest.length ? rest : config.categories;
|
|
210
|
+
if (!wanted.length) {
|
|
211
|
+
out("no categories are on — nothing to fetch");
|
|
212
|
+
out(` turn one on with: moshcode dns filter add ${DEFAULT_CATEGORIES[0]}`);
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
const unknown = wanted.filter((id) => !CATALOG_BY_ID.has(id));
|
|
216
|
+
if (unknown.length) {
|
|
217
|
+
out(`! no such list: ${unknown.join(", ")}`);
|
|
218
|
+
out(` the catalogue is: ${FILTER_CATALOG.map((e) => e.id).join(", ")}`);
|
|
219
|
+
return 1;
|
|
220
|
+
}
|
|
221
|
+
let failed = 0;
|
|
222
|
+
for (const id of wanted) {
|
|
223
|
+
try {
|
|
224
|
+
const result = await updateList(dir, id, { fetchImpl });
|
|
225
|
+
out(`ok ${id.padEnd(9)} ${result.count.toLocaleString()} names`);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
failed += 1;
|
|
228
|
+
// Named and survived rather than thrown: one dead source should not
|
|
229
|
+
// stop the other seven from refreshing.
|
|
230
|
+
out(`! ${id.padEnd(9)} ${err?.message || err}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
await writeConfig(dir, { ...config, updatedAt: now().toISOString() });
|
|
234
|
+
if (failed) out(`\n${plural(failed, "list")} did not refresh — the cached copy is still in use`);
|
|
235
|
+
if (config.enabled) out("\na running bridge picks these up within about five seconds");
|
|
236
|
+
else out("\nfiltering is off — turn it on with: moshcode dns filter on");
|
|
237
|
+
return failed === wanted.length ? 1 : 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* --------------------------------------------------------------- on / off */
|
|
241
|
+
|
|
242
|
+
if (sub === "on" || sub === "off") {
|
|
243
|
+
const mode = flagValue(args, "--mode");
|
|
244
|
+
if (mode && !BLOCK_MODES.includes(mode)) {
|
|
245
|
+
out(`! --mode must be one of: ${BLOCK_MODES.join(", ")}`);
|
|
246
|
+
return 1;
|
|
247
|
+
}
|
|
248
|
+
const chosen = flagValue(args, "--lists");
|
|
249
|
+
const categories = chosen
|
|
250
|
+
? chosen.split(",").map((s) => s.trim()).filter(Boolean)
|
|
251
|
+
: (config.categories.length ? config.categories : DEFAULT_CATEGORIES.slice());
|
|
252
|
+
const unknown = categories.filter((id) => !CATALOG_BY_ID.has(id));
|
|
253
|
+
if (unknown.length) {
|
|
254
|
+
out(`! no such list: ${unknown.join(", ")}`);
|
|
255
|
+
out(` the catalogue is: ${FILTER_CATALOG.map((e) => e.id).join(", ")}`);
|
|
256
|
+
return 1;
|
|
257
|
+
}
|
|
258
|
+
const next = await writeConfig(dir, {
|
|
259
|
+
...config,
|
|
260
|
+
enabled: sub === "on",
|
|
261
|
+
mode: mode || config.mode,
|
|
262
|
+
categories,
|
|
263
|
+
});
|
|
264
|
+
if (sub === "off") {
|
|
265
|
+
out("filtering off — lists and rules kept, nothing is being blocked");
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const missing = [];
|
|
270
|
+
for (const id of next.categories) {
|
|
271
|
+
if (!(await listStatus(dir, id)).cached) missing.push(id);
|
|
272
|
+
}
|
|
273
|
+
out(`filtering on — ${next.categories.join(", ")}, blocked names answered as ${next.mode}`);
|
|
274
|
+
if (missing.length) {
|
|
275
|
+
// The state that would otherwise read as success and block nothing.
|
|
276
|
+
out(`! ${plural(missing.length, "list")} ${missing.length === 1 ? "has" : "have"} never been fetched: ${missing.join(", ")}`);
|
|
277
|
+
out(" nothing is blocked from them until you run: moshcode dns filter update");
|
|
278
|
+
}
|
|
279
|
+
const bridge = await bridgeLine({ host, port, presence, recorded });
|
|
280
|
+
if (!bridge.found.answering) {
|
|
281
|
+
out(`! no bridge is answering on ${host}:${port} — ${bridge.text}`);
|
|
282
|
+
out(" the filter runs inside the bridge, so nothing is filtered until one does.");
|
|
283
|
+
out(" turn DNS on deliberately with: sudo moshcode dns enable");
|
|
284
|
+
} else if (bridge.found.forwards === false) {
|
|
285
|
+
out(`! the bridge on ${host}:${port} answers Moshpit names but does not forward clearnet ones`);
|
|
286
|
+
out(" it is not in the path of ordinary lookups, so only Moshpit names are filtered");
|
|
287
|
+
}
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* ------------------------------------------------------ categories, rules */
|
|
292
|
+
|
|
293
|
+
const listVerbs = { add: true, remove: true };
|
|
294
|
+
if (listVerbs[sub]) {
|
|
295
|
+
if (!rest.length) {
|
|
296
|
+
out(`usage: moshcode dns filter ${sub} <list>...`);
|
|
297
|
+
return 1;
|
|
298
|
+
}
|
|
299
|
+
const unknown = rest.filter((id) => !CATALOG_BY_ID.has(id));
|
|
300
|
+
if (unknown.length) {
|
|
301
|
+
out(`! no such list: ${unknown.join(", ")}`);
|
|
302
|
+
out(` the catalogue is: ${FILTER_CATALOG.map((e) => e.id).join(", ")}`);
|
|
303
|
+
return 1;
|
|
304
|
+
}
|
|
305
|
+
const set = new Set(config.categories);
|
|
306
|
+
for (const id of rest) (sub === "add" ? set.add(id) : set.delete(id));
|
|
307
|
+
const next = await writeConfig(dir, { ...config, categories: Array.from(set) });
|
|
308
|
+
out(next.categories.length ? `lists: ${next.categories.join(", ")}` : "lists: none");
|
|
309
|
+
if (sub === "add") {
|
|
310
|
+
const missing = [];
|
|
311
|
+
for (const id of rest) if (!(await listStatus(dir, id)).cached) missing.push(id);
|
|
312
|
+
if (missing.length) out(` fetch ${missing.join(", ")} with: moshcode dns filter update ${missing.join(" ")}`);
|
|
313
|
+
}
|
|
314
|
+
return 0;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const ruleVerbs = {
|
|
318
|
+
block: { field: "block", add: true, said: "blocked" },
|
|
319
|
+
allow: { field: "allow", add: true, said: "allowed" },
|
|
320
|
+
unblock: { field: "block", add: false, said: "no longer blocked by rule" },
|
|
321
|
+
unallow: { field: "allow", add: false, said: "no longer allowed by rule" },
|
|
322
|
+
};
|
|
323
|
+
if (ruleVerbs[sub]) {
|
|
324
|
+
const { field, add, said } = ruleVerbs[sub];
|
|
325
|
+
if (!rest.length) {
|
|
326
|
+
out(`usage: moshcode dns filter ${sub} <name>...`);
|
|
327
|
+
return 1;
|
|
328
|
+
}
|
|
329
|
+
const names = [];
|
|
330
|
+
for (const raw of rest) {
|
|
331
|
+
const clean = normaliseName(raw);
|
|
332
|
+
if (!clean) {
|
|
333
|
+
out(`! ${raw} is not a name`);
|
|
334
|
+
return 1;
|
|
335
|
+
}
|
|
336
|
+
names.push(clean);
|
|
337
|
+
}
|
|
338
|
+
const set = new Set(config[field]);
|
|
339
|
+
for (const name of names) (add ? set.add(name) : set.delete(name));
|
|
340
|
+
const next = await writeConfig(dir, { ...config, [field]: Array.from(set) });
|
|
341
|
+
out(`${names.join(", ")} — ${said}${add ? ", along with everything under it" : ""}`);
|
|
342
|
+
out(` ${plural(next[field].length, "rule")} in your ${field} list`);
|
|
343
|
+
if (!next.enabled) out(" filtering is off, so this takes effect when you turn it on");
|
|
344
|
+
return 0;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/* ----------------------------------------------------------------- status */
|
|
348
|
+
|
|
349
|
+
if (sub !== "status") {
|
|
350
|
+
out(`unknown: moshcode dns filter ${sub}`);
|
|
351
|
+
out(USAGE);
|
|
352
|
+
return 1;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const stats = await readStats(dir);
|
|
356
|
+
const cached = [];
|
|
357
|
+
for (const id of config.categories) cached.push(await listStatus(dir, id));
|
|
358
|
+
const bridge = await bridgeLine({ host, port, presence, recorded });
|
|
359
|
+
|
|
360
|
+
if (json) {
|
|
361
|
+
out(JSON.stringify({
|
|
362
|
+
dir,
|
|
363
|
+
enabled: config.enabled,
|
|
364
|
+
mode: config.mode,
|
|
365
|
+
categories: config.categories,
|
|
366
|
+
block: config.block,
|
|
367
|
+
allow: config.allow,
|
|
368
|
+
lists: cached,
|
|
369
|
+
bridge: { kind: bridge.found.kind, answering: Boolean(bridge.found.answering), forwards: Boolean(bridge.found.forwards) },
|
|
370
|
+
stats,
|
|
371
|
+
}, null, 2));
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
out(`filter ${config.enabled ? `on — answering blocked names as ${config.mode}` : "off"}`);
|
|
376
|
+
out(`bridge ${bridge.text}`);
|
|
377
|
+
out(`lists ${config.categories.length ? config.categories.join(", ") : "none"}`);
|
|
378
|
+
const never = cached.filter((c) => !c.cached);
|
|
379
|
+
if (never.length) out(` ! never fetched: ${never.map((c) => c.id).join(", ")} — run \`moshcode dns filter update\``);
|
|
380
|
+
if (config.block.length || config.allow.length) {
|
|
381
|
+
out(`rules ${plural(config.block.length, "block")}, ${plural(config.allow.length, "allow")}`);
|
|
382
|
+
}
|
|
383
|
+
if (stats) {
|
|
384
|
+
const share = stats.queries ? `${((stats.blocked / stats.queries) * 100).toFixed(1)}%` : "0%";
|
|
385
|
+
out(`blocked ${stats.blocked.toLocaleString()} of ${stats.queries.toLocaleString()} queries (${share}) as of ${String(stats.at).slice(0, 19).replace("T", " ")}`);
|
|
386
|
+
for (const [id, count] of Object.entries(stats.byList || {}).sort((a, b) => b[1] - a[1])) {
|
|
387
|
+
out(` ${String(count).padStart(7)} ${id}`);
|
|
388
|
+
}
|
|
389
|
+
if (stats.recent?.length) {
|
|
390
|
+
out("recent " + stats.recent.slice(0, 5).map((r) => r.name).join(", "));
|
|
391
|
+
}
|
|
392
|
+
} else if (config.enabled) {
|
|
393
|
+
// The counters are written by the bridge, so their absence is a fact about
|
|
394
|
+
// the bridge rather than about the filter.
|
|
395
|
+
out("blocked no counts yet — the bridge writes them once it is answering");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (config.enabled && !bridge.found.answering) {
|
|
399
|
+
out("");
|
|
400
|
+
out(`! nothing is being filtered: the filter runs inside the bridge and none is answering on ${host}:${port}`);
|
|
401
|
+
out(" turn DNS on deliberately with: sudo moshcode dns enable");
|
|
402
|
+
}
|
|
403
|
+
out("");
|
|
404
|
+
out(`config ${configPath(dir)}`);
|
|
405
|
+
out(`lists at ${listPath(dir, "<list>")}`);
|
|
406
|
+
return 0;
|
|
407
|
+
}
|