diff --git a/compilation/install_linux_server.sh b/compilation/install_linux_server.sh index 60d8534..5d96b8e 100644 --- a/compilation/install_linux_server.sh +++ b/compilation/install_linux_server.sh @@ -3,7 +3,6 @@ set -euo pipefail INSTALL_DIR="/opt/logar-server" CONFIG_DIR="/etc/logar" -LIC_FILE="" # Parse command line options while [[ $# -gt 0 ]]; do @@ -12,10 +11,6 @@ while [[ $# -gt 0 ]]; do CONFIG_DIR="$2" shift 2 ;; - --lic|--license|--import-lic) - LIC_FILE="$2" - shift 2 - ;; *) shift ;; @@ -27,16 +22,11 @@ echo " LOGAR Server Enterprise Linux Installer" echo "============================================================" # Interactive configuration if running in a terminal without CLI arguments -if [ -t 0 ] && [ -z "${LIC_FILE}" ]; then +if [ -t 0 ]; then read -r -p "Enter Data and Configuration Directory [${CONFIG_DIR}]: " USER_CFG if [ -n "${USER_CFG}" ]; then CONFIG_DIR="${USER_CFG}" fi - - read -r -p "Enter path to .lic license file (press Enter to skip): " USER_LIC - if [ -n "${USER_LIC}" ]; then - LIC_FILE="${USER_LIC}" - fi fi mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}" "/var/log/logar" @@ -55,12 +45,6 @@ if [ -f "${INSTALL_DIR}/Server" ]; then chmod +x "${INSTALL_DIR}/Server" fi -# If license file provided, import it during setup -if [ -n "${LIC_FILE}" ] && [ -f "${LIC_FILE}" ]; then - echo "[*] Importing license file: ${LIC_FILE}..." - "${INSTALL_DIR}/Server" --config "${CONFIG_DIR}/server_config.json" --import-lic "${LIC_FILE}" || echo "[!] License import returned warning. Continuing." -fi - cat < /etc/systemd/system/logar-server.service [Unit] Description=LOGAR Hub and Aggregator Engine diff --git a/compilation/installer_server.iss b/compilation/installer_server.iss index 31be24f..feaa556 100644 --- a/compilation/installer_server.iss +++ b/compilation/installer_server.iss @@ -51,7 +51,6 @@ Type: filesandordirs; Name: "{commonappdata}\LOGAR-Server" [Code] var DataDirPage: TInputDirWizardPage; - LicFilePage: TInputFileWizardPage; procedure InitializeWizard; begin @@ -66,15 +65,6 @@ begin ); DataDirPage.Add('Data Directory:'); DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR-Server'); - - LicFilePage := CreateInputFilePage( - DataDirPage.ID, - 'Enterprise License Activation (Optional)', - 'Do you have an activated .lic license file to import during installation?', - 'Select your signed .lic file to activate the server immediately.'#13#10#13#10 + - 'You can also leave this blank to initialize with a default evaluation license and activate later via the web dashboard.' - ); - LicFilePage.Add('License File (.lic):', 'License Files (*.lic)|*.lic|All Files (*.*)|*.*', '.lic'); end; function GetDataDir(Param: String): String; @@ -84,32 +74,11 @@ begin Result := ExpandConstant('{commonappdata}\LOGAR-Server'); end; -function GetLicFile(Param: String): String; -begin - Result := LicFilePage.Values[0]; -end; - procedure CurStepChanged(CurStep: TSetupStep); -var - LicPath: String; - ResultCode: Integer; begin if CurStep = ssPostInstall then begin RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'DataDir', GetDataDir('')); - - LicPath := GetLicFile(''); - if (LicPath <> '') and FileExists(LicPath) then - begin - Exec( - ExpandConstant('{app}\Server.exe'), - '--config "' + GetDataDir('') + '\server_config.json" --import-lic "' + LicPath + '"', - ExpandConstant('{app}'), - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ); - end; end; end; diff --git a/compilation/installer_server.nsi b/compilation/installer_server.nsi index 0ea7335..f0c825e 100644 --- a/compilation/installer_server.nsi +++ b/compilation/installer_server.nsi @@ -17,10 +17,6 @@ VIAddVersionKey "FileVersion" "0.3.1.0" VIAddVersionKey "ProductVersion" "0.3.1.0" Var DATADIR -Var LICFILE -Var LicDialog -Var LicFileInput -Var LicBrowseBtn # --- UI Pages --- !define MUI_ABORTWARNING @@ -44,9 +40,6 @@ Var LicBrowseBtn !define MUI_DIRECTORYPAGE_VARIABLE $DATADIR !insertmacro MUI_PAGE_DIRECTORY -# Custom Page: License File Selection -Page custom LicensePageCreate LicensePageLeave - # Instfiles Page !insertmacro MUI_PAGE_INSTFILES @@ -64,44 +57,8 @@ Page custom LicensePageCreate LicensePageLeave Function .onInit SetShellVarContext all StrCpy $DATADIR "$APPDATA\LOGAR-Server" - StrCpy $LICFILE "" FunctionEnd -Function LicensePageCreate - nsDialogs::Create 1018 - Pop $LicDialog - ${If} $LicDialog == error - Abort - ${EndIf} - - !insertmacro MUI_HEADER_TEXT "Enterprise License Activation (Optional)" "Choose a .lic license file to activate your server during installation." - - ${NSD_CreateLabel} 0 0 100% 30u "If you have received an activated .lic license file, select it below to activate the server immediately.$\r$\n$\r$\nLeave this field blank to use the default evaluation license." - Pop $0 - - ${NSD_CreateText} 0 38u 75% 12u "$LICFILE" - Pop $LicFileInput - - ${NSD_CreateButton} 78% 37u 22% 14u "Browse..." - Pop $LicBrowseBtn - ${NSD_OnClick} $LicBrowseBtn OnBrowseLic - - nsDialogs::Show -Function - -Function OnBrowseLic - nsDialogs::SelectFileDialog open "" "LOGAR License Files (*.lic)|*.lic|All Files (*.*)|*.*" - Pop $0 - ${If} $0 != "" - StrCpy $LICFILE $0 - ${NSD_SetText} $LicFileInput $LICFILE - ${EndIf} -Function - -Function LicensePageLeave - ${NSD_GetText} $LicFileInput $LICFILE -Function - Section "Install" SetShellVarContext all SetOutPath "$INSTDIR" @@ -112,12 +69,6 @@ Section "Install" CreateDirectory "$DATADIR" ExecWait 'icacls "$DATADIR" /grant "*S-1-5-32-545:(OI)(CI)M" /T /Q' - # If license file was selected, import before starting service - ${If} $LICFILE != "" - ${AndIf} ${FileExists} "$LICFILE" - ExecWait '"$INSTDIR\Server.exe" --config "$DATADIR\server_config.json" --import-lic "$LICFILE"' - ${EndIf} - # 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"' diff --git a/src/Server.py b/src/Server.py index 1e37d8b..55be71a 100644 --- a/src/Server.py +++ b/src/Server.py @@ -470,7 +470,7 @@ def load_or_init_config(config_path: str = CONFIG_FILE_NAME) -> Dict[str, Any]: if "enrollment_secret" not in config: config["enrollment_secret"] = secrets.token_hex(24) if "max_seats" not in config: - config["max_seats"] = 10 + config["max_seats"] = 0 if "cert_dir" not in config: config["cert_dir"] = "certs" if "tls_enabled" not in config: @@ -517,7 +517,8 @@ def load_or_init_config(config_path: str = CONFIG_FILE_NAME) -> Dict[str, Any]: "auth_token": auth_token, "enrollment_secret": enrollment_secret, "session_secret": session_sec, - "max_seats": 10, + "max_seats": 0, + "license_status": "unlicensed", "license_server_url": "https://logar.eibl.tech", "cert_dir": cert_dir, "tls_enabled": True, @@ -646,7 +647,7 @@ def format_relative_time(dt_str: Optional[str]) -> str: return dt_str[:16] if dt_str else "N/A" -def init_db(db_path: str, enrollment_secret: Optional[str] = None, max_seats: int = 10) -> str: +def init_db(db_path: str, enrollment_secret: Optional[str] = None, max_seats: int = 0) -> str: """Initializes the SQLite schema for multi-run temporal tracking, client tracking, users, and license quota.""" def _execute_schema(target_path: str): target_dir = os.path.dirname(os.path.abspath(target_path)) @@ -692,13 +693,13 @@ def init_db(db_path: str, enrollment_secret: Optional[str] = None, max_seats: in conn.execute(""" CREATE TABLE IF NOT EXISTS license_config ( id INTEGER PRIMARY KEY CHECK (id = 1), - max_seats INTEGER NOT NULL DEFAULT 10, + max_seats INTEGER NOT NULL DEFAULT 0, enrollment_secret TEXT NOT NULL, license_id TEXT, license_server_url TEXT, issued_at TEXT, expires_at TEXT, - license_status TEXT DEFAULT 'active' + license_status TEXT DEFAULT 'unlicensed' ) """) @@ -708,7 +709,7 @@ def init_db(db_path: str, enrollment_secret: Optional[str] = None, max_seats: in ("license_server_url", "TEXT"), ("issued_at", "TEXT"), ("expires_at", "TEXT"), - ("license_status", "TEXT DEFAULT 'active'") + ("license_status", "TEXT DEFAULT 'unlicensed'") ]: try: conn.execute(f"ALTER TABLE license_config ADD COLUMN {col} {col_def}") @@ -769,8 +770,8 @@ def init_db(db_path: str, enrollment_secret: Optional[str] = None, max_seats: in if enrollment_secret: conn.execute(""" - INSERT OR IGNORE INTO license_config (id, max_seats, enrollment_secret) - VALUES (1, ?, ?) + INSERT OR IGNORE INTO license_config (id, max_seats, enrollment_secret, license_status) + VALUES (1, ?, ?, 'unlicensed') """, (max_seats, enrollment_secret)) conn.commit() @@ -1317,13 +1318,18 @@ def enroll_client(req: ClientEnrollRequest, request: Request = None): c = conn.cursor() # 1. Validate enrollment secret against license_config - c.execute("SELECT enrollment_secret, max_seats FROM license_config WHERE id = 1") + c.execute("SELECT enrollment_secret, max_seats, license_status FROM license_config WHERE id = 1") row = c.fetchone() if not row: conn.close() raise HTTPException(status_code=500, detail="License configuration not initialized") - expected_secret, max_seats = row + expected_secret, max_seats = row[0], row[1] + license_status = row[2] if len(row) > 2 and row[2] else "unlicensed" + if max_seats <= 0 or license_status != "active": + conn.close() + raise HTTPException(status_code=403, detail="Server is unlicensed. Import an activated .lic file via the WebUI before enrolling edge clients.") + if not secrets.compare_digest(str(req.enrollment_secret), str(expected_secret)): conn.close() record_failed_ip_attempt(client_ip, _ENROLL_FAILURES) @@ -1647,14 +1653,18 @@ def get_license_summary() -> Dict[str, Any]: ca_path = os.path.join(cert_dir, "ca.crt") db_path = config.get("db_path", DEFAULT_DB_FILE) - max_seats = config.get("max_seats", 10) + max_seats = config.get("max_seats", 0) + license_status_db = "unlicensed" try: conn = sqlite3.connect(db_path) c = conn.cursor() - c.execute("SELECT max_seats FROM license_config WHERE id = 1") + c.execute("SELECT max_seats, license_status FROM license_config WHERE id = 1") row = c.fetchone() - if row and row[0]: - max_seats = row[0] + if row: + if row[0] is not None: + max_seats = row[0] + if len(row) > 1 and row[1]: + license_status_db = row[1] c.execute("SELECT COUNT(*) FROM clients WHERE status = 'active'") active_seats = c.fetchone()[0] c.execute("SELECT COUNT(*) FROM clients WHERE status = 'revoked'") @@ -1679,13 +1689,15 @@ def get_license_summary() -> Dict[str, Any]: else: info = {"valid": False, "is_self_signed": False, "is_expired": False, "days_remaining": 0} - license_id = info.get("license_id") or "UNLICENSED-LOCAL" + license_id = info.get("license_id") or "UNLICENSED" issuer_url = info.get("issuer_url") or config.get("license_server_url", "https://logar.eibl.tech") - status_label = "ACTIVE" + if info.get("is_expired"): status_label = "EXPIRED" - elif not info.get("seats") and info.get("is_self_signed"): - status_label = "SELF-SIGNED" + elif max_seats <= 0 or license_status_db != "active" or info.get("is_self_signed") or not info.get("seats"): + status_label = "UNLICENSED" + else: + status_label = "ACTIVE" pct = int((active_seats / max_seats * 100)) if max_seats > 0 else 0 return { @@ -1749,7 +1761,7 @@ 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") + 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") @@ -1762,13 +1774,18 @@ async def import_signed_license(req: Request): cert_dir = config.get("cert_dir", "certs") try: - res = enrollment.import_license_package(raw_text, cert_dir=cert_dir) + res = enrollment.import_license_package( + raw_text, + cert_dir=cert_dir, + server_fingerprint=config.get("server_fingerprint", ""), + hostname=config.get("server_name", socket.gethostname()) + ) except ValueError as ve: raise HTTPException(status_code=400, detail=str(ve)) info = res["license_info"] ca_cert_pem = res["ca_cert_pem"] - new_seats = info.get("seats") or config.get("max_seats", 10) + new_seats = info.get("seats") or config.get("max_seats", 0) config["max_seats"] = new_seats db_path = config.get("db_path", DEFAULT_DB_FILE) conn = sqlite3.connect(db_path) @@ -2013,6 +2030,24 @@ def serve_webui(request: Request = None): bar_color = "#10b981" if pct < 70 else ("#f59e0b" if pct < 90 else "#ef4444") badge_status_cls = "badge-active" if summary["status"] == "ACTIVE" else ("badge-warning" if summary["status"] == "SELF-SIGNED" else "badge-revoked") + unlicensed_banner_html = "" + if summary["status"] == "UNLICENSED" or summary["max_seats"] == 0: + unlicensed_banner_html = f""" +
+
+
⚠️
+
+
LOGAR Server is Unlicensed (0 Active Seats)
+
Forwarder enrollment and log ingestion are restricted. Import an activated .lic package to enable edge client connections and seat provisioning.
+
+
+
+ + +
+
+ """ + detected_host = (request.headers.get("host", "").split(":")[0] if request and request.headers else "") or config.get("tcp_host", "127.0.0.1") if detected_host in ("0.0.0.0", "localhost", ""): detected_host = socket.gethostname() @@ -2232,6 +2267,8 @@ def serve_webui(request: Request = None):
+ {unlicensed_banner_html} +
@@ -2383,7 +2420,10 @@ def serve_webui(request: Request = None):
PKI & Seat Synchronization:
- + + + +
@@ -2459,6 +2499,28 @@ def serve_webui(request: Request = None):
+ + +