arthexis 0.1.13__py3-none-any.whl → 0.1.15__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.
- {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/METADATA +224 -221
- arthexis-0.1.15.dist-info/RECORD +110 -0
- {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/licenses/LICENSE +674 -674
- config/__init__.py +5 -5
- config/active_app.py +15 -15
- config/asgi.py +43 -43
- config/auth_app.py +7 -7
- config/celery.py +32 -32
- config/context_processors.py +67 -69
- config/horologia_app.py +7 -7
- config/loadenv.py +11 -11
- config/logging.py +59 -48
- config/middleware.py +25 -25
- config/offline.py +49 -49
- config/settings.py +691 -682
- config/settings_helpers.py +109 -109
- config/urls.py +171 -166
- config/wsgi.py +17 -17
- core/admin.py +3795 -2809
- core/admin_history.py +50 -50
- core/admindocs.py +151 -151
- core/apps.py +356 -272
- core/auto_upgrade.py +57 -57
- core/backends.py +265 -236
- core/changelog.py +342 -0
- core/entity.py +149 -133
- core/environment.py +61 -61
- core/fields.py +168 -168
- core/form_fields.py +75 -75
- core/github_helper.py +188 -25
- core/github_issues.py +178 -172
- core/github_repos.py +72 -0
- core/lcd_screen.py +78 -78
- core/liveupdate.py +25 -25
- core/log_paths.py +114 -100
- core/mailer.py +85 -85
- core/middleware.py +91 -91
- core/models.py +3637 -2795
- core/notifications.py +105 -105
- core/public_wifi.py +267 -227
- core/reference_utils.py +108 -108
- core/release.py +840 -368
- core/rfid_import_export.py +113 -0
- core/sigil_builder.py +149 -149
- core/sigil_context.py +20 -20
- core/sigil_resolver.py +315 -315
- core/system.py +952 -493
- core/tasks.py +408 -394
- core/temp_passwords.py +181 -181
- core/test_system_info.py +186 -139
- core/tests.py +2168 -1521
- core/tests_liveupdate.py +17 -17
- core/urls.py +11 -11
- core/user_data.py +641 -633
- core/views.py +2201 -1417
- core/widgets.py +213 -94
- core/workgroup_urls.py +17 -17
- core/workgroup_views.py +94 -94
- nodes/admin.py +1720 -1161
- nodes/apps.py +87 -85
- nodes/backends.py +160 -160
- nodes/dns.py +203 -203
- nodes/feature_checks.py +133 -133
- nodes/lcd.py +165 -165
- nodes/models.py +1764 -1597
- nodes/reports.py +411 -411
- nodes/rfid_sync.py +195 -0
- nodes/signals.py +18 -0
- nodes/tasks.py +46 -46
- nodes/tests.py +3830 -3116
- nodes/urls.py +15 -14
- nodes/utils.py +121 -105
- nodes/views.py +683 -619
- ocpp/admin.py +948 -948
- ocpp/apps.py +25 -25
- ocpp/consumers.py +1565 -1459
- ocpp/evcs.py +844 -844
- ocpp/evcs_discovery.py +158 -158
- ocpp/models.py +917 -917
- ocpp/reference_utils.py +42 -42
- ocpp/routing.py +11 -11
- ocpp/simulator.py +745 -745
- ocpp/status_display.py +26 -26
- ocpp/store.py +601 -541
- ocpp/tasks.py +31 -31
- ocpp/test_export_import.py +130 -130
- ocpp/test_rfid.py +913 -702
- ocpp/tests.py +4445 -4094
- ocpp/transactions_io.py +189 -189
- ocpp/urls.py +50 -50
- ocpp/views.py +1479 -1251
- pages/admin.py +769 -539
- pages/apps.py +10 -10
- pages/checks.py +40 -40
- pages/context_processors.py +127 -119
- pages/defaults.py +13 -13
- pages/forms.py +198 -198
- pages/middleware.py +209 -153
- pages/models.py +643 -426
- pages/tasks.py +74 -0
- pages/tests.py +3025 -2200
- pages/urls.py +26 -25
- pages/utils.py +23 -12
- pages/views.py +1176 -1128
- arthexis-0.1.13.dist-info/RECORD +0 -105
- nodes/actions.py +0 -70
- {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/WHEEL +0 -0
- {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/top_level.txt +0 -0
core/public_wifi.py
CHANGED
|
@@ -1,227 +1,267 @@
|
|
|
1
|
-
"""Utilities for managing public Wi-Fi access control."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
import re
|
|
7
|
-
import shutil
|
|
8
|
-
import subprocess
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import Iterable
|
|
11
|
-
|
|
12
|
-
from django.conf import settings
|
|
13
|
-
from django.utils import timezone
|
|
14
|
-
|
|
15
|
-
logger = logging.getLogger(__name__)
|
|
16
|
-
|
|
17
|
-
_MAC_RE = re.compile(r"(?P<mac>([0-9a-f]{2}:){5}[0-9a-f]{2})", re.IGNORECASE)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _lock_dir() -> Path:
|
|
21
|
-
return Path(settings.BASE_DIR) / "locks"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _mode_lock() -> Path:
|
|
25
|
-
return _lock_dir() / "public_wifi_mode.lck"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _allowlist_path() -> Path:
|
|
29
|
-
return _lock_dir() / "public_wifi_allow.list"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _normalize_mac(mac: str) -> str:
|
|
33
|
-
return mac.strip().lower()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def resolve_mac_address(ip_address: str | None) -> str | None:
|
|
37
|
-
"""Attempt to resolve the MAC address for ``ip_address``.
|
|
38
|
-
|
|
39
|
-
The lookup prefers ``ip neigh`` and falls back to ``arp`` when available.
|
|
40
|
-
Returns ``None`` when the MAC cannot be determined.
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
if not ip_address:
|
|
44
|
-
return None
|
|
45
|
-
|
|
46
|
-
commands: Iterable[list[str]] = []
|
|
47
|
-
ip_cmd = shutil.which("ip")
|
|
48
|
-
if ip_cmd:
|
|
49
|
-
commands = [[ip_cmd, "neigh", "show", ip_address]]
|
|
50
|
-
arp_cmd = shutil.which("arp")
|
|
51
|
-
if arp_cmd:
|
|
52
|
-
commands = list(commands) + [[arp_cmd, "-n", ip_address]]
|
|
53
|
-
for command in commands:
|
|
54
|
-
try:
|
|
55
|
-
result = subprocess.run(
|
|
56
|
-
command,
|
|
57
|
-
capture_output=True,
|
|
58
|
-
text=True,
|
|
59
|
-
check=False,
|
|
60
|
-
timeout=1,
|
|
61
|
-
)
|
|
62
|
-
except Exception: # pragma: no cover - defensive
|
|
63
|
-
continue
|
|
64
|
-
if result.returncode != 0:
|
|
65
|
-
continue
|
|
66
|
-
match = _MAC_RE.search(result.stdout)
|
|
67
|
-
if match:
|
|
68
|
-
mac = match.group("mac")
|
|
69
|
-
return _normalize_mac(mac)
|
|
70
|
-
return None
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _iptables_available() -> bool:
|
|
74
|
-
return shutil.which("iptables") is not None
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def _run_iptables(args: list[str]) -> None:
|
|
78
|
-
try:
|
|
79
|
-
subprocess.run(["iptables", *args], check=False, timeout=2)
|
|
80
|
-
except Exception: # pragma: no cover - defensive
|
|
81
|
-
logger.exception("iptables command failed: %s", " ".join(args))
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
1
|
+
"""Utilities for managing public Wi-Fi access control."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Iterable
|
|
11
|
+
|
|
12
|
+
from django.conf import settings
|
|
13
|
+
from django.utils import timezone
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
_MAC_RE = re.compile(r"(?P<mac>([0-9a-f]{2}:){5}[0-9a-f]{2})", re.IGNORECASE)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _lock_dir() -> Path:
|
|
21
|
+
return Path(settings.BASE_DIR) / "locks"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _mode_lock() -> Path:
|
|
25
|
+
return _lock_dir() / "public_wifi_mode.lck"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _allowlist_path() -> Path:
|
|
29
|
+
return _lock_dir() / "public_wifi_allow.list"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _normalize_mac(mac: str) -> str:
|
|
33
|
+
return mac.strip().lower()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_mac_address(ip_address: str | None) -> str | None:
|
|
37
|
+
"""Attempt to resolve the MAC address for ``ip_address``.
|
|
38
|
+
|
|
39
|
+
The lookup prefers ``ip neigh`` and falls back to ``arp`` when available.
|
|
40
|
+
Returns ``None`` when the MAC cannot be determined.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
if not ip_address:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
commands: Iterable[list[str]] = []
|
|
47
|
+
ip_cmd = shutil.which("ip")
|
|
48
|
+
if ip_cmd:
|
|
49
|
+
commands = [[ip_cmd, "neigh", "show", ip_address]]
|
|
50
|
+
arp_cmd = shutil.which("arp")
|
|
51
|
+
if arp_cmd:
|
|
52
|
+
commands = list(commands) + [[arp_cmd, "-n", ip_address]]
|
|
53
|
+
for command in commands:
|
|
54
|
+
try:
|
|
55
|
+
result = subprocess.run(
|
|
56
|
+
command,
|
|
57
|
+
capture_output=True,
|
|
58
|
+
text=True,
|
|
59
|
+
check=False,
|
|
60
|
+
timeout=1,
|
|
61
|
+
)
|
|
62
|
+
except Exception: # pragma: no cover - defensive
|
|
63
|
+
continue
|
|
64
|
+
if result.returncode != 0:
|
|
65
|
+
continue
|
|
66
|
+
match = _MAC_RE.search(result.stdout)
|
|
67
|
+
if match:
|
|
68
|
+
mac = match.group("mac")
|
|
69
|
+
return _normalize_mac(mac)
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _iptables_available() -> bool:
|
|
74
|
+
return shutil.which("iptables") is not None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _run_iptables(args: list[str]) -> None:
|
|
78
|
+
try:
|
|
79
|
+
subprocess.run(["iptables", *args], check=False, timeout=2)
|
|
80
|
+
except Exception: # pragma: no cover - defensive
|
|
81
|
+
logger.exception("iptables command failed: %s", " ".join(args))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _ensure_wlan0_drop_rule() -> None:
|
|
85
|
+
if not _iptables_available():
|
|
86
|
+
return
|
|
87
|
+
check_args = [
|
|
88
|
+
"-C",
|
|
89
|
+
"FORWARD",
|
|
90
|
+
"-i",
|
|
91
|
+
"wlan0",
|
|
92
|
+
"-o",
|
|
93
|
+
"wlan1",
|
|
94
|
+
"-j",
|
|
95
|
+
"DROP",
|
|
96
|
+
]
|
|
97
|
+
try:
|
|
98
|
+
result = subprocess.run(
|
|
99
|
+
["iptables", *check_args],
|
|
100
|
+
capture_output=True,
|
|
101
|
+
text=True,
|
|
102
|
+
check=False,
|
|
103
|
+
timeout=2,
|
|
104
|
+
)
|
|
105
|
+
except Exception: # pragma: no cover - defensive
|
|
106
|
+
logger.exception("iptables check failed for wlan0 drop rule")
|
|
107
|
+
result = None
|
|
108
|
+
if result is None or result.returncode != 0:
|
|
109
|
+
_run_iptables(
|
|
110
|
+
[
|
|
111
|
+
"-A",
|
|
112
|
+
"FORWARD",
|
|
113
|
+
"-i",
|
|
114
|
+
"wlan0",
|
|
115
|
+
"-o",
|
|
116
|
+
"wlan1",
|
|
117
|
+
"-j",
|
|
118
|
+
"DROP",
|
|
119
|
+
]
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _load_allowlist() -> set[str]:
|
|
124
|
+
path = _allowlist_path()
|
|
125
|
+
if not path.exists():
|
|
126
|
+
return set()
|
|
127
|
+
try:
|
|
128
|
+
content = path.read_text().splitlines()
|
|
129
|
+
except OSError: # pragma: no cover - defensive
|
|
130
|
+
logger.exception("Unable to read public Wi-Fi allow list")
|
|
131
|
+
return set()
|
|
132
|
+
return {line.strip().lower() for line in content if line.strip()}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _save_allowlist(macs: Iterable[str]) -> None:
|
|
136
|
+
path = _allowlist_path()
|
|
137
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
138
|
+
lines = sorted({m.lower() for m in macs if m})
|
|
139
|
+
try:
|
|
140
|
+
path.write_text("\n".join(lines) + ("\n" if lines else ""))
|
|
141
|
+
except OSError: # pragma: no cover - defensive
|
|
142
|
+
logger.exception("Unable to write public Wi-Fi allow list")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def allow_mac(mac: str) -> None:
|
|
146
|
+
mac = _normalize_mac(mac)
|
|
147
|
+
if not mac:
|
|
148
|
+
return
|
|
149
|
+
allowlist = _load_allowlist()
|
|
150
|
+
if mac not in allowlist:
|
|
151
|
+
allowlist.add(mac)
|
|
152
|
+
_save_allowlist(allowlist)
|
|
153
|
+
if _iptables_available():
|
|
154
|
+
_ensure_wlan0_drop_rule()
|
|
155
|
+
check_args = [
|
|
156
|
+
"-C",
|
|
157
|
+
"FORWARD",
|
|
158
|
+
"-i",
|
|
159
|
+
"wlan0",
|
|
160
|
+
"-m",
|
|
161
|
+
"mac",
|
|
162
|
+
"--mac-source",
|
|
163
|
+
mac,
|
|
164
|
+
"-j",
|
|
165
|
+
"ACCEPT",
|
|
166
|
+
]
|
|
167
|
+
try:
|
|
168
|
+
result = subprocess.run(
|
|
169
|
+
["iptables", *check_args],
|
|
170
|
+
capture_output=True,
|
|
171
|
+
text=True,
|
|
172
|
+
check=False,
|
|
173
|
+
timeout=2,
|
|
174
|
+
)
|
|
175
|
+
exists = result.returncode == 0
|
|
176
|
+
except Exception: # pragma: no cover - defensive
|
|
177
|
+
logger.exception("iptables check failed for %s", mac)
|
|
178
|
+
exists = True
|
|
179
|
+
if not exists:
|
|
180
|
+
_run_iptables(
|
|
181
|
+
[
|
|
182
|
+
"-I",
|
|
183
|
+
"FORWARD",
|
|
184
|
+
"1",
|
|
185
|
+
"-i",
|
|
186
|
+
"wlan0",
|
|
187
|
+
"-m",
|
|
188
|
+
"mac",
|
|
189
|
+
"--mac-source",
|
|
190
|
+
mac,
|
|
191
|
+
"-j",
|
|
192
|
+
"ACCEPT",
|
|
193
|
+
]
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def revoke_mac(mac: str) -> None:
|
|
198
|
+
mac = _normalize_mac(mac)
|
|
199
|
+
if not mac:
|
|
200
|
+
return
|
|
201
|
+
allowlist = _load_allowlist()
|
|
202
|
+
if mac in allowlist:
|
|
203
|
+
allowlist.remove(mac)
|
|
204
|
+
_save_allowlist(allowlist)
|
|
205
|
+
if _iptables_available():
|
|
206
|
+
while True:
|
|
207
|
+
try:
|
|
208
|
+
result = subprocess.run(
|
|
209
|
+
[
|
|
210
|
+
"iptables",
|
|
211
|
+
"-D",
|
|
212
|
+
"FORWARD",
|
|
213
|
+
"-i",
|
|
214
|
+
"wlan0",
|
|
215
|
+
"-m",
|
|
216
|
+
"mac",
|
|
217
|
+
"--mac-source",
|
|
218
|
+
mac,
|
|
219
|
+
"-j",
|
|
220
|
+
"ACCEPT",
|
|
221
|
+
],
|
|
222
|
+
capture_output=True,
|
|
223
|
+
text=True,
|
|
224
|
+
check=False,
|
|
225
|
+
timeout=2,
|
|
226
|
+
)
|
|
227
|
+
except Exception: # pragma: no cover - defensive
|
|
228
|
+
logger.exception("iptables delete failed for %s", mac)
|
|
229
|
+
break
|
|
230
|
+
if result.returncode != 0:
|
|
231
|
+
break
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def public_mode_lock_exists() -> bool:
|
|
235
|
+
return _mode_lock().exists()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def grant_public_access(user, mac: str):
|
|
239
|
+
from core.models import PublicWifiAccess
|
|
240
|
+
|
|
241
|
+
mac = _normalize_mac(mac)
|
|
242
|
+
if not mac:
|
|
243
|
+
return None
|
|
244
|
+
access, created = PublicWifiAccess.objects.get_or_create(
|
|
245
|
+
user=user,
|
|
246
|
+
mac_address=mac,
|
|
247
|
+
defaults={"revoked_on": None},
|
|
248
|
+
)
|
|
249
|
+
if access.revoked_on is not None:
|
|
250
|
+
access.revoked_on = None
|
|
251
|
+
access.save(update_fields=["revoked_on", "updated_on"])
|
|
252
|
+
allow_mac(mac)
|
|
253
|
+
return access
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def revoke_public_access(access) -> None:
|
|
257
|
+
if access.revoked_on is None:
|
|
258
|
+
access.revoked_on = timezone.now()
|
|
259
|
+
access.save(update_fields=["revoked_on", "updated_on"])
|
|
260
|
+
revoke_mac(access.mac_address)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def revoke_public_access_for_user(user) -> None:
|
|
264
|
+
from core.models import PublicWifiAccess
|
|
265
|
+
|
|
266
|
+
for access in PublicWifiAccess.objects.filter(user=user, revoked_on__isnull=True):
|
|
267
|
+
revoke_public_access(access)
|