/* * 2012-3 Red Hat Inc. and/or its affiliates and other contributors. * * 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 org.overlord.rtgov.elasticsearch.rest; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.http.HttpServletRequest; import org.apache.http.HttpEntity; 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.methods.AbortableHttpRequest; import org.apache.http.client.utils.URIUtils; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.PoolingClientConnectionManager; import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicHttpEntityEnclosingRequest; import org.apache.http.message.BasicHttpRequest; import org.apache.http.message.HeaderGroup; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpParams; import org.overlord.rtgov.common.util.RTGovProperties; /** * The ElasticSearch HTTP client. * * Based on the http servlet proxy implemented by David Smiley: * https://github.com/dsmiley/HTTP-Proxy-Servlet * */ public class ElasticsearchHttpClient { private static final String DEFAULT_ELASTIC_SEARCH_URL = "http://localhost:9200"; private static final Logger LOG=Logger.getLogger(ElasticsearchHttpClient.class.getName()); private HttpClient _proxyClient; private String _url; /** * The default constructor. */ public ElasticsearchHttpClient() { HttpParams hcParams = new BasicHttpParams(); _proxyClient = new DefaultHttpClient(new PoolingClientConnectionManager(),hcParams); // Get URL _url = RTGovProperties.getProperties().getProperty("Elasticsearch.server", DEFAULT_ELASTIC_SEARCH_URL); } /** * This method processes the supplied HTTP request. * * @param request The request * @return The response * @throws Exception Failed to process the request */ public HttpResponse process(HttpServletRequest request) throws Exception { HttpRequest proxyRequest; String proxyRequestUri = rewriteUrlFromRequest(request); if (LOG.isLoggable(Level.FINEST)) { LOG.finest("Rewritten URL: "+proxyRequestUri); } //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( request.getMethod(), proxyRequestUri); // Transfer content to byte array java.io.InputStream is=request.getInputStream(); java.io.ByteArrayOutputStream baos=new java.io.ByteArrayOutputStream(); while (true) { byte[] b=new byte[10240]; int len=is.read(b); if (len == -1) { break; } baos.write(b, 0, len); } is.close(); baos.close(); HttpEntity entity=new ByteArrayEntity(baos.toByteArray()); is.close(); eProxyRequest.setEntity(entity); proxyRequest = eProxyRequest; } else { proxyRequest = new BasicHttpRequest(request.getMethod(), proxyRequestUri); } copyRequestHeaders(request, proxyRequest, proxyRequestUri); try { // Execute the request if (LOG.isLoggable(Level.FINER)) { LOG.finer("proxy " + request.getMethod() + " uri: " + request.getRequestURI() + " -- " + proxyRequest.getRequestLine().getUri()); } return (_proxyClient.execute(URIUtils.extractHost( new java.net.URI(proxyRequestUri)), proxyRequest)); } catch (Exception e) { //abort request, according to best practice with HttpClient if (proxyRequest instanceof AbortableHttpRequest) { AbortableHttpRequest abortableHttpRequest = (AbortableHttpRequest) proxyRequest; abortableHttpRequest.abort(); } throw e; } } /** * {@inheritDoc} */ public void close() { if (_proxyClient != null) { _proxyClient.getConnectionManager().shutdown(); } } /** * Copy request headers from the servlet client to the proxy request. * * @param request The client request * @param proxyRequest The request being sent to the target service * @param uri The target service URI * @throws Exception Failed to copy headers */ protected void copyRequestHeaders(HttpServletRequest request, HttpRequest proxyRequest, String uri) throws Exception { java.util.Enumeration<String> iter=request.getHeaderNames(); while (iter.hasMoreElements()) { String key=iter.nextElement(); //Instead the content-length is effectively set via HttpEntity if (key.equalsIgnoreCase(HttpHeaders.CONTENT_LENGTH)) { continue; } if (HOPBYHOPHEADERS.containsHeader(key)) { continue; } // 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 String headerValue=(String)request.getHeader(key); if (key.equalsIgnoreCase(HttpHeaders.HOST)) { HttpHost host = URIUtils.extractHost(new java.net.URI(uri)); headerValue = host.getHostName(); if (host.getPort() != -1) { headerValue += ":"+host.getPort(); } } proxyRequest.addHeader(key, headerValue); } } /** Reads the request URI from {@code servletRequest} and rewrites it, considering {@link * #targetUri}. It's used to make the new request. */ protected String rewriteUrlFromRequest(HttpServletRequest request) { StringBuilder uri = new StringBuilder(500); uri.append(_url); String pathInfo=request.getPathInfo(); // Basic support for filtering based on user - just to ensure they have different // (partitioned) custom dashboards. When fine grained authentication supported, might // want to include this as part of a more general mechanism. if (pathInfo.startsWith("/kibana-int/dashboard/")) { pathInfo = pathInfo.replaceFirst("dashboard", "dashboard-"+request.getUserPrincipal().getName()); } // Append path uri.append(pathInfo); // Handle the query string java.util.Enumeration<String> iter=request.getParameterNames(); boolean f_first=true; if (iter.hasMoreElements()) { uri.append('?'); } while (iter.hasMoreElements()) { String name=iter.nextElement(); String[] values=request.getParameterValues(name); for (int i=0; i < values.length; i++) { if (!f_first) { uri.append('&'); } uri.append(name); uri.append('='); uri.append(values[i]); f_first = false; } } String ret=uri.toString(); ret = ret.replaceAll(" ", "%20"); return (ret); } /** These are the "hop-by-hop" headers that should not be copied. * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html * I use an HttpClient HeaderGroup class instead of Set<String> because this * approach does case insensitive lookup faster. */ private static final HeaderGroup HOPBYHOPHEADERS; static { HOPBYHOPHEADERS = new HeaderGroup(); String[] headers = new String[] { "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "TE", "Trailers", "Transfer-Encoding", "Upgrade" }; for (String header : headers) { HOPBYHOPHEADERS.addHeader(new BasicHeader(header, null)); } } }