HttpClientRequestExecutor.java
/*
* 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.esigate.http;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CookieStore;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.cookie.CookieSpecProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.esigate.ConfigurationException;
import org.esigate.Driver;
import org.esigate.HttpErrorPage;
import org.esigate.Parameters;
import org.esigate.RequestExecutor;
import org.esigate.cache.CacheConfigHelper;
import org.esigate.cookie.CookieManager;
import org.esigate.events.EventManager;
import org.esigate.events.impl.FragmentEvent;
import org.esigate.events.impl.HttpClientBuilderEvent;
import org.esigate.extension.ExtensionFactory;
import org.esigate.http.cookie.CustomBrowserCompatSpecFactory;
import org.esigate.impl.DriverRequest;
import org.esigate.util.HttpRequestHelper;
import org.esigate.util.UriUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* HttpClientHelper is responsible for creating Apache HttpClient requests from incoming requests. It can copy a request
* with its method and entity or simply create a new GET request to the same URI. Some parameters enable to control
* which http headers have to be copied and whether or not to preserve the original host header.
*
* @author Francois-Xavier Bonnet
*/
public final class HttpClientRequestExecutor implements RequestExecutor {
private static final Logger LOG = LoggerFactory.getLogger(HttpClientRequestExecutor.class);
private static final Set<String> SIMPLE_METHODS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("GET",
"HEAD", "OPTIONS", "TRACE", "DELETE")));
private static final Set<String> ENTITY_METHODS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("POST",
"PUT", "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK")));
private boolean preserveHost;
private CookieManager cookieManager;
private HttpClient httpClient;
private EventManager eventManager = null;
private int connectTimeout;
private int socketTimeout;
private HttpHost firstBaseUrlHost;
/**
* Builder class used to produce an immutable instance.
*
* @author Francois-Xavier Bonnet
*/
public static final class HttpClientRequestExecutorBuilder implements RequestExecutorBuilder {
private EventManager eventManager;
private Properties properties;
private Driver driver;
private HttpClientConnectionManager connectionManager;
private CookieManager cookieManager;
@Override
public HttpClientRequestExecutorBuilder setDriver(Driver pDriver) {
this.driver = pDriver;
return this;
}
@Override
public HttpClientRequestExecutorBuilder setProperties(Properties pProperties) {
this.properties = pProperties;
return this;
}
@Override
public HttpClientRequestExecutor build() {
if (eventManager == null) {
throw new ConfigurationException("eventManager is mandatory");
}
if (driver == null) {
throw new ConfigurationException("driver is mandatory");
}
if (properties == null) {
throw new ConfigurationException("properties is mandatory");
}
HttpClientRequestExecutor result = new HttpClientRequestExecutor();
result.eventManager = eventManager;
result.preserveHost = Parameters.PRESERVE_HOST.getValue(properties);
if (cookieManager == null) {
cookieManager = ExtensionFactory.getExtension(properties, Parameters.COOKIE_MANAGER, driver);
}
result.cookieManager = cookieManager;
result.connectTimeout = Parameters.CONNECT_TIMEOUT.getValue(properties);
result.socketTimeout = Parameters.SOCKET_TIMEOUT.getValue(properties);
result.httpClient = buildHttpClient();
String firstBaseURL = Parameters.REMOTE_URL_BASE.getValue(properties)[0];
result.firstBaseUrlHost = UriUtils.extractHost(firstBaseURL);
return result;
}
@Override
public HttpClientRequestExecutorBuilder setContentTypeHelper(ContentTypeHelper contentTypeHelper) {
return this;
}
public HttpClientRequestExecutorBuilder setConnectionManager(HttpClientConnectionManager pConnectionManager) {
this.connectionManager = pConnectionManager;
return this;
}
@Override
public HttpClientRequestExecutorBuilder setEventManager(EventManager pEventManager) {
this.eventManager = pEventManager;
return this;
}
public HttpClientRequestExecutorBuilder setCookieManager(CookieManager pCookieManager) {
this.cookieManager = pCookieManager;
return this;
}
private HttpClient buildHttpClient() {
HttpHost proxyHost = null;
Credentials proxyCredentials = null;
// Proxy settings
String proxyHostParameter = Parameters.PROXY_HOST.getValue(properties);
if (proxyHostParameter != null) {
int proxyPort = Parameters.PROXY_PORT.getValue(properties);
proxyHost = new HttpHost(proxyHostParameter, proxyPort);
String proxyUser = Parameters.PROXY_USER.getValue(properties);
if (proxyUser != null) {
String proxyPassword = Parameters.PROXY_PASSWORD.getValue(properties);
proxyCredentials = new UsernamePasswordCredentials(proxyUser, proxyPassword);
}
}
ProxyingHttpClientBuilder httpClientBuilder = new ProxyingHttpClientBuilder();
httpClientBuilder.disableContentCompression();
httpClientBuilder.setProperties(properties);
httpClientBuilder.setMaxConnPerRoute(Parameters.MAX_CONNECTIONS_PER_HOST.getValue(properties));
httpClientBuilder.setMaxConnTotal(Parameters.MAX_CONNECTIONS_PER_HOST.getValue(properties));
// Proxy settings
if (proxyHost != null) {
httpClientBuilder.setProxy(proxyHost);
if (proxyCredentials != null) {
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope(proxyHost), proxyCredentials);
httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
}
}
// Cache settings
boolean useCache = Parameters.USE_CACHE.getValue(properties);
httpClientBuilder.setUseCache(Parameters.USE_CACHE.getValue(properties));
if (useCache) {
httpClientBuilder.setHttpCacheStorage(CacheConfigHelper.createCacheStorage(properties));
httpClientBuilder.setCacheConfig(CacheConfigHelper.createCacheConfig(properties));
}
// Event manager
httpClientBuilder.setEventManager(eventManager);
// Used for tests to skip connection manager and return hard coded
// responses
if (connectionManager != null) {
httpClientBuilder.setConnectionManager(connectionManager);
}
Registry<CookieSpecProvider> cookieSpecRegistry =
RegistryBuilder
.<CookieSpecProvider>create()
.register(CustomBrowserCompatSpecFactory.CUSTOM_BROWSER_COMPATIBILITY,
new CustomBrowserCompatSpecFactory()).build();
RequestConfig config =
RequestConfig.custom().setCookieSpec(CustomBrowserCompatSpecFactory.CUSTOM_BROWSER_COMPATIBILITY)
.build();
httpClientBuilder.setDefaultCookieSpecRegistry(cookieSpecRegistry).setDefaultRequestConfig(config);
driver.getEventManager().fire(EventManager.EVENT_HTTP_BUILDER_INITIALIZATION,
new HttpClientBuilderEvent(httpClientBuilder));
return httpClientBuilder.build();
}
}
public static HttpClientRequestExecutorBuilder builder() {
return new HttpClientRequestExecutorBuilder();
}
private HttpClientRequestExecutor() {
}
@Override
public OutgoingRequest createOutgoingRequest(DriverRequest originalRequest, String uri, boolean proxy) {
// Extract the host in the URI. This is the host we have to send the
// request to physically.
HttpHost physicalHost = UriUtils.extractHost(uri);
if (!originalRequest.isExternal()) {
if (preserveHost) {
// Preserve host if required
HttpHost virtualHost = HttpRequestHelper.getHost(originalRequest.getOriginalRequest());
// Rewrite the uri with the virtualHost
uri = UriUtils.rewriteURI(uri, virtualHost);
} else {
uri = UriUtils.rewriteURI(uri, firstBaseUrlHost);
}
}
RequestConfig.Builder builder = RequestConfig.custom();
builder.setConnectTimeout(connectTimeout);
builder.setSocketTimeout(socketTimeout);
// Use browser compatibility cookie policy. This policy is the closest
// to the behavior of a real browser.
builder.setCookieSpec(CustomBrowserCompatSpecFactory.CUSTOM_BROWSER_COMPATIBILITY);
builder.setRedirectsEnabled(false);
RequestConfig config = builder.build();
OutgoingRequestContext context = new OutgoingRequestContext();
String method = "GET";
if (proxy) {
method = originalRequest.getOriginalRequest().getRequestLine().getMethod().toUpperCase();
}
OutgoingRequest outgoingRequest =
new OutgoingRequest(method, uri, originalRequest.getOriginalRequest().getProtocolVersion(),
originalRequest, config, context);
if (ENTITY_METHODS.contains(method)) {
outgoingRequest.setEntity(originalRequest.getOriginalRequest().getEntity());
} else if (!SIMPLE_METHODS.contains(method)) {
throw new UnsupportedHttpMethodException(method + " " + uri);
}
context.setPhysicalHost(physicalHost);
context.setOutgoingRequest(outgoingRequest);
context.setProxy(proxy);
return outgoingRequest;
}
/**
* Execute a HTTP request.
*
* @param httpRequest
* HTTP request to execute.
* @return HTTP response.
* @throws HttpErrorPage
* if server returned no response or if the response as an error status code.
*/
@Override
public CloseableHttpResponse execute(OutgoingRequest httpRequest) throws HttpErrorPage {
OutgoingRequestContext context = httpRequest.getContext();
IncomingRequest originalRequest = httpRequest.getOriginalRequest().getOriginalRequest();
if (cookieManager != null) {
CookieStore cookieStore = new RequestCookieStore(cookieManager, httpRequest.getOriginalRequest());
context.setCookieStore(cookieStore);
}
HttpResponse result;
// Create request event
FragmentEvent event = new FragmentEvent(originalRequest, httpRequest, context);
// EVENT pre
eventManager.fire(EventManager.EVENT_FRAGMENT_PRE, event);
// If exit : stop immediately.
if (!event.isExit()) {
// Proceed to request only if extensions did not inject a response.
if (event.getHttpResponse() == null) {
if (httpRequest.containsHeader(HttpHeaders.EXPECT)) {
event.setHttpResponse(HttpErrorPage.generateHttpResponse(HttpStatus.SC_EXPECTATION_FAILED,
"'Expect' request header is not supported"));
} else {
try {
HttpHost physicalHost = context.getPhysicalHost();
result = httpClient.execute(physicalHost, httpRequest, context);
} catch (IOException e) {
result = HttpErrorPage.generateHttpResponse(e);
LOG.warn(httpRequest.getRequestLine() + " -> " + result.getStatusLine().toString());
}
event.setHttpResponse(BasicCloseableHttpResponse.adapt(result));
}
}
// EVENT post
eventManager.fire(EventManager.EVENT_FRAGMENT_POST, event);
}
CloseableHttpResponse httpResponse = event.getHttpResponse();
if (httpResponse == null) {
throw new HttpErrorPage(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Request was cancelled by server",
"Request was cancelled by server");
}
if (HttpResponseUtils.isError(httpResponse)) {
throw new HttpErrorPage(httpResponse);
}
return httpResponse;
}
}