Class CaldavRelayService
/caldav/rest/dav/{serverId}/** and this service forwards
each request to the one host the registry row names, injecting the
connected user's stored credentials. This is the piece that removes every
reason for the browser to hold a CalDAV password or to reach a CalDAV
origin directly, and it holds the whole security posture of the relay:
- SSRF boundary — the target host comes only from the
administrator-managed registry (
CaldavServerService); the client chooses aserverIdand a path on that host, never a URL. An unknown, inactive or non-registry target is refused, not forwarded, and the refusal happens before any socket is opened. - Credential custody — the credentials are produced by the provider
the server row is configured with (
CaldavCredentialsResolver) and injected here, never assembled from anything this class holds; any Authorization or Cookie header the browser sends is dropped. Nothing auth-shaped is ever logged. The one exception isprobeAccount, which tries a credential pair typed in the connect drawer — material no provider can produce, because nothing stores it yet. - Method and header allow-lists — only the DAV verbs and headers the shipped connector actually issues pass through; everything else is attack surface and is refused or dropped.
- Per-server namespace — responses advertise hrefs rooted at the
upstream host's own path space (BlueMind roots everything at
/dav/); they are rewritten under the relay prefix of that one server, so two servers can both advertise/dav/without colliding — the collision a dev rig previously papered over with nginxsub_filter.
-
Field Summary
FieldsModifier and TypeFieldDescriptionThe DAV verbs the relay forwards: exactly what the shipped browser connector issues — PROPFIND (discovery, calendar and ETag listings), REPORT (calendar-query/multiget reads), GET (single-object read before a conditional write), PUT (create/update of an ICS object), DELETE (object removal), MKCALENDAR (mirror-calendar creation) — plus OPTIONS and HEAD, read-only capability probes some clients open with.static final StringMessage code for a relay path that is not a plain DAV resource path.static final StringMessage code for a relay request naming a method outside the allow-list.static final StringMessage code for a user with no stored CalDAV credentials.static final StringMessage code for a probe request missing its credentials.static final StringResponse header carrying the relay's own machine-readable outcome code, so the browser can tell a relay refusal (or a translated upstream credential rejection) from the same status genuinely answered by the upstream server, which always arrives without this header.static final StringMessage code for an upstream response larger than the configured cap.static final StringMessage code for a target server declared but deactivated.static final StringMessage code for a target that is not the server the account is connected to. -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionlongThe size cap applied to request and response bodies alike, so the REST layer can refuse an oversized upload with the same limit this service enforces on downloads.org.exoplatform.caldav.model.CaldavProbeResultprobeAccount(Long serverId, String username, String password) Probes a CalDAV account server-side, before anything is stored: the connect drawer sends the typed credentials once, here, instead of the browser probing the CalDAV origin directly — which BlueMind's missing CORS headers made impossible without an nginx front.org.exoplatform.caldav.model.CaldavRelayedResponserelay(org.exoplatform.caldav.model.CaldavRelayRequest relayRequest) Relays one DAV request of a connected user to the declared server it targets.protected StringrewriteHrefs(String xml, URI upstreamBase, String relayPrefix) Rewrites every DAV href of a multistatus body into relay space, so that the URL space the browser sees is closed under the relay: any href a response advertises — BlueMind's/dav/-rooted ones included — resolves back under/caldav/rest/dav/{serverId}/instead of escaping to the platform root, which is the exact bug class that produced "cannot find homeUrl" against BlueMind when hrefs resolved against the wrong base.protected StringrewriteHrefValue(String value, URI upstreamBase, String relayPrefix) Rewrites one href value into relay space: a path-rooted href is prefixed, an absolute URL naming the upstream host is folded onto the prefix, and anything else — notably an absolute URL naming a foreign host — is left untouched, because rewriting it would turn the relay into an open proxy toward hosts no administrator declared.
-
Field Details
-
METHOD_NOT_ALLOWED_MESSAGE
Message code for a relay request naming a method outside the allow-list.- See Also:
-
INVALID_PATH_MESSAGE
Message code for a relay path that is not a plain DAV resource path.- See Also:
-
NOT_CONNECTED_MESSAGE
Message code for a user with no stored CalDAV credentials.- See Also:
-
SERVER_INACTIVE_MESSAGE
Message code for a target server declared but deactivated.- See Also:
-
SERVER_MISMATCH_MESSAGE
Message code for a target that is not the server the account is connected to.- See Also:
-
RESPONSE_TOO_LARGE_MESSAGE
Message code for an upstream response larger than the configured cap.- See Also:
-
PROBE_CREDENTIALS_MESSAGE
Message code for a probe request missing its credentials.- See Also:
-
RELAY_CODE_HEADER
Response header carrying the relay's own machine-readable outcome code, so the browser can tell a relay refusal (or a translated upstream credential rejection) from the same status genuinely answered by the upstream server, which always arrives without this header.- See Also:
-
ALLOWED_METHODS
The DAV verbs the relay forwards: exactly what the shipped browser connector issues — PROPFIND (discovery, calendar and ETag listings), REPORT (calendar-query/multiget reads), GET (single-object read before a conditional write), PUT (create/update of an ICS object), DELETE (object removal), MKCALENDAR (mirror-calendar creation) — plus OPTIONS and HEAD, read-only capability probes some clients open with. Deliberately absent: PROPPATCH, POST, COPY, MOVE, LOCK, UNLOCK, MKCOL — no current flow sends them, so relaying them would be pure attack surface; the sync engine that may need PROPPATCH later talks to servers directly, not through this relay.Public because it is the single source of truth two gates read: this service's own 405 refusal, and the webapp's Spring Security
StrictHttpFirewall, whose allowed-method list is built as standard-HTTP ∪ this set (CaldavHttpFirewallConfiguration) — the firewall would otherwise reject every DAV verb as a 400 before the dispatcher ever saw it. Sharing the constant means a verb added here is automatically admitted by the firewall, and a verb only the firewall admits (the standard ones the admin REST needs) still meets this service's 405 on the relay path.
-
-
Constructor Details
-
CaldavRelayService
public CaldavRelayService()
-
-
Method Details
-
getMaxBodyBytes
public long getMaxBodyBytes()The size cap applied to request and response bodies alike, so the REST layer can refuse an oversized upload with the same limit this service enforces on downloads.- Returns:
- the cap in bytes
-
relay
public org.exoplatform.caldav.model.CaldavRelayedResponse relay(org.exoplatform.caldav.model.CaldavRelayRequest relayRequest) throws org.exoplatform.commons.exception.ObjectNotFoundException, IllegalAccessException Relays one DAV request of a connected user to the declared server it targets. The target host comes exclusively from the registry row — refused before any I/O when the row is unknown (ObjectNotFoundException), deactivated, or not the server the user's account is connected to (IllegalAccessException) — and the stored credentials of the user are injected as Basic auth. Upstream statuses pass through untouched so the browser connector's own DAV logic (207 parsing, 412 conflicts, BlueMind's 500-for-absent-object quirk) keeps holding, with one translation: an upstream 401/407 means the stored credentials are stale, so it becomes a 403 carryingcaldav.error.credentialsin the "x-caldav-relay-code" header, never a 401 the platform or the browser could read as an eXo authentication failure.- Parameters:
relayRequest- the request to relay, target named by serverId only- Returns:
- the upstream response, headers allow-listed, hrefs rewritten
- Throws:
org.exoplatform.commons.exception.ObjectNotFoundException- when no registration carries the idIllegalAccessException- when the server is deactivated or is not the one the user's account is connected toIllegalStateException- when the user has no stored credentialsIllegalArgumentException- when the method is outside the allow-list or the path is not a plain DAV resource path
-
probeAccount
public org.exoplatform.caldav.model.CaldavProbeResult probeAccount(Long serverId, String username, String password) throws org.exoplatform.commons.exception.ObjectNotFoundException, IllegalAccessException Probes a CalDAV account server-side, before anything is stored: the connect drawer sends the typed credentials once, here, instead of the browser probing the CalDAV origin directly — which BlueMind's missing CORS headers made impossible without an nginx front. The probe is a Depth:0 PROPFIND against the declared server's own URL and classifies the answer with the same stable codes the browser probe historically produced. It never stores anything and injects nothing from the user's stored setting: only the typed credentials are tried, only against a registry row.This is the one request a refused connection makes — the drawer stores nothing until this says yes — so it is also the only place a refused connection can be heard from. Every outcome but acceptance is therefore written down here, exactly once per attempt: at warning when somebody other than the person typing has to act (the server unreachable, or answering as something that is not a calendar), at debug when they do not (their own username or password refused, which the drawer already tells them in words). A gateway status is classified as unreachable rather than as "not a CalDAV calendar": a 502 in front of a banned or stopped server used to tell the user their administrator had configured the wrong address (EXO-89806).
- Parameters:
serverId- identifier of the declared server, or null for the resolution fallback (the seed registration)username- account username to trypassword- account password to try- Returns:
- the classified outcome, never null
- Throws:
org.exoplatform.commons.exception.ObjectNotFoundException- when no registration resolvesIllegalAccessException- when the resolved server is deactivatedIllegalArgumentException- when the credentials are blank or the username cannot be part of a URL path
-
rewriteHrefs
Rewrites every DAV href of a multistatus body into relay space, so that the URL space the browser sees is closed under the relay: any href a response advertises — BlueMind's/dav/-rooted ones included — resolves back under/caldav/rest/dav/{serverId}/instead of escaping to the platform root, which is the exact bug class that produced "cannot find homeUrl" against BlueMind when hrefs resolved against the wrong base.- Parameters:
xml- the multistatus bodyupstreamBase- the upstream base URI, for absolute-href matchingrelayPrefix- the relay prefix to root hrefs at- Returns:
- the body with every rewritable href moved into relay space
-
rewriteHrefValue
Rewrites one href value into relay space: a path-rooted href is prefixed, an absolute URL naming the upstream host is folded onto the prefix, and anything else — notably an absolute URL naming a foreign host — is left untouched, because rewriting it would turn the relay into an open proxy toward hosts no administrator declared.- Parameters:
value- the href text, possibly percent-encodedupstreamBase- the upstream base URIrelayPrefix- the relay prefix to root the href at- Returns:
- the rewritten href, or the value unchanged
-