Fix GST operator agent localhost HTTPS detection
This commit is contained in:
@@ -1,18 +1,27 @@
|
|||||||
ARRR GST Operator Agent 1.0.0
|
ARRR GST Operator Agent 1.0.1
|
||||||
|
|
||||||
Purpose
|
Purpose
|
||||||
- Runs only on the ERP operator workstation.
|
- Runs only on the GST operator's Windows workstation.
|
||||||
- Listens on 127.0.0.1:8791 only.
|
- Listens only on 127.0.0.1:8791.
|
||||||
- Opens installed Chrome/Edge visibly.
|
- Exposes a trusted local HTTPS endpoint at https://localhost:8791.
|
||||||
- Receives only a short-lived ERP job token from the ERP page.
|
- Opens visible Chrome/Edge for GST Portal authentication and return download.
|
||||||
- Redeems GST username/password directly from ERP Credential Vault over HTTPS.
|
- Autofills the selected Credential Vault username/password supplied through the ERP short-lived job token.
|
||||||
- Autofills username/password; CAPTCHA/OTP remain manual.
|
- CAPTCHA/OTP remains interactive with the operator.
|
||||||
- Downloads selected GST return data and uploads it back to ERP for transfer to the configured Storage Agent.
|
- Does not run Tally and does not hold the client storage.
|
||||||
- Does not provide Tally, Accounting Mirror or client storage services.
|
- Completed GST packages continue through the ERP to the already configured Local Storage Agent.
|
||||||
|
|
||||||
Install
|
Install
|
||||||
Right-click PowerShell and run:
|
1. Extract the downloaded ZIP.
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\install_gst_operator_agent.ps1
|
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
|
Upgrade from 1.0.0
|
||||||
Python 3.11 or later and installed Google Chrome or Microsoft Edge.
|
- 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 json
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import ssl
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import zipfile
|
import zipfile
|
||||||
@@ -13,7 +14,7 @@ from urllib.parse import parse_qs, urlsplit
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
VERSION = "1.0.0"
|
VERSION = "1.0.1"
|
||||||
PORT = 8791
|
PORT = 8791
|
||||||
ROOT = Path(__file__).resolve().parent
|
ROOT = Path(__file__).resolve().parent
|
||||||
DATA = ROOT / "data"
|
DATA = ROOT / "data"
|
||||||
@@ -36,7 +37,7 @@ def read_config() -> dict:
|
|||||||
try:
|
try:
|
||||||
return json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
return json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
||||||
except Exception:
|
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:
|
def safe(value: str, fallback: str = "item") -> str:
|
||||||
@@ -340,7 +341,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
path = urlsplit(self.path)
|
path = urlsplit(self.path)
|
||||||
if path.path == "/api/status":
|
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":
|
if path.path == "/api/gst/status":
|
||||||
job_id = str((parse_qs(path.query).get("job_id") or [""])[0])
|
job_id = str((parse_qs(path.query).get("job_id") or [""])[0])
|
||||||
return self.reply({"ok": True, "job": read_json(job_path(job_id))})
|
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)
|
DATA.mkdir(parents=True, exist_ok=True)
|
||||||
cfg = read_config()
|
cfg = read_config()
|
||||||
port = int(cfg.get("port") or PORT)
|
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)
|
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()
|
server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ $Source = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|||||||
$InstallRoot = Join-Path $env:LOCALAPPDATA "ARRR\GSTOperatorAgent"
|
$InstallRoot = Join-Path $env:LOCALAPPDATA "ARRR\GSTOperatorAgent"
|
||||||
$Venv = Join-Path $InstallRoot ".venv"
|
$Venv = Join-Path $InstallRoot ".venv"
|
||||||
$Startup = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup\ARRR GST Operator Agent.cmd"
|
$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"
|
Write-Host "Install root: $InstallRoot"
|
||||||
|
|
||||||
$Python = $null
|
$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." }
|
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
|
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
|
$RuntimeFiles = @(
|
||||||
Copy-Item (Join-Path $Source "requirements.txt") (Join-Path $InstallRoot "requirements.txt") -Force
|
"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))
|
[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"))) {
|
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")
|
& $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." }
|
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"
|
$Pythonw = Join-Path $Venv "Scripts\pythonw.exe"
|
||||||
$Cmd = "@echo off`r`nstart `"`" `"$Pythonw`" `"$InstallRoot\gst_operator_agent.py`"`r`n"
|
$Cmd = "@echo off`r`nstart `"`" `"$Pythonw`" `"$InstallRoot\gst_operator_agent.py`"`r`n"
|
||||||
[System.IO.File]::WriteAllText($Startup, $Cmd, [System.Text.ASCIIEncoding]::new())
|
[System.IO.File]::WriteAllText($Startup, $Cmd, [System.Text.ASCIIEncoding]::new())
|
||||||
|
|
||||||
# Stop an older copy bound to the operator port, if present.
|
|
||||||
try {
|
try {
|
||||||
$Connections = Get-NetTCPConnection -LocalPort 8791 -State Listen -ErrorAction SilentlyContinue
|
$Connections = Get-NetTCPConnection -LocalPort 8791 -State Listen -ErrorAction SilentlyContinue
|
||||||
foreach ($Connection in $Connections) {
|
foreach ($Connection in $Connections) {
|
||||||
@@ -47,11 +78,12 @@ try {
|
|||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
Start-Process -FilePath $Pythonw -ArgumentList @((Join-Path $InstallRoot "gst_operator_agent.py")) -WorkingDirectory $InstallRoot
|
Start-Process -FilePath $Pythonw -ArgumentList @((Join-Path $InstallRoot "gst_operator_agent.py")) -WorkingDirectory $InstallRoot
|
||||||
Start-Sleep -Seconds 2
|
Start-Sleep -Seconds 3
|
||||||
try {
|
try {
|
||||||
$Status = Invoke-RestMethod -Uri "http://127.0.0.1:8791/api/status" -TimeoutSec 5
|
$Status = Invoke-RestMethod -Uri "https://localhost:8791/api/status" -TimeoutSec 8
|
||||||
Write-Host "Agent status: $($Status.name) v$($Status.version) - Online" -ForegroundColor Green
|
Write-Host "Agent status: $($Status.name) v$($Status.version) - Online via HTTPS" -ForegroundColor Green
|
||||||
} catch {
|
} 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 "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"
|
$ErrorActionPreference = "Stop"
|
||||||
$InstallRoot = Join-Path $env:LOCALAPPDATA "ARRR\GSTOperatorAgent"
|
$InstallRoot = Join-Path $env:LOCALAPPDATA "ARRR\GSTOperatorAgent"
|
||||||
$Startup = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup\ARRR GST Operator Agent.cmd"
|
$Startup = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup\ARRR GST Operator Agent.cmd"
|
||||||
|
$ThumbprintFile = Join-Path $InstallRoot "cert_thumbprint.txt"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Get-NetTCPConnection -LocalPort 8791 -State Listen -ErrorAction SilentlyContinue | ForEach-Object {
|
Get-NetTCPConnection -LocalPort 8791 -State Listen -ErrorAction SilentlyContinue | ForEach-Object {
|
||||||
if ($_.OwningProcess) { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }
|
if ($_.OwningProcess) { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }
|
||||||
}
|
}
|
||||||
} catch {}
|
} 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 $Startup -Force -ErrorAction SilentlyContinue
|
||||||
Remove-Item $InstallRoot -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item $InstallRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
Write-Host "ARRR GST Operator Agent removed." -ForegroundColor Green
|
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_PURPOSE = "gst_lightweight_operator_v2"
|
||||||
_TOKEN_MINUTES = 15
|
_TOKEN_MINUTES = 15
|
||||||
_UPLOAD_ROOT = Path(tempfile.gettempdir()) / "audit_firm_gst_operator_uploads"
|
_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_AGENT_PORT = 8791
|
||||||
_OPERATOR_RUNTIME_ROOT = Path(__file__).resolve().parent / "gst_operator_agent_runtime"
|
_OPERATOR_RUNTIME_ROOT = Path(__file__).resolve().parent / "gst_operator_agent_runtime"
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
<script>
|
<script>
|
||||||
(() => {
|
(() => {
|
||||||
const port = {{ operator_agent_port|int }};
|
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 statusEl = document.getElementById('gst-agent-status');
|
||||||
const panel = document.getElementById('gst-job-panel');
|
const panel = document.getElementById('gst-job-panel');
|
||||||
const bar = document.getElementById('gst-job-bar');
|
const bar = document.getElementById('gst-job-bar');
|
||||||
@@ -154,8 +154,9 @@
|
|||||||
statusEl.className = 'font-semibold text-emerald-700';
|
statusEl.className = 'font-semibold text-emerald-700';
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
statusEl.textContent = 'Not installed / not running';
|
statusEl.textContent = 'Browser cannot reach local agent';
|
||||||
statusEl.className = 'font-semibold text-red-600';
|
statusEl.className = 'font-semibold text-amber-700';
|
||||||
|
statusEl.title = e && e.message ? e.message : 'Local HTTPS connection failed';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,7 +195,7 @@
|
|||||||
if (msg) msg.textContent = 'Starting visible GST login on this workstation…';
|
if (msg) msg.textContent = 'Starting visible GST login on this workstation…';
|
||||||
try {
|
try {
|
||||||
const online = await agentStatus();
|
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', {
|
const r = await fetch(base + '/api/gst/start', {
|
||||||
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({token:prepared.token})
|
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({token:prepared.token})
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user