dodil/erp-brick-tax
DODIL ERP brick: tax
src/brick_tax/domain/fraud.py
"""HMRC's fraud-prevention headers for a web app that calls HMRC through a server (connection method WEB_APP_VIA_SERVER, TAX-31).
The browser supplies what only it knows (user agent, device id, screens, window size, timezone, public IP and port with their
timestamp, multi-factor); this brick adds what only the server knows (the connection method, the user id, the vendor's own
product, version, licence, public IP and the forwarding chain). A header either side cannot supply refuses the submission: a
script has no browser, and HMRC rejects a call without them.
"""
import os
from typing import Optional
from urllib.parse import quote
from brick_tax.domain.errors import unmet
METHOD = "WEB_APP_VIA_SERVER"
CLIENT = ("Gov-Client-Browser-JS-User-Agent", "Gov-Client-Device-ID", "Gov-Client-Multi-Factor", "Gov-Client-Public-IP",
"Gov-Client-Public-IP-Timestamp", "Gov-Client-Public-Port", "Gov-Client-Screens", "Gov-Client-Timezone",
"Gov-Client-Window-Size")
SERVER = ("Gov-Client-Connection-Method", "Gov-Client-User-IDs", "Gov-Vendor-Forwarded", "Gov-Vendor-License-IDs",
"Gov-Vendor-Product-Name", "Gov-Vendor-Public-IP", "Gov-Vendor-Version")
REQUIRED = tuple(sorted(CLIENT + SERVER))
def build(client: Optional[dict], *, user_id: str, version: str) -> dict:
"""The full header set for a call made for a person, or a refusal {gate: fraud_headers_complete} naming the first missing one."""
given = {str(k).lower(): str(v).strip() for k, v in (client or {}).items() if v is not None}
vendor_ip = os.environ.get("VENDOR_PUBLIC_IP", "").strip()
client_ip = given.get("gov-client-public-ip", "")
headers = {h: given.get(h.lower(), "") for h in CLIENT}
headers.update({
"Gov-Client-Connection-Method": METHOD,
"Gov-Client-User-IDs": f"dodil={quote(str(user_id), safe='')}" if user_id else "",
"Gov-Vendor-Product-Name": "DODIL%20Tax",
"Gov-Vendor-Version": f"dodil-tax={version}",
"Gov-Vendor-License-IDs": os.environ.get("VENDOR_LICENSE_IDS", "").strip(),
"Gov-Vendor-Public-IP": vendor_ip,
"Gov-Vendor-Forwarded": f"by={vendor_ip}&for={client_ip}" if vendor_ip and client_ip else "",
})
missing = [h for h in REQUIRED if not headers.get(h)]
if missing:
raise unmet("fraud_headers_complete", f"HMRC requires {missing[0]}", "submit from the web screen, not a script")
return headers