Java tutorial
/** * Copyright MITRE * Copyright (C) 2016-2018, Falko Brutigam. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.mapzone.controller.vm.http; import java.util.Enumeration; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpHeaders; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.AbortableHttpRequest; import org.apache.http.client.utils.URIUtils; import org.apache.http.entity.InputStreamEntity; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.message.BasicHttpEntityEnclosingRequest; import org.apache.http.message.BasicHttpRequest; import org.polymap.core.runtime.config.Config2; import org.polymap.core.runtime.config.DefaultBoolean; import org.polymap.core.runtime.config.Mandatory; import org.polymap.rhei.batik.BatikApplication; /** * An HTTP reverse proxy/gateway servlet. It is designed to be extended for * customization if desired. Most of the work is handled by <a * href="http://hc.apache.org/httpcomponents-client-ga/">Apache HttpClient</a>. * <p/> * There are alternatives to a servlet based proxy such as Apache mod_proxy if that * is available to you. However this servlet is easily customizable by Java, * secure-able by your web application's security (e.g. spring-security), portable * across servlet engines, and is embeddable into another web application. * <p/> * Inspiration: http://httpd.apache.org/docs/2.0/mod/mod_proxy.html * * @see <a href="https://github.com/mitre/HTTP-Proxy-Servlet/blob/master/src/main/java/org/mitre/dsmiley/httpproxy/ProxyServlet.java">Origin</a> * @author David Smiley dsmiley@mitre.org * @author <a href="http://www.polymap.de">Falko Brutigam</a> */ @SuppressWarnings("deprecation") public abstract class HttpRequestForwarder extends HttpForwarder { private static final Log log = LogFactory.getLog(HttpRequestForwarder.class); /** Set to {@link PoolingHttpClientConnectionManager#setDefaultMaxPerRoute(int)}. */ public static final int MAX_CONNECTIONS_PER_ROUTE = 12; /** Set to {@link PoolingHttpClientConnectionManager#setMaxTotal(int)}. */ public static final int MAX_CONNECTIONS = 100; /** Must be longer than {@link BatikApplication#REQUEST_CHECK_INTERVAL}. */ public static final int REQUEST_TIMEOUT = BatikApplication.REQUEST_CHECK_INTERVAL + (10 * 1000); protected static HttpClient proxyClient; protected static final ThreadLocal<HttpRequestForwarder> active = new ThreadLocal(); static { InterceptableHttpClientConnectionFactory factory = new InterceptableHttpClientConnectionFactory() { @Override protected void onRequestSubmitted(HttpRequest request) { active.get().onRequestSubmitted(request); } }; PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager(factory); manager.setDefaultMaxPerRoute(MAX_CONNECTIONS_PER_ROUTE); manager.setMaxTotal(MAX_CONNECTIONS); RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(REQUEST_TIMEOUT) .setConnectionRequestTimeout(REQUEST_TIMEOUT).setSocketTimeout(REQUEST_TIMEOUT).build(); proxyClient = HttpClientBuilder.create().disableCookieManagement().setConnectionManagerShared(true) .setConnectionManager(manager).setDefaultRequestConfig(requestConfig).build(); // // as of HttpComponents v4.2, this class is better since it uses System Properties: // HttpParams hcParams = new BasicHttpParams(); // hcParams.setParameter( ClientPNames.COOKIE_POLICY, CookiePolicy.IGNORE_COOKIES ); // hcParams.setParameter( ClientPNames.HANDLE_REDIRECTS, Boolean.TRUE ); // return new SystemDefaultHttpClient( hcParams ); } // instance ******************************************* @DefaultBoolean(false) public Config2<HttpRequestForwarder, Boolean> doForwardIP; /** User agents shouldn't send the url fragment but what if it does? */ @DefaultBoolean(false) public Config2<HttpRequestForwarder, Boolean> doSendUrlFragment; @Mandatory public Config2<HttpRequestForwarder, String> cookieNamePrefix; /** */ @Mandatory public Config2<HttpRequestForwarder, String> targetUri; protected URI targetUriObj; // new URI(targetUri) protected HttpHost targetHost; // URIUtils.extractHost(targetUriObj); protected HttpResponse proxyResponse; protected HttpRequest proxyRequest; /** * Executed when the request is send - before start waiting on response. See * {@link InterceptableHttpClientConnectionFactory} for detail. */ protected void onRequestSubmitted(HttpRequest request) { } public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException, URISyntaxException { targetUriObj = new URI(targetUri.get()); targetHost = URIUtils.extractHost(targetUriObj); // Make the Request // note: we won't transfer the protocol version because I'm not sure it would // truly be compatible String method = request.getMethod(); String proxyRequestUri = rewriteUrlFromRequest(request); // spec: RFC 2616, sec 4.3: either of these two headers signal that there is // a message body. if (request.getHeader(HttpHeaders.CONTENT_LENGTH) != null || request.getHeader(HttpHeaders.TRANSFER_ENCODING) != null) { HttpEntityEnclosingRequest eProxyRequest = new BasicHttpEntityEnclosingRequest(method, proxyRequestUri); // Add the input entity (streamed) // note: we don't bother ensuring we close the servletInputStream since // the container handles it eProxyRequest.setEntity(new InputStreamEntity(request.getInputStream(), request.getContentLength())); proxyRequest = eProxyRequest; } else { proxyRequest = new BasicHttpRequest(method, proxyRequestUri); } copyRequestHeaders(request); setXForwardedForHeader(request); // Execute the request try { active.set(this); log.debug("REQUEST " + "[" + StringUtils.right(Thread.currentThread().getName(), 2) + "] " + method + ": " + request.getRequestURI() + " -- " + proxyRequest.getRequestLine().getUri()); proxyResponse = proxyClient.execute(targetHost, proxyRequest); } catch (Exception e) { // abort request, according to best practice with HttpClient if (proxyRequest instanceof AbortableHttpRequest) { AbortableHttpRequest abortableHttpRequest = (AbortableHttpRequest) proxyRequest; abortableHttpRequest.abort(); } if (e instanceof RuntimeException) { throw (RuntimeException) e; } if (e instanceof ServletException) { throw (ServletException) e; } // noinspection ConstantConditions if (e instanceof IOException) { throw (IOException) e; } throw new RuntimeException(e); } finally { active.set(null); } // Note: Don't need to close servlet outputStream: // http://stackoverflow.com/questions/1159168/should-one-call-close-on-httpservletresponse-getoutputstream-getwriter } /** Copy request headers from the servlet client to the proxy request. */ protected void copyRequestHeaders(HttpServletRequest servletRequest) { Enumeration enumerationOfHeaderNames = servletRequest.getHeaderNames(); while (enumerationOfHeaderNames.hasMoreElements()) { String headerName = (String) enumerationOfHeaderNames.nextElement(); // Instead the content-length is effectively set via InputStreamEntity if (headerName.equalsIgnoreCase(HttpHeaders.CONTENT_LENGTH)) { continue; } if (hopByHopHeaders.containsHeader(headerName)) { log.debug(" Header: " + headerName + " ... skipped."); continue; } Enumeration headers = servletRequest.getHeaders(headerName); while (headers.hasMoreElements()) { String headerValue = (String) headers.nextElement(); // In case the proxy host is running multiple virtual servers, // rewrite the Host header to ensure that we get content from // the correct virtual server if (headerName.equalsIgnoreCase(HttpHeaders.HOST)) { HttpHost host = targetHost; headerValue = host.getHostName(); if (host.getPort() != -1) { headerValue += ":" + host.getPort(); } } // else if (headerName.equalsIgnoreCase(org.apache.http.cookie.SM.COOKIE)) { headerValue = getRealCookie(headerValue); } proxyRequest.addHeader(headerName, headerValue); log.debug(" Header: " + headerName + " = " + headerValue); } } } /** * Take any client cookies that were originally from the proxy and prepare them * to send to the proxy. This relies on cookie headers being set correctly * according to RFC 6265 Sec 5.4. This also blocks any local cookies from being * sent to the proxy. */ protected String getRealCookie(String cookieValue) { StringBuilder escapedCookie = new StringBuilder(); String cookies[] = cookieValue.split("; "); for (String cookie : cookies) { String cookieSplit[] = cookie.split("="); if (cookieSplit.length == 2) { String cookieName = cookieSplit[0]; if (cookieName.startsWith(cookieNamePrefix.get())) { cookieName = cookieName.substring(cookieNamePrefix.get().length()); if (escapedCookie.length() > 0) { escapedCookie.append("; "); } escapedCookie.append(cookieName).append("=").append(cookieSplit[1]); } } cookieValue = escapedCookie.toString(); } return cookieValue; } protected void setXForwardedForHeader(HttpServletRequest servletRequest) { String headerName = "X-Forwarded-For"; if (doForwardIP.get()) { String newHeader = servletRequest.getRemoteAddr(); String existingHeader = servletRequest.getHeader(headerName); if (existingHeader != null) { newHeader = existingHeader + ", " + newHeader; } proxyRequest.setHeader(headerName, newHeader); } } /** * Reads the request URI from {@code servletRequest} and rewrites it, considering * targetUri. It's used to make the new request. */ protected String rewriteUrlFromRequest(HttpServletRequest servletRequest) { StringBuilder uri = new StringBuilder(500); uri.append(targetUriObj); // Handle the path given to the servlet if (servletRequest.getPathInfo() != null) {// ex: /my/path.html uri.append(encodeUriQuery(rewritePath(servletRequest.getPathInfo()))); } // Handle the query string & fragment String queryString = servletRequest.getQueryString();// ex:(following '?'): // name=value&foo=bar#fragment String fragment = null; // split off fragment from queryString, updating queryString if found if (queryString != null) { int fragIdx = queryString.indexOf('#'); if (fragIdx >= 0) { fragment = queryString.substring(fragIdx + 1); queryString = queryString.substring(0, fragIdx); } } queryString = rewriteQueryString(queryString); if (queryString != null && queryString.length() > 0) { uri.append('?'); uri.append(encodeUriQuery(queryString)); } if (doSendUrlFragment.get() && fragment != null) { uri.append('#'); uri.append(encodeUriQuery(fragment)); } return uri.toString(); } protected String rewritePath(String path) { return path; } protected String rewriteQueryString(String queryString) { return queryString; } }