macblock 0.2.1__py3-none-any.whl
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.
- macblock/__init__.py +8 -0
- macblock/__main__.py +4 -0
- macblock/blocklists.py +260 -0
- macblock/cli.py +323 -0
- macblock/colors.py +62 -0
- macblock/constants.py +81 -0
- macblock/control.py +266 -0
- macblock/daemon.py +449 -0
- macblock/dns_test.py +165 -0
- macblock/dnsmasq.py +36 -0
- macblock/doctor.py +338 -0
- macblock/errors.py +10 -0
- macblock/exec.py +39 -0
- macblock/fs.py +19 -0
- macblock/help.py +447 -0
- macblock/install.py +677 -0
- macblock/launchd.py +44 -0
- macblock/lists.py +98 -0
- macblock/logs.py +140 -0
- macblock/platform.py +15 -0
- macblock/resolvers.py +76 -0
- macblock/state.py +146 -0
- macblock/status.py +145 -0
- macblock/system_dns.py +205 -0
- macblock/ui.py +308 -0
- macblock/users.py +75 -0
- macblock-0.2.1.dist-info/METADATA +195 -0
- macblock-0.2.1.dist-info/RECORD +32 -0
- macblock-0.2.1.dist-info/WHEEL +5 -0
- macblock-0.2.1.dist-info/entry_points.txt +2 -0
- macblock-0.2.1.dist-info/licenses/LICENSE +21 -0
- macblock-0.2.1.dist-info/top_level.txt +1 -0
macblock/__init__.py
ADDED
macblock/__main__.py
ADDED
macblock/blocklists.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
5
|
+
import urllib.request
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from macblock.constants import (
|
|
9
|
+
APP_LABEL,
|
|
10
|
+
DEFAULT_BLOCKLIST_SOURCE,
|
|
11
|
+
SYSTEM_BLACKLIST_FILE,
|
|
12
|
+
SYSTEM_BLOCKLIST_FILE,
|
|
13
|
+
SYSTEM_RAW_BLOCKLIST_FILE,
|
|
14
|
+
SYSTEM_STATE_FILE,
|
|
15
|
+
SYSTEM_WHITELIST_FILE,
|
|
16
|
+
VAR_DB_DNSMASQ_PID,
|
|
17
|
+
BLOCKLIST_SOURCES,
|
|
18
|
+
)
|
|
19
|
+
from macblock.launchd import kickstart, service_exists
|
|
20
|
+
from macblock.errors import MacblockError
|
|
21
|
+
from macblock.exec import run
|
|
22
|
+
from macblock.fs import atomic_write_text
|
|
23
|
+
from macblock.state import load_state, replace_state, save_state_atomic
|
|
24
|
+
from macblock.ui import (
|
|
25
|
+
dim,
|
|
26
|
+
green,
|
|
27
|
+
result_success,
|
|
28
|
+
Spinner,
|
|
29
|
+
step_done,
|
|
30
|
+
SYMBOL_ACTIVE,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_domain_re = re.compile(
|
|
35
|
+
r"^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*$",
|
|
36
|
+
re.IGNORECASE,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def normalize_domain(domain: str) -> str:
|
|
41
|
+
d = domain.strip().lower().strip(".")
|
|
42
|
+
if not d:
|
|
43
|
+
raise MacblockError("invalid domain")
|
|
44
|
+
if not _domain_re.match(d):
|
|
45
|
+
raise MacblockError("invalid domain")
|
|
46
|
+
return d
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _read_lines(path: Path) -> list[str]:
|
|
50
|
+
if not path.exists():
|
|
51
|
+
return []
|
|
52
|
+
out: list[str] = []
|
|
53
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
54
|
+
s = line.strip()
|
|
55
|
+
if not s or s.startswith("#"):
|
|
56
|
+
continue
|
|
57
|
+
out.append(s)
|
|
58
|
+
return out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_hosts_domains(text: str) -> set[str]:
|
|
62
|
+
domains: set[str] = set()
|
|
63
|
+
|
|
64
|
+
for raw_line in text.splitlines():
|
|
65
|
+
line = raw_line.strip()
|
|
66
|
+
if not line or line.startswith("#"):
|
|
67
|
+
continue
|
|
68
|
+
if "#" in line:
|
|
69
|
+
line = line.split("#", 1)[0].strip()
|
|
70
|
+
parts = line.split()
|
|
71
|
+
if len(parts) < 2:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
for token in parts[1:]:
|
|
75
|
+
try:
|
|
76
|
+
d = normalize_domain(token)
|
|
77
|
+
except MacblockError:
|
|
78
|
+
continue
|
|
79
|
+
if d in {"localhost", "localhost.localdomain"}:
|
|
80
|
+
continue
|
|
81
|
+
domains.add(d)
|
|
82
|
+
|
|
83
|
+
return domains
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def compile_blocklist(
|
|
87
|
+
raw_path: Path, whitelist_path: Path, blacklist_path: Path, out_path: Path
|
|
88
|
+
) -> int:
|
|
89
|
+
raw = raw_path.read_text(encoding="utf-8") if raw_path.exists() else ""
|
|
90
|
+
base = _parse_hosts_domains(raw)
|
|
91
|
+
|
|
92
|
+
allow = {normalize_domain(x) for x in _read_lines(whitelist_path)}
|
|
93
|
+
deny = {normalize_domain(x) for x in _read_lines(blacklist_path)}
|
|
94
|
+
|
|
95
|
+
final = (base - allow) | deny
|
|
96
|
+
|
|
97
|
+
lines = [f"server=/{d}/" for d in sorted(final)]
|
|
98
|
+
atomic_write_text(out_path, "\n".join(lines) + ("\n" if lines else ""), mode=0o644)
|
|
99
|
+
|
|
100
|
+
return len(final)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
_MAX_BLOCKLIST_BYTES = 20 * 1024 * 1024
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _download(url: str, *, expected_sha256: str | None = None) -> str:
|
|
107
|
+
req = urllib.request.Request(
|
|
108
|
+
url,
|
|
109
|
+
headers={"User-Agent": "macblock/0.0.0"},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
hasher = hashlib.sha256()
|
|
113
|
+
chunks: list[bytes] = []
|
|
114
|
+
total = 0
|
|
115
|
+
|
|
116
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
117
|
+
while True:
|
|
118
|
+
chunk = resp.read(64 * 1024)
|
|
119
|
+
if not chunk:
|
|
120
|
+
break
|
|
121
|
+
total += len(chunk)
|
|
122
|
+
if total > _MAX_BLOCKLIST_BYTES:
|
|
123
|
+
raise MacblockError(
|
|
124
|
+
f"blocklist too large (>{_MAX_BLOCKLIST_BYTES} bytes)"
|
|
125
|
+
)
|
|
126
|
+
hasher.update(chunk)
|
|
127
|
+
chunks.append(chunk)
|
|
128
|
+
|
|
129
|
+
if expected_sha256 is not None:
|
|
130
|
+
expected = expected_sha256.strip().lower()
|
|
131
|
+
actual = hasher.hexdigest()
|
|
132
|
+
if expected != actual:
|
|
133
|
+
raise MacblockError(f"sha256 mismatch: expected {expected}, got {actual}")
|
|
134
|
+
|
|
135
|
+
return b"".join(chunks).decode("utf-8", errors="replace")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def reload_dnsmasq() -> None:
|
|
139
|
+
label = f"{APP_LABEL}.dnsmasq"
|
|
140
|
+
if service_exists(label):
|
|
141
|
+
kickstart(label)
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
if VAR_DB_DNSMASQ_PID.exists():
|
|
145
|
+
try:
|
|
146
|
+
pid = int(VAR_DB_DNSMASQ_PID.read_text(encoding="utf-8").strip())
|
|
147
|
+
except Exception:
|
|
148
|
+
pid = 0
|
|
149
|
+
if pid > 1:
|
|
150
|
+
run(["/bin/kill", "-HUP", str(pid)])
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def list_blocklist_sources() -> int:
|
|
154
|
+
st = load_state(SYSTEM_STATE_FILE)
|
|
155
|
+
current = st.blocklist_source or DEFAULT_BLOCKLIST_SOURCE
|
|
156
|
+
|
|
157
|
+
print()
|
|
158
|
+
for key in sorted(BLOCKLIST_SOURCES.keys()):
|
|
159
|
+
if key == current:
|
|
160
|
+
print(
|
|
161
|
+
f" {green(SYMBOL_ACTIVE)} {green(key)} - {BLOCKLIST_SOURCES[key]['name']}"
|
|
162
|
+
)
|
|
163
|
+
else:
|
|
164
|
+
print(f" {key} - {dim(str(BLOCKLIST_SOURCES[key]['name']))}")
|
|
165
|
+
print()
|
|
166
|
+
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def set_blocklist_source(source: str) -> int:
|
|
171
|
+
src = source.strip()
|
|
172
|
+
|
|
173
|
+
if not src:
|
|
174
|
+
raise MacblockError("source is required")
|
|
175
|
+
|
|
176
|
+
if not (src.startswith("https://") or src in BLOCKLIST_SOURCES):
|
|
177
|
+
raise MacblockError("unknown source")
|
|
178
|
+
|
|
179
|
+
st = load_state(SYSTEM_STATE_FILE)
|
|
180
|
+
save_state_atomic(
|
|
181
|
+
SYSTEM_STATE_FILE,
|
|
182
|
+
replace_state(st, blocklist_source=src),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
step_done(f"Blocklist source set to {src}")
|
|
186
|
+
print()
|
|
187
|
+
return 0
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def update_blocklist(source: str | None = None, sha256: str | None = None) -> int:
|
|
191
|
+
st = load_state(SYSTEM_STATE_FILE)
|
|
192
|
+
chosen = source or st.blocklist_source or DEFAULT_BLOCKLIST_SOURCE
|
|
193
|
+
|
|
194
|
+
if chosen.startswith("https://"):
|
|
195
|
+
url = chosen
|
|
196
|
+
source_name = "custom URL"
|
|
197
|
+
elif chosen in BLOCKLIST_SOURCES:
|
|
198
|
+
url = str(BLOCKLIST_SOURCES[chosen]["url"])
|
|
199
|
+
source_name = chosen
|
|
200
|
+
else:
|
|
201
|
+
raise MacblockError("unknown source")
|
|
202
|
+
|
|
203
|
+
print()
|
|
204
|
+
|
|
205
|
+
# Download with spinner
|
|
206
|
+
with Spinner(f"Downloading blocklist ({source_name})") as spinner:
|
|
207
|
+
try:
|
|
208
|
+
raw = _download(url, expected_sha256=sha256)
|
|
209
|
+
except Exception as e:
|
|
210
|
+
spinner.fail(f"Download failed: {e}")
|
|
211
|
+
raise
|
|
212
|
+
|
|
213
|
+
if not raw.strip():
|
|
214
|
+
spinner.fail("Downloaded blocklist is empty")
|
|
215
|
+
raise MacblockError("downloaded blocklist is empty")
|
|
216
|
+
|
|
217
|
+
head = raw.lstrip()[:200].lower()
|
|
218
|
+
if (
|
|
219
|
+
head.startswith("<!doctype html")
|
|
220
|
+
or head.startswith("<html")
|
|
221
|
+
or "<html" in head
|
|
222
|
+
):
|
|
223
|
+
spinner.fail("Downloaded file looks like HTML, not a blocklist")
|
|
224
|
+
raise MacblockError("downloaded blocklist looks like HTML")
|
|
225
|
+
|
|
226
|
+
spinner.succeed("Downloaded blocklist")
|
|
227
|
+
|
|
228
|
+
# Parse and compile
|
|
229
|
+
count = 0
|
|
230
|
+
with Spinner("Compiling blocklist") as spinner:
|
|
231
|
+
count_raw = len(_parse_hosts_domains(raw))
|
|
232
|
+
if count_raw < 1000:
|
|
233
|
+
spinner.warn(f"Blocklist looks small ({count_raw} domains)")
|
|
234
|
+
else:
|
|
235
|
+
atomic_write_text(SYSTEM_RAW_BLOCKLIST_FILE, raw, mode=0o644)
|
|
236
|
+
count = compile_blocklist(
|
|
237
|
+
SYSTEM_RAW_BLOCKLIST_FILE,
|
|
238
|
+
SYSTEM_WHITELIST_FILE,
|
|
239
|
+
SYSTEM_BLACKLIST_FILE,
|
|
240
|
+
SYSTEM_BLOCKLIST_FILE,
|
|
241
|
+
)
|
|
242
|
+
spinner.succeed(f"Compiled {count:,} domains")
|
|
243
|
+
|
|
244
|
+
# Save state
|
|
245
|
+
save_state_atomic(
|
|
246
|
+
SYSTEM_STATE_FILE,
|
|
247
|
+
replace_state(st, blocklist_source=chosen),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Reload dnsmasq
|
|
251
|
+
with Spinner("Reloading dnsmasq") as spinner:
|
|
252
|
+
try:
|
|
253
|
+
reload_dnsmasq()
|
|
254
|
+
spinner.succeed("Reloaded dnsmasq")
|
|
255
|
+
except Exception:
|
|
256
|
+
spinner.warn("Could not reload dnsmasq (may need restart)")
|
|
257
|
+
|
|
258
|
+
result_success(f"Blocklist updated: {count:,} domains blocked")
|
|
259
|
+
print()
|
|
260
|
+
return 0
|
macblock/cli.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from macblock import __version__
|
|
9
|
+
from macblock.errors import MacblockError, PrivilegeError, UnsupportedPlatformError
|
|
10
|
+
from macblock.install import do_install, do_uninstall
|
|
11
|
+
from macblock.blocklists import (
|
|
12
|
+
list_blocklist_sources,
|
|
13
|
+
set_blocklist_source,
|
|
14
|
+
update_blocklist,
|
|
15
|
+
)
|
|
16
|
+
from macblock.control import do_disable, do_enable, do_pause, do_resume
|
|
17
|
+
from macblock.doctor import run_diagnostics
|
|
18
|
+
from macblock.dns_test import test_domain
|
|
19
|
+
from macblock.help import show_main_help, show_command_help
|
|
20
|
+
from macblock.lists import (
|
|
21
|
+
add_blacklist,
|
|
22
|
+
add_whitelist,
|
|
23
|
+
list_blacklist,
|
|
24
|
+
list_whitelist,
|
|
25
|
+
remove_blacklist,
|
|
26
|
+
remove_whitelist,
|
|
27
|
+
)
|
|
28
|
+
from macblock.platform import is_root, require_macos
|
|
29
|
+
from macblock.status import show_status
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _has_help_flag(args: list[str]) -> bool:
|
|
33
|
+
"""Check if args contain help flags."""
|
|
34
|
+
return "-h" in args or "--help" in args
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _remove_help_flags(args: list[str]) -> list[str]:
|
|
38
|
+
"""Remove help flags from args."""
|
|
39
|
+
return [a for a in args if a not in ("-h", "--help")]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_help_context(args: list[str]) -> str | None:
|
|
43
|
+
"""Extract command context for help display.
|
|
44
|
+
|
|
45
|
+
Returns the command (and subcommand if applicable) that help was requested for.
|
|
46
|
+
"""
|
|
47
|
+
clean = _remove_help_flags(args)
|
|
48
|
+
if not clean:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
cmd = clean[0]
|
|
52
|
+
# Handle subcommands for sources, allow, deny
|
|
53
|
+
if cmd in ("sources", "allow", "deny") and len(clean) >= 2:
|
|
54
|
+
subcmd = clean[1]
|
|
55
|
+
if subcmd not in ("-h", "--help"):
|
|
56
|
+
return f"{cmd} {subcmd}"
|
|
57
|
+
|
|
58
|
+
return cmd
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _needs_root(cmd: str, args: dict) -> bool:
|
|
62
|
+
if cmd in {
|
|
63
|
+
"install",
|
|
64
|
+
"uninstall",
|
|
65
|
+
"enable",
|
|
66
|
+
"disable",
|
|
67
|
+
"pause",
|
|
68
|
+
"resume",
|
|
69
|
+
"update",
|
|
70
|
+
}:
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
if cmd == "sources":
|
|
74
|
+
return args.get("sources_cmd") == "set"
|
|
75
|
+
|
|
76
|
+
if cmd == "allow":
|
|
77
|
+
return args.get("allow_cmd") in ("add", "remove")
|
|
78
|
+
|
|
79
|
+
if cmd == "deny":
|
|
80
|
+
return args.get("deny_cmd") in ("add", "remove")
|
|
81
|
+
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _exec_sudo(argv: list[str]) -> None:
|
|
86
|
+
sudo = shutil.which("sudo")
|
|
87
|
+
if sudo is None:
|
|
88
|
+
raise PrivilegeError("sudo not found")
|
|
89
|
+
|
|
90
|
+
if os.environ.get("MACBLOCK_ELEVATED") == "1":
|
|
91
|
+
raise PrivilegeError("failed to elevate privileges")
|
|
92
|
+
|
|
93
|
+
env = dict(os.environ)
|
|
94
|
+
env["MACBLOCK_ELEVATED"] = "1"
|
|
95
|
+
|
|
96
|
+
exe = shutil.which(sys.argv[0])
|
|
97
|
+
if exe:
|
|
98
|
+
os.execve(sudo, [sudo, "-E", exe, *argv], env)
|
|
99
|
+
|
|
100
|
+
os.execve(sudo, [sudo, "-E", sys.executable, "-m", "macblock", *argv], env)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _parse_args(argv: list[str]) -> tuple[str | None, dict]:
|
|
104
|
+
"""Simple argument parser.
|
|
105
|
+
|
|
106
|
+
Returns (command, args_dict). If help was requested, args will contain
|
|
107
|
+
{"_help": True, "_help_context": "command"} and command will be "_help".
|
|
108
|
+
"""
|
|
109
|
+
args: dict = {}
|
|
110
|
+
|
|
111
|
+
if not argv:
|
|
112
|
+
return "status", args
|
|
113
|
+
|
|
114
|
+
# Handle --version first (can appear anywhere)
|
|
115
|
+
if "--version" in argv or "-V" in argv:
|
|
116
|
+
print(f"macblock {__version__}")
|
|
117
|
+
sys.exit(0)
|
|
118
|
+
|
|
119
|
+
# Check for help flag anywhere in args
|
|
120
|
+
if _has_help_flag(argv):
|
|
121
|
+
help_context = _get_help_context(argv)
|
|
122
|
+
args["_help"] = True
|
|
123
|
+
args["_help_context"] = help_context
|
|
124
|
+
return "_help", args
|
|
125
|
+
|
|
126
|
+
# Handle explicit "help" command
|
|
127
|
+
if argv[0] == "help":
|
|
128
|
+
rest = argv[1:]
|
|
129
|
+
if rest:
|
|
130
|
+
# "help sources set" -> "sources set"
|
|
131
|
+
if len(rest) >= 2 and rest[0] in ("sources", "allow", "deny"):
|
|
132
|
+
args["_help_context"] = f"{rest[0]} {rest[1]}"
|
|
133
|
+
else:
|
|
134
|
+
args["_help_context"] = rest[0]
|
|
135
|
+
else:
|
|
136
|
+
args["_help_context"] = None
|
|
137
|
+
args["_help"] = True
|
|
138
|
+
return "_help", args
|
|
139
|
+
|
|
140
|
+
cmd = argv[0]
|
|
141
|
+
rest = argv[1:]
|
|
142
|
+
|
|
143
|
+
# Handle subcommands
|
|
144
|
+
if cmd == "logs":
|
|
145
|
+
args["component"] = "daemon"
|
|
146
|
+
args["lines"] = 200
|
|
147
|
+
args["follow"] = False
|
|
148
|
+
args["stderr"] = False
|
|
149
|
+
i = 0
|
|
150
|
+
while i < len(rest):
|
|
151
|
+
if rest[i] == "--component" and i + 1 < len(rest):
|
|
152
|
+
args["component"] = rest[i + 1]
|
|
153
|
+
i += 2
|
|
154
|
+
elif rest[i] == "--lines" and i + 1 < len(rest):
|
|
155
|
+
args["lines"] = int(rest[i + 1])
|
|
156
|
+
i += 2
|
|
157
|
+
elif rest[i] == "--follow":
|
|
158
|
+
args["follow"] = True
|
|
159
|
+
i += 1
|
|
160
|
+
elif rest[i] == "--stderr":
|
|
161
|
+
args["stderr"] = True
|
|
162
|
+
i += 1
|
|
163
|
+
else:
|
|
164
|
+
i += 1
|
|
165
|
+
|
|
166
|
+
elif cmd == "install":
|
|
167
|
+
args["force"] = "--force" in rest
|
|
168
|
+
args["skip_update"] = "--skip-update" in rest
|
|
169
|
+
|
|
170
|
+
elif cmd == "uninstall":
|
|
171
|
+
args["force"] = "--force" in rest
|
|
172
|
+
|
|
173
|
+
elif cmd == "pause":
|
|
174
|
+
if rest:
|
|
175
|
+
args["duration"] = rest[0]
|
|
176
|
+
else:
|
|
177
|
+
raise MacblockError("pause requires a duration (e.g., 10m, 2h, 1d)")
|
|
178
|
+
|
|
179
|
+
elif cmd == "test":
|
|
180
|
+
if rest:
|
|
181
|
+
args["domain"] = rest[0]
|
|
182
|
+
else:
|
|
183
|
+
raise MacblockError("test requires a domain")
|
|
184
|
+
|
|
185
|
+
elif cmd == "update":
|
|
186
|
+
args["source"] = None
|
|
187
|
+
args["sha256"] = None
|
|
188
|
+
i = 0
|
|
189
|
+
while i < len(rest):
|
|
190
|
+
if rest[i] == "--source" and i + 1 < len(rest):
|
|
191
|
+
args["source"] = rest[i + 1]
|
|
192
|
+
i += 2
|
|
193
|
+
elif rest[i] == "--sha256" and i + 1 < len(rest):
|
|
194
|
+
args["sha256"] = rest[i + 1]
|
|
195
|
+
i += 2
|
|
196
|
+
else:
|
|
197
|
+
i += 1
|
|
198
|
+
|
|
199
|
+
elif cmd == "sources":
|
|
200
|
+
if not rest:
|
|
201
|
+
raise MacblockError("sources requires a subcommand: list or set")
|
|
202
|
+
args["sources_cmd"] = rest[0]
|
|
203
|
+
if rest[0] == "set":
|
|
204
|
+
if len(rest) < 2:
|
|
205
|
+
raise MacblockError("sources set requires a source name")
|
|
206
|
+
args["source"] = rest[1]
|
|
207
|
+
|
|
208
|
+
elif cmd == "allow":
|
|
209
|
+
if not rest:
|
|
210
|
+
raise MacblockError("allow requires a subcommand: add, remove, or list")
|
|
211
|
+
args["allow_cmd"] = rest[0]
|
|
212
|
+
if rest[0] in ("add", "remove"):
|
|
213
|
+
if len(rest) < 2:
|
|
214
|
+
raise MacblockError(f"allow {rest[0]} requires a domain")
|
|
215
|
+
args["domain"] = rest[1]
|
|
216
|
+
|
|
217
|
+
elif cmd == "deny":
|
|
218
|
+
if not rest:
|
|
219
|
+
raise MacblockError("deny requires a subcommand: add, remove, or list")
|
|
220
|
+
args["deny_cmd"] = rest[0]
|
|
221
|
+
if rest[0] in ("add", "remove"):
|
|
222
|
+
if len(rest) < 2:
|
|
223
|
+
raise MacblockError(f"deny {rest[0]} requires a domain")
|
|
224
|
+
args["domain"] = rest[1]
|
|
225
|
+
|
|
226
|
+
return cmd, args
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def main(argv: list[str] | None = None) -> int:
|
|
230
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
require_macos()
|
|
234
|
+
|
|
235
|
+
cmd, args = _parse_args(argv)
|
|
236
|
+
|
|
237
|
+
# Show help if no command provided
|
|
238
|
+
if cmd is None:
|
|
239
|
+
show_main_help()
|
|
240
|
+
return 0
|
|
241
|
+
|
|
242
|
+
# Handle help requests
|
|
243
|
+
if cmd == "_help":
|
|
244
|
+
help_context = args.get("_help_context")
|
|
245
|
+
if help_context:
|
|
246
|
+
show_command_help(help_context)
|
|
247
|
+
else:
|
|
248
|
+
show_main_help()
|
|
249
|
+
return 0
|
|
250
|
+
|
|
251
|
+
# Check if we need root
|
|
252
|
+
if _needs_root(cmd, args) and not is_root():
|
|
253
|
+
_exec_sudo(argv)
|
|
254
|
+
|
|
255
|
+
if cmd == "status":
|
|
256
|
+
return show_status()
|
|
257
|
+
if cmd == "doctor":
|
|
258
|
+
return run_diagnostics()
|
|
259
|
+
if cmd == "daemon":
|
|
260
|
+
from macblock.daemon import run_daemon
|
|
261
|
+
|
|
262
|
+
return run_daemon()
|
|
263
|
+
if cmd == "logs":
|
|
264
|
+
show_logs = importlib.import_module("macblock.logs").show_logs
|
|
265
|
+
return show_logs(
|
|
266
|
+
component=str(args.get("component", "daemon")),
|
|
267
|
+
lines=int(args.get("lines", 200)),
|
|
268
|
+
follow=bool(args.get("follow", False)),
|
|
269
|
+
stderr=bool(args.get("stderr", False)),
|
|
270
|
+
)
|
|
271
|
+
if cmd == "install":
|
|
272
|
+
return do_install(
|
|
273
|
+
force=bool(args.get("force")), skip_update=bool(args.get("skip_update"))
|
|
274
|
+
)
|
|
275
|
+
if cmd == "uninstall":
|
|
276
|
+
return do_uninstall(force=bool(args.get("force")))
|
|
277
|
+
if cmd == "enable":
|
|
278
|
+
return do_enable()
|
|
279
|
+
if cmd == "disable":
|
|
280
|
+
return do_disable()
|
|
281
|
+
if cmd == "pause":
|
|
282
|
+
return do_pause(args["duration"])
|
|
283
|
+
if cmd == "resume":
|
|
284
|
+
return do_resume()
|
|
285
|
+
if cmd == "test":
|
|
286
|
+
return test_domain(args["domain"])
|
|
287
|
+
if cmd == "update":
|
|
288
|
+
return update_blocklist(
|
|
289
|
+
source=args.get("source"), sha256=args.get("sha256")
|
|
290
|
+
)
|
|
291
|
+
if cmd == "sources":
|
|
292
|
+
if args.get("sources_cmd") == "list":
|
|
293
|
+
return list_blocklist_sources()
|
|
294
|
+
return set_blocklist_source(args["source"])
|
|
295
|
+
if cmd == "allow":
|
|
296
|
+
if args.get("allow_cmd") == "add":
|
|
297
|
+
return add_whitelist(args["domain"])
|
|
298
|
+
if args.get("allow_cmd") == "remove":
|
|
299
|
+
return remove_whitelist(args["domain"])
|
|
300
|
+
return list_whitelist()
|
|
301
|
+
if cmd == "deny":
|
|
302
|
+
if args.get("deny_cmd") == "add":
|
|
303
|
+
return add_blacklist(args["domain"])
|
|
304
|
+
if args.get("deny_cmd") == "remove":
|
|
305
|
+
return remove_blacklist(args["domain"])
|
|
306
|
+
return list_blacklist()
|
|
307
|
+
|
|
308
|
+
print(f"error: unknown command: {cmd}", file=sys.stderr)
|
|
309
|
+
print("Run 'macblock --help' for usage.", file=sys.stderr)
|
|
310
|
+
return 2
|
|
311
|
+
|
|
312
|
+
except UnsupportedPlatformError as e:
|
|
313
|
+
print(str(e), file=sys.stderr)
|
|
314
|
+
return 2
|
|
315
|
+
except PrivilegeError as e:
|
|
316
|
+
print(str(e), file=sys.stderr)
|
|
317
|
+
return 2
|
|
318
|
+
except MacblockError as e:
|
|
319
|
+
print(f"error: {e}", file=sys.stderr)
|
|
320
|
+
return 1
|
|
321
|
+
except Exception as e:
|
|
322
|
+
print(f"error: {e}", file=sys.stderr)
|
|
323
|
+
return 1
|
macblock/colors.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Colors:
|
|
7
|
+
RED = "\033[91m"
|
|
8
|
+
GREEN = "\033[92m"
|
|
9
|
+
YELLOW = "\033[93m"
|
|
10
|
+
BLUE = "\033[94m"
|
|
11
|
+
MAGENTA = "\033[95m"
|
|
12
|
+
CYAN = "\033[96m"
|
|
13
|
+
WHITE = "\033[97m"
|
|
14
|
+
BOLD = "\033[1m"
|
|
15
|
+
DIM = "\033[2m"
|
|
16
|
+
RESET = "\033[0m"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def color(text: str, *styles: str) -> str:
|
|
20
|
+
if not sys.stdout.isatty():
|
|
21
|
+
return text
|
|
22
|
+
return "".join(styles) + text + Colors.RESET
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def success(text: str) -> str:
|
|
26
|
+
return color(text, Colors.GREEN)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def error(text: str) -> str:
|
|
30
|
+
return color(text, Colors.RED)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def warning(text: str) -> str:
|
|
34
|
+
return color(text, Colors.YELLOW)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def info(text: str) -> str:
|
|
38
|
+
return color(text, Colors.CYAN)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def bold(text: str) -> str:
|
|
42
|
+
return color(text, Colors.BOLD)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def dim(text: str) -> str:
|
|
46
|
+
return color(text, Colors.DIM)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def print_success(text: str) -> None:
|
|
50
|
+
print(success(text))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def print_warning(text: str) -> None:
|
|
54
|
+
print(warning(text))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def print_info(text: str) -> None:
|
|
58
|
+
print(info(text))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def print_error(text: str) -> None:
|
|
62
|
+
print(error(text), file=sys.stderr)
|