From b40966ac4c7d52b3876f6f5bddf58bf33aca8a05 Mon Sep 17 00:00:00 2001 From: max Date: Sat, 26 Sep 2026 20:21:14 +0200 Subject: [PATCH] fix(database, installer): enable WAL mode, grant ProgramData permissions in NSIS, and fallback to LocalAppData when read-only --- compilation/installer_client.nsi | 11 ++-- compilation/installer_server.nsi | 11 ++-- src/Server.py | 100 +++++++++++++++++++++++-------- src/Win_Client.py | 74 +++++++++++++++++------ 4 files changed, 147 insertions(+), 49 deletions(-) diff --git a/compilation/installer_client.nsi b/compilation/installer_client.nsi index 28b1b24..0e778e3 100644 --- a/compilation/installer_client.nsi +++ b/compilation/installer_client.nsi @@ -16,24 +16,27 @@ RequestExecutionLevel admin !insertmacro MUI_LANGUAGE "English" Section "Install" + SetShellVarContext all SetOutPath "$INSTDIR" File "../dist/Win_Client.exe" File "../compilation/nssm.exe" File /r "../dist/runtime" - CreateDirectory "$COMMONAPPDATA\LOGAR" + CreateDirectory "$APPDATA\LOGAR" + ExecWait 'icacls "$APPDATA\LOGAR" /grant "*S-1-5-32-545:(OI)(CI)M" /T /Q' # Install and configure service via NSSM ExecWait '"$INSTDIR\nssm.exe" install LOGAR_Client "$INSTDIR\Win_Client.exe"' ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppDirectory "$INSTDIR"' - ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppStdout "$COMMONAPPDATA\LOGAR\client.log"' - ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppStderr "$COMMONAPPDATA\LOGAR\client_err.log"' + ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppStdout "$APPDATA\LOGAR\client.log"' + ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppStderr "$APPDATA\LOGAR\client_err.log"' ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Client' WriteUninstaller "$INSTDIR\uninstall.exe" SectionEnd Section "Uninstall" + SetShellVarContext all ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Client' ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Client confirm' RMDir /r "$INSTDIR\runtime" @@ -45,5 +48,5 @@ Section "Uninstall" Delete "$INSTDIR\nssm.exe" Delete "$INSTDIR\uninstall.exe" RMDir /r "$INSTDIR" - RMDir /r "$COMMONAPPDATA\LOGAR" + RMDir /r "$APPDATA\LOGAR" SectionEnd diff --git a/compilation/installer_server.nsi b/compilation/installer_server.nsi index 4e5eac9..6ae3201 100644 --- a/compilation/installer_server.nsi +++ b/compilation/installer_server.nsi @@ -16,24 +16,27 @@ RequestExecutionLevel admin !insertmacro MUI_LANGUAGE "English" Section "Install" + SetShellVarContext all SetOutPath "$INSTDIR" File "../dist/Server.exe" File "../compilation/nssm.exe" File /r "../dist/runtime" - CreateDirectory "$COMMONAPPDATA\LOGAR-Server" + CreateDirectory "$APPDATA\LOGAR-Server" + ExecWait 'icacls "$APPDATA\LOGAR-Server" /grant "*S-1-5-32-545:(OI)(CI)M" /T /Q' # Install and configure service via NSSM ExecWait '"$INSTDIR\nssm.exe" install LOGAR_Server "$INSTDIR\Server.exe"' ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppDirectory "$INSTDIR"' - ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppStdout "$COMMONAPPDATA\LOGAR-Server\server.log"' - ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppStderr "$COMMONAPPDATA\LOGAR-Server\server_err.log"' + ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppStdout "$APPDATA\LOGAR-Server\server.log"' + ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppStderr "$APPDATA\LOGAR-Server\server_err.log"' ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Server' WriteUninstaller "$INSTDIR\uninstall.exe" SectionEnd Section "Uninstall" + SetShellVarContext all ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Server' ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Server confirm' RMDir /r "$INSTDIR\runtime" @@ -46,5 +49,5 @@ Section "Uninstall" Delete "$INSTDIR\nssm.exe" Delete "$INSTDIR\uninstall.exe" RMDir /r "$INSTDIR" - RMDir /r "$COMMONAPPDATA\LOGAR-Server" + RMDir /r "$APPDATA\LOGAR-Server" SectionEnd diff --git a/src/Server.py b/src/Server.py index 17c0a96..1e37d8b 100644 --- a/src/Server.py +++ b/src/Server.py @@ -384,32 +384,75 @@ def generate_server_keypair(server_name: str): return private_key_armored, public_key_armored, fingerprint +def is_path_writable(filepath: str) -> bool: + """Checks if a file or directory path is actually writable by the current process.""" + try: + if os.path.exists(filepath): + if os.path.isdir(filepath): + test_f = os.path.join(filepath, f".perm_{os.getpid()}") + with open(test_f, "w") as f: + f.write("1") + os.remove(test_f) + return True + else: + with open(filepath, "a") as f: + pass + return True + else: + parent = os.path.dirname(os.path.abspath(filepath)) or "." + os.makedirs(parent, exist_ok=True) + test_f = os.path.join(parent, f".perm_{os.getpid()}") + with open(test_f, "w") as f: + f.write("1") + os.remove(test_f) + return True + except Exception: + return False + + def get_default_data_dir(app_name: str = "LOGAR-Server") -> str: """Returns a suitable writable application data directory.""" if sys.platform == "win32": + # 1. ProgramData (C:\ProgramData\LOGAR-Server) prog_data = os.environ.get("ProgramData", r"C:\ProgramData") - target = os.path.join(prog_data, app_name) - cwd = os.getcwd() - if os.path.exists(target) or "Program Files" in cwd or not os.access(cwd, os.W_OK): - try: - os.makedirs(target, exist_ok=True) - return target - except Exception: - pass - return os.getcwd() + target_prog = os.path.join(prog_data, app_name) + if is_path_writable(target_prog): + return target_prog + + # 2. LocalAppData (C:\Users\\AppData\Local\LOGAR-Server) + local_app = os.environ.get("LOCALAPPDATA") or os.path.expanduser(r"~\AppData\Local") + target_local = os.path.join(local_app, app_name) + if is_path_writable(target_local): + return target_local + + # 3. Temp directory (%TEMP%\LOGAR-Server) + temp_dir = os.path.join(os.environ.get("TEMP", r"C:\Windows\Temp"), app_name) + if is_path_writable(temp_dir): + return temp_dir + + cwd = os.getcwd() + if is_path_writable(cwd): + return cwd + return os.path.expanduser("~") def resolve_writable_path(path: str, app_name: str = "LOGAR-Server", default_filename: str = "logar_state.db") -> str: - """Resolves a file or directory path, falling back to ProgramData if running in a read-only directory (e.g. Program Files).""" + """Resolves a file or directory path, ensuring it is writable or falling back to a writable data directory.""" if not path: path = default_filename - if os.path.isabs(path): + + # If already absolute and writable, keep it + if os.path.isabs(path) and is_path_writable(path): return path - cwd = os.getcwd() - if sys.platform == "win32" and ("Program Files" in cwd or not os.access(cwd, os.W_OK)): - data_dir = get_default_data_dir(app_name) - return os.path.join(data_dir, path) - return os.path.abspath(path) + + # If relative and CWD is writable and not in Program Files, use CWD + if not os.path.isabs(path) and "Program Files" not in os.getcwd() and is_path_writable(os.getcwd()): + return os.path.abspath(path) + + # Otherwise route into the guaranteed writable data directory + data_dir = get_default_data_dir(app_name) + fname = os.path.basename(path) + return os.path.join(data_dir, fname) def load_or_init_config(config_path: str = CONFIG_FILE_NAME) -> Dict[str, Any]: @@ -450,8 +493,8 @@ def load_or_init_config(config_path: str = CONFIG_FILE_NAME) -> Dict[str, Any]: SESSION_SECRET = config["session_secret"] return config - # Fallback to ProgramData if CWD is read-only / Program Files - if not os.path.isabs(config_path) and ("Program Files" in os.getcwd() or not os.access(os.getcwd(), os.W_OK)): + # Fallback to writable data dir if CWD is read-only / Program Files + if not os.path.isabs(config_path) and ("Program Files" in os.getcwd() or not is_path_writable(os.getcwd())): config_path = os.path.join(get_default_data_dir("LOGAR-Server"), os.path.basename(config_path)) print(f"[!] Config '{config_path}' not found. Initializing first-run configuration...") @@ -609,7 +652,12 @@ def init_db(db_path: str, enrollment_secret: Optional[str] = None, max_seats: in target_dir = os.path.dirname(os.path.abspath(target_path)) if target_dir: os.makedirs(target_dir, exist_ok=True) - conn = sqlite3.connect(target_path) + conn = sqlite3.connect(target_path, timeout=10.0) + try: + conn.execute("PRAGMA journal_mode=WAL;") + conn.execute("PRAGMA busy_timeout=5000;") + except Exception: + pass # 1. Active Issues conn.execute(""" @@ -732,11 +780,15 @@ def init_db(db_path: str, enrollment_secret: Optional[str] = None, max_seats: in _execute_schema(db_path) return db_path except (sqlite3.OperationalError, PermissionError, OSError) as e: - if "readonly" in str(e).lower() or "permission" in str(e).lower() or isinstance(e, PermissionError): - fallback_path = os.path.join(get_default_data_dir("LOGAR-Server"), "logar_state.db") - print(f"[!] Warning: Database path '{db_path}' is not writable ({e}). Falling back to '{fallback_path}'...") - _execute_schema(fallback_path) - return fallback_path + if "readonly" in str(e).lower() or "permission" in str(e).lower() or "locked" in str(e).lower() or isinstance(e, PermissionError): + local_app = os.environ.get("LOCALAPPDATA") or os.path.expanduser(r"~\AppData\Local") + fallback_dir = os.path.join(local_app, "LOGAR-Server") + os.makedirs(fallback_dir, exist_ok=True) + fallback_path = os.path.join(fallback_dir, "logar_state.db") + if fallback_path != db_path: + print(f"[!] Warning: Database path '{db_path}' is not writable ({e}). Falling back to '{fallback_path}'...") + _execute_schema(fallback_path) + return fallback_path raise diff --git a/src/Win_Client.py b/src/Win_Client.py index 493fbdb..71d782b 100644 --- a/src/Win_Client.py +++ b/src/Win_Client.py @@ -115,39 +115,79 @@ def get_tls_socket(hub_host: str, hub_port: int, cert_dir: str): return ctx.wrap_socket(raw_sock, server_hostname=hub_host) +def is_path_writable(filepath: str) -> bool: + """Checks if a file or directory path is actually writable by the current process.""" + try: + if os.path.exists(filepath): + if os.path.isdir(filepath): + test_f = os.path.join(filepath, f".perm_{os.getpid()}") + with open(test_f, "w") as f: + f.write("1") + os.remove(test_f) + return True + else: + with open(filepath, "a") as f: + pass + return True + else: + parent = os.path.dirname(os.path.abspath(filepath)) or "." + os.makedirs(parent, exist_ok=True) + test_f = os.path.join(parent, f".perm_{os.getpid()}") + with open(test_f, "w") as f: + f.write("1") + os.remove(test_f) + return True + except Exception: + return False + + def get_default_data_dir(app_name: str = "LOGAR") -> str: """Returns a suitable writable application data directory.""" if sys.platform == "win32": + # 1. ProgramData (C:\ProgramData\LOGAR) prog_data = os.environ.get("ProgramData", r"C:\ProgramData") - target = os.path.join(prog_data, app_name) - cwd = os.getcwd() - if os.path.exists(target) or "Program Files" in cwd or not os.access(cwd, os.W_OK): - try: - os.makedirs(target, exist_ok=True) - return target - except Exception: - pass - return os.getcwd() + target_prog = os.path.join(prog_data, app_name) + if is_path_writable(target_prog): + return target_prog + + # 2. LocalAppData (C:\Users\\AppData\Local\LOGAR) + local_app = os.environ.get("LOCALAPPDATA") or os.path.expanduser(r"~\AppData\Local") + target_local = os.path.join(local_app, app_name) + if is_path_writable(target_local): + return target_local + + # 3. Temp directory (%TEMP%\LOGAR) + temp_dir = os.path.join(os.environ.get("TEMP", r"C:\Windows\Temp"), app_name) + if is_path_writable(temp_dir): + return temp_dir + + cwd = os.getcwd() + if is_path_writable(cwd): + return cwd + return os.path.expanduser("~") def resolve_writable_path(path: str, app_name: str = "LOGAR", default_filename: str = "client_state.json") -> str: - """Resolves a file or directory path, falling back to ProgramData if running in a read-only directory (e.g. Program Files).""" + """Resolves a file or directory path, ensuring it is writable or falling back to a writable data directory.""" if not path: path = default_filename - if os.path.isabs(path): + + if os.path.isabs(path) and is_path_writable(path): return path - cwd = os.getcwd() - if sys.platform == "win32" and ("Program Files" in cwd or not os.access(cwd, os.W_OK)): - data_dir = get_default_data_dir(app_name) - return os.path.join(data_dir, path) - return os.path.abspath(path) + + if not os.path.isabs(path) and "Program Files" not in os.getcwd() and is_path_writable(os.getcwd()): + return os.path.abspath(path) + + data_dir = get_default_data_dir(app_name) + fname = os.path.basename(path) + return os.path.join(data_dir, fname) def get_state_path(config_path: str, custom_state_path: Optional[str] = None) -> str: if custom_state_path: return custom_state_path config_dir = os.path.dirname(os.path.abspath(config_path)) - if sys.platform == "win32" and ("Program Files" in config_dir or not os.access(config_dir, os.W_OK)): + if sys.platform == "win32" and ("Program Files" in config_dir or not is_path_writable(config_dir)): return os.path.join(get_default_data_dir("LOGAR"), STATE_FILE_NAME) return os.path.join(config_dir, STATE_FILE_NAME)