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
@@ -16,8 +16,11 @@ erzeugt ein Zertifikat für `vpn.example.com` **und** `*.vpn.example.com`.
|
|||||||
## 1. Voraussetzungen
|
## 1. Voraussetzungen
|
||||||
|
|
||||||
* Docker (oder lokal Python 3.10+)
|
* Docker (oder lokal Python 3.10+)
|
||||||
* Die Domain muss auf dem Plesk-Server als DNS-Zone liegen und Plesk muss der
|
* Die **Zone** muss auf dem Plesk-Server liegen und Plesk muss der
|
||||||
**autoritative Nameserver** dafür sein (sonst kann die DNS-Challenge nicht validiert werden).
|
**autoritative Nameserver** dafür sein (sonst kann die DNS-Challenge nicht validiert werden).
|
||||||
|
Der Name selbst braucht *kein* eigenes Abo und keine eigene Domain zu sein: für
|
||||||
|
`endian.fon-aria.de` reicht die Zone `fon-aria.de` – das Tool sucht sich die
|
||||||
|
passende Zone von unten nach oben zusammen und legt dort einfach die Records an.
|
||||||
* Ein Plesk-API-Zugang (API-Key empfohlen).
|
* Ein Plesk-API-Zugang (API-Key empfohlen).
|
||||||
|
|
||||||
### Plesk API-Key erzeugen
|
### Plesk API-Key erzeugen
|
||||||
@@ -88,6 +91,7 @@ python -m app.main vpn.example.com 192.0.2.10
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `--ip <adresse>` | Alternative zum zweiten Positionsargument (IPv4 → A, IPv6 → AAAA) |
|
| `--ip <adresse>` | Alternative zum zweiten Positionsargument (IPv4 → A, IPv6 → AAAA) |
|
||||||
| `--san <name>` | zusätzlicher Name im Zertifikat (mehrfach möglich) |
|
| `--san <name>` | zusätzlicher Name im Zertifikat (mehrfach möglich) |
|
||||||
|
| `--zone <zone>` | Plesk-Zone explizit angeben statt sie zu ermitteln |
|
||||||
| `--no-wildcard` | nur der reine Name, ohne `*.<fqdn>` |
|
| `--no-wildcard` | nur der reine Name, ohne `*.<fqdn>` |
|
||||||
| `--skip-dns` | A/AAAA-Record unangetastet lassen |
|
| `--skip-dns` | A/AAAA-Record unangetastet lassen |
|
||||||
| `--dns-only` | nur DNS, kein Zertifikat |
|
| `--dns-only` | nur DNS, kein Zertifikat |
|
||||||
@@ -130,7 +134,10 @@ openssl pkcs12 -info -in certs/vpn.example.com/cert.pfx -nodes -passin pass:<CER
|
|||||||
|
|
||||||
## 5. Wie es abläuft
|
## 5. Wie es abläuft
|
||||||
|
|
||||||
1. Plesk-Zone zum Namen suchen (längste passende Zone, z. B. `example.com` für `vpn.example.com`).
|
1. Plesk nach der Zone fragen – erst nach dem vollen Namen, dann Label für Label
|
||||||
|
nach oben (`endian.fon-aria.de` → `fon-aria.de`), jeweils als Domain und als Abo.
|
||||||
|
Der Server muss dafür nicht alle Domains herausrücken; mit `--zone` lässt sich
|
||||||
|
die Zone auch fest vorgeben.
|
||||||
2. A/AAAA-Record anlegen oder auf die neue IP korrigieren.
|
2. A/AAAA-Record anlegen oder auf die neue IP korrigieren.
|
||||||
3. Prüfen, ob überhaupt erneuert werden muss (`RENEW_DAYS_BEFORE_EXPIRY`, Standard 30 Tage).
|
3. Prüfen, ob überhaupt erneuert werden muss (`RENEW_DAYS_BEFORE_EXPIRY`, Standard 30 Tage).
|
||||||
4. Key + CSR für `<fqdn>` und `*.<fqdn>` erzeugen.
|
4. Key + CSR für `<fqdn>` und `*.<fqdn>` erzeugen.
|
||||||
@@ -155,7 +162,7 @@ Das Skript ist idempotent: Läuft es, obwohl das Zertifikat noch länger als
|
|||||||
| Symptom | Ursache / Lösung |
|
| Symptom | Ursache / Lösung |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `Plesk rejected the credentials (HTTP 401)` | API-Key falsch oder nicht für diese Quell-IP erzeugt |
|
| `Plesk rejected the credentials (HTTP 401)` | API-Key falsch oder nicht für diese Quell-IP erzeugt |
|
||||||
| `No Plesk DNS zone found for …` | Domain liegt nicht auf diesem Plesk oder der API-User sieht sie nicht |
|
| `No Plesk DNS zone found for …` | Zone liegt nicht auf diesem Plesk oder der API-User sieht sie nicht. Die Meldung listet auf, welche Namen probiert wurden und was Plesk dazu gesagt hat. Notfalls die Zone mit `--zone example.com` fest vorgeben |
|
||||||
| `DNS propagation timed out` | Plesk ist nicht der autoritative NS, oder die Zone ist deaktiviert. Mit `-v` prüfen, welcher Nameserver antwortet |
|
| `DNS propagation timed out` | Plesk ist nicht der autoritative NS, oder die Zone ist deaktiviert. Mit `-v` prüfen, welcher Nameserver antwortet |
|
||||||
| `no dns-01 challenge` | Wildcards gehen ausschließlich über DNS-01 – Domain-Validierung per HTTP ist nicht möglich |
|
| `no dns-01 challenge` | Wildcards gehen ausschließlich über DNS-01 – Domain-Validierung per HTTP ist nicht möglich |
|
||||||
| `urn:ietf:params:acme:error:rateLimited` | Rate-Limit von Let's Encrypt (5 Zertifikate pro Woche pro Domain). Erst mit `--staging` testen |
|
| `urn:ietf:params:acme:error:rateLimited` | Rate-Limit von Let's Encrypt (5 Zertifikate pro Woche pro Domain). Erst mit `--staging` testen |
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
"""Wildcard Let's Encrypt certificate creator for Plesk-managed DNS zones."""
|
"""Wildcard Let's Encrypt certificate creator for Plesk-managed DNS zones."""
|
||||||
|
|
||||||
__version__ = "1.0.0"
|
__version__ = "1.1.0"
|
||||||
|
|||||||
@@ -95,6 +95,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
parser.add_argument("--ip", dest="ip_opt", help="alternative to the positional IP argument")
|
parser.add_argument("--ip", dest="ip_opt", help="alternative to the positional IP argument")
|
||||||
parser.add_argument("--san", action="append", default=[],
|
parser.add_argument("--san", action="append", default=[],
|
||||||
help="additional SAN (repeatable)")
|
help="additional SAN (repeatable)")
|
||||||
|
parser.add_argument("--zone",
|
||||||
|
help="name the Plesk DNS zone explicitly instead of detecting it "
|
||||||
|
"(e.g. --zone example.com for host.example.com)")
|
||||||
parser.add_argument("--no-wildcard", action="store_true",
|
parser.add_argument("--no-wildcard", action="store_true",
|
||||||
help="only the plain name, without *.<fqdn>")
|
help="only the plain name, without *.<fqdn>")
|
||||||
parser.add_argument("--skip-dns", action="store_true",
|
parser.add_argument("--skip-dns", action="store_true",
|
||||||
@@ -150,6 +153,7 @@ def run(args: argparse.Namespace) -> int:
|
|||||||
log.info("=" * 62)
|
log.info("=" * 62)
|
||||||
|
|
||||||
plesk = PleskClient(cfg)
|
plesk = PleskClient(cfg)
|
||||||
|
plesk.zone_hint = args.zone
|
||||||
zone, domain_id = plesk.find_zone(fqdn)
|
zone, domain_id = plesk.find_zone(fqdn)
|
||||||
log.info("Plesk DNS zone: %s (domain id %s)", zone, domain_id)
|
log.info("Plesk DNS zone: %s (domain id %s)", zone, domain_id)
|
||||||
|
|
||||||
|
|||||||
+87
-15
@@ -69,7 +69,10 @@ class PleskClient:
|
|||||||
self.session.headers.update(headers)
|
self.session.headers.update(headers)
|
||||||
if not cfg.plesk_verify_tls:
|
if not cfg.plesk_verify_tls:
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
self.zone_hint: str | None = None
|
||||||
self._zone_cache: dict[str, str] | None = None
|
self._zone_cache: dict[str, str] | None = None
|
||||||
|
self._list_problems: list[str] = []
|
||||||
|
self._fqdn_zone_cache: dict[str, tuple[str, str]] = {}
|
||||||
|
|
||||||
# -- low level ---------------------------------------------------------
|
# -- low level ---------------------------------------------------------
|
||||||
|
|
||||||
@@ -122,6 +125,7 @@ class PleskClient:
|
|||||||
return self._zone_cache
|
return self._zone_cache
|
||||||
|
|
||||||
zones: dict[str, str] = {}
|
zones: dict[str, str] = {}
|
||||||
|
problems: list[str] = []
|
||||||
for operator in ("site", "webspace"):
|
for operator in ("site", "webspace"):
|
||||||
op = ET.Element(operator)
|
op = ET.Element(operator)
|
||||||
get = ET.SubElement(op, "get")
|
get = ET.SubElement(op, "get")
|
||||||
@@ -132,10 +136,14 @@ class PleskClient:
|
|||||||
root = self._request(op)
|
root = self._request(op)
|
||||||
except PleskError as exc:
|
except PleskError as exc:
|
||||||
log.debug("%s.get failed: %s", operator, exc)
|
log.debug("%s.get failed: %s", operator, exc)
|
||||||
|
problems.append(f"{operator}.get: {exc}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for result in root.findall(f"./{operator}/get/result"):
|
for result in root.findall(f"./{operator}/get/result"):
|
||||||
if result.findtext("status") != "ok":
|
if result.findtext("status") != "ok":
|
||||||
|
errtext = result.findtext("errtext")
|
||||||
|
if errtext:
|
||||||
|
problems.append(f"{operator}.get: {errtext}")
|
||||||
continue
|
continue
|
||||||
domain_id = result.findtext("id")
|
domain_id = result.findtext("id")
|
||||||
gen_info = result.find("./data/gen_info")
|
gen_info = result.find("./data/gen_info")
|
||||||
@@ -146,26 +154,86 @@ class PleskClient:
|
|||||||
if name:
|
if name:
|
||||||
zones.setdefault(normalise_name(name), domain_id)
|
zones.setdefault(normalise_name(name), domain_id)
|
||||||
|
|
||||||
if not zones:
|
|
||||||
raise PleskError(
|
|
||||||
"No domains found on the Plesk server. Does the API user have access to any subscription?"
|
|
||||||
)
|
|
||||||
self._zone_cache = zones
|
self._zone_cache = zones
|
||||||
|
self._list_problems = problems
|
||||||
log.debug("Known Plesk zones: %s", sorted(zones))
|
log.debug("Known Plesk zones: %s", sorted(zones))
|
||||||
return zones
|
return zones
|
||||||
|
|
||||||
|
def _domain_id_by_name(self, name: str) -> str | None:
|
||||||
|
"""Ask Plesk directly for one domain/subscription. None if it does not exist."""
|
||||||
|
for operator in ("site", "webspace"):
|
||||||
|
op = ET.Element(operator)
|
||||||
|
get = ET.SubElement(op, "get")
|
||||||
|
flt = ET.SubElement(get, "filter")
|
||||||
|
ET.SubElement(flt, "name").text = name
|
||||||
|
dataset = ET.SubElement(get, "dataset")
|
||||||
|
ET.SubElement(dataset, "gen_info")
|
||||||
|
try:
|
||||||
|
root = self._request(op)
|
||||||
|
except PleskError as exc:
|
||||||
|
log.debug("%s.get(%s) failed: %s", operator, name, exc)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for result in root.findall(f"./{operator}/get/result"):
|
||||||
|
if result.findtext("status") == "ok":
|
||||||
|
domain_id = result.findtext("id")
|
||||||
|
if domain_id:
|
||||||
|
log.debug("Plesk %s %r has id %s", operator, name, domain_id)
|
||||||
|
return domain_id
|
||||||
|
else:
|
||||||
|
log.debug("%s.get(%s): %s", operator, name, result.findtext("errtext", ""))
|
||||||
|
return None
|
||||||
|
|
||||||
def find_zone(self, fqdn: str) -> tuple[str, str]:
|
def find_zone(self, fqdn: str) -> tuple[str, str]:
|
||||||
"""Find the most specific Plesk zone hosting *fqdn*. Returns (zone_name, domain_id)."""
|
"""Find the Plesk zone hosting *fqdn*. Returns (zone_name, domain_id).
|
||||||
|
|
||||||
|
Asks Plesk for each candidate name from the most specific one upwards
|
||||||
|
(``a.b.example.com`` -> ``b.example.com`` -> ``example.com``), so a name
|
||||||
|
that only exists as a DNS record inside a zone works as well. Enumerating
|
||||||
|
every domain on the server is not required - and often not permitted.
|
||||||
|
"""
|
||||||
fqdn = normalise_name(fqdn)
|
fqdn = normalise_name(fqdn)
|
||||||
|
cached = self._fqdn_zone_cache.get(fqdn)
|
||||||
|
if cached:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
if self.zone_hint:
|
||||||
|
zone = normalise_name(self.zone_hint)
|
||||||
|
if fqdn != zone and not fqdn.endswith("." + zone):
|
||||||
|
raise PleskError(f"{fqdn!r} is not inside the zone {zone!r} given via --zone.")
|
||||||
|
domain_id = self._domain_id_by_name(zone)
|
||||||
|
if not domain_id:
|
||||||
|
raise PleskError(f"Plesk does not know a domain or subscription named {zone!r}.")
|
||||||
|
self._fqdn_zone_cache[fqdn] = (zone, domain_id)
|
||||||
|
return zone, domain_id
|
||||||
|
|
||||||
|
labels = fqdn.split(".")
|
||||||
|
tried: list[str] = []
|
||||||
|
for index in range(len(labels) - 1):
|
||||||
|
candidate = ".".join(labels[index:])
|
||||||
|
tried.append(candidate)
|
||||||
|
domain_id = self._domain_id_by_name(candidate)
|
||||||
|
if domain_id:
|
||||||
|
if candidate != fqdn:
|
||||||
|
log.info("%s is a record inside the Plesk zone %s", fqdn, candidate)
|
||||||
|
self._fqdn_zone_cache[fqdn] = (candidate, domain_id)
|
||||||
|
return candidate, domain_id
|
||||||
|
|
||||||
|
# Nothing matched - build the most helpful error we can.
|
||||||
|
message = [
|
||||||
|
f"No Plesk DNS zone found for {fqdn!r}.",
|
||||||
|
f"Tried: {', '.join(tried)}.",
|
||||||
|
]
|
||||||
zones = self.list_zones()
|
zones = self.list_zones()
|
||||||
candidates = [z for z in zones if fqdn == z or fqdn.endswith("." + z)]
|
if zones:
|
||||||
if not candidates:
|
message.append(f"Domains visible to this API user: {', '.join(sorted(zones))}.")
|
||||||
raise PleskError(
|
else:
|
||||||
f"No Plesk DNS zone found for {fqdn!r}. "
|
message.append(
|
||||||
f"Known zones: {', '.join(sorted(zones)) or '<none>'}"
|
"This API user cannot see any domain on the server either "
|
||||||
|
"(use --zone <zone> to name the zone explicitly)."
|
||||||
)
|
)
|
||||||
zone = max(candidates, key=len)
|
message.extend(f"Plesk said: {p}" for p in self._list_problems)
|
||||||
return zone, zones[zone]
|
raise PleskError(" ".join(message))
|
||||||
|
|
||||||
# -- records -----------------------------------------------------------
|
# -- records -----------------------------------------------------------
|
||||||
|
|
||||||
@@ -180,9 +248,13 @@ class PleskClient:
|
|||||||
for result in root.findall("./dns/get_rec/result"):
|
for result in root.findall("./dns/get_rec/result"):
|
||||||
status = result.findtext("status")
|
status = result.findtext("status")
|
||||||
if status != "ok":
|
if status != "ok":
|
||||||
errtext = result.findtext("errtext", "")
|
# A zone without records reports an error instead of an empty list -
|
||||||
# An empty zone reports an error instead of an empty list on some versions.
|
# worth a warning, since a healthy zone always has SOA/NS entries.
|
||||||
log.debug("get_rec result not ok: %s", errtext)
|
log.warning(
|
||||||
|
"Plesk returned no DNS records for domain id %s: %s",
|
||||||
|
domain_id,
|
||||||
|
result.findtext("errtext", "unknown reason"),
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
data = result.find("data")
|
data = result.find("data")
|
||||||
if data is None:
|
if data is None:
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ fi
|
|||||||
export PUID="${PUID:-$(id -u)}"
|
export PUID="${PUID:-$(id -u)}"
|
||||||
export PGID="${PGID:-$(id -g)}"
|
export PGID="${PGID:-$(id -g)}"
|
||||||
|
|
||||||
if ! docker image inspect wildcard-lets-encrypt-cert-plesk-creator:latest >/dev/null 2>&1; then
|
# Immer bauen - ist bei unveraendertem Code dank Cache in ~1s durch und
|
||||||
echo ">> Baue Docker-Image ..."
|
# vermeidet, dass ein alter Stand im Image haengen bleibt.
|
||||||
$DC build
|
$DC build -q >/dev/null
|
||||||
fi
|
|
||||||
|
|
||||||
exec $DC run --rm wildcard-cert "$@"
|
exec $DC run --rm wildcard-cert "$@"
|
||||||
|
|||||||
+31
-6
@@ -42,15 +42,32 @@ ZONE.add("NS", "", "ns1.example.com.")
|
|||||||
ZONE.add("A", "vpn", "203.0.113.1")
|
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):
|
def fake_request(self, body):
|
||||||
tag = body.tag
|
tag = body.tag
|
||||||
if tag == "site":
|
if tag in ("site", "webspace"):
|
||||||
return ET.fromstring(
|
wanted = body.findtext("./get/filter/name")
|
||||||
'<packet><site><get><result><status>ok</status><id>3</id>'
|
if wanted is None and not ENUMERATION_ALLOWED:
|
||||||
'<data><gen_info><name>example.com</name></gen_info></data></result></get></site></packet>'
|
# _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()
|
||||||
)
|
)
|
||||||
if tag == "webspace":
|
else:
|
||||||
return ET.fromstring('<packet><webspace><get></get></webspace></packet>')
|
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:
|
if body.find("get_rec") is not None:
|
||||||
parts = "".join(
|
parts = "".join(
|
||||||
f'<result><status>ok</status><id>{r["id"]}</id><data><site-id>3</site-id>'
|
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 rc == 0 and not issued
|
||||||
assert any(r["host"] == "www.example.com." and r["value"] == "203.0.113.55" for r in ZONE.records)
|
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")
|
print("\n--- error paths")
|
||||||
assert appmain.main(["vpn.example.com", "not-an-ip", "--env", "/dev/null"]) == 2
|
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
|
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")
|
print("host helpers ok")
|
||||||
|
|
||||||
# ---- Plesk XML parsing with a stubbed transport ---------------------------
|
# ---- Plesk XML parsing with a stubbed transport ---------------------------
|
||||||
SITES = """<?xml version="1.0"?><packet><site><get>
|
# Only these two exist on the fake Plesk; 'sub.example.com' is an addon domain
|
||||||
<result><status>ok</status><id>3</id><data><gen_info><name>example.com</name>
|
# with its own zone, everything else is just a record inside a zone.
|
||||||
<ascii-name>example.com</ascii-name></gen_info></data></result>
|
SITE_ZONES = {"example.com": "3", "sub.example.com": "7"}
|
||||||
<result><status>ok</status><id>7</id><data><gen_info><name>sub.example.com</name></gen_info></data></result>
|
WEBSPACE_ZONES = {"example.com": "3"}
|
||||||
</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>
|
def domains_response(operator, zones, wanted):
|
||||||
</get></webspace></packet>"""
|
"""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>
|
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>
|
<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>
|
<host>vpn.example.com.</host><value>10.0.0.1</value></data></result>
|
||||||
@@ -68,10 +79,10 @@ class StubPlesk(PleskClient):
|
|||||||
def _request(self, body):
|
def _request(self, body):
|
||||||
payload = ET.tostring(body, encoding="unicode")
|
payload = ET.tostring(body, encoding="unicode")
|
||||||
calls.append(payload)
|
calls.append(payload)
|
||||||
if body.tag == "site":
|
if body.tag in ("site", "webspace"):
|
||||||
return ET.fromstring(SITES)
|
wanted = body.findtext("./get/filter/name")
|
||||||
if body.tag == "webspace":
|
zones = SITE_ZONES if body.tag == "site" else WEBSPACE_ZONES
|
||||||
return ET.fromstring(WEBSPACES)
|
return ET.fromstring(domains_response(body.tag, zones, wanted))
|
||||||
if body.find("get_rec") is not None:
|
if body.find("get_rec") is not None:
|
||||||
return ET.fromstring(RECORDS)
|
return ET.fromstring(RECORDS)
|
||||||
if body.find("add_rec") is not None:
|
if body.find("add_rec") is not None:
|
||||||
@@ -83,8 +94,29 @@ class StubPlesk(PleskClient):
|
|||||||
|
|
||||||
p = StubPlesk(cfg)
|
p = StubPlesk(cfg)
|
||||||
assert p.list_zones() == {"example.com": "3", "sub.example.com": "7"}, p.list_zones()
|
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("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("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")
|
recs = p.get_records("3")
|
||||||
assert [r.type for r in recs] == ["A", "NS", "TXT"]
|
assert [r.type for r in recs] == ["A", "NS", "TXT"]
|
||||||
assert recs[2].value == "oldvalue", recs[2].value # quotes stripped
|
assert recs[2].value == "oldvalue", recs[2].value # quotes stripped
|
||||||
|
|||||||
Reference in New Issue
Block a user