add script to first connection
This commit is contained in:
1 parent
9add6c5c20
commit
7d6ce40709
3 files changed
+289
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
.env
|
||||||
|
after-login.html
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# eZICOM Login Helper
|
||||||
|
|
||||||
|
Dieses Verzeichnis enthaelt ein kleines Python-Script, das den Login in die
|
||||||
|
eZICOM5-Weboberflaeche automatisiert.
|
||||||
|
|
||||||
|
Das Script macht keinen API-Login. Es ahmt den Browser-Login nach:
|
||||||
|
|
||||||
|
1. `GET /` zum Aufbau der Session-Cookie
|
||||||
|
2. `POST /user` mit den Formularfeldern `action=login`, `uid` und `pw0`
|
||||||
|
|
||||||
|
## Dateien
|
||||||
|
|
||||||
|
- `ezicom_login.py`: Login-Script
|
||||||
|
- `plan.copilot`: kurze Untersuchungsnotizen
|
||||||
|
- `Firefox Connection.html`: gespeicherte Firefox-Profiler-Ausgabe
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- Python 3.10 oder neuer
|
||||||
|
- Keine externen Python-Pakete erforderlich
|
||||||
|
- Netzwerkzugriff auf `https://zm.sufw.de:31443`
|
||||||
|
|
||||||
|
## Nutzung
|
||||||
|
|
||||||
|
Am sichersten ist der interaktive Modus. Dabei werden keine Zugangsdaten in der
|
||||||
|
Shell-History abgelegt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./ezicom_login.py --insecure
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Script fragt dann nach Benutzername und Passwort.
|
||||||
|
|
||||||
|
Optional kann die HTML-Antwort nach dem Login gespeichert werden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./ezicom_login.py --insecure --save-response after-login.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn du Zugangsdaten ueber Environment-Variablen setzen willst:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
EZICOM_USERNAME='dein-user' EZICOM_PASSWORD='dein-passwort' ./ezicom_login.py --insecure
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder trage sie in `.env` ein:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
EZICOM_USERNAME=dein-user
|
||||||
|
EZICOM_PASSWORD=dein-passwort
|
||||||
|
```
|
||||||
|
|
||||||
|
Danach reicht:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./ezicom_login.py --insecure
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternativ per CLI-Argument:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./ezicom_login.py -u 'dein-user' -p 'dein-passwort' --insecure
|
||||||
|
```
|
||||||
|
|
||||||
|
Hinweis: CLI-Argumente koennen in der Shell-History und kurzzeitig in der
|
||||||
|
Prozessliste sichtbar sein. Fuer echte Nutzung ist der interaktive Modus oder
|
||||||
|
ein sauber geschuetzter Secret-Store besser.
|
||||||
|
|
||||||
|
## TLS / Zertifikat
|
||||||
|
|
||||||
|
Bei lokalen Tests konnte Python die Zertifikatskette der eZICOM-Installation
|
||||||
|
nicht verifizieren. Deshalb gibt es die Option:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
--insecure
|
||||||
|
```
|
||||||
|
|
||||||
|
Diese Option deaktiviert die TLS-Zertifikatspruefung. Sauberer waere es, die
|
||||||
|
ausstellende CA lokal zu installieren und das Script ohne `--insecure` zu
|
||||||
|
verwenden.
|
||||||
|
|
||||||
|
## Exit-Codes
|
||||||
|
|
||||||
|
- `0`: Login sieht erfolgreich aus
|
||||||
|
- `1`: Login wurde vom Server abgelehnt, zum Beispiel HTTP 403
|
||||||
|
- `2`: technischer Fehler, zum Beispiel Netzwerk- oder TLS-Problem
|
||||||
|
|
||||||
|
## Bekannte Grenzen
|
||||||
|
|
||||||
|
Das Script loggt nur ein und prueft die unmittelbare Antwort. Es bucht noch
|
||||||
|
keine Zeiten und ruft keine eZICOM-API auf.
|
||||||
|
|
||||||
|
Fuer den naechsten Schritt muessen wir nach erfolgreichem Login untersuchen,
|
||||||
|
welche Requests beim eigentlichen Buchen von Zeiten gesendet werden.
|
||||||
Executable
+190
@@ -0,0 +1,190 @@
|
|||||||
|
#!/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())
|
||||||
Reference in new issue
Block a user