windows installer fixes
This commit is contained in:
@@ -50,6 +50,7 @@ Type: filesandordirs; Name: "{commonappdata}\LOGAR"
|
||||
[Code]
|
||||
var
|
||||
DataDirPage: TInputDirWizardPage;
|
||||
ConfigFilePage: TInputFileWizardPage;
|
||||
|
||||
procedure InitializeWizard;
|
||||
begin
|
||||
@@ -64,6 +65,16 @@ begin
|
||||
);
|
||||
DataDirPage.Add('Data Directory:');
|
||||
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR');
|
||||
|
||||
ConfigFilePage := CreateInputFilePage(
|
||||
DataDirPage.ID,
|
||||
'Import Client Configuration (Optional)',
|
||||
'Do you have a pre-exported client_config.json profile to import?',
|
||||
'Select your client_config.json profile exported from the LOGAR Server Hub.'#13#10#13#10 +
|
||||
'If selected, this configuration will be copied into the data directory and used for automated server connection and enrollment.'#13#10#13#10 +
|
||||
'You can also leave this blank to place client_config.json in the data directory manually later.'
|
||||
);
|
||||
ConfigFilePage.Add('Client Configuration (client_config.json):', 'JSON Files (*.json)|*.json|All Files (*.*)|*.*', '.json');
|
||||
end;
|
||||
|
||||
function GetDataDir(Param: String): String;
|
||||
@@ -73,11 +84,25 @@ begin
|
||||
Result := ExpandConstant('{commonappdata}\LOGAR');
|
||||
end;
|
||||
|
||||
function GetConfigFile(Param: String): String;
|
||||
begin
|
||||
Result := ConfigFilePage.Values[0];
|
||||
end;
|
||||
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
var
|
||||
CfgPath, DestCfg: String;
|
||||
begin
|
||||
if CurStep = ssPostInstall then
|
||||
begin
|
||||
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'DataDir', GetDataDir(''));
|
||||
|
||||
CfgPath := GetConfigFile('');
|
||||
if (CfgPath <> '') and FileExists(CfgPath) then
|
||||
begin
|
||||
DestCfg := GetDataDir('') + '\client_config.json';
|
||||
FileCopy(CfgPath, DestCfg, False);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ VIAddVersionKey "FileVersion" "0.3.2.0"
|
||||
VIAddVersionKey "ProductVersion" "0.3.2.0"
|
||||
|
||||
Var DATADIR
|
||||
Var CFGFILE
|
||||
Var CfgDialog
|
||||
Var CfgFileInput
|
||||
Var CfgBrowseBtn
|
||||
|
||||
# --- UI Pages ---
|
||||
!define MUI_ABORTWARNING
|
||||
@@ -40,6 +44,9 @@ Var DATADIR
|
||||
!define MUI_DIRECTORYPAGE_VARIABLE $DATADIR
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
|
||||
# Custom Page: Config File Selection
|
||||
Page custom ConfigPageCreate ConfigPageLeave
|
||||
|
||||
# Instfiles Page
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
|
||||
@@ -57,8 +64,44 @@ Var DATADIR
|
||||
Function .onInit
|
||||
SetShellVarContext all
|
||||
StrCpy $DATADIR "$APPDATA\LOGAR"
|
||||
StrCpy $CFGFILE ""
|
||||
FunctionEnd
|
||||
|
||||
Function ConfigPageCreate
|
||||
nsDialogs::Create 1018
|
||||
Pop $CfgDialog
|
||||
${If} $CfgDialog == error
|
||||
Abort
|
||||
${EndIf}
|
||||
|
||||
!insertmacro MUI_HEADER_TEXT "Import Client Configuration (Optional)" "Choose a client_config.json profile exported from LOGAR Server."
|
||||
|
||||
${NSD_CreateLabel} 0 0 100% 30u "If you have exported a client_config.json profile bundle from your LOGAR Server dashboard, select it below to configure the forwarder immediately.$\r$\n$\r$\nLeave this field blank to configure later."
|
||||
Pop $0
|
||||
|
||||
${NSD_CreateText} 0 38u 75% 12u "$CFGFILE"
|
||||
Pop $CfgFileInput
|
||||
|
||||
${NSD_CreateButton} 78% 37u 22% 14u "Browse..."
|
||||
Pop $CfgBrowseBtn
|
||||
${NSD_OnClick} $CfgBrowseBtn OnBrowseCfg
|
||||
|
||||
nsDialogs::Show
|
||||
Function
|
||||
|
||||
Function OnBrowseCfg
|
||||
nsDialogs::SelectFileDialog open "" "LOGAR Client Profiles (*.json)|*.json|All Files (*.*)|*.*"
|
||||
Pop $0
|
||||
${If} $0 != ""
|
||||
StrCpy $CFGFILE $0
|
||||
${NSD_SetText} $CfgFileInput $CFGFILE
|
||||
${EndIf}
|
||||
Function
|
||||
|
||||
Function ConfigPageLeave
|
||||
${NSD_GetText} $CfgFileInput $CFGFILE
|
||||
Function
|
||||
|
||||
Section "Install"
|
||||
SetShellVarContext all
|
||||
SetOutPath "$INSTDIR"
|
||||
@@ -69,6 +112,12 @@ Section "Install"
|
||||
CreateDirectory "$DATADIR"
|
||||
ExecWait 'icacls "$DATADIR" /grant "*S-1-5-32-545:(OI)(CI)M" /T /Q'
|
||||
|
||||
# If client config file was selected, copy to data directory
|
||||
${If} $CFGFILE != ""
|
||||
${AndIf} ${FileExists} "$CFGFILE"
|
||||
CopyFiles /SILENT "$CFGFILE" "$DATADIR\client_config.json"
|
||||
${EndIf}
|
||||
|
||||
# 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"'
|
||||
|
||||
@@ -15,10 +15,24 @@ BUILD_TEMP = os.path.join(ROOT_DIR, "build_temp")
|
||||
|
||||
def clean_and_prep():
|
||||
if os.path.exists(DIST_DIR):
|
||||
shutil.rmtree(DIST_DIR)
|
||||
for item in os.listdir(DIST_DIR):
|
||||
if item == "runtime":
|
||||
continue
|
||||
item_path = os.path.join(DIST_DIR, item)
|
||||
try:
|
||||
if os.path.isdir(item_path):
|
||||
shutil.rmtree(item_path)
|
||||
else:
|
||||
os.remove(item_path)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
os.makedirs(DIST_DIR, exist_ok=True)
|
||||
if os.path.exists(BUILD_TEMP):
|
||||
try:
|
||||
shutil.rmtree(BUILD_TEMP)
|
||||
except Exception:
|
||||
pass
|
||||
os.makedirs(BUILD_TEMP, exist_ok=True)
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,14 @@ import warnings
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional
|
||||
|
||||
# Ensure script directory and repo root are in sys.path
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if SCRIPT_DIR not in sys.path:
|
||||
sys.path.insert(0, SCRIPT_DIR)
|
||||
REPO_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
||||
if REPO_ROOT not in sys.path:
|
||||
sys.path.insert(0, REPO_ROOT)
|
||||
|
||||
# Suppress cryptography / pgpy deprecation notices
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
+42
-7
@@ -17,6 +17,14 @@ import threading
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
|
||||
# Ensure script directory and repo root are in sys.path
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if SCRIPT_DIR not in sys.path:
|
||||
sys.path.insert(0, SCRIPT_DIR)
|
||||
REPO_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
||||
if REPO_ROOT not in sys.path:
|
||||
sys.path.insert(0, REPO_ROOT)
|
||||
|
||||
# Suppress cryptography / pgpy deprecation notices for a clean terminal output
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
@@ -910,6 +918,12 @@ def init_mtls_server_context(cert_dir: str = "certs") -> ssl.SSLContext:
|
||||
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
ctx.load_cert_chain(certfile=srv_cert, keyfile=srv_key)
|
||||
ctx.load_verify_locations(cafile=ca_file)
|
||||
upstream_file = os.path.join(cert_dir, "upstream_ca.crt")
|
||||
if os.path.exists(upstream_file):
|
||||
try:
|
||||
ctx.load_verify_locations(cafile=upstream_file)
|
||||
except Exception:
|
||||
pass
|
||||
ctx.verify_mode = ssl.CERT_REQUIRED
|
||||
ctx.minimum_version = ssl.TLSVersion.TLSv1_3
|
||||
return ctx
|
||||
@@ -926,6 +940,12 @@ def reload_mtls_context(ssl_ctx: ssl.SSLContext, cert_dir: str = "certs"):
|
||||
|
||||
ssl_ctx.load_cert_chain(certfile=srv_cert, keyfile=srv_key)
|
||||
ssl_ctx.load_verify_locations(cafile=ca_file)
|
||||
upstream_file = os.path.join(cert_dir, "upstream_ca.crt")
|
||||
if os.path.exists(upstream_file):
|
||||
try:
|
||||
ssl_ctx.load_verify_locations(cafile=upstream_file)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def check_and_rotate_server_certs(
|
||||
@@ -1347,6 +1367,18 @@ def enroll_client(req: ClientEnrollRequest, request: Request = None):
|
||||
conn.close()
|
||||
raise HTTPException(status_code=500, detail="Root CA not loaded on server")
|
||||
|
||||
# Ensure full trust chain (subordinate CA + upstream root CA if present) is delivered to edge clients
|
||||
cert_dir = SERVER_STATE.get("config", {}).get("cert_dir", "certs")
|
||||
upstream_ca_path = os.path.join(cert_dir, "upstream_ca.crt")
|
||||
if os.path.exists(upstream_ca_path):
|
||||
try:
|
||||
with open(upstream_ca_path, "r", encoding="utf-8") as f:
|
||||
upstream_pem = f.read().strip()
|
||||
if upstream_pem and upstream_pem not in ca_cert_pem:
|
||||
ca_cert_pem = ca_cert_pem.strip() + "\n" + upstream_pem
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. Check if client_id already registered
|
||||
c.execute("SELECT status FROM clients WHERE client_id = ?", (req.client_id,))
|
||||
client_row = c.fetchone()
|
||||
@@ -1766,14 +1798,17 @@ async def import_signed_license(req: Request):
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid JSON body")
|
||||
|
||||
raw_input = body.get("ca_cert") or body.get("lic") or body.get("raw") or body.get("license") or body.get("license_file")
|
||||
if not raw_input:
|
||||
raise HTTPException(status_code=400, detail="Missing 'lic' or 'ca_cert' payload in request")
|
||||
|
||||
if isinstance(raw_input, dict):
|
||||
raw_text = json.dumps(raw_input)
|
||||
if isinstance(body, dict):
|
||||
if "master_root_ca" in body or "upstream_ca" in body:
|
||||
raw_text = json.dumps(body)
|
||||
elif "lic" in body:
|
||||
raw_text = json.dumps(body["lic"]) if isinstance(body["lic"], dict) else str(body["lic"])
|
||||
elif "ca_cert" in body and not body.get("master_root_ca"):
|
||||
raw_text = str(body["ca_cert"])
|
||||
else:
|
||||
raw_text = str(raw_input)
|
||||
raw_text = json.dumps(body)
|
||||
else:
|
||||
raw_text = str(body)
|
||||
|
||||
config = SERVER_STATE.get("config", {})
|
||||
cert_dir = config.get("cert_dir", "certs")
|
||||
|
||||
@@ -10,6 +10,14 @@ import warnings
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional
|
||||
|
||||
# Ensure script directory and repo root are in sys.path
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if SCRIPT_DIR not in sys.path:
|
||||
sys.path.insert(0, SCRIPT_DIR)
|
||||
REPO_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
||||
if REPO_ROOT not in sys.path:
|
||||
sys.path.insert(0, REPO_ROOT)
|
||||
|
||||
# Suppress cryptography / pgpy deprecation notices
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
@@ -251,8 +251,24 @@ def generate_server_cert_if_needed(
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
).decode("utf-8")
|
||||
|
||||
# Bundle subordinate CA and upstream CA into server.crt for complete TLS handshake chain
|
||||
full_server_cert_pem = server_cert_pem
|
||||
ca_cert_path = os.path.join(cert_dir, "ca.crt")
|
||||
if os.path.exists(ca_cert_path):
|
||||
with open(ca_cert_path, "r", encoding="utf-8") as f:
|
||||
ca_pem = f.read().strip()
|
||||
if ca_pem and ca_pem not in full_server_cert_pem:
|
||||
full_server_cert_pem = full_server_cert_pem.strip() + "\n" + ca_pem
|
||||
|
||||
upstream_ca_path = os.path.join(cert_dir, "upstream_ca.crt")
|
||||
if os.path.exists(upstream_ca_path):
|
||||
with open(upstream_ca_path, "r", encoding="utf-8") as f:
|
||||
upstream_pem = f.read().strip()
|
||||
if upstream_pem and upstream_pem not in full_server_cert_pem:
|
||||
full_server_cert_pem = full_server_cert_pem.strip() + "\n" + upstream_pem
|
||||
|
||||
with open(server_cert_path, "w", encoding="utf-8") as f:
|
||||
f.write(server_cert_pem)
|
||||
f.write(full_server_cert_pem)
|
||||
with open(server_key_path, "w", encoding="utf-8") as f:
|
||||
f.write(server_key_pem)
|
||||
|
||||
|
||||
+76
-1
@@ -8,6 +8,7 @@ import struct
|
||||
import sqlite3
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import shutil
|
||||
import warnings
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
@@ -51,9 +52,84 @@ def run_tests():
|
||||
print(f"[OK] Fingerprint verified: {client_conf['server_fingerprint']}")
|
||||
|
||||
cert_dir = os.path.join(ROOT_DIR, "test_pipeline_certs")
|
||||
shutil.rmtree(cert_dir, ignore_errors=True)
|
||||
os.makedirs(cert_dir, exist_ok=True)
|
||||
client_id = "test-edge-node.corp.internal"
|
||||
enrollment_secret = server_conf.get("enrollment_secret") or client_conf.get("enrollment_secret")
|
||||
auth_token = server_conf.get("auth_token", "")
|
||||
|
||||
print("\n=== [1.5] Testing Server License Activation via Web API (/api/license/import) ===")
|
||||
import server_enrollment
|
||||
|
||||
# 1. Check current license status
|
||||
req_status_init = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/license/status")
|
||||
with urllib.request.urlopen(req_status_init, timeout=5) as resp:
|
||||
init_lic_stat = json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
if init_lic_stat.get("status") == "UNLICENSED":
|
||||
unlicensed_enroll_payload = {
|
||||
"client_id": client_id,
|
||||
"hostname": client_id,
|
||||
"os": "linux",
|
||||
"enrollment_secret": enrollment_secret
|
||||
}
|
||||
req_unlicensed = urllib.request.Request(
|
||||
f"http://{HERMES_HOST}:{HERMES_PORT}/api/client/enroll",
|
||||
data=json.dumps(unlicensed_enroll_payload).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req_unlicensed, timeout=5):
|
||||
assert False, "Expected HTTP 403 on unlicensed server enrollment"
|
||||
except urllib.error.HTTPError as e:
|
||||
assert e.code == 403, f"Expected HTTP 403 on unlicensed server, got {e.code}"
|
||||
print("[OK] Unlicensed server correctly rejected edge client enrollment.")
|
||||
|
||||
# 2. Fetch CA CSR from Server
|
||||
req_csr = urllib.request.Request(
|
||||
f"http://{HERMES_HOST}:{HERMES_PORT}/api/license/csr",
|
||||
headers={"Authorization": f"Bearer {auth_token}"}
|
||||
)
|
||||
with urllib.request.urlopen(req_csr, timeout=5) as resp:
|
||||
assert resp.status == 200
|
||||
csr_data = json.loads(resp.read().decode("utf-8"))
|
||||
csr_pem = csr_data["csr"]
|
||||
print("[OK] Fetched subordinate CA CSR from server.")
|
||||
|
||||
# 3. Generate test master CA and sign subordinate CA with 10 seats
|
||||
master_ca_dir = os.path.join(ROOT_DIR, "test_pipeline_master_ca")
|
||||
master_cert, master_key, master_pem, master_key_pem = server_enrollment.generate_ca_if_needed(cert_dir=master_ca_dir, common_name="LOGAR-Master-Root-CA")
|
||||
sub_pem = server_enrollment.sign_ca_csr_with_seats(
|
||||
csr_pem=csr_pem,
|
||||
master_ca_cert_pem=master_pem,
|
||||
master_ca_key_pem=master_key_pem,
|
||||
license_id="LIC-TEST-PIPELINE-10",
|
||||
seats=10,
|
||||
days_valid=365
|
||||
)
|
||||
|
||||
# 4. Import signed license package to server
|
||||
import_payload = {"ca_cert": sub_pem, "master_root_ca": master_pem}
|
||||
req_import = urllib.request.Request(
|
||||
f"http://{HERMES_HOST}:{HERMES_PORT}/api/license/import",
|
||||
data=json.dumps(import_payload).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json", "Authorization": f"Bearer {auth_token}"}
|
||||
)
|
||||
with urllib.request.urlopen(req_import, timeout=5) as resp:
|
||||
assert resp.status == 200
|
||||
import_res = json.loads(resp.read().decode("utf-8"))
|
||||
assert import_res["status"] == "success"
|
||||
assert import_res["seats"] == 10
|
||||
print("[OK] Successfully imported signed license (.lic). Server activated with 10 seats.")
|
||||
|
||||
# 5. Verify license status
|
||||
req_status = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/license/status")
|
||||
with urllib.request.urlopen(req_status, timeout=5) as resp:
|
||||
assert resp.status == 200
|
||||
lic_stat = json.loads(resp.read().decode("utf-8"))
|
||||
assert lic_stat["status"] == "ACTIVE"
|
||||
assert lic_stat["max_seats"] == 10
|
||||
print("[OK] Verified server license status is ACTIVE.")
|
||||
|
||||
print("\n=== [2] Testing Client Dynamic PKI Enrollment API (/api/client/enroll) ===")
|
||||
enroll_url = f"http://{HERMES_HOST}:{HERMES_PORT}/api/client/enroll"
|
||||
@@ -244,7 +320,6 @@ def run_tests():
|
||||
linux_logs = get_recent_linux_logs(hours=24)
|
||||
print(f"[Linux_Client] Successfully queried Linux logs: {len(linux_logs)} candidate entries.")
|
||||
|
||||
import shutil
|
||||
if os.path.exists(cert_dir):
|
||||
shutil.rmtree(cert_dir, ignore_errors=True)
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ class TestServerComponent(unittest.TestCase):
|
||||
test_cert_dir = "test_certs_enroll"
|
||||
try:
|
||||
ca_cert, ca_key, ca_pem, _ = Server.enrollment.generate_ca_if_needed(cert_dir=test_cert_dir)
|
||||
Server.SERVER_STATE["config"] = {"db_path": self.test_db}
|
||||
Server.SERVER_STATE["config"] = {"db_path": self.test_db, "cert_dir": test_cert_dir}
|
||||
Server.SERVER_STATE["ca_cert"] = ca_cert
|
||||
Server.SERVER_STATE["ca_key"] = ca_key
|
||||
Server.SERVER_STATE["ca_cert_pem"] = ca_pem
|
||||
@@ -246,7 +246,7 @@ class TestServerComponent(unittest.TestCase):
|
||||
resp1 = Server.enroll_client(req1)
|
||||
self.assertIn("client_cert", resp1)
|
||||
self.assertIn("client_key", resp1)
|
||||
self.assertEqual(resp1["ca_cert"], ca_pem)
|
||||
self.assertTrue(resp1["ca_cert"].strip().startswith(ca_pem.strip()))
|
||||
|
||||
# 3. Valid enrollment for client 2
|
||||
req2 = Server.ClientEnrollRequest(
|
||||
|
||||
Reference in New Issue
Block a user