Fix GST operator agent localhost HTTPS detection

This commit is contained in:
A R R R Associates
2026-09-11 11:49:47 +05:30
parent 3e660ec0b9
commit 26c998023d
6 changed files with 90 additions and 30 deletions
@@ -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.
@@ -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()
@@ -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
@@ -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
@@ -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"
@@ -136,7 +136,7 @@
<script>
(() => {
const port = {{ operator_agent_port|int }};
const base = `http://127.0.0.1:${port}`;
const base = `https://localhost:${port}`;
const statusEl = document.getElementById('gst-agent-status');
const panel = document.getElementById('gst-job-panel');
const bar = document.getElementById('gst-job-bar');
@@ -154,8 +154,9 @@
statusEl.className = 'font-semibold text-emerald-700';
return true;
} catch (e) {
statusEl.textContent = 'Not installed / not running';
statusEl.className = 'font-semibold text-red-600';
statusEl.textContent = 'Browser cannot reach local agent';
statusEl.className = 'font-semibold text-amber-700';
statusEl.title = e && e.message ? e.message : 'Local HTTPS connection failed';
return false;
}
}
@@ -194,7 +195,7 @@
if (msg) msg.textContent = 'Starting visible GST login on this workstation…';
try {
const online = await agentStatus();
if (!online) throw new Error('Install/start the GST Operator Agent using the button above, then retry Start GST Download.');
if (!online) throw new Error('The GST Operator Agent is not reachable over local HTTPS. Install/update the agent using the button above, then refresh this page.');
const r = await fetch(base + '/api/gst/start', {
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({token:prepared.token})
});