Zone gezielt bei Plesk erfragen statt alle Domains aufzulisten
Ein Name wie endian.fon-aria.de ist oft nur ein Record in der Zone fon-aria.de und existiert weder als Domain noch als Abo. Ausserdem darf nicht jeder API-User alle Domains des Servers auflisten - dann brach das Tool mit "No domains found on the Plesk server" ab, obwohl die Zone da war. Die Zone wird jetzt Label fuer Label von unten nach oben gezielt abgefragt (endian.fon-aria.de -> fon-aria.de), jeweils per site.get und webspace.get mit Namensfilter. Das Auflisten aller Domains dient nur noch der Fehlermeldung, die jetzt auch zeigt, welche Namen probiert wurden und was Plesk dazu gesagt hat. - neue Option --zone, um die Zone bei Bedarf fest vorzugeben - get_rec meldet jetzt als Warnung, wenn Plesk keine Records liefert - run.sh baut das Image immer (Cache), damit kein alter Stand haengen bleibt - Tests bilden den Fall nach: Zone nur als Abo auffindbar, Auflisten verboten Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
fd76c18eb0
commit
611593b5c0
+32
-7
@@ -42,15 +42,32 @@ ZONE.add("NS", "", "ns1.example.com.")
|
||||
ZONE.add("A", "vpn", "203.0.113.1")
|
||||
|
||||
|
||||
# Wie beim echten Server von Stefan: die Zone haengt an einem Abo (webspace),
|
||||
# 'site' kennt sie nicht, und dieser API-User darf ueberhaupt nichts auflisten.
|
||||
SITE_ZONES = {}
|
||||
WEBSPACE_ZONES = {"example.com": "3"}
|
||||
ENUMERATION_ALLOWED = False
|
||||
|
||||
|
||||
def fake_request(self, body):
|
||||
tag = body.tag
|
||||
if tag == "site":
|
||||
return ET.fromstring(
|
||||
'<packet><site><get><result><status>ok</status><id>3</id>'
|
||||
'<data><gen_info><name>example.com</name></gen_info></data></result></get></site></packet>'
|
||||
)
|
||||
if tag == "webspace":
|
||||
return ET.fromstring('<packet><webspace><get></get></webspace></packet>')
|
||||
if tag in ("site", "webspace"):
|
||||
wanted = body.findtext("./get/filter/name")
|
||||
if wanted is None and not ENUMERATION_ALLOWED:
|
||||
# _request() wandelt einen <system>-Fehler in eine PleskError um
|
||||
raise pleskmod.PleskError(f"Plesk API error 1005: Permission denied ({tag}.get)")
|
||||
zones = SITE_ZONES if tag == "site" else WEBSPACE_ZONES
|
||||
selected = {n: i for n, i in zones.items() if wanted is None or n == wanted}
|
||||
if selected:
|
||||
inner = "".join(
|
||||
f"<result><status>ok</status><id>{i}</id><data><gen_info>"
|
||||
f"<name>{n}</name></gen_info></data></result>"
|
||||
for n, i in selected.items()
|
||||
)
|
||||
else:
|
||||
inner = ("<result><status>error</status><errcode>1013</errcode>"
|
||||
"<errtext>Object not found</errtext></result>")
|
||||
return ET.fromstring(f"<packet><{tag}><get>{inner}</get></{tag}></packet>")
|
||||
if body.find("get_rec") is not None:
|
||||
parts = "".join(
|
||||
f'<result><status>ok</status><id>{r["id"]}</id><data><site-id>3</site-id>'
|
||||
@@ -151,6 +168,14 @@ rc = appmain.main(["www.example.com", "203.0.113.55", "--dns-only", "--env", "/d
|
||||
assert rc == 0 and not issued
|
||||
assert any(r["host"] == "www.example.com." and r["value"] == "203.0.113.55" for r in ZONE.records)
|
||||
|
||||
print("\n--- run 6: --zone erzwingt die Zone")
|
||||
issued.clear()
|
||||
rc = appmain.main(["endian.example.com", "203.0.113.77", "--zone", "example.com",
|
||||
"--env", "/dev/null"])
|
||||
assert rc == 0 and issued["domains"] == ["endian.example.com", "*.endian.example.com"], issued
|
||||
assert any(r["host"] == "endian.example.com." and r["value"] == "203.0.113.77"
|
||||
for r in ZONE.records), ZONE.records
|
||||
|
||||
print("\n--- error paths")
|
||||
assert appmain.main(["vpn.example.com", "not-an-ip", "--env", "/dev/null"]) == 2
|
||||
assert appmain.main(["vpn.otherdomain.tld", "203.0.113.1", "--env", "/dev/null"]) == 1
|
||||
|
||||
+44
-12
@@ -40,14 +40,25 @@ except PleskError:
|
||||
print("host helpers ok")
|
||||
|
||||
# ---- Plesk XML parsing with a stubbed transport ---------------------------
|
||||
SITES = """<?xml version="1.0"?><packet><site><get>
|
||||
<result><status>ok</status><id>3</id><data><gen_info><name>example.com</name>
|
||||
<ascii-name>example.com</ascii-name></gen_info></data></result>
|
||||
<result><status>ok</status><id>7</id><data><gen_info><name>sub.example.com</name></gen_info></data></result>
|
||||
</get></site></packet>"""
|
||||
WEBSPACES = """<?xml version="1.0"?><packet><webspace><get>
|
||||
<result><status>ok</status><id>3</id><data><gen_info><name>example.com</name></gen_info></data></result>
|
||||
</get></webspace></packet>"""
|
||||
# Only these two exist on the fake Plesk; 'sub.example.com' is an addon domain
|
||||
# with its own zone, everything else is just a record inside a zone.
|
||||
SITE_ZONES = {"example.com": "3", "sub.example.com": "7"}
|
||||
WEBSPACE_ZONES = {"example.com": "3"}
|
||||
|
||||
|
||||
def domains_response(operator, zones, wanted):
|
||||
"""Mimic <site>/<webspace> get, with and without a name filter."""
|
||||
selected = {n: i for n, i in zones.items() if wanted is None or n == wanted}
|
||||
if not selected:
|
||||
body = ('<result><status>error</status><errcode>1013</errcode>'
|
||||
'<errtext>Object not found</errtext></result>')
|
||||
else:
|
||||
body = "".join(
|
||||
f'<result><status>ok</status><id>{i}</id><data><gen_info>'
|
||||
f'<name>{n}</name><ascii-name>{n}</ascii-name></gen_info></data></result>'
|
||||
for n, i in selected.items()
|
||||
)
|
||||
return f"<packet><{operator}><get>{body}</get></{operator}></packet>"
|
||||
RECORDS = """<?xml version="1.0"?><packet><dns><get_rec>
|
||||
<result><status>ok</status><id>101</id><data><site-id>3</site-id><type>A</type>
|
||||
<host>vpn.example.com.</host><value>10.0.0.1</value></data></result>
|
||||
@@ -68,10 +79,10 @@ class StubPlesk(PleskClient):
|
||||
def _request(self, body):
|
||||
payload = ET.tostring(body, encoding="unicode")
|
||||
calls.append(payload)
|
||||
if body.tag == "site":
|
||||
return ET.fromstring(SITES)
|
||||
if body.tag == "webspace":
|
||||
return ET.fromstring(WEBSPACES)
|
||||
if body.tag in ("site", "webspace"):
|
||||
wanted = body.findtext("./get/filter/name")
|
||||
zones = SITE_ZONES if body.tag == "site" else WEBSPACE_ZONES
|
||||
return ET.fromstring(domains_response(body.tag, zones, wanted))
|
||||
if body.find("get_rec") is not None:
|
||||
return ET.fromstring(RECORDS)
|
||||
if body.find("add_rec") is not None:
|
||||
@@ -83,8 +94,29 @@ class StubPlesk(PleskClient):
|
||||
|
||||
p = StubPlesk(cfg)
|
||||
assert p.list_zones() == {"example.com": "3", "sub.example.com": "7"}, p.list_zones()
|
||||
|
||||
# A name that is only a record inside a zone must resolve to that zone ...
|
||||
assert p.find_zone("vpn.example.com") == ("example.com", "3")
|
||||
assert p.find_zone("deep.down.example.com") == ("example.com", "3")
|
||||
# ... while a real addon domain wins over its parent (most specific first).
|
||||
assert p.find_zone("sub.example.com") == ("sub.example.com", "7")
|
||||
assert p.find_zone("x.sub.example.com") == ("sub.example.com", "7")
|
||||
assert p.find_zone("example.com") == ("example.com", "3")
|
||||
|
||||
# The zone lookup must not depend on being allowed to enumerate all domains.
|
||||
enumerating = StubPlesk(cfg)
|
||||
enumerating.list_zones = lambda: (_ for _ in ()).throw(AssertionError("enumerated all domains"))
|
||||
assert enumerating.find_zone("vpn.example.com") == ("example.com", "3")
|
||||
|
||||
# --zone overrides the detection
|
||||
hinted = StubPlesk(cfg)
|
||||
hinted.zone_hint = "example.com"
|
||||
assert hinted.find_zone("x.sub.example.com") == ("example.com", "3")
|
||||
try:
|
||||
hinted.find_zone("vpn.other.tld")
|
||||
raise SystemExit("FAIL: zone hint mismatch not detected")
|
||||
except PleskError:
|
||||
pass
|
||||
recs = p.get_records("3")
|
||||
assert [r.type for r in recs] == ["A", "NS", "TXT"]
|
||||
assert recs[2].value == "oldvalue", recs[2].value # quotes stripped
|
||||
|
||||
Reference in New Issue
Block a user