Java tutorial
/* * ***** BEGIN LICENSE BLOCK ***** * Zimbra Collaboration Suite Server * Copyright (C) 2005, 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 ***** */ /* * Created on 2005. 4. 5. */ package com.zimbra.cs.servlet; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.httpclient.Cookie; 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.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.InputStreamRequestEntity; import org.apache.commons.httpclient.methods.PostMethod; 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.Element; import com.zimbra.common.soap.SoapProtocol; import com.zimbra.common.util.ByteUtil; import com.zimbra.common.util.HttpUtil; import com.zimbra.common.util.Log; import com.zimbra.common.util.LogFactory; import com.zimbra.common.util.RemoteIP; import com.zimbra.common.util.ZimbraCookie; 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.Domain; import com.zimbra.cs.account.Provisioning; import com.zimbra.cs.account.Server; import com.zimbra.cs.httpclient.URLUtil; import com.zimbra.cs.service.AuthProvider; import com.zimbra.cs.servlet.util.AuthUtil; import com.zimbra.cs.util.Zimbra; /** * Superclass for all Zimbra servlets. Supports port filtering and * provides some utility methods to subclasses. */ public class ZimbraServlet extends HttpServlet { private static final long serialVersionUID = 5025244890767551679L; private static Log mLog = LogFactory.getLog(ZimbraServlet.class); private static final String PARAM_ALLOWED_PORTS = "allowed.ports"; public static final String QP_ZAUTHTOKEN = "zauthtoken"; protected String getRealmHeader(String realm) { if (realm == null) realm = "Zimbra"; return "BASIC realm=\"" + realm + "\""; } protected String getRealmHeader(HttpServletRequest req, Domain domain) { String realm = null; if (domain == null) { // get domain by virtual host String host = HttpUtil.getVirtualHost(req); if (host != null) { // to defend against DOS attack, use the negative domain cache try { domain = Provisioning.getInstance().getDomain(Key.DomainBy.virtualHostname, host.toLowerCase(), true); } catch (ServiceException e) { mLog.warn("caught exception while getting domain by virtual host: " + host, e); } } } if (domain != null) realm = domain.getBasicAuthRealm(); return getRealmHeader(realm); } public static final String ZIMBRA_FAULT_CODE_HEADER = "X-Zimbra-Fault-Code"; public static final String ZIMBRA_FAULT_MESSAGE_HEADER = "X-Zimbra-Fault-Message"; private static final int MAX_PROXY_HOPCOUNT = 3; private static Map<String, ZimbraServlet> sServlets = new HashMap<String, ZimbraServlet>(); private int[] mAllowedPorts; @Override public void init() throws ServletException { try { String portsCSV = getInitParameter(PARAM_ALLOWED_PORTS); if (portsCSV != null) { // Split on zero-or-more spaces followed by comma followed by // zero-or-more spaces. String[] vals = portsCSV.split("\\s*,\\s*"); if (vals == null || vals.length == 0) throw new ServletException("Must specify comma-separated list of port numbers for " + PARAM_ALLOWED_PORTS + " parameter"); List<Integer> allowedPorts = new ArrayList<Integer>(); int port; for (int i = 0; i < vals.length; i++) { try { port = Integer.parseInt(vals[i]); } catch (NumberFormatException e) { throw new ServletException( "Invalid port number \"" + vals[i] + "\" in " + PARAM_ALLOWED_PORTS + " parameter"); } if (port < 0) throw new ServletException("Invalid port number " + vals[i] + " in " + PARAM_ALLOWED_PORTS + " parameter; port number must be greater than zero"); else if (port != 0) // 0 is a legit value for those ports that are disabled allowedPorts.add(port); } mAllowedPorts = new int[allowedPorts.size()]; for (int i = 0; i < allowedPorts.size(); i++) mAllowedPorts[i] = allowedPorts.get(i); } // Store reference to this servlet for accessor synchronized (sServlets) { String name = getServletName(); if (sServlets.containsKey(name)) { Zimbra.halt("Attempted to instantiate a second instance of " + name); } sServlets.put(getServletName(), this); mLog.debug("Added " + getServletName() + " to the servlet list"); } } catch (Throwable t) { Zimbra.halt("Unable to initialize servlet " + getServletName() + "; halting", t); } } public static ZimbraServlet getServlet(String name) { synchronized (sServlets) { return sServlets.get(name); } } protected boolean isRequestOnAllowedPort(HttpServletRequest request) { if (mAllowedPorts != null && mAllowedPorts.length > 0) { int incoming = request.getLocalPort(); for (int i = 0; i < mAllowedPorts.length; i++) { if (mAllowedPorts[i] == incoming) { return true; } } return false; } return true; } /** * Filter the request based on incoming port. If the allowed.ports * parameter is specified for the servlet, the incoming port must * match one of the listed ports. */ @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { boolean allowed = isRequestOnAllowedPort(request); if (!allowed) { SoapProtocol soapProto = SoapProtocol.Soap12; ServiceException e = ServiceException.FAILURE("Request not allowed on port " + request.getLocalPort(), null); ZimbraLog.soap.warn(null, e); Element fault = SoapProtocol.Soap12.soapFault(e); Element envelope = SoapProtocol.Soap12.soapEnvelope(fault); byte[] soapBytes = envelope.toUTF8(); response.setContentType(soapProto.getContentType()); response.setBufferSize(soapBytes.length + 2048); response.setContentLength(soapBytes.length); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); response.getOutputStream().write(soapBytes); return; } super.service(request, response); } public static AuthToken getAuthTokenFromCookie(HttpServletRequest req, HttpServletResponse resp) throws IOException { return getAuthTokenFromHttpReq(req, resp, false, false); } public static AuthToken getAuthTokenFromCookie(HttpServletRequest req, HttpServletResponse resp, boolean doNotSendHttpError) throws IOException { return getAuthTokenFromHttpReq(req, resp, false, doNotSendHttpError); } public static AuthToken getAdminAuthTokenFromCookie(HttpServletRequest req, HttpServletResponse resp) throws IOException { return getAuthTokenFromHttpReq(req, resp, true, false); } public static AuthToken getAdminAuthTokenFromCookie(HttpServletRequest req, HttpServletResponse resp, boolean doNotSendHttpError) throws IOException { return getAuthTokenFromHttpReq(req, resp, true, doNotSendHttpError); } public static AuthToken getAdminAuthTokenFromCookie(HttpServletRequest req) { return getAuthTokenFromHttpReq(req, true); } public static AuthToken getAuthTokenFromHttpReq(HttpServletRequest req, HttpServletResponse resp, boolean isAdminReq, boolean doNotSendHttpError) throws IOException { AuthToken authToken = null; try { authToken = AuthProvider.getAuthToken(req, isAdminReq); if (authToken == null) { if (!doNotSendHttpError) resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "no authtoken cookie"); return null; } if (authToken.isExpired() || !authToken.isRegistered()) { if (!doNotSendHttpError) resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "authtoken expired"); return null; } return authToken; } catch (AuthTokenException e) { if (!doNotSendHttpError) resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "unable to parse authtoken"); return null; } } public static AuthToken getAuthTokenFromHttpReq(HttpServletRequest req, boolean isAdminReq) { AuthToken authToken = null; try { authToken = AuthProvider.getAuthToken(req, isAdminReq); if (authToken == null) return null; if (authToken.isExpired()) return null; if (!authToken.isRegistered()) return null; return authToken; } catch (AuthTokenException e) { return null; } } public static void proxyServletRequest(HttpServletRequest req, HttpServletResponse resp, String accountId) throws IOException, ServiceException { Provisioning prov = Provisioning.getInstance(); Account acct = prov.get(AccountBy.id, accountId); if (acct == null) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no such user"); return; } proxyServletRequest(req, resp, prov.getServer(acct), null); } public static void proxyServletRequest(HttpServletRequest req, HttpServletResponse resp, Server server, AuthToken authToken) throws IOException, ServiceException { String uri = req.getRequestURI(); String qs = req.getQueryString(); if (qs != null) { uri += '?' + qs; } proxyServletRequest(req, resp, server, uri, authToken); } public static void proxyServletRequest(HttpServletRequest req, HttpServletResponse resp, Server server, String uri, AuthToken authToken) throws IOException, ServiceException { if (server == null) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "cannot find remote server"); return; } HttpMethod method; String url = getProxyUrl(req, server, uri); mLog.debug("Proxy URL = %s", url); if (req.getMethod().equalsIgnoreCase("GET")) { method = new GetMethod(url.toString()); } else if (req.getMethod().equalsIgnoreCase("POST") || req.getMethod().equalsIgnoreCase("PUT")) { PostMethod post = new PostMethod(url.toString()); post.setRequestEntity(new InputStreamRequestEntity(req.getInputStream())); method = post; } else { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "cannot proxy method: " + req.getMethod()); return; } HttpState state = new HttpState(); String hostname = method.getURI().getHost(); if (authToken != null) { authToken.encode(state, false, hostname); } try { proxyServletRequest(req, resp, method, state); } finally { method.releaseConnection(); } } private static boolean hasZimbraAuthCookie(HttpState state) { Cookie[] cookies = state.getCookies(); if (cookies == null) return false; for (Cookie c : cookies) { if (c.getName().equals(ZimbraCookie.COOKIE_ZM_AUTH_TOKEN)) return true; } return false; } public static void proxyServletRequest(HttpServletRequest req, HttpServletResponse resp, HttpMethod method, HttpState state) throws IOException, ServiceException { // create an HTTP client with the same cookies javax.servlet.http.Cookie cookies[] = req.getCookies(); String hostname = method.getURI().getHost(); boolean hasZMAuth = hasZimbraAuthCookie(state); if (cookies != null) { for (int i = 0; i < cookies.length; i++) { if (cookies[i].getName().equals(ZimbraCookie.COOKIE_ZM_AUTH_TOKEN) && hasZMAuth) continue; state.addCookie( new Cookie(hostname, cookies[i].getName(), cookies[i].getValue(), "/", null, false)); } } HttpClient client = ZimbraHttpConnectionManager.getInternalHttpConnMgr().newHttpClient(); if (state != null) client.setState(state); int hopcount = 0; for (Enumeration<?> enm = req.getHeaderNames(); enm.hasMoreElements();) { String hname = (String) enm.nextElement(), hlc = hname.toLowerCase(); if (hlc.equals("x-zimbra-hopcount")) try { hopcount = Math.max(Integer.parseInt(req.getHeader(hname)), 0); } catch (NumberFormatException e) { } else if (hlc.startsWith("x-") || hlc.startsWith("content-") || hlc.equals("authorization")) method.addRequestHeader(hname, req.getHeader(hname)); } if (hopcount >= MAX_PROXY_HOPCOUNT) throw ServiceException.TOO_MANY_HOPS(HttpUtil.getFullRequestURL(req)); method.addRequestHeader("X-Zimbra-Hopcount", Integer.toString(hopcount + 1)); if (method.getRequestHeader("X-Zimbra-Orig-Url") == null) method.addRequestHeader("X-Zimbra-Orig-Url", req.getRequestURL().toString()); String ua = req.getHeader("User-Agent"); if (ua != null) method.setRequestHeader("User-Agent", ua); // dispatch the request and copy over the results int statusCode = -1; for (int retryCount = 3; statusCode == -1 && retryCount > 0; retryCount--) { statusCode = HttpClientUtil.executeMethod(client, method); } if (statusCode == -1) { resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "retry limit reached"); return; } else if (statusCode >= 300) { resp.sendError(statusCode, method.getStatusText()); return; } Header[] headers = method.getResponseHeaders(); for (int i = 0; i < headers.length; i++) { String hname = headers[i].getName(), hlc = hname.toLowerCase(); if (hlc.startsWith("x-") || hlc.startsWith("content-") || hlc.startsWith("www-")) resp.addHeader(hname, headers[i].getValue()); } InputStream responseStream = method.getResponseBodyAsStream(); if (responseStream == null || resp.getOutputStream() == null) return; ByteUtil.copy(method.getResponseBodyAsStream(), false, resp.getOutputStream(), false); } protected boolean isAdminRequest(HttpServletRequest req) throws ServiceException { int adminPort = Provisioning.getInstance().getLocalServer().getIntAttr(Provisioning.A_zimbraAdminPort, -1); if (req.getLocalPort() == adminPort) { //can still be in offline server where port=adminPort int mailPort = Provisioning.getInstance().getLocalServer().getIntAttr(Provisioning.A_zimbraMailPort, -1); if (mailPort == adminPort) //we are in offline, so check cookie return getAdminAuthTokenFromCookie(req) != null; else return true; } return false; } public AuthToken cookieAuthRequest(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServiceException { AuthToken at = isAdminRequest(req) ? getAdminAuthTokenFromCookie(req, resp, true) : getAuthTokenFromCookie(req, resp, true); return at; } /** * Note that if this method returns null, it isn't clear whether resp.sendError has been called or not. * For that reason, it has been deprecated. Believe there is no Zimbra code which still calls it but * left in case customization code uses it. */ @Deprecated public Account basicAuthRequest(HttpServletRequest req, HttpServletResponse resp, boolean sendChallenge) throws IOException, ServiceException { return AuthUtil.basicAuthRequest(req, resp, this, sendChallenge); } public static String getAccountPath(Account acct) { return "/" + acct.getName(); } public static String getServiceUrl(Account acct, String path) throws ServiceException { Provisioning prov = Provisioning.getInstance(); Server server = prov.getServer(acct); if (server == null) { throw ServiceException.FAILURE("unable to retrieve server for account" + acct.getName(), null); } return getServiceUrl(server, prov.getDomain(acct), path + getAccountPath(acct)); } public static String getServiceUrl(Server server, Domain domain, String path) throws ServiceException { return URLUtil.getPublicURLForDomain(server, domain, path, true); } protected static String getProxyUrl(HttpServletRequest req, Server server, String path) throws ServiceException { int servicePort = (req == null) ? -1 : req.getLocalPort(); Provisioning prov = Provisioning.getInstance(); Server localServer = prov.getLocalServer(); if (!prov.isOfflineProxyServer(server) && servicePort == localServer.getIntAttr(Provisioning.A_zimbraAdminPort, 0)) return URLUtil.getAdminURL(server, path); else return URLUtil.getServiceURL(server, path, servicePort == localServer.getIntAttr(Provisioning.A_zimbraMailSSLPort, 0)); } protected void returnError(HttpServletResponse resp, ServiceException e) { resp.setHeader(ZIMBRA_FAULT_CODE_HEADER, e.getCode()); resp.setHeader(ZIMBRA_FAULT_MESSAGE_HEADER, e.getMessage()); resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } public static String getOrigIp(HttpServletRequest req) { RemoteIP remoteIp = new RemoteIP(req, getTrustedIPs()); return remoteIp.getOrigIP(); } public static String getClientIp(HttpServletRequest req) { RemoteIP remoteIp = new RemoteIP(req, getTrustedIPs()); return remoteIp.getClientIP(); } public static void addRemoteIpToLoggingContext(HttpServletRequest req) { RemoteIP remoteIp = new RemoteIP(req, getTrustedIPs()); remoteIp.addToLoggingContext(); } public static RemoteIP.TrustedIPs getTrustedIPs() { try { Server server = Provisioning.getInstance().getLocalServer(); return new RemoteIP.TrustedIPs(server.getMultiAttr(Provisioning.A_zimbraMailTrustedIP)); } catch (ServiceException e) { ZimbraLog.misc.warn("failed to get trusted IPs, only localhost will be trusted", e); } return new RemoteIP.TrustedIPs(null); } public static void addUAToLoggingContext(HttpServletRequest req) { ZimbraLog.addUserAgentToContext(req.getHeader("User-Agent")); } }