#!/usr/bin/env python3 """Small eZICOM5 login helper. Credentials are read from environment variables, command line arguments, or interactive prompts. They are never written to disk by this script. """ from __future__ import annotations import argparse import getpass import http.cookiejar import os import ssl import sys import urllib.error import urllib.parse import urllib.request from pathlib import Path DEFAULT_BASE_URL = "https://zm.sufw.de:31443" DEFAULT_USER_AGENT = ( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:151.0) " "Gecko/20100101 Firefox/151.0" ) def load_dotenv(path: Path = Path(".env")) -> None: if not path.exists(): return for line in path.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) key = key.strip() value = value.strip().strip("'\"") if key and key not in os.environ: os.environ[key] = value def make_opener(insecure: bool) -> urllib.request.OpenerDirector: cookie_jar = http.cookiejar.CookieJar() handlers: list[urllib.request.BaseHandler] = [ urllib.request.HTTPCookieProcessor(cookie_jar) ] if insecure: context = ssl._create_unverified_context() handlers.append(urllib.request.HTTPSHandler(context=context)) return urllib.request.build_opener(*handlers) def request( opener: urllib.request.OpenerDirector, url: str, *, method: str = "GET", data: dict[str, str] | None = None, headers: dict[str, str] | None = None, timeout: float = 20.0, ) -> tuple[int, str, str]: body = None request_headers = { "User-Agent": DEFAULT_USER_AGENT, "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", } if headers: request_headers.update(headers) if data is not None: body = urllib.parse.urlencode(data).encode("ascii") request_headers["Content-Type"] = "application/x-www-form-urlencoded" req = urllib.request.Request( url, data=body, headers=request_headers, method=method, ) try: with opener.open(req, timeout=timeout) as response: raw = response.read() charset = response.headers.get_content_charset() or "iso-8859-1" return response.status, response.geturl(), raw.decode(charset, "replace") except urllib.error.HTTPError as error: raw = error.read() charset = error.headers.get_content_charset() or "iso-8859-1" return error.code, error.geturl(), raw.decode(charset, "replace") def login( base_url: str, username: str, password: str, *, insecure: bool, save_response: Path | None, ) -> int: base_url = base_url.rstrip("/") opener = make_opener(insecure) try: initial_status, _, _ = request(opener, base_url + "/") except urllib.error.URLError as error: print(f"Could not open initial page: {error}", file=sys.stderr) if "CERTIFICATE_VERIFY_FAILED" in str(error): print("Hint: retry with --insecure or install the issuing CA.", file=sys.stderr) return 2 if initial_status >= 400: print(f"Initial page returned HTTP {initial_status}", file=sys.stderr) return 2 try: status, final_url, html = request( opener, base_url + "/user", method="POST", data={"action": "login", "uid": username, "pw0": password}, headers={ "Origin": base_url, "Referer": base_url + "/", }, ) except urllib.error.URLError as error: print(f"Could not submit login form: {error}", file=sys.stderr) if "CERTIFICATE_VERIFY_FAILED" in str(error): print("Hint: retry with --insecure or install the issuing CA.", file=sys.stderr) return 2 if save_response: save_response.write_text(html, encoding="utf-8") lower_html = html.lower() failed = ( status in {401, 403} or "benutzername und/oder kennwort" in lower_html or "error 403" in lower_html or "403 - verboten" in lower_html ) if failed: print(f"Login failed: HTTP {status} ({final_url})", file=sys.stderr) return 1 print(f"Login looks successful: HTTP {status} ({final_url})") if save_response: print(f"Saved response HTML to {save_response}") return 0 def parse_args() -> argparse.Namespace: load_dotenv() parser = argparse.ArgumentParser( description="Log in to the SUFW eZICOM5 web UI." ) parser.add_argument("--base-url", default=DEFAULT_BASE_URL) parser.add_argument("-u", "--username", default=os.getenv("EZICOM_USERNAME")) parser.add_argument("-p", "--password", default=os.getenv("EZICOM_PASSWORD")) parser.add_argument( "--insecure", action="store_true", help="Disable TLS certificate verification if the server certificate cannot be verified.", ) parser.add_argument( "--save-response", type=Path, help="Optional path for saving the post-login HTML response for inspection.", ) return parser.parse_args() def main() -> int: args = parse_args() username = args.username or input("eZICOM username: ") password = args.password or getpass.getpass("eZICOM password: ") return login( args.base_url, username, password, insecure=args.insecure, save_response=args.save_response, ) if __name__ == "__main__": raise SystemExit(main())