Fix CalDAV/CardDAV discovery: use GET /.well-known + PROPFIND for principal
This commit is contained in:
@@ -739,24 +739,122 @@ def parse_dav_propfind(xml_text):
|
||||
return results
|
||||
|
||||
|
||||
# CalDAV/CardDAV helpers
|
||||
|
||||
DAV_NS = {"D": "DAV:"}
|
||||
CAL_NS = {"CAL": "urn:ietf:params:xml:ns:caldav"}
|
||||
CARD_NS = {"CARD": "urn:ietf:params:xml:ns:carddav"}
|
||||
|
||||
|
||||
def dav_session():
|
||||
if not config.DAV_BASE_URL or not config.DAV_USERNAME or not config.DAV_PASSWORD:
|
||||
raise ValueError("DAV_BASE_URL, DAV_USERNAME, or DAV_PASSWORD not configured")
|
||||
s = requests.Session()
|
||||
s.auth = (config.DAV_USERNAME, config.DAV_PASSWORD)
|
||||
s.verify = config.DAV_VERIFY_TLS
|
||||
return s
|
||||
|
||||
|
||||
def dav_request(s, method, url, data=None, headers=None, timeout=20):
|
||||
try:
|
||||
r = s.request(method, url, data=data, headers=headers or {}, timeout=timeout)
|
||||
return r
|
||||
except Exception as e:
|
||||
logger.error(f"DAV request error: {method} {url}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def parse_dav_propfind(xml_text):
|
||||
try:
|
||||
root = ET.fromstring(xml_text)
|
||||
except ET.ParseError:
|
||||
return []
|
||||
|
||||
results = []
|
||||
for response in root.findall(".//D:response", DAV_NS):
|
||||
href_el = response.find("D:href", DAV_NS)
|
||||
href = (href_el.text or "").strip() if href_el is not None else ""
|
||||
props = {}
|
||||
propfind = response.find("D:propstat/D:prop", DAV_NS)
|
||||
if propfind is not None:
|
||||
for child in propfind:
|
||||
tag = re.sub(r".*\{.*\}", "", child.tag)
|
||||
text = (child.text or "").strip()
|
||||
props[tag] = text
|
||||
if href:
|
||||
results.append({"href": href, "props": props})
|
||||
return results
|
||||
|
||||
|
||||
def get_user_principal(service="caldav"):
|
||||
"""
|
||||
Discover the user's principal URL using /.well-known/{service} redirect
|
||||
and then PROPFIND for current-user-principal.
|
||||
"""
|
||||
base = config.DAV_BASE_URL.rstrip("/")
|
||||
well_known = base + f"/.well-known/{service}"
|
||||
s = dav_session()
|
||||
|
||||
# First: GET /.well-known/caldav (or carddav) to get the redirect URL
|
||||
r = dav_request(s, "GET", well_known)
|
||||
r.raise_for_status()
|
||||
dave_url = r.url # after redirects
|
||||
|
||||
# Then: PROPFIND on that URL to find current-user-principal
|
||||
r = dav_request(s, "PROPFIND", dave_url,
|
||||
headers={"Depth": "0", "Content-Type": "application/xml"},
|
||||
data="""<?xml version="1.0"?>
|
||||
<D:propfind xmlns:D="DAV:">
|
||||
<D:prop><D:current-user-principal/></D:prop>
|
||||
</D:propfind>""")
|
||||
r.raise_for_status()
|
||||
entries = parse_dav_propfind(r.text)
|
||||
for e in entries:
|
||||
principal = e.get("props", {}).get("current-user-principal", "")
|
||||
if principal:
|
||||
return principal
|
||||
raise RuntimeError("Could not find user principal")
|
||||
|
||||
|
||||
# CardDAV helpers
|
||||
|
||||
def carddav_base_url():
|
||||
return config.DAV_BASE_URL.rstrip("/") + "/cards/"
|
||||
|
||||
|
||||
def list_carddav_addressbooks():
|
||||
"""
|
||||
Discover addressbooks via the user principal and addressbook-home-set.
|
||||
"""
|
||||
s = dav_session()
|
||||
r = dav_request(s, "PROPFIND", carddav_base_url(),
|
||||
headers={"Depth": "1",
|
||||
"Content-Type": "application/xml"},
|
||||
principal = get_user_principal("carddav")
|
||||
|
||||
# PROPFIND on principal to get addressbook-home-set
|
||||
r = dav_request(s, "PROPFIND", principal,
|
||||
headers={"Depth": "0", "Content-Type": "application/xml"},
|
||||
data="""<?xml version="1.0"?>
|
||||
<D:propfind xmlns:D="DAV:">
|
||||
<D:prop>
|
||||
<D:resourcetype/>
|
||||
<D:displayname/>
|
||||
</D:prop>
|
||||
</D:propfind>""")
|
||||
<D:propfind xmlns:D="DAV:">
|
||||
<D:prop>
|
||||
<CARD:addressbook-home-set xmlns:CARD="urn:ietf:params:xml:ns:carddav"/>
|
||||
</D:prop>
|
||||
</D:propfind>""")
|
||||
r.raise_for_status()
|
||||
entries = parse_dav_propfind(r.text)
|
||||
home_set = None
|
||||
for e in entries:
|
||||
val = e.get("props", {}).get("addressbook-home-set", "")
|
||||
if val:
|
||||
home_set = val
|
||||
break
|
||||
if not home_set:
|
||||
return []
|
||||
|
||||
# PROPFIND on addressbook-home-set to list addressbooks
|
||||
r = dav_request(s, "PROPFIND", home_set,
|
||||
headers={"Depth": "1", "Content-Type": "application/xml"},
|
||||
data="""<?xml version="1.0"?>
|
||||
<D:propfind xmlns:D="DAV:">
|
||||
<D:prop>
|
||||
<D:resourcetype/>
|
||||
<D:displayname/>
|
||||
</D:prop>
|
||||
</D:propfind>""")
|
||||
r.raise_for_status()
|
||||
entries = parse_dav_propfind(r.text)
|
||||
addressbooks = []
|
||||
@@ -764,7 +862,7 @@ def list_carddav_addressbooks():
|
||||
href = e["href"]
|
||||
props = e.get("props", {})
|
||||
rt = props.get("resourcetype", "")
|
||||
if "/cards/" in href and href.endswith("/") and ("addressbook" in rt.lower() or "addressbook" in href.lower()):
|
||||
if href.endswith("/") and ("addressbook" in rt.lower() or "addressbook" in href.lower()):
|
||||
addressbooks.append({
|
||||
"href": href,
|
||||
"name": props.get("displayname", href),
|
||||
@@ -785,16 +883,14 @@ def search_carddav_contacts(addressbook_href: str, query: str):
|
||||
s = dav_session()
|
||||
safe_query = _xml_escape(query)
|
||||
req_body = f"""<?xml version="1.0"?>
|
||||
<v:carddav xmlns:v="urn:ietf:params:xml:ns:carddav"
|
||||
xmlns:D="DAV:">
|
||||
<v:addressbook-query>
|
||||
<v:filter>
|
||||
<v:prop-filter name="FN">
|
||||
<v:text-match>{safe_query}</v:text-match>
|
||||
</v:prop-filter>
|
||||
</v:filter>
|
||||
</v:addressbook-query>
|
||||
</v:carddav>"""
|
||||
<v:addressbook-query xmlns:v="urn:ietf:params:xml:ns:carddav"
|
||||
xmlns:D="DAV:">
|
||||
<v:filter>
|
||||
<v:prop-filter name="FN">
|
||||
<v:text-match>{safe_query}</v:text-match>
|
||||
</v:prop-filter>
|
||||
</v:filter>
|
||||
</v:addressbook-query>"""
|
||||
r = dav_request(s, "REPORT", addressbook_href,
|
||||
headers={"Content-Type": "application/xml"},
|
||||
data=req_body)
|
||||
@@ -851,22 +947,43 @@ def delete_carddav_contact(href: str):
|
||||
|
||||
# CalDAV helpers
|
||||
|
||||
def caldav_base_url():
|
||||
return config.DAV_BASE_URL.rstrip("/") + "/calendars/"
|
||||
|
||||
|
||||
def list_caldav_calendars():
|
||||
"""
|
||||
Discover calendars via the user principal and calendar-home-set.
|
||||
"""
|
||||
s = dav_session()
|
||||
r = dav_request(s, "PROPFIND", caldav_base_url(),
|
||||
headers={"Depth": "1",
|
||||
"Content-Type": "application/xml"},
|
||||
principal = get_user_principal("caldav")
|
||||
|
||||
# PROPFIND on principal to get calendar-home-set
|
||||
r = dav_request(s, "PROPFIND", principal,
|
||||
headers={"Depth": "0", "Content-Type": "application/xml"},
|
||||
data="""<?xml version="1.0"?>
|
||||
<D:propfind xmlns:D="DAV:">
|
||||
<D:prop>
|
||||
<D:resourcetype/>
|
||||
<D:displayname/>
|
||||
</D:prop>
|
||||
</D:propfind>""")
|
||||
<D:propfind xmlns:D="DAV:">
|
||||
<D:prop>
|
||||
<CAL:calendar-home-set xmlns:CAL="urn:ietf:params:xml:ns:caldav"/>
|
||||
</D:prop>
|
||||
</D:propfind>""")
|
||||
r.raise_for_status()
|
||||
entries = parse_dav_propfind(r.text)
|
||||
home_set = None
|
||||
for e in entries:
|
||||
val = e.get("props", {}).get("calendar-home-set", "")
|
||||
if val:
|
||||
home_set = val
|
||||
break
|
||||
if not home_set:
|
||||
return []
|
||||
|
||||
# PROPFIND on calendar-home-set to list calendars
|
||||
r = dav_request(s, "PROPFIND", home_set,
|
||||
headers={"Depth": "1", "Content-Type": "application/xml"},
|
||||
data="""<?xml version="1.0"?>
|
||||
<D:propfind xmlns:D="DAV:">
|
||||
<D:prop>
|
||||
<D:resourcetype/>
|
||||
<D:displayname/>
|
||||
</D:prop>
|
||||
</D:propfind>""")
|
||||
r.raise_for_status()
|
||||
entries = parse_dav_propfind(r.text)
|
||||
calendars = []
|
||||
@@ -874,7 +991,7 @@ def list_caldav_calendars():
|
||||
href = e["href"]
|
||||
props = e.get("props", {})
|
||||
rt = props.get("resourcetype", "")
|
||||
if "/calendars/" in href and href.endswith("/") and ("calendar" in rt.lower() or "calendar" in href.lower()):
|
||||
if href.endswith("/") and ("calendar" in rt.lower() or "calendar" in href.lower()):
|
||||
calendars.append({
|
||||
"href": href,
|
||||
"name": props.get("displayname", href.split("/")[-2]),
|
||||
|
||||
Reference in New Issue
Block a user