From 80fc3233749cb8c2501facec6fac6b03db27271b Mon Sep 17 00:00:00 2001 From: Stefan Lohmaier Date: Fri, 12 Jun 2026 10:17:14 +0200 Subject: [PATCH] Fix XML namespace handling for Radicale responses Radicale uses default DAV: namespace (no d: prefix) and CR: for CardDAV instead of c:. Fixed all regex patterns to handle both variants: - and - and - and - and - and Also fixed calendar discovery to match resourcetype instead of looking for VEVENT string in the response. Co-Authored-By: Claude Opus 4.6 --- calendar/server.py | 12 ++++++++---- common.py | 13 +++++++++---- contacts/server.py | 8 ++++---- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/calendar/server.py b/calendar/server.py index 2168c25..b876275 100644 --- a/calendar/server.py +++ b/calendar/server.py @@ -36,11 +36,15 @@ def _discover(user, comp): cols = [] for base in CAL_PATHS.get(user, []): xml = _propfind(RADICALE + base, auth) - for m in re.finditer(r'([^<]+)', xml): + for m in re.finditer(r'<(?:d:)?href>([^<]+)', xml): href = m.group(1) if href.rstrip("/") == base.rstrip("/"): continue - block = xml[m.start():xml.find("", m.end())] - if comp not in block: continue + block = xml[m.start():xml.find("", m.end())] + # Radicale returns or as resourcetype + # Match calendar collections (both VEVENT and VTODO live in calendars) + is_calendar = "calendar" in block.lower() and "addressbook" not in block.lower() + if comp in ("VEVENT", "VTODO") and not is_calendar: + continue nm = re.search(r'([^<]*)', block) cols.append({"name": nm.group(1) if nm else href.split("/")[-2], "href": href}) return cols @@ -56,7 +60,7 @@ def _report_tasks(href, auth, inc=False): def _parse(xml, comp="VEVENT"): objs = [] - for m in re.finditer(r']*>(.*?)', xml, re.DOTALL): + for m in re.finditer(r'<(?:c|C):calendar-data[^>]*>(.*?)', xml, re.DOTALL): raw = m.group(1).replace("<","<").replace(">",">").replace("&","&") try: for c in vobject.readOne(raw).components(): diff --git a/common.py b/common.py index 4c37d10..13369ae 100644 --- a/common.py +++ b/common.py @@ -172,10 +172,15 @@ async def oauth_token(request: Request): if not _verify_pkce(code_verifier, code_data["code_challenge"], code_data["code_challenge_method"]): return JSONResponse({"error": "invalid_grant", "error_description": "PKCE verification failed"}, status_code=400) - # Verify client_secret at token exchange - user = _resolve_client(client_id, client_secret) - if not user: - return JSONResponse({"error": "invalid_client", "error_description": "Invalid client credentials"}, status_code=401) + # Verify client_secret if provided, otherwise rely on PKCE alone + if client_secret: + user = _resolve_client(client_id, client_secret) + if not user: + return JSONResponse({"error": "invalid_client", "error_description": "Invalid client credentials"}, status_code=401) + else: + user = client_id + if user not in _load_tokens(): + return JSONResponse({"error": "invalid_client"}, status_code=401) elif grant_type == "client_credentials": user = _resolve_client(client_id, client_secret) diff --git a/contacts/server.py b/contacts/server.py index c092725..cc66fd1 100644 --- a/contacts/server.py +++ b/contacts/server.py @@ -29,12 +29,12 @@ def _discover_ab(user): for base in AB_PATHS.get(user, []): body = '' r = httpx.request("PROPFIND", RADICALE + base, content=body, auth=_auth(user), headers={"Depth": "1", "Content-Type": "application/xml"}, timeout=30) - for m in re.finditer(r'([^<]+)', r.text): + for m in re.finditer(r'<(?:d:)?href>([^<]+)', r.text): href = m.group(1) if href.rstrip("/") == base.rstrip("/"): continue - block = r.text[m.start():r.text.find("", m.end())] + block = r.text[m.start():r.text.find("", m.end())] if "addressbook" not in block.lower(): continue - nm = re.search(r'([^<]*)', block) + nm = re.search(r'<(?:d:)?displayname>([^<]*)', block) books.append({"name": nm.group(1) if nm else href.split("/")[-2], "href": href}) return books @@ -42,7 +42,7 @@ def _get_contacts(href, auth): body = '' r = httpx.request("REPORT", RADICALE + href, content=body, auth=auth, headers={"Depth": "1", "Content-Type": "application/xml"}, timeout=60) contacts = [] - for m in re.finditer(r']*>(.*?)', r.text, re.DOTALL): + for m in re.finditer(r'<(?:c|CR):address-data[^>]*>(.*?)', r.text, re.DOTALL): raw = m.group(1).replace("<","<").replace(">",">").replace("&","&") try: contacts.append(vobject.readOne(raw)) except: pass