From 26c998023d08151d42ad2bb989b8929aacc2de09 Mon Sep 17 00:00:00 2001 From: A R R R Associates Date: Fri, 11 Sep 2026 11:49:47 +0530 Subject: [PATCH] Fix GST operator agent localhost HTTPS detection --- .../gst_operator_agent_runtime/README.txt | 35 ++++++++----- .../gst_operator_agent.py | 15 ++++-- .../install_gst_operator_agent.ps1 | 50 +++++++++++++++---- .../uninstall_gst_operator_agent.ps1 | 9 ++++ .../accounting/gst_reconciliation_ui.py | 2 +- .../accounting/gst_reconciliation.html | 9 ++-- 6 files changed, 90 insertions(+), 30 deletions(-) diff --git a/app/modules/accounting/gst_operator_agent_runtime/README.txt b/app/modules/accounting/gst_operator_agent_runtime/README.txt index b3512ec..b7d312c 100644 --- a/app/modules/accounting/gst_operator_agent_runtime/README.txt +++ b/app/modules/accounting/gst_operator_agent_runtime/README.txt @@ -1,18 +1,27 @@ -ARRR GST Operator Agent 1.0.0 +ARRR GST Operator Agent 1.0.1 Purpose -- Runs only on the ERP operator workstation. -- Listens on 127.0.0.1:8791 only. -- Opens installed Chrome/Edge visibly. -- Receives only a short-lived ERP job token from the ERP page. -- Redeems GST username/password directly from ERP Credential Vault over HTTPS. -- Autofills username/password; CAPTCHA/OTP remain manual. -- Downloads selected GST return data and uploads it back to ERP for transfer to the configured Storage Agent. -- Does not provide Tally, Accounting Mirror or client storage services. +- Runs only on the GST operator's Windows workstation. +- Listens only on 127.0.0.1:8791. +- Exposes a trusted local HTTPS endpoint at https://localhost:8791. +- Opens visible Chrome/Edge for GST Portal authentication and return download. +- Autofills the selected Credential Vault username/password supplied through the ERP short-lived job token. +- CAPTCHA/OTP remains interactive with the operator. +- Does not run Tally and does not hold the client storage. +- Completed GST packages continue through the ERP to the already configured Local Storage Agent. Install -Right-click PowerShell and run: - powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\install_gst_operator_agent.ps1 +1. Extract the downloaded ZIP. +2. Run PowerShell in the extracted ARRR_GST_Operator_Agent folder: + powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\install_gst_operator_agent.ps1 +3. The installer creates a localhost TLS certificate and trusts it only for the current Windows user. +4. Verify https://localhost:8791/api/status. +5. Refresh the ERP GST Return Reconciliation page. -Requirement -Python 3.11 or later and installed Google Chrome or Microsoft Edge. +Upgrade from 1.0.0 +- Download the current agent ZIP from ERP and run install_gst_operator_agent.ps1 again. +- The installer stops the old HTTP listener, replaces the runtime, installs the trusted localhost certificate and starts v1.0.1 over HTTPS. + +Uninstall +- Run uninstall_gst_operator_agent.ps1 from the installed/downloaded package. +- The installer-created certificate is also removed from the current user's trusted root store. diff --git a/app/modules/accounting/gst_operator_agent_runtime/gst_operator_agent.py b/app/modules/accounting/gst_operator_agent_runtime/gst_operator_agent.py index 85aa0bc..6bc86a5 100644 --- a/app/modules/accounting/gst_operator_agent_runtime/gst_operator_agent.py +++ b/app/modules/accounting/gst_operator_agent_runtime/gst_operator_agent.py @@ -3,6 +3,7 @@ from __future__ import annotations import json import re import shutil +import ssl import threading import time import zipfile @@ -13,7 +14,7 @@ from urllib.parse import parse_qs, urlsplit import requests -VERSION = "1.0.0" +VERSION = "1.0.1" PORT = 8791 ROOT = Path(__file__).resolve().parent DATA = ROOT / "data" @@ -36,7 +37,7 @@ def read_config() -> dict: try: return json.loads(CONFIG_PATH.read_text(encoding="utf-8")) except Exception: - return {"erp_base_url": "https://office.arrrassociates.com", "port": PORT} + return {"erp_base_url": "https://office.arrrassociates.com", "port": PORT, "certfile": str(ROOT / "localhost-cert.pem"), "keyfile": str(ROOT / "localhost-key.pem")} def safe(value: str, fallback: str = "item") -> str: @@ -340,7 +341,7 @@ class Handler(BaseHTTPRequestHandler): def do_GET(self): path = urlsplit(self.path) if path.path == "/api/status": - return self.reply({"ok": True, "name": "ARRR GST Operator Agent", "version": VERSION, "port": PORT}) + return self.reply({"ok": True, "name": "ARRR GST Operator Agent", "version": VERSION, "port": PORT, "transport": "https", "host": "localhost"}) if path.path == "/api/gst/status": job_id = str((parse_qs(path.query).get("job_id") or [""])[0]) return self.reply({"ok": True, "job": read_json(job_path(job_id))}) @@ -362,7 +363,15 @@ def main(): DATA.mkdir(parents=True, exist_ok=True) cfg = read_config() port = int(cfg.get("port") or PORT) + certfile = Path(str(cfg.get("certfile") or (ROOT / "localhost-cert.pem"))) + keyfile = Path(str(cfg.get("keyfile") or (ROOT / "localhost-key.pem"))) + if not certfile.is_file() or not keyfile.is_file(): + raise RuntimeError("GST Operator Agent localhost TLS certificate is missing. Re-run the agent installer.") server = ThreadingHTTPServer(("127.0.0.1", port), Handler) + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.minimum_version = ssl.TLSVersion.TLSv1_2 + context.load_cert_chain(certfile=str(certfile), keyfile=str(keyfile)) + server.socket = context.wrap_socket(server.socket, server_side=True) server.serve_forever() diff --git a/app/modules/accounting/gst_operator_agent_runtime/install_gst_operator_agent.ps1 b/app/modules/accounting/gst_operator_agent_runtime/install_gst_operator_agent.ps1 index b4ec288..0d03fa8 100644 --- a/app/modules/accounting/gst_operator_agent_runtime/install_gst_operator_agent.ps1 +++ b/app/modules/accounting/gst_operator_agent_runtime/install_gst_operator_agent.ps1 @@ -6,8 +6,9 @@ $Source = Split-Path -Parent $MyInvocation.MyCommand.Path $InstallRoot = Join-Path $env:LOCALAPPDATA "ARRR\GSTOperatorAgent" $Venv = Join-Path $InstallRoot ".venv" $Startup = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup\ARRR GST Operator Agent.cmd" +$ThumbprintFile = Join-Path $InstallRoot "cert_thumbprint.txt" -Write-Host "ARRR GST Operator Agent 1.0.0" -ForegroundColor Cyan +Write-Host "ARRR GST Operator Agent 1.0.1" -ForegroundColor Cyan Write-Host "Install root: $InstallRoot" $Python = $null @@ -16,10 +17,25 @@ elseif (Get-Command python -ErrorAction SilentlyContinue) { $Python = @("python" else { throw "Python 3 is required on this workstation. Install Python 3.11+ and run this installer again." } New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null -Copy-Item (Join-Path $Source "gst_operator_agent.py") (Join-Path $InstallRoot "gst_operator_agent.py") -Force -Copy-Item (Join-Path $Source "requirements.txt") (Join-Path $InstallRoot "requirements.txt") -Force +$RuntimeFiles = @( + "gst_operator_agent.py", + "generate_localhost_cert.py", + "requirements.txt", + "README.txt", + "uninstall_gst_operator_agent.ps1" +) +foreach ($Name in $RuntimeFiles) { + $From = Join-Path $Source $Name + if (-not (Test-Path $From)) { throw "Required agent file is missing: $Name" } + Copy-Item $From (Join-Path $InstallRoot $Name) -Force +} -$config = @{ erp_base_url = $ErpBaseUrl.TrimEnd('/'); port = 8791 } | ConvertTo-Json +$config = @{ + erp_base_url = $ErpBaseUrl.TrimEnd('/') + port = 8791 + certfile = (Join-Path $InstallRoot "localhost-cert.pem") + keyfile = (Join-Path $InstallRoot "localhost-key.pem") +} | ConvertTo-Json [System.IO.File]::WriteAllText((Join-Path $InstallRoot "config.json"), $config, [System.Text.UTF8Encoding]::new($false)) if (-not (Test-Path (Join-Path $Venv "Scripts\python.exe"))) { @@ -34,11 +50,26 @@ if ($LASTEXITCODE -ne 0) { throw "pip upgrade failed." } & $VenvPython -m pip install --disable-pip-version-check -r (Join-Path $InstallRoot "requirements.txt") if ($LASTEXITCODE -ne 0) { throw "GST Operator Agent dependencies could not be installed." } +# Remove only the certificate previously installed by this agent instance. +if (Test-Path $ThumbprintFile) { + $OldThumbprint = (Get-Content $ThumbprintFile -Raw -ErrorAction SilentlyContinue).Trim() + if ($OldThumbprint) { + Remove-Item "Cert:\CurrentUser\Root\$OldThumbprint" -Force -ErrorAction SilentlyContinue + } +} + +# Create a localhost-only TLS certificate and trust it for the current Windows user. +& $VenvPython (Join-Path $InstallRoot "generate_localhost_cert.py") +if ($LASTEXITCODE -ne 0) { throw "Could not create the GST Operator Agent localhost certificate." } +$CertFile = Join-Path $InstallRoot "localhost-cert.cer" +$ImportedCert = Import-Certificate -FilePath $CertFile -CertStoreLocation "Cert:\CurrentUser\Root" +if (-not $ImportedCert -or -not $ImportedCert.Thumbprint) { throw "Could not trust the GST Operator Agent localhost certificate." } +[System.IO.File]::WriteAllText($ThumbprintFile, $ImportedCert.Thumbprint, [System.Text.ASCIIEncoding]::new()) + $Pythonw = Join-Path $Venv "Scripts\pythonw.exe" $Cmd = "@echo off`r`nstart `"`" `"$Pythonw`" `"$InstallRoot\gst_operator_agent.py`"`r`n" [System.IO.File]::WriteAllText($Startup, $Cmd, [System.Text.ASCIIEncoding]::new()) -# Stop an older copy bound to the operator port, if present. try { $Connections = Get-NetTCPConnection -LocalPort 8791 -State Listen -ErrorAction SilentlyContinue foreach ($Connection in $Connections) { @@ -47,11 +78,12 @@ try { } catch {} Start-Process -FilePath $Pythonw -ArgumentList @((Join-Path $InstallRoot "gst_operator_agent.py")) -WorkingDirectory $InstallRoot -Start-Sleep -Seconds 2 +Start-Sleep -Seconds 3 try { - $Status = Invoke-RestMethod -Uri "http://127.0.0.1:8791/api/status" -TimeoutSec 5 - Write-Host "Agent status: $($Status.name) v$($Status.version) - Online" -ForegroundColor Green + $Status = Invoke-RestMethod -Uri "https://localhost:8791/api/status" -TimeoutSec 8 + Write-Host "Agent status: $($Status.name) v$($Status.version) - Online via HTTPS" -ForegroundColor Green } catch { - throw "GST Operator Agent was installed but did not answer on http://127.0.0.1:8791. $($_.Exception.Message)" + throw "GST Operator Agent was installed but did not answer on https://localhost:8791. $($_.Exception.Message)" } Write-Host "Installed successfully. It will start automatically when this Windows user logs in." -ForegroundColor Green +Write-Host "Local status URL: https://localhost:8791/api/status" -ForegroundColor Cyan diff --git a/app/modules/accounting/gst_operator_agent_runtime/uninstall_gst_operator_agent.ps1 b/app/modules/accounting/gst_operator_agent_runtime/uninstall_gst_operator_agent.ps1 index 5856420..edabca6 100644 --- a/app/modules/accounting/gst_operator_agent_runtime/uninstall_gst_operator_agent.ps1 +++ b/app/modules/accounting/gst_operator_agent_runtime/uninstall_gst_operator_agent.ps1 @@ -1,11 +1,20 @@ $ErrorActionPreference = "Stop" $InstallRoot = Join-Path $env:LOCALAPPDATA "ARRR\GSTOperatorAgent" $Startup = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup\ARRR GST Operator Agent.cmd" +$ThumbprintFile = Join-Path $InstallRoot "cert_thumbprint.txt" + try { Get-NetTCPConnection -LocalPort 8791 -State Listen -ErrorAction SilentlyContinue | ForEach-Object { if ($_.OwningProcess) { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue } } } catch {} + +if (Test-Path $ThumbprintFile) { + $Thumbprint = (Get-Content $ThumbprintFile -Raw -ErrorAction SilentlyContinue).Trim() + if ($Thumbprint) { + Remove-Item "Cert:\CurrentUser\Root\$Thumbprint" -Force -ErrorAction SilentlyContinue + } +} Remove-Item $Startup -Force -ErrorAction SilentlyContinue Remove-Item $InstallRoot -Recurse -Force -ErrorAction SilentlyContinue Write-Host "ARRR GST Operator Agent removed." -ForegroundColor Green diff --git a/app/modules/accounting/gst_reconciliation_ui.py b/app/modules/accounting/gst_reconciliation_ui.py index 38a849c..66b10be 100644 --- a/app/modules/accounting/gst_reconciliation_ui.py +++ b/app/modules/accounting/gst_reconciliation_ui.py @@ -147,7 +147,7 @@ router = APIRouter(prefix="/tools/accounting/gst-reconciliation", tags=["account _TOKEN_PURPOSE = "gst_lightweight_operator_v2" _TOKEN_MINUTES = 15 _UPLOAD_ROOT = Path(tempfile.gettempdir()) / "audit_firm_gst_operator_uploads" -_OPERATOR_AGENT_VERSION = "1.0.0" +_OPERATOR_AGENT_VERSION = "1.0.1" _OPERATOR_AGENT_PORT = 8791 _OPERATOR_RUNTIME_ROOT = Path(__file__).resolve().parent / "gst_operator_agent_runtime" diff --git a/app/modules/accounting/templates/accounting/gst_reconciliation.html b/app/modules/accounting/templates/accounting/gst_reconciliation.html index ea56d95..fd8aa29 100644 --- a/app/modules/accounting/templates/accounting/gst_reconciliation.html +++ b/app/modules/accounting/templates/accounting/gst_reconciliation.html @@ -136,7 +136,7 @@