optimized installer
This commit is contained in:
+607
@@ -0,0 +1,607 @@
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import subprocess
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
from .database import get_db
|
||||
|
||||
|
||||
SOCKET_PATH = Path("/run/parental-control.sock")
|
||||
|
||||
BUFFER_SIZE = 65536
|
||||
|
||||
POLKIT_ACTION = "org.sarlink.parentalcontrol.modify"
|
||||
|
||||
|
||||
def get_peer_credentials(connection):
|
||||
credentials = connection.getsockopt(
|
||||
socket.SOL_SOCKET,
|
||||
socket.SO_PEERCRED,
|
||||
struct.calcsize("3i"),
|
||||
)
|
||||
|
||||
return struct.unpack("3i", credentials)
|
||||
|
||||
|
||||
def authorize_write(pid: int) -> bool:
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"pkcheck",
|
||||
"--action-id",
|
||||
POLKIT_ACTION,
|
||||
"--process",
|
||||
str(pid),
|
||||
"--allow-user-interaction",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
return result.returncode == 0
|
||||
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
|
||||
class IPCOperations:
|
||||
|
||||
@staticmethod
|
||||
def get_users():
|
||||
with get_db() as db:
|
||||
rows = db.execute(
|
||||
"""
|
||||
SELECT *
|
||||
FROM users
|
||||
ORDER BY username COLLATE NOCASE
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
@staticmethod
|
||||
def get_user(user_id):
|
||||
with get_db() as db:
|
||||
row = db.execute(
|
||||
"""
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE id = ?
|
||||
""",
|
||||
(user_id,),
|
||||
).fetchone()
|
||||
|
||||
return dict(row) if row else None
|
||||
|
||||
@staticmethod
|
||||
def get_remaining_time(user_id):
|
||||
with get_db() as db:
|
||||
allowance_row = db.execute(
|
||||
"""
|
||||
SELECT COALESCE(
|
||||
allowance_seconds,
|
||||
0
|
||||
) AS total
|
||||
FROM daily_allowances
|
||||
WHERE user_id = ?
|
||||
AND weekday = CAST(
|
||||
strftime('%w', 'now', 'localtime')
|
||||
AS INTEGER
|
||||
)
|
||||
""",
|
||||
(user_id,),
|
||||
).fetchone()
|
||||
|
||||
allowance = (
|
||||
allowance_row["total"]
|
||||
if allowance_row
|
||||
else 0
|
||||
)
|
||||
|
||||
usage_row = db.execute(
|
||||
"""
|
||||
SELECT COALESCE(
|
||||
used_seconds,
|
||||
0
|
||||
) AS total
|
||||
FROM usage
|
||||
WHERE user_id = ?
|
||||
AND date = date('now', 'localtime')
|
||||
""",
|
||||
(user_id,),
|
||||
).fetchone()
|
||||
|
||||
used = (
|
||||
usage_row["total"]
|
||||
if usage_row
|
||||
else 0
|
||||
)
|
||||
|
||||
remaining = max(
|
||||
allowance - used,
|
||||
0,
|
||||
)
|
||||
|
||||
return remaining, allowance
|
||||
|
||||
@staticmethod
|
||||
def get_allowances(user_id):
|
||||
with get_db() as db:
|
||||
rows = db.execute(
|
||||
"""
|
||||
SELECT weekday, allowance_seconds
|
||||
FROM daily_allowances
|
||||
WHERE user_id = ?
|
||||
""",
|
||||
(user_id,),
|
||||
).fetchall()
|
||||
|
||||
return {
|
||||
row["weekday"]: row["allowance_seconds"]
|
||||
for row in rows
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_access_windows(user_id):
|
||||
with get_db() as db:
|
||||
rows = db.execute(
|
||||
"""
|
||||
SELECT weekday, start_minute, end_minute
|
||||
FROM access_windows
|
||||
WHERE user_id = ?
|
||||
ORDER BY weekday, start_minute
|
||||
""",
|
||||
(user_id,),
|
||||
).fetchall()
|
||||
|
||||
result = {}
|
||||
|
||||
for row in rows:
|
||||
result.setdefault(
|
||||
row["weekday"],
|
||||
[],
|
||||
).append(
|
||||
(
|
||||
row["start_minute"],
|
||||
row["end_minute"],
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def create_user(
|
||||
username,
|
||||
enabled,
|
||||
allowances,
|
||||
access_windows,
|
||||
):
|
||||
with get_db() as db:
|
||||
cursor = db.execute(
|
||||
"""
|
||||
INSERT INTO users (
|
||||
username,
|
||||
enabled
|
||||
)
|
||||
VALUES (?, ?)
|
||||
""",
|
||||
(
|
||||
username,
|
||||
1 if enabled else 0,
|
||||
),
|
||||
)
|
||||
|
||||
user_id = cursor.lastrowid
|
||||
|
||||
for weekday, seconds in allowances.items():
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO daily_allowances (
|
||||
user_id,
|
||||
weekday,
|
||||
allowance_seconds
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
weekday,
|
||||
seconds,
|
||||
),
|
||||
)
|
||||
|
||||
for weekday, windows in access_windows.items():
|
||||
for start_minute, end_minute in windows:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO access_windows (
|
||||
user_id,
|
||||
weekday,
|
||||
start_minute,
|
||||
end_minute
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
weekday,
|
||||
start_minute,
|
||||
end_minute,
|
||||
),
|
||||
)
|
||||
|
||||
return user_id
|
||||
|
||||
@staticmethod
|
||||
def update_user(
|
||||
user_id,
|
||||
enabled,
|
||||
allowances,
|
||||
access_windows,
|
||||
):
|
||||
with get_db() as db:
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE users
|
||||
SET enabled = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(
|
||||
1 if enabled else 0,
|
||||
user_id,
|
||||
),
|
||||
)
|
||||
|
||||
for weekday, seconds in allowances.items():
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO daily_allowances (
|
||||
user_id,
|
||||
weekday,
|
||||
allowance_seconds
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(user_id, weekday)
|
||||
DO UPDATE SET
|
||||
allowance_seconds =
|
||||
excluded.allowance_seconds
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
weekday,
|
||||
seconds,
|
||||
),
|
||||
)
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
DELETE FROM access_windows
|
||||
WHERE user_id = ?
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
|
||||
for weekday, windows in access_windows.items():
|
||||
for start_minute, end_minute in windows:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO access_windows (
|
||||
user_id,
|
||||
weekday,
|
||||
start_minute,
|
||||
end_minute
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
weekday,
|
||||
start_minute,
|
||||
end_minute,
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def delete_user(user_id):
|
||||
with get_db() as db:
|
||||
db.execute(
|
||||
"""
|
||||
DELETE FROM users
|
||||
WHERE id = ?
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def set_enabled(user_id, enabled):
|
||||
with get_db() as db:
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE users
|
||||
SET enabled = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(
|
||||
1 if enabled else 0,
|
||||
user_id,
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_temporary_time(
|
||||
user_id,
|
||||
seconds,
|
||||
):
|
||||
with get_db() as db:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO temporary_grants (
|
||||
user_id,
|
||||
seconds,
|
||||
remaining_seconds
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
seconds,
|
||||
seconds,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
READ_METHODS = {
|
||||
"get_users",
|
||||
"get_user",
|
||||
"get_remaining_time",
|
||||
"get_allowances",
|
||||
"get_access_windows",
|
||||
}
|
||||
|
||||
|
||||
WRITE_METHODS = {
|
||||
"create_user",
|
||||
"update_user",
|
||||
"delete_user",
|
||||
"set_enabled",
|
||||
"add_temporary_time",
|
||||
}
|
||||
|
||||
|
||||
def handle_request(request, pid):
|
||||
method = request.get("method")
|
||||
arguments = request.get("args", {})
|
||||
|
||||
if method in READ_METHODS:
|
||||
operation = getattr(
|
||||
IPCOperations,
|
||||
method,
|
||||
None,
|
||||
)
|
||||
|
||||
if operation is None:
|
||||
raise RuntimeError(
|
||||
f"Unknown method: {method}"
|
||||
)
|
||||
|
||||
return operation(**arguments)
|
||||
|
||||
if method in WRITE_METHODS:
|
||||
if not authorize_write(pid):
|
||||
raise PermissionError(
|
||||
"Administrative authorization required"
|
||||
)
|
||||
|
||||
operation = getattr(
|
||||
IPCOperations,
|
||||
method,
|
||||
None,
|
||||
)
|
||||
|
||||
if operation is None:
|
||||
raise RuntimeError(
|
||||
f"Unknown method: {method}"
|
||||
)
|
||||
|
||||
return operation(**arguments)
|
||||
|
||||
raise RuntimeError(
|
||||
f"Unknown IPC method: {method}"
|
||||
)
|
||||
|
||||
|
||||
def handle_client(connection):
|
||||
try:
|
||||
pid, uid, gid = get_peer_credentials(
|
||||
connection
|
||||
)
|
||||
|
||||
data = bytearray()
|
||||
|
||||
while True:
|
||||
chunk = connection.recv(
|
||||
BUFFER_SIZE
|
||||
)
|
||||
|
||||
if not chunk:
|
||||
break
|
||||
|
||||
data.extend(chunk)
|
||||
|
||||
if b"\n" in chunk:
|
||||
break
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
line = bytes(data).split(
|
||||
b"\n",
|
||||
1,
|
||||
)[0]
|
||||
|
||||
request = json.loads(
|
||||
line.decode("utf-8")
|
||||
)
|
||||
|
||||
try:
|
||||
result = handle_request(
|
||||
request,
|
||||
pid,
|
||||
)
|
||||
|
||||
response = {
|
||||
"ok": True,
|
||||
"result": result,
|
||||
}
|
||||
|
||||
except PermissionError as exc:
|
||||
response = {
|
||||
"ok": False,
|
||||
"error": str(exc),
|
||||
"error_type": "authorization",
|
||||
}
|
||||
|
||||
except Exception as exc:
|
||||
response = {
|
||||
"ok": False,
|
||||
"error": str(exc),
|
||||
"error_type": "server",
|
||||
}
|
||||
|
||||
payload = (
|
||||
json.dumps(
|
||||
response,
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
+ b"\n"
|
||||
)
|
||||
|
||||
connection.sendall(payload)
|
||||
|
||||
except Exception:
|
||||
try:
|
||||
connection.sendall(
|
||||
b'{"ok":false,"error":"Invalid request",'
|
||||
b'"error_type":"server"}\n'
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
class IPCServer:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
socket_path=SOCKET_PATH,
|
||||
):
|
||||
self.socket_path = Path(
|
||||
socket_path
|
||||
)
|
||||
|
||||
self.server_socket = None
|
||||
self._thread = None
|
||||
self._stop_event = threading.Event()
|
||||
|
||||
def start(self):
|
||||
if self._thread is not None:
|
||||
return
|
||||
|
||||
self.socket_path.parent.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
try:
|
||||
self.socket_path.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
self.server_socket = socket.socket(
|
||||
socket.AF_UNIX,
|
||||
socket.SOCK_STREAM,
|
||||
)
|
||||
|
||||
self.server_socket.bind(
|
||||
str(self.socket_path)
|
||||
)
|
||||
|
||||
os.chmod(
|
||||
self.socket_path,
|
||||
0o666,
|
||||
)
|
||||
|
||||
self.server_socket.listen(16)
|
||||
|
||||
self._stop_event.clear()
|
||||
|
||||
self._thread = threading.Thread(
|
||||
target=self._run,
|
||||
name="parental-control-ipc",
|
||||
daemon=True,
|
||||
)
|
||||
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
self._stop_event.set()
|
||||
|
||||
if self.server_socket is not None:
|
||||
try:
|
||||
self.server_socket.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self._thread is not None:
|
||||
self._thread.join(timeout=2)
|
||||
|
||||
self._thread = None
|
||||
self.server_socket = None
|
||||
|
||||
try:
|
||||
self.socket_path.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def _run(self):
|
||||
while not self._stop_event.is_set():
|
||||
try:
|
||||
connection, _ = (
|
||||
self.server_socket.accept()
|
||||
)
|
||||
|
||||
except OSError:
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
|
||||
continue
|
||||
|
||||
thread = threading.Thread(
|
||||
target=handle_client,
|
||||
args=(connection,),
|
||||
daemon=True,
|
||||
)
|
||||
|
||||
thread.start()
|
||||
|
||||
|
||||
ipc_server = IPCServer()
|
||||
|
||||
|
||||
def start_ipc_server():
|
||||
ipc_server.start()
|
||||
|
||||
|
||||
def stop_ipc_server():
|
||||
ipc_server.stop()
|
||||
@@ -2,19 +2,24 @@ import time
|
||||
|
||||
from .database import initialize_database
|
||||
from .scheduler import start_scheduler, stop_scheduler
|
||||
from .ipc import start_ipc_server, stop_ipc_server
|
||||
|
||||
|
||||
def main():
|
||||
initialize_database()
|
||||
start_ipc_server()
|
||||
start_scheduler()
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(60)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
finally:
|
||||
stop_scheduler()
|
||||
stop_ipc_server()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user