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 StringThe one-click path refusing a connector that does expect the user to type something.static final StringThe one-click path refusing a server whose connector agenda has switched off.static final StringThe one-click path refusing a connector whose provider cannot name the account.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 TypeMethodDescriptionorg.exoplatform.caldav.model.CaldavProbeResultconnectThroughProvider(Long serverId, String exoLogin) Connects a user to a registration whose provider asks them for nothing, and records it.org.exoplatform.caldav.model.CaldavProbeResultconnectThroughProvider(Long serverId, String exoLogin, boolean byManagedMode, BooleanSupplier stillWanted) Connects a user through the server's provider, asconnectThroughProvider(Long, String)does, and records who made the connection: managed mode at login, or the user themselves.voiddisconnectForUser(long userIdentityId, String username) Disconnects a user on the platform's initiative - an administrator's change, or the login that finds managed mode no longer governs them.longThe 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:
-
PROVIDER_ASKS_MESSAGE
The one-click path refusing a connector that does expect the user to type something.- See Also:
-
PROVIDER_NAMES_NOBODY_MESSAGE
The one-click path refusing a connector whose provider cannot name the account.- See Also:
-
PROVIDER_DISABLED_MESSAGE
The one-click path refusing a server whose connector agenda has switched off.- 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
-
connectThroughProvider
public org.exoplatform.caldav.model.CaldavProbeResult connectThroughProvider(Long serverId, String exoLogin) throws org.exoplatform.commons.exception.ObjectNotFoundException, IllegalAccessException Connects a user to a registration whose provider asks them for nothing, and records it.The verification still happens - with the service account's own material, which is what this connection will use for every later request. Answering "connected" without probing would move the failure of a misconfigured technical account to the first synchronisation, where the user sees an empty calendar and nobody is told why.
Nothing is recorded unless the server answered as a calendar: a connection that is stored and does not work is worse than one that was refused, because only the first looks fine on screen.
- Parameters:
serverId- registration to connect to, or null for the legacy oneexoLogin- the eXo login connecting- Returns:
- the probe outcome; the connection is recorded only on
CaldavProbeResult.OK - Throws:
org.exoplatform.commons.exception.ObjectNotFoundException- when no such registration is declaredIllegalAccessException- when the registration is deactivated, when agenda has switched its connector off, or when the provider named no account
-
connectThroughProvider
public org.exoplatform.caldav.model.CaldavProbeResult connectThroughProvider(Long serverId, String exoLogin, boolean byManagedMode, BooleanSupplier stillWanted) throws org.exoplatform.commons.exception.ObjectNotFoundException, IllegalAccessException Connects a user through the server's provider, asconnectThroughProvider(Long, String)does, and records who made the connection: managed mode at login, or the user themselves. Only a connection made by managed mode is marked; one the user makes clears the mark, since it is their own choice from then on.The connection is recorded only if
stillWantedstill answers true at the moment of the writes. The login-time attachment passes "the user still has no configuration": the probe can take as long as the server's timeout, and a connection the user made in that window is theirs, not something to overwrite.- Parameters:
serverId- the server to connect to, null for the default oneexoLogin- the eXo login connectingbyManagedMode- true when managed mode makes the connection at loginstillWanted- asked once, after a probe that passed and right before anything is written- Returns:
- the probe's outcome, or
CaldavProbeResult.SUPERSEDEDwhen the probe passed andstillWanteddeclined the writes; the connection is recorded only when it is OK - Throws:
org.exoplatform.commons.exception.ObjectNotFoundException- when no server is declaredIllegalAccessException- when the server is inactive, when agenda has switched its connector off, or when the provider named no account
-
disconnectForUser
Disconnects a user on the platform's initiative - an administrator's change, or the login that finds managed mode no longer governs them. The counterpart ofconnectThroughProvider(Long, String, boolean, BooleanSupplier): agenda's record of the connection goes as well as caldav's, or "My calendars" would still show a connector whose account is gone. A user's own disconnection goes through the front, which removes agenda's record itself.- Parameters:
userIdentityId- the identity to disconnectusername- the eXo login, null when the identity no longer names anybody
-
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
-