Class CaldavRelayService

java.lang.Object
org.exoplatform.caldav.service.CaldavRelayService

@Service public class CaldavRelayService extends Object
The server-side DAV relay: the browser addresses declared CalDAV servers through /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 a serverId and 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 is probeAccount, 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 nginx sub_filter.
  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    static final Set<String>
    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.
    static final String
    Message code for a relay path that is not a plain DAV resource path.
    static final String
    Message code for a relay request naming a method outside the allow-list.
    static final String
    Message code for a user with no stored CalDAV credentials.
    static final String
    Message code for a probe request missing its credentials.
    static final String
    The one-click path refusing a connector that does expect the user to type something.
    static final String
    The one-click path refusing a server whose connector agenda has switched off.
    static final String
    The one-click path refusing a connector whose provider cannot name the account.
    static final String
    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.
    static final String
    Message code for an upstream response larger than the configured cap.
    static final String
    Message code for a target server declared but deactivated.
    static final String
    Message code for a target that is not the server the account is connected to.
  • Constructor Summary

    Constructors
    Constructor
    Description
     
  • Method Summary

    Modifier and Type
    Method
    Description
    org.exoplatform.caldav.model.CaldavProbeResult
    connectThroughProvider(Long serverId, String exoLogin)
    Connects a user to a registration whose provider asks them for nothing, and records it.
    org.exoplatform.caldav.model.CaldavProbeResult
    connectThroughProvider(Long serverId, String exoLogin, boolean byManagedMode, BooleanSupplier stillWanted)
    Connects a user through the server's provider, as connectThroughProvider(Long, String) does, and records who made the connection: managed mode at login, or the user themselves.
    void
    disconnectForUser(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.
    long
    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.
    org.exoplatform.caldav.model.CaldavProbeResult
    probeAccount(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.CaldavRelayedResponse
    relay(org.exoplatform.caldav.model.CaldavRelayRequest relayRequest)
    Relays one DAV request of a connected user to the declared server it targets.
    protected String
    rewriteHrefs(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 String
    rewriteHrefValue(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.

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Field Details

    • METHOD_NOT_ALLOWED_MESSAGE

      public static final String METHOD_NOT_ALLOWED_MESSAGE
      Message code for a relay request naming a method outside the allow-list.
      See Also:
    • INVALID_PATH_MESSAGE

      public static final String INVALID_PATH_MESSAGE
      Message code for a relay path that is not a plain DAV resource path.
      See Also:
    • NOT_CONNECTED_MESSAGE

      public static final String NOT_CONNECTED_MESSAGE
      Message code for a user with no stored CalDAV credentials.
      See Also:
    • SERVER_INACTIVE_MESSAGE

      public static final String SERVER_INACTIVE_MESSAGE
      Message code for a target server declared but deactivated.
      See Also:
    • SERVER_MISMATCH_MESSAGE

      public static final String SERVER_MISMATCH_MESSAGE
      Message code for a target that is not the server the account is connected to.
      See Also:
    • RESPONSE_TOO_LARGE_MESSAGE

      public static final String RESPONSE_TOO_LARGE_MESSAGE
      Message code for an upstream response larger than the configured cap.
      See Also:
    • PROBE_CREDENTIALS_MESSAGE

      public static final String PROBE_CREDENTIALS_MESSAGE
      Message code for a probe request missing its credentials.
      See Also:
    • PROVIDER_ASKS_MESSAGE

      public static final String PROVIDER_ASKS_MESSAGE
      The one-click path refusing a connector that does expect the user to type something.
      See Also:
    • PROVIDER_NAMES_NOBODY_MESSAGE

      public static final String PROVIDER_NAMES_NOBODY_MESSAGE
      The one-click path refusing a connector whose provider cannot name the account.
      See Also:
    • PROVIDER_DISABLED_MESSAGE

      public static final String PROVIDER_DISABLED_MESSAGE
      The one-click path refusing a server whose connector agenda has switched off.
      See Also:
    • RELAY_CODE_HEADER

      public static final String 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

      public static final Set<String> 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 carrying caldav.error.credentials in 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 id
      IllegalAccessException - when the server is deactivated or is not the one the user's account is connected to
      IllegalStateException - when the user has no stored credentials
      IllegalArgumentException - 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 try
      password - account password to try
      Returns:
      the classified outcome, never null
      Throws:
      org.exoplatform.commons.exception.ObjectNotFoundException - when no registration resolves
      IllegalAccessException - when the resolved server is deactivated
      IllegalArgumentException - 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 one
      exoLogin - 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 declared
      IllegalAccessException - 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, as connectThroughProvider(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 stillWanted still 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 one
      exoLogin - the eXo login connecting
      byManagedMode - true when managed mode makes the connection at login
      stillWanted - asked once, after a probe that passed and right before anything is written
      Returns:
      the probe's outcome, or CaldavProbeResult.SUPERSEDED when the probe passed and stillWanted declined the writes; the connection is recorded only when it is OK
      Throws:
      org.exoplatform.commons.exception.ObjectNotFoundException - when no server is declared
      IllegalAccessException - when the server is inactive, when agenda has switched its connector off, or when the provider named no account
    • disconnectForUser

      public void disconnectForUser(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. The counterpart of connectThroughProvider(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 disconnect
      username - the eXo login, null when the identity no longer names anybody
    • rewriteHrefs

      protected String rewriteHrefs(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.
      Parameters:
      xml - the multistatus body
      upstreamBase - the upstream base URI, for absolute-href matching
      relayPrefix - the relay prefix to root hrefs at
      Returns:
      the body with every rewritable href moved into relay space
    • rewriteHrefValue

      protected String rewriteHrefValue(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.
      Parameters:
      value - the href text, possibly percent-encoded
      upstreamBase - the upstream base URI
      relayPrefix - the relay prefix to root the href at
      Returns:
      the rewritten href, or the value unchanged