Java tutorial
/* * ***** BEGIN LICENSE BLOCK ***** * Zimbra Collaboration Suite Server * Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software Foundation, * version 2 of the License. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * You should have received a copy of the GNU General Public License along with this program. * If not, see <https://www.gnu.org/licenses/>. * ***** END LICENSE BLOCK ***** */ package com.zimbra.cs.dav.service; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URLDecoder; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.HttpState; import org.dom4j.Document; import org.dom4j.Element; import com.google.common.collect.ImmutableSet; import com.google.common.net.HttpHeaders; import com.zimbra.client.ZFolder; import com.zimbra.client.ZMailbox; import com.zimbra.common.account.Key; import com.zimbra.common.account.Key.AccountBy; import com.zimbra.common.httpclient.HttpClientUtil; import com.zimbra.common.service.ServiceException; import com.zimbra.common.soap.W3cDomUtil; import com.zimbra.common.soap.XmlParseException; import com.zimbra.common.util.ByteUtil; import com.zimbra.common.util.HttpUtil; import com.zimbra.common.util.Log.Level; import com.zimbra.common.util.Pair; import com.zimbra.common.util.ZimbraHttpConnectionManager; import com.zimbra.common.util.ZimbraLog; import com.zimbra.cs.account.Account; import com.zimbra.cs.account.AuthToken; import com.zimbra.cs.account.AuthTokenException; import com.zimbra.cs.account.Provisioning; import com.zimbra.cs.account.Server; import com.zimbra.cs.dav.DavContext; import com.zimbra.cs.dav.DavContext.KnownUserAgent; import com.zimbra.cs.dav.DavElements; import com.zimbra.cs.dav.DavException; import com.zimbra.cs.dav.DavProtocol; import com.zimbra.cs.dav.DomUtil; import com.zimbra.cs.dav.service.method.Acl; import com.zimbra.cs.dav.service.method.Copy; import com.zimbra.cs.dav.service.method.Delete; import com.zimbra.cs.dav.service.method.Get; import com.zimbra.cs.dav.service.method.Head; import com.zimbra.cs.dav.service.method.Lock; import com.zimbra.cs.dav.service.method.MkCalendar; import com.zimbra.cs.dav.service.method.MkCol; import com.zimbra.cs.dav.service.method.Move; import com.zimbra.cs.dav.service.method.Options; import com.zimbra.cs.dav.service.method.Post; import com.zimbra.cs.dav.service.method.PropFind; import com.zimbra.cs.dav.service.method.PropPatch; import com.zimbra.cs.dav.service.method.Put; import com.zimbra.cs.dav.service.method.Report; import com.zimbra.cs.dav.service.method.Unlock; import com.zimbra.cs.mailbox.Folder; import com.zimbra.cs.mailbox.MailServiceException; import com.zimbra.cs.mailbox.Mailbox; import com.zimbra.cs.mailbox.MailboxManager; import com.zimbra.cs.mailbox.Mountpoint; import com.zimbra.cs.mailbox.calendar.cache.AccountCtags; import com.zimbra.cs.mailbox.calendar.cache.AccountKey; import com.zimbra.cs.mailbox.calendar.cache.CalendarCacheManager; import com.zimbra.cs.mailbox.calendar.cache.CtagInfo; import com.zimbra.cs.mailbox.calendar.cache.CtagResponseCache; import com.zimbra.cs.mailbox.calendar.cache.CtagResponseCache.CtagResponseCacheKey; import com.zimbra.cs.mailbox.calendar.cache.CtagResponseCache.CtagResponseCacheValue; import com.zimbra.cs.memcached.MemcachedConnector; import com.zimbra.cs.service.AuthProvider; import com.zimbra.cs.service.FileUploadServlet.Upload; import com.zimbra.cs.service.util.ItemId; import com.zimbra.cs.servlet.ZimbraServlet; import com.zimbra.cs.servlet.util.AuthUtil; import com.zimbra.cs.util.AccountUtil; import com.zimbra.cs.util.BuildInfo; @SuppressWarnings("serial") public class DavServlet extends ZimbraServlet { public static final String DAV_PATH = "/dav"; private static Map<String, DavMethod> sMethods; @Override public void init() throws ServletException { super.init(); sMethods = new HashMap<String, DavMethod>(); addMethod(new Copy()); addMethod(new Delete()); addMethod(new Get()); addMethod(new Head()); addMethod(new Lock()); addMethod(new MkCol()); addMethod(new Move()); addMethod(new Options()); addMethod(new Post()); addMethod(new Put()); addMethod(new PropFind()); addMethod(new PropPatch()); addMethod(new Unlock()); addMethod(new MkCalendar()); addMethod(new Report()); addMethod(new Acl()); } protected void addMethod(DavMethod method) { sMethods.put(method.getName(), method); } public static void setAllowHeader(HttpServletResponse resp) { Set<String> methods = sMethods.keySet(); StringBuilder buf = new StringBuilder(); for (String method : methods) { if (buf.length() > 0) buf.append(", "); buf.append(method); } DavMethod.setResponseHeader(resp, DavProtocol.HEADER_ALLOW, buf.toString()); } enum RequestType { password, authtoken, both, none }; private RequestType getAllowedRequestType(HttpServletRequest req) { if (!super.isRequestOnAllowedPort(req)) return RequestType.none; Server server = null; try { server = Provisioning.getInstance().getLocalServer(); } catch (Exception e) { return RequestType.none; } boolean allowPassword = server.getBooleanAttr(Provisioning.A_zimbraCalendarCalDavClearTextPasswordEnabled, true); int sslPort = server.getIntAttr(Provisioning.A_zimbraMailSSLPort, 443); int mailPort = server.getIntAttr(Provisioning.A_zimbraMailPort, 80); int incomingPort = req.getLocalPort(); if (incomingPort == sslPort) return RequestType.both; else if (incomingPort == mailPort && allowPassword) return RequestType.both; else return RequestType.authtoken; } private void logRequestInfo(HttpServletRequest req) { if (!ZimbraLog.dav.isDebugEnabled()) { return; } StringBuilder hdrs = new StringBuilder(); hdrs.append("DAV REQUEST:\n"); hdrs.append(req.getMethod()).append(" ").append(req.getRequestURL().toString()).append(" ") .append(req.getProtocol()); Enumeration<String> paramNames = req.getParameterNames(); if (paramNames != null && paramNames.hasMoreElements()) { hdrs.append("\nDAV REQUEST PARAMS:"); while (paramNames.hasMoreElements()) { String paramName = paramNames.nextElement(); if (paramName.contains("Auth")) { hdrs.append("\n").append(paramName).append("=*** REPLACED ***"); continue; } String params[] = req.getParameterValues(paramName); if (params != null) { for (String param : params) { hdrs.append("\n").append(paramName).append("=").append(param); } } } } /* Headers can include vital information which affects the request like "If-None-Match" headers, * so useful to be able to log them, skipping authentication related headers to avoid leaking passwords */ Enumeration<String> namesEn = req.getHeaderNames(); if (namesEn != null && namesEn.hasMoreElements()) { hdrs.append("\nDAV REQUEST HEADERS:"); while (namesEn.hasMoreElements()) { String hdrName = namesEn.nextElement(); if (hdrName.contains("Auth") || (hdrName.contains(HttpHeaders.COOKIE))) { hdrs.append("\n").append(hdrName).append(": *** REPLACED ***"); continue; } Enumeration<String> vals = req.getHeaders(hdrName); while (vals.hasMoreElements()) { hdrs.append("\n").append(hdrName).append(": ").append(vals.nextElement()); } } } ZimbraLog.dav.debug(hdrs.toString()); } public static StringBuilder addResponseHeaderLoggingInfo(HttpServletResponse resp, StringBuilder sb) { if (!ZimbraLog.dav.isDebugEnabled()) { return sb; } sb.append("DAV RESPONSE:\n"); String statusLine = DavResponse.sStatusTextMap.get(resp.getStatus()); if (statusLine != null) { sb.append(statusLine); } else { sb.append("HTTP/1.1 ").append(resp.getStatus()); } Collection<String> hdrNames = resp.getHeaderNames(); if (hdrNames != null && !hdrNames.isEmpty()) { for (String hdrName : hdrNames) { if (hdrName.contains("Auth") || (hdrName.contains(HttpHeaders.COOKIE))) { sb.append("\n").append(hdrName).append(": *** REPLACED ***"); continue; } Collection<String> vals = resp.getHeaders(hdrName); for (String val : vals) { sb.append("\n").append(hdrName).append(": ").append(val); } } } sb.append("\n\n"); return sb; } protected static void logResponseInfo(HttpServletResponse resp) { if (!ZimbraLog.dav.isDebugEnabled()) { return; } StringBuilder hdrs = addResponseHeaderLoggingInfo(resp, new StringBuilder()); ZimbraLog.dav.debug(hdrs.toString()); } private void sendError(HttpServletResponse resp, int statusCode, String logMsg, Exception e, Level level) throws IOException { try { resp.sendError(statusCode); if (ZimbraLog.dav.isEnabledFor(level)) { if (e == null) { ZimbraLog.dav.log(level, "%s. Sending HTTP Error - StatusCode %s", logMsg, statusCode); } else { ZimbraLog.dav.log(level, "%s. Sending HTTP Error - StatusCode %s", logMsg, statusCode, e); } } } catch (Exception except) { if (e == null) { ZimbraLog.dav.log(level, "2nd call to sendError will be ignored %s. StatusCode=%s newException=%s:%s", logMsg, statusCode, except.getClass().getName(), except.getMessage()); } else { ZimbraLog.dav.log(level, "2nd call to sendError will be ignored %s. StatusCode=%s 1st exception=%s newException=%s:%s", logMsg, statusCode, e.getMessage(), except.getClass().getName(), except.getMessage()); } throw except; } } private void sendError(HttpServletResponse resp, int statusCode, String logMsg, Exception e) throws IOException { sendError(resp, statusCode, logMsg, e, Level.error); } @Override public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ZimbraLog.clearContext(); addRemoteIpToLoggingContext(req); ZimbraLog.addUserAgentToContext(req.getHeader(DavProtocol.HEADER_USER_AGENT)); //bug fix - send 400 for Range requests String rangeHeader = req.getHeader(DavProtocol.HEADER_RANGE); if (null != rangeHeader) { sendError(resp, HttpServletResponse.SC_BAD_REQUEST, "Range header not supported", null, Level.debug); return; } RequestType rtype = getAllowedRequestType(req); ZimbraLog.dav.debug("Allowable request types %s", rtype); if (rtype == RequestType.none) { sendError(resp, HttpServletResponse.SC_NOT_ACCEPTABLE, "Not an allowed request type", null, Level.debug); return; } logRequestInfo(req); Account authUser = null; DavContext ctxt; try { AuthToken at = AuthProvider.getAuthToken(req, false); if (at != null && (at.isExpired() || !at.isRegistered())) { at = null; } if (at != null && (rtype == RequestType.both || rtype == RequestType.authtoken)) { authUser = Provisioning.getInstance().get(AccountBy.id, at.getAccountId()); } else if (at == null && (rtype == RequestType.both || rtype == RequestType.password)) { AuthUtil.AuthResult result = AuthUtil.basicAuthRequest(req, resp, true, this); if (result.sendErrorCalled) { logResponseInfo(resp); return; } authUser = result.authorizedAccount; } if (authUser == null) { try { sendError(resp, HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed", null, Level.debug); } catch (Exception e) { } return; } ZimbraLog.addToContext(ZimbraLog.C_ANAME, authUser.getName()); ctxt = new DavContext(req, resp, authUser); } catch (AuthTokenException e) { sendError(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "error getting authenticated user", e); return; } catch (ServiceException e) { sendError(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "error getting authenticated user", e); return; } DavMethod method = sMethods.get(req.getMethod()); if (method == null) { setAllowHeader(resp); sendError(resp, HttpServletResponse.SC_METHOD_NOT_ALLOWED, "Not an allowed method", null, Level.debug); return; } long t0 = System.currentTimeMillis(); CacheStates cache = null; try { if (ZimbraLog.dav.isDebugEnabled()) { try { Upload upload = ctxt.getUpload(); if (upload.getSize() > 0 && upload.getContentType().startsWith("text")) { if (ZimbraLog.dav.isDebugEnabled()) { StringBuilder logMsg = new StringBuilder("REQUEST\n").append( new String(ByteUtil.readInput(upload.getInputStream(), -1, 20480), "UTF-8")); ZimbraLog.dav.debug(logMsg.toString()); } } } catch (DavException de) { throw de; } catch (Exception e) { ZimbraLog.dav.debug("ouch", e); } } cache = checkCachedResponse(ctxt, authUser); if (!ctxt.isResponseSent() && !isProxyRequest(ctxt, method)) { method.checkPrecondition(ctxt); method.handle(ctxt); method.checkPostcondition(ctxt); if (!ctxt.isResponseSent()) { resp.setStatus(ctxt.getStatus()); } } if (!ctxt.isResponseSent()) { logResponseInfo(resp); } } catch (DavException e) { if (e.getCause() instanceof MailServiceException.NoSuchItemException || e.getStatus() == HttpServletResponse.SC_NOT_FOUND) ZimbraLog.dav.info(ctxt.getUri() + " not found"); else if (e.getStatus() == HttpServletResponse.SC_MOVED_TEMPORARILY || e.getStatus() == HttpServletResponse.SC_MOVED_PERMANENTLY) ZimbraLog.dav.info("sending redirect"); try { if (e.isStatusSet()) { resp.setStatus(e.getStatus()); if (e.hasErrorMessage()) e.writeErrorMsg(resp.getOutputStream()); if (ZimbraLog.dav.isDebugEnabled()) { ZimbraLog.dav.info("sending http error %d because: %s", e.getStatus(), e.getMessage(), e); } else { ZimbraLog.dav.info("sending http error %d because: %s", e.getStatus(), e.getMessage()); } if (e.getCause() != null) ZimbraLog.dav.debug("exception: ", e.getCause()); } else { sendError(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "error handling method " + method.getName(), e); } } catch (IllegalStateException ise) { ZimbraLog.dav.debug("can't write error msg", ise); } } catch (ServiceException e) { if (e instanceof MailServiceException.NoSuchItemException) { sendError(resp, HttpServletResponse.SC_NOT_FOUND, ctxt.getUri() + " not found", null, Level.info); return; } sendError(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "error handling method " + method.getName(), e); } catch (Exception e) { try { sendError(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "error handling method " + method.getName(), e); } catch (Exception ex) { } } finally { long t1 = System.currentTimeMillis(); ZimbraLog.dav.info("DavServlet operation " + method.getName() + " to " + req.getPathInfo() + " (depth: " + ctxt.getDepth().name() + ") finished in " + (t1 - t0) + "ms"); if (cache != null) cacheCleanUp(ctxt, cache); ctxt.cleanup(); } } public static String getDavUrl(String user) throws DavException, ServiceException { Provisioning prov = Provisioning.getInstance(); Account account = prov.get(AccountBy.name, user); if (account == null) throw new DavException("unknown user " + user, HttpServletResponse.SC_NOT_FOUND, null); return getServiceUrl(account, DAV_PATH); } private boolean isCtagRequest(DavContext ctxt) throws DavException { String httpMethod = ctxt.getRequest().getMethod(); if (PropFind.PROPFIND.equalsIgnoreCase(httpMethod) && ctxt.hasRequestMessage()) { Document doc = ctxt.getRequestMessage(); Element top = doc.getRootElement(); if (top == null || !top.getQName().equals(DavElements.E_PROPFIND)) return false; Element prop = top.element(DavElements.E_PROP); if (prop == null) return false; Iterator<?> iter = prop.elementIterator(); while (iter.hasNext()) { prop = (Element) iter.next(); if (prop.getQName().equals(DavElements.E_GETCTAG)) return true; } } return false; } private static class CacheStates { boolean ctagCacheEnabled = MemcachedConnector.isConnected(); boolean gzipAccepted = false; boolean cacheThisCtagResponse = false; CtagResponseCacheKey ctagCacheKey = null; String acctVerSnapshot = null; Map<Integer /* calendar folder id */, String /* ctag */> ctagsSnapshot = null; CtagResponseCache ctagResponseCache = null; } private CacheStates checkCachedResponse(DavContext ctxt, Account authUser) throws IOException, DavException, ServiceException { CacheStates cache = new CacheStates(); // Are we running with cache enabled, and is this a cachable CalDAV ctag request? if (cache.ctagCacheEnabled && isCtagRequest(ctxt)) { cache.ctagResponseCache = CalendarCacheManager.getInstance().getCtagResponseCache(); cache.gzipAccepted = ctxt.isGzipAccepted(); String targetUser = ctxt.getUser(); Account targetAcct = Provisioning.getInstance().get(AccountBy.name, targetUser); boolean ownAcct = targetAcct != null && targetAcct.getId().equals(authUser.getId()); String parentPath = ctxt.getPath(); KnownUserAgent knownUA = ctxt.getKnownUserAgent(); // Use cache only when requesting own account and User-Agent and path are well-defined. if (ownAcct && knownUA != null && parentPath != null) { AccountKey accountKey = new AccountKey(targetAcct.getId()); AccountCtags allCtagsData = CalendarCacheManager.getInstance().getCtags(accountKey); // We can't use cache if it doesn't have data for this user. if (allCtagsData != null) { boolean validRoot = true; int rootFolderId = Mailbox.ID_FOLDER_USER_ROOT; if (!"/".equals(parentPath)) { CtagInfo calInfoRoot = allCtagsData.getByPath(parentPath); if (calInfoRoot != null) rootFolderId = calInfoRoot.getId(); else validRoot = false; } if (validRoot) { // Is there a previously cached response? cache.ctagCacheKey = new CtagResponseCacheKey(targetAcct.getId(), knownUA.toString(), rootFolderId); CtagResponseCacheValue ctagResponse = cache.ctagResponseCache.get(cache.ctagCacheKey); if (ctagResponse != null) { // Found a cached response. Let's check if it's stale. // 1. If calendar list has been updated since, cached response is no good. String currentCalListVer = allCtagsData.getVersion(); if (currentCalListVer.equals(ctagResponse.getVersion())) { // 2. We have to examine ctags of individual calendars. boolean cacheHit = true; Map<Integer, String> oldCtags = ctagResponse.getCtags(); // We're good if ctags from before are unchanged. for (Map.Entry<Integer, String> entry : oldCtags.entrySet()) { int calFolderId = entry.getKey(); String ctag = entry.getValue(); CtagInfo calInfoCurr = allCtagsData.getById(calFolderId); if (calInfoCurr == null) { // Just a sanity check. The cal list version check should have // already taken care of added/removed calendars. cacheHit = false; break; } if (!ctag.equals(calInfoCurr.getCtag())) { // A calendar has been modified. Stale! cacheHit = false; break; } } if (cacheHit) { ZimbraLog.dav.debug("CTAG REQUEST CACHE HIT"); // All good. Send cached response. ctxt.setStatus(DavProtocol.STATUS_MULTI_STATUS); HttpServletResponse response = ctxt.getResponse(); response.setStatus(ctxt.getStatus()); response.setContentType(DavProtocol.DAV_CONTENT_TYPE); byte[] respData = ctagResponse.getResponseBody(); response.setContentLength(ctagResponse.getRawLength()); byte[] unzipped = null; if (ZimbraLog.dav.isDebugEnabled() || (ctagResponse.isGzipped() && !cache.gzipAccepted)) { if (ctagResponse.isGzipped()) { ByteArrayInputStream bais = new ByteArrayInputStream(respData); ByteArrayOutputStream baos = new ByteArrayOutputStream(); GZIPInputStream gzis = null; try { gzis = new GZIPInputStream(bais, respData.length); ByteUtil.copy(gzis, false, baos, true); } finally { ByteUtil.closeStream(gzis); } unzipped = baos.toByteArray(); } else { unzipped = respData; } if (ZimbraLog.dav.isDebugEnabled()) { ZimbraLog.dav.debug("RESPONSE:\n" + new String(unzipped, "UTF-8")); } } if (!ctagResponse.isGzipped()) { response.getOutputStream().write(respData); } else { if (cache.gzipAccepted) { response.addHeader(DavProtocol.HEADER_CONTENT_ENCODING, DavProtocol.ENCODING_GZIP); response.getOutputStream().write(respData); } else { assert (unzipped != null); response.getOutputStream().write(unzipped); } } // Tell the context the response has been sent. ctxt.responseSent(); } } } if (!ctxt.isResponseSent()) { // Cache miss, or cached response is stale. We're gonna have to generate the // response the hard way. Capture a snapshot of current state of calendars // to attach to the response to be cached later. cache.cacheThisCtagResponse = true; cache.acctVerSnapshot = allCtagsData.getVersion(); cache.ctagsSnapshot = new HashMap<Integer, String>(); Collection<CtagInfo> childCals = allCtagsData.getChildren(rootFolderId); if (rootFolderId != Mailbox.ID_FOLDER_USER_ROOT) { CtagInfo ctagRoot = allCtagsData.getById(rootFolderId); if (ctagRoot != null) cache.ctagsSnapshot.put(rootFolderId, ctagRoot.getCtag()); } for (CtagInfo calInfo : childCals) { cache.ctagsSnapshot.put(calInfo.getId(), calInfo.getCtag()); } } } } if (!ctxt.isResponseSent()) ZimbraLog.dav.debug("CTAG REQUEST CACHE MISS"); } } return cache; } private void cacheCleanUp(DavContext ctxt, CacheStates cache) throws IOException { if (cache.ctagCacheEnabled && cache.cacheThisCtagResponse && ctxt.getStatus() == DavProtocol.STATUS_MULTI_STATUS) { assert (cache.ctagCacheKey != null && cache.acctVerSnapshot != null && !cache.ctagsSnapshot.isEmpty()); DavResponse dresp = ctxt.getDavResponse(); ByteArrayOutputStream baosRaw = null; try { baosRaw = new ByteArrayOutputStream(); dresp.writeTo(baosRaw); } finally { ByteUtil.closeStream(baosRaw); } byte[] respData = baosRaw.toByteArray(); int rawLen = respData.length; boolean forceGzip = true; // Cache gzipped response if client supports it. boolean responseGzipped = forceGzip || cache.gzipAccepted; if (responseGzipped) { ByteArrayOutputStream baosGzipped = new ByteArrayOutputStream(); GZIPOutputStream gzos = null; try { gzos = new GZIPOutputStream(baosGzipped); gzos.write(respData); } finally { ByteUtil.closeStream(gzos); } respData = baosGzipped.toByteArray(); } CtagResponseCacheValue ctagCacheVal = new CtagResponseCacheValue(respData, rawLen, responseGzipped, cache.acctVerSnapshot, cache.ctagsSnapshot); try { cache.ctagResponseCache.put(cache.ctagCacheKey, ctagCacheVal); } catch (ServiceException e) { ZimbraLog.dav.warn("Unable to cache ctag response", e); // No big deal if we can't cache the response. Just move on. } } } private static final Set<String> PROXY_REQUEST_HEADERS = ImmutableSet.of(DavProtocol.HEADER_DAV, DavProtocol.HEADER_DEPTH, DavProtocol.HEADER_CONTENT_TYPE, DavProtocol.HEADER_ETAG, DavProtocol.HEADER_IF_MATCH, DavProtocol.HEADER_OVERWRITE, DavProtocol.HEADER_DESTINATION); private static final Set<String> IGNORABLE_PROXY_REQUEST_HEADERS = ImmutableSet.of( DavProtocol.HEADER_AUTHORIZATION, DavProtocol.HEADER_HOST, DavProtocol.HEADER_USER_AGENT, DavProtocol.HEADER_CONTENT_LENGTH); private static final Set<String> PROXY_RESPONSE_HEADERS = ImmutableSet.of(DavProtocol.HEADER_DAV, DavProtocol.HEADER_ALLOW, DavProtocol.HEADER_CONTENT_TYPE, DavProtocol.HEADER_ETAG, DavProtocol.HEADER_LOCATION); private static final Set<String> IGNORABLE_PROXY_RESPONSE_HEADERS = ImmutableSet.of(DavProtocol.HEADER_DATE, DavProtocol.HEADER_CONTENT_LENGTH); private boolean isProxyRequest(DavContext ctxt, DavMethod m) throws IOException, DavException, ServiceException { Provisioning prov = Provisioning.getInstance(); ItemId target = null; String extraPath = null; String requestPath = ctxt.getPath(); try { if (ctxt.getUser() == null) { return false; } if (requestPath == null || requestPath.length() < 2) { return false; } Account account = prov.getAccountByName(ctxt.getUser()); if (account == null) { return false; } Mailbox mbox = MailboxManager.getInstance().getMailboxByAccount(account); Pair<Folder, String> match = mbox.getFolderByPathLongestMatch(ctxt.getOperationContext(), Mailbox.ID_FOLDER_USER_ROOT, requestPath); Folder targetFolder = match.getFirst(); if (!(targetFolder instanceof Mountpoint)) { return false; } Mountpoint mp = (Mountpoint) targetFolder; target = new ItemId(mp.getOwnerId(), mp.getRemoteId()); extraPath = match.getSecond(); } catch (ServiceException e) { ZimbraLog.dav.debug("can't get path", e); return false; } // we don't proxy zero depth PROPFIND, and all PROPPATCH on mountpoints, // because the mountpoint object contains WebDAV properties that are // private to the user. // we also don't proxy DELETE on a mountpoint. if (extraPath == null && (m.getName().equals(PropFind.PROPFIND) && ctxt.getDepth() == DavContext.Depth.zero || m.getName().equals(PropPatch.PROPPATCH) || m.getName().equals(Delete.DELETE))) { return false; } String prefix = ctxt.getPath(); if (extraPath != null) { prefix = prefix.substring(0, prefix.indexOf(extraPath)); } prefix = HttpUtil.urlEscape(DAV_PATH + "/" + ctxt.getUser() + prefix); if (!prefix.endsWith("/")) { prefix += "/"; } // make sure the target account exists. Account acct = prov.getAccountById(target.getAccountId()); if (acct == null) { return false; } Server server = prov.getServer(acct); if (server == null) { return false; } // get the path to the target mail item AuthToken authToken = AuthProvider.getAuthToken(ctxt.getAuthAccount()); ZMailbox.Options zoptions = new ZMailbox.Options(authToken.toZAuthToken(), AccountUtil.getSoapUri(acct)); zoptions.setNoSession(true); zoptions.setTargetAccount(target.getAccountId()); zoptions.setTargetAccountBy(Key.AccountBy.id); ZMailbox zmbx = ZMailbox.getMailbox(zoptions); ZFolder f = zmbx.getFolderById("" + target.toString()); if (f == null) { return false; } String path = f.getPath(); String newPrefix = HttpUtil.urlEscape(DAV_PATH + "/" + acct.getName() + f.getPath()); if (ctxt.hasRequestMessage()) { // replace the path in <href> of the request with the path to the target mail item. Document req = ctxt.getRequestMessage(); for (Object hrefObj : req.getRootElement().elements(DavElements.E_HREF)) { if (!(hrefObj instanceof Element)) { continue; } Element href = (Element) hrefObj; String v = href.getText(); // prefix matching is not as straightforward as we have jetty redirect from /dav to /home/dav. href.setText(newPrefix + "/" + v.substring(v.lastIndexOf('/') + 1)); } } // build proxy request String url = getProxyUrl(ctxt.getRequest(), server, DAV_PATH) + HttpUtil.urlEscape("/" + acct.getName() + path + "/" + (extraPath == null ? "" : extraPath)); HttpState state = new HttpState(); authToken.encode(state, false, server.getAttr(Provisioning.A_zimbraServiceHostname)); HttpClient client = ZimbraHttpConnectionManager.getInternalHttpConnMgr().newHttpClient(); client.setState(state); HttpMethod method = m.toHttpMethod(ctxt, url); method.setRequestHeader(new Header(DavProtocol.HEADER_USER_AGENT, "Zimbra-DAV/" + BuildInfo.VERSION)); if (ZimbraLog.dav.isDebugEnabled()) { Enumeration<String> headers = ctxt.getRequest().getHeaderNames(); while (headers.hasMoreElements()) { String hdr = headers.nextElement(); if (!PROXY_REQUEST_HEADERS.contains(hdr) && !IGNORABLE_PROXY_REQUEST_HEADERS.contains(hdr)) { ZimbraLog.dav.debug( "Dropping header(s) with name [%s] from proxy request (not in PROXY_REQUEST_HEADERS)", hdr); } } } for (String h : PROXY_REQUEST_HEADERS) { String hval = ctxt.getRequest().getHeader(h); if (hval != null) { method.addRequestHeader(h, hval); } } int statusCode = HttpClientUtil.executeMethod(client, method); if (ZimbraLog.dav.isDebugEnabled()) { for (Header hval : method.getResponseHeaders()) { String hdrName = hval.getName(); if (!PROXY_RESPONSE_HEADERS.contains(hdrName) && !IGNORABLE_PROXY_RESPONSE_HEADERS.contains(hdrName)) { ZimbraLog.dav.debug("Dropping header [%s] from proxy response (not in PROXY_RESPONSE_HEADERS)", hval); } } } for (String h : PROXY_RESPONSE_HEADERS) { for (Header hval : method.getResponseHeaders(h)) { String hdrValue = hval.getValue(); if (DavProtocol.HEADER_LOCATION.equals(h)) { int pfxLastSlashPos = prefix.lastIndexOf('/'); int lastSlashPos = hdrValue.lastIndexOf('/'); if ((lastSlashPos > 0) && (pfxLastSlashPos > 0)) { hdrValue = prefix.substring(0, pfxLastSlashPos) + hdrValue.substring(lastSlashPos); ZimbraLog.dav.debug("Original [%s] from proxy response new value '%s'", hval, hdrValue); } } ctxt.getResponse().addHeader(h, hdrValue); } } ctxt.getResponse().setStatus(statusCode); ctxt.setStatus(statusCode); try (InputStream in = method.getResponseBodyAsStream()) { switch (statusCode) { case DavProtocol.STATUS_MULTI_STATUS: // rewrite the <href> element in the response to point to local mountpoint. try { Document response = W3cDomUtil.parseXMLToDom4jDocUsingSecureProcessing(in); Element top = response.getRootElement(); for (Object responseObj : top.elements(DavElements.E_RESPONSE)) { if (!(responseObj instanceof Element)) { continue; } Element href = ((Element) responseObj).element(DavElements.E_HREF); String v = href.getText(); v = URLDecoder.decode(v); // Bug:106438, because v contains URL encoded value(%40) for '@' the comparison fails if (v.startsWith(newPrefix)) { href.setText(prefix + v.substring(newPrefix.length() + 1)); } } if (ZimbraLog.dav.isDebugEnabled()) { ZimbraLog.dav.debug("PROXY RESPONSE:\n%s", new String(DomUtil.getBytes(response), "UTF-8")); } DomUtil.writeDocumentToStream(response, ctxt.getResponse().getOutputStream()); ctxt.responseSent(); } catch (XmlParseException e) { ZimbraLog.dav.warn("proxy request failed", e); return false; } break; default: if (in != null) { ByteUtil.copy(in, true, ctxt.getResponse().getOutputStream(), false); } ctxt.responseSent(); break; } return true; } } }