Java tutorial
/* * ***** BEGIN LICENSE BLOCK ***** * Zimbra Collaboration Suite Server * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 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.zimlet; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Arrays; import java.util.Enumeration; import java.util.List; import java.util.Set; 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.HttpException; import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.HttpState; import org.apache.commons.httpclient.UsernamePasswordCredentials; import org.apache.commons.httpclient.auth.AuthScope; import org.apache.commons.httpclient.methods.ByteArrayRequestEntity; import org.apache.commons.httpclient.methods.DeleteMethod; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.methods.PutMethod; import com.zimbra.common.account.Key.AccountBy; import com.zimbra.common.httpclient.HttpClientUtil; import com.zimbra.common.localconfig.LC; import com.zimbra.common.mime.ContentDisposition; import com.zimbra.common.mime.ContentType; import com.zimbra.common.service.ServiceException; import com.zimbra.common.util.ByteUtil; 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.Cos; import com.zimbra.cs.account.Provisioning; import com.zimbra.cs.httpclient.HttpProxyUtil; import com.zimbra.cs.mailbox.MailServiceException; import com.zimbra.cs.service.AuthProvider; import com.zimbra.cs.service.FileUploadServlet; import com.zimbra.cs.service.FileUploadServlet.Upload; import com.zimbra.cs.servlet.ZimbraServlet; /** * @author jylee */ @SuppressWarnings("serial") public class ProxyServlet extends ZimbraServlet { private static final String TARGET_PARAM = "target"; private static final String UPLOAD_PARAM = "upload"; private static final String FILENAME_PARAM = "filename"; private static final String FORMAT_PARAM = "fmt"; private static final String USER_PARAM = "user"; private static final String PASS_PARAM = "pass"; private static final String AUTH_PARAM = "auth"; private static final String AUTH_BASIC = "basic"; private Set<String> getAllowedDomains(AuthToken auth) throws ServiceException { Provisioning prov = Provisioning.getInstance(); Account acct = prov.get(AccountBy.id, auth.getAccountId(), auth); Cos cos = prov.getCOS(acct); Set<String> allowedDomains = cos.getMultiAttrSet(Provisioning.A_zimbraProxyAllowedDomains); ZimbraLog.zimlet.debug("get allowedDomains result: " + allowedDomains); return allowedDomains; } private boolean checkPermissionOnTarget(URL target, AuthToken auth) { String host = target.getHost().toLowerCase(); ZimbraLog.zimlet.debug("checking allowedDomains permission on target host: " + host); Set<String> domains; try { domains = getAllowedDomains(auth); } catch (ServiceException se) { ZimbraLog.zimlet.info("error getting allowedDomains: " + se.getMessage()); return false; } for (String domain : domains) { if (domain.equals("*")) { return true; } if (domain.charAt(0) == '*') { domain = domain.substring(1); } if (host.endsWith(domain)) { return true; } } return false; } private boolean canProxyHeader(String header) { if (header == null) return false; header = header.toLowerCase(); if (header.startsWith("accept") || header.equals("content-length") || header.equals("connection") || header.equals("keep-alive") || header.equals("pragma") || header.equals("host") || //header.equals("user-agent") || header.equals("cache-control") || header.equals("cookie") || header.equals("transfer-encoding")) { return false; } return true; } private byte[] copyPostedData(HttpServletRequest req) throws IOException { int size = req.getContentLength(); if (req.getMethod().equalsIgnoreCase("GET") || size <= 0) { return null; } InputStream is = req.getInputStream(); ByteArrayOutputStream baos = null; try { if (size < 0) size = 0; baos = new ByteArrayOutputStream(size); byte[] buffer = new byte[8192]; int num; while ((num = is.read(buffer)) != -1) { baos.write(buffer, 0, num); } return baos.toByteArray(); } finally { ByteUtil.closeStream(baos); } } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { doProxy(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { doProxy(req, resp); } @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException { doProxy(req, resp); } @Override protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException { doProxy(req, resp); } @Override protected boolean isAdminRequest(HttpServletRequest req) { return req.getServerPort() == LC.zimbra_admin_service_port.intValue(); } private static final String DEFAULT_CTYPE = "text/xml"; private void doProxy(HttpServletRequest req, HttpServletResponse resp) throws IOException { ZimbraLog.clearContext(); boolean isAdmin = isAdminRequest(req); AuthToken authToken = isAdmin ? getAdminAuthTokenFromCookie(req, resp, true) : getAuthTokenFromCookie(req, resp, true); if (authToken == null) { String zAuthToken = req.getParameter(QP_ZAUTHTOKEN); if (zAuthToken != null) { try { authToken = AuthProvider.getAuthToken(zAuthToken); if (authToken.isExpired()) { resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "authtoken expired"); return; } if (!authToken.isRegistered()) { resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "authtoken is invalid"); return; } if (isAdmin && !authToken.isAdmin()) { resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "permission denied"); return; } } catch (AuthTokenException e) { resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "unable to parse authtoken"); return; } } } if (authToken == null) { resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "no authtoken cookie"); return; } // get the posted body before the server read and parse them. byte[] body = copyPostedData(req); // sanity check String target = req.getParameter(TARGET_PARAM); if (target == null) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST); return; } // check for permission URL url = new URL(target); if (!isAdmin && !checkPermissionOnTarget(url, authToken)) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } // determine whether to return the target inline or store it as an upload String uploadParam = req.getParameter(UPLOAD_PARAM); boolean asUpload = uploadParam != null && (uploadParam.equals("1") || uploadParam.equalsIgnoreCase("true")); HttpMethod method = null; try { HttpClient client = ZimbraHttpConnectionManager.getExternalHttpConnMgr().newHttpClient(); HttpProxyUtil.configureProxy(client); String reqMethod = req.getMethod(); if (reqMethod.equalsIgnoreCase("GET")) { method = new GetMethod(target); } else if (reqMethod.equalsIgnoreCase("POST")) { PostMethod post = new PostMethod(target); if (body != null) post.setRequestEntity(new ByteArrayRequestEntity(body, req.getContentType())); method = post; } else if (reqMethod.equalsIgnoreCase("PUT")) { PutMethod put = new PutMethod(target); if (body != null) put.setRequestEntity(new ByteArrayRequestEntity(body, req.getContentType())); method = put; } else if (reqMethod.equalsIgnoreCase("DELETE")) { method = new DeleteMethod(target); } else { ZimbraLog.zimlet.info("unsupported request method: " + reqMethod); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); return; } // handle basic auth String auth, user, pass; auth = req.getParameter(AUTH_PARAM); user = req.getParameter(USER_PARAM); pass = req.getParameter(PASS_PARAM); if (auth != null && user != null && pass != null) { if (!auth.equals(AUTH_BASIC)) { ZimbraLog.zimlet.info("unsupported auth type: " + auth); resp.sendError(HttpServletResponse.SC_BAD_REQUEST); return; } HttpState state = new HttpState(); state.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(user, pass)); client.setState(state); method.setDoAuthentication(true); } Enumeration headers = req.getHeaderNames(); while (headers.hasMoreElements()) { String hdr = (String) headers.nextElement(); ZimbraLog.zimlet.debug("incoming: " + hdr + ": " + req.getHeader(hdr)); if (canProxyHeader(hdr)) { ZimbraLog.zimlet.debug("outgoing: " + hdr + ": " + req.getHeader(hdr)); if (hdr.equalsIgnoreCase("x-host")) method.getParams().setVirtualHost(req.getHeader(hdr)); else method.addRequestHeader(hdr, req.getHeader(hdr)); } } try { if (!(reqMethod.equalsIgnoreCase("POST") || reqMethod.equalsIgnoreCase("PUT"))) { method.setFollowRedirects(true); } HttpClientUtil.executeMethod(client, method); } catch (HttpException ex) { ZimbraLog.zimlet.info("exception while proxying " + target, ex); resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } int status = method.getStatusLine() == null ? HttpServletResponse.SC_INTERNAL_SERVER_ERROR : method.getStatusCode(); // workaround for Alexa Thumbnails paid web service, which doesn't bother to return a content-type line Header ctHeader = method.getResponseHeader("Content-Type"); String contentType = ctHeader == null || ctHeader.getValue() == null ? DEFAULT_CTYPE : ctHeader.getValue(); InputStream targetResponseBody = method.getResponseBodyAsStream(); if (asUpload) { String filename = req.getParameter(FILENAME_PARAM); if (filename == null || filename.equals("")) filename = new ContentType(contentType).getParameter("name"); if ((filename == null || filename.equals("")) && method.getResponseHeader("Content-Disposition") != null) filename = new ContentDisposition(method.getResponseHeader("Content-Disposition").getValue()) .getParameter("filename"); if (filename == null || filename.equals("")) filename = "unknown"; List<Upload> uploads = null; if (targetResponseBody != null) { try { Upload up = FileUploadServlet.saveUpload(targetResponseBody, filename, contentType, authToken.getAccountId()); uploads = Arrays.asList(up); } catch (ServiceException e) { if (e.getCode().equals(MailServiceException.UPLOAD_REJECTED)) status = HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE; else status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; } } resp.setStatus(status); FileUploadServlet.sendResponse(resp, status, req.getParameter(FORMAT_PARAM), null, uploads, null); } else { resp.setStatus(status); resp.setContentType(contentType); for (Header h : method.getResponseHeaders()) if (canProxyHeader(h.getName())) resp.addHeader(h.getName(), h.getValue()); if (targetResponseBody != null) ByteUtil.copy(targetResponseBody, true, resp.getOutputStream(), true); } } finally { if (method != null) method.releaseConnection(); } } }