Files
SUFW-Timetracking/ezicom_login.py
T

191 lines
5.6 KiB
Python
Executable File

#!/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())