/*
* Copyright 2013-2015 the original author or authors.
*
* 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.springframework.cloud.netflix.hystrix.dashboard;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.apachecommons.CommonsLog;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.client.actuator.HasFeatures;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.ui.freemarker.SpringTemplateLoader;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
/**
* @author Dave Syer
* @author Roy Clarkson
*/
@Configuration
@EnableConfigurationProperties(HystrixDashboardProperties.class)
public class HystrixDashboardConfiguration {
private static final String DEFAULT_TEMPLATE_LOADER_PATH = "classpath:/templates/";
private static final String DEFAULT_CHARSET = "UTF-8";
@Autowired
private HystrixDashboardProperties dashboardProperties;
@Bean
public HasFeatures hystrixDashboardFeature() {
return HasFeatures.namedFeature("Hystrix Dashboard", HystrixDashboardConfiguration.class);
}
/**
* Overrides Spring Boot's {@link FreeMarkerAutoConfiguration} to prefer using a
* {@link SpringTemplateLoader} instead of the file system. This corrects an issue
* where Spring Boot may use an empty 'templates' file resource to resolve templates
* instead of the packaged Hystrix classpath templates.
* @return FreeMarker configuration
*/
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPaths(DEFAULT_TEMPLATE_LOADER_PATH);
configurer.setDefaultEncoding(DEFAULT_CHARSET);
configurer.setPreferFileSystemAccess(false);
return configurer;
}
@Bean
public ServletRegistrationBean proxyStreamServlet() {
ProxyStreamServlet proxyStreamServlet = new ProxyStreamServlet();
proxyStreamServlet.setEnableIgnoreConnectionCloseHeader(dashboardProperties
.isEnableIgnoreConnectionCloseHeader());
return new ServletRegistrationBean(proxyStreamServlet, "/proxy.stream");
}
@Bean
public HystrixDashboardController hsytrixDashboardController() {
return new HystrixDashboardController();
}
/**
* Proxy an EventStream request (data.stream via proxy.stream) since EventStream does
* not yet support CORS (https://bugs.webkit.org/show_bug.cgi?id=61862) so that a UI
* can request a stream from a different server.
*/
@CommonsLog
public static class ProxyStreamServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final String CONNECTION_CLOSE_VALUE = "close";
private boolean enableIgnoreConnectionCloseHeader = false;
public void setEnableIgnoreConnectionCloseHeader(
boolean enableIgnoreConnectionCloseHeader) {
this.enableIgnoreConnectionCloseHeader = enableIgnoreConnectionCloseHeader;
}
public ProxyStreamServlet() {
super();
}
/**
* @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest
* request, javax.servlet.http.HttpServletResponse response)
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String origin = request.getParameter("origin");
if (origin == null) {
response.setStatus(500);
response.getWriter()
.println(
"Required parameter 'origin' missing. Example: 107.20.175.135:7001");
}
origin = origin.trim();
HttpGet httpget = null;
InputStream is = null;
boolean hasFirstParameter = false;
StringBuilder url = new StringBuilder();
if (!origin.startsWith("http")) {
url.append("http://");
}
url.append(origin);
if (origin.contains("?")) {
hasFirstParameter = true;
}
Map<String, String[]> params = request.getParameterMap();
for (String key : params.keySet()) {
if (!key.equals("origin")) {
String[] values = params.get(key);
String value = values[0].trim();
if (hasFirstParameter) {
url.append("&");
}
else {
url.append("?");
hasFirstParameter = true;
}
url.append(key).append("=").append(value);
}
}
String proxyUrl = url.toString();
log.info("\n\nProxy opening connection to: " + proxyUrl + "\n\n");
try {
httpget = new HttpGet(proxyUrl);
HttpClient client = ProxyConnectionManager.httpClient;
HttpResponse httpResponse = client.execute(httpget);
int statusCode = httpResponse.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK) {
// writeTo swallows exceptions and never quits even if outputstream is
// throwing IOExceptions (such as broken pipe) ... since the
// inputstream is infinite
// httpResponse.getEntity().writeTo(new
// OutputStreamWrapper(response.getOutputStream()));
// so I copy it manually ...
is = httpResponse.getEntity().getContent();
// set headers
copyHeadersToServletResponse(httpResponse.getAllHeaders(), response);
// copy data from source to response
OutputStream os = response.getOutputStream();
int b = -1;
while ((b = is.read()) != -1) {
try {
os.write(b);
if (b == 10 /** flush buffer on line feed */
) {
os.flush();
}
}
catch (Exception ex) {
if (ex.getClass().getSimpleName()
.equalsIgnoreCase("ClientAbortException")) {
// don't throw an exception as this means the user closed
// the connection
log.debug("Connection closed by client. Will stop proxying ...");
// break out of the while loop
break;
}
else {
// received unknown error while writing so throw an
// exception
throw new RuntimeException(ex);
}
}
}
}
else {
log.warn("Failed opening connection to " + proxyUrl + " : "
+ statusCode + " : " + httpResponse.getStatusLine());
}
}
catch (Exception ex) {
log.error("Error proxying request: " + url, ex);
}
finally {
if (httpget != null) {
try {
httpget.abort();
}
catch (Exception ex) {
log.error("failed aborting proxy connection.", ex);
}
}
// httpget.abort() MUST be called first otherwise is.close() hangs
// (because data is still streaming?)
if (is != null) {
// this should already be closed by httpget.abort() above
try {
is.close();
}
catch (Exception ex) {
// ignore errors on close
}
}
}
}
private void copyHeadersToServletResponse(Header[] headers,
HttpServletResponse response) {
for (Header header : headers) {
// Some versions of Cloud Foundry (HAProxy) are
// incorrectly setting a "Connection: close" header
// causing the Hystrix dashboard to close the connection
// to the stream
// https://github.com/cloudfoundry/gorouter/issues/71
if (this.enableIgnoreConnectionCloseHeader
&& HttpHeaders.CONNECTION.equalsIgnoreCase(header.getName())
&& CONNECTION_CLOSE_VALUE.equalsIgnoreCase(header.getValue())) {
log.warn("Ignoring 'Connection: close' header from stream response");
}
else if (!HttpHeaders.TRANSFER_ENCODING.equalsIgnoreCase(header.getName())) {
response.addHeader(header.getName(), header.getValue());
}
}
}
@SuppressWarnings("deprecation")
private static class ProxyConnectionManager {
private final static PoolingClientConnectionManager threadSafeConnectionManager = new PoolingClientConnectionManager();
private final static HttpClient httpClient = new DefaultHttpClient(
threadSafeConnectionManager);
static {
log.debug("Initialize ProxyConnectionManager");
/* common settings */
HttpParams httpParams = httpClient.getParams();
HttpConnectionParams.setConnectionTimeout(httpParams, 5000);
HttpConnectionParams.setSoTimeout(httpParams, 10000);
/* number of connections to allow */
threadSafeConnectionManager.setDefaultMaxPerRoute(400);
threadSafeConnectionManager.setMaxTotal(400);
}
}
}
}