KNOX-1842 - Upgrade httpclient to 4.5.10 (#176)
HttpClient 4.5.7 broke url normalization. Knox
didn't have any tests for this case and so we
had to revert after the fact. HttpClient 4.5.8
fixed a lot of the url normalization and some
libraries decided to turn url normalization off.
This commit does the following:
* Adds a test for %2F - KNOX-1005
* This test passes under HttpClient 4.5.6 and 4.5.8+
* It breaks as expected under HttpClient 4.5.7
* Adds an explicit config enabling url normalization
* Ensures that we are in control of url normalization
* Adds a test for this configuration as well
* Test with both HttpClient normalization enabled and disabled
* `rest-assured` doesn't expose `RequestConfig` to disable
url normalization
* Shows how to use HttpClient in `GatewayBasicFuncTest`
All the url safe characters like %2F are fixed by HTTPCLIENT-1968.
The case of `/abc///def` is normalized to `/abc/def` the same
way that Knox does internally with `getPathInfo` and Java URI.
Signed-off-by: Kevin Risden <krisden@apache.org>
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/DefaultHttpClientFactory.java b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/DefaultHttpClientFactory.java
index d505d12..2db6c12 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/DefaultHttpClientFactory.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/DefaultHttpClientFactory.java
@@ -192,7 +192,7 @@
}
}
- private static RequestConfig getRequestConfig( FilterConfig config ) {
+ static RequestConfig getRequestConfig( FilterConfig config ) {
RequestConfig.Builder builder = RequestConfig.custom();
int connectionTimeout = getConnectionTimeout( config );
if ( connectionTimeout != -1 ) {
@@ -203,6 +203,15 @@
if( socketTimeout != -1 ) {
builder.setSocketTimeout( socketTimeout );
}
+
+ // HttpClient 4.5.7 is broken for %2F handling with url normalization.
+ // However, HttpClient 4.5.8+ (HTTPCLIENT-1968) has reasonable url
+ // normalization that matches what Knox already does related to url handling.
+ //
+ // If this view changes later, need to change here as well as make sure
+ // rest-assured doesn't use the old HttpClient behavior.
+ builder.setNormalizeUri(true);
+
return builder.build();
}
diff --git a/gateway-spi/src/test/java/org/apache/knox/gateway/dispatch/DefaultHttpClientFactoryTest.java b/gateway-spi/src/test/java/org/apache/knox/gateway/dispatch/DefaultHttpClientFactoryTest.java
index 14a55a3..6ceeeb2 100644
--- a/gateway-spi/src/test/java/org/apache/knox/gateway/dispatch/DefaultHttpClientFactoryTest.java
+++ b/gateway-spi/src/test/java/org/apache/knox/gateway/dispatch/DefaultHttpClientFactoryTest.java
@@ -26,11 +26,13 @@
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import org.apache.http.client.HttpClient;
+import org.apache.http.client.config.RequestConfig;
import org.apache.knox.gateway.config.GatewayConfig;
-import org.apache.knox.gateway.services.ServiceType;
import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
import org.apache.knox.gateway.services.security.AliasService;
import org.apache.knox.gateway.services.security.KeystoreService;
import org.junit.Test;
@@ -200,6 +202,29 @@
verify(keystoreService, gatewayServices, filterConfig);
}
+ @Test
+ public void testHttpClientPathNormalization() {
+ GatewayConfig gatewayConfig = createMock(GatewayConfig.class);
+ expect(gatewayConfig.getHttpClientConnectionTimeout()).andReturn(20000).once();
+ expect(gatewayConfig.getHttpClientSocketTimeout()).andReturn(20000).once();
+
+ ServletContext servletContext = createMock(ServletContext.class);
+ expect(servletContext.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE)).andReturn(gatewayConfig).atLeastOnce();
+
+ FilterConfig filterConfig = createMock(FilterConfig.class);
+ expect(filterConfig.getServletContext()).andReturn(servletContext).atLeastOnce();
+ expect(filterConfig.getInitParameter("httpclient.connectionTimeout")).andReturn(null).once();
+ expect(filterConfig.getInitParameter("httpclient.socketTimeout")).andReturn(null).once();
+
+ replay(gatewayConfig, servletContext, filterConfig);
+
+ RequestConfig requestConfig = DefaultHttpClientFactory.getRequestConfig(filterConfig);
+
+ assertTrue(requestConfig.isNormalizeUri());
+
+ verify(gatewayConfig, servletContext, filterConfig);
+ }
+
private KeyStore loadKeyStore(String keyStoreFile, String password, String storeType)
throws CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException {
KeyStore keyStore = KeyStore.getInstance(storeType);
@@ -208,4 +233,4 @@
}
return keyStore;
}
-}
\ No newline at end of file
+}
diff --git a/gateway-test-utils/src/main/java/org/apache/knox/test/mock/MockRequestMatcher.java b/gateway-test-utils/src/main/java/org/apache/knox/test/mock/MockRequestMatcher.java
index 9191922..f8f6915 100644
--- a/gateway-test-utils/src/main/java/org/apache/knox/test/mock/MockRequestMatcher.java
+++ b/gateway-test-utils/src/main/java/org/apache/knox/test/mock/MockRequestMatcher.java
@@ -56,6 +56,7 @@
private MockResponseProvider response;
private Set<String> methods;
private String pathInfo;
+ private String requestURI;
private String requestURL;
Map<String,Matcher> headers;
Set<Cookie> cookies;
@@ -95,6 +96,11 @@
return this;
}
+ public MockRequestMatcher requestURI( String requestURI ) {
+ this.requestURI = requestURI;
+ return this;
+ }
+
public MockRequestMatcher requestUrl( String requestUrl ) {
this.requestURL = requestUrl;
return this;
@@ -208,6 +214,12 @@
" does not have the expected pathInfo",
request.getPathInfo(), is( pathInfo ) );
}
+ if( requestURI != null ) {
+ assertThat(
+ "Request " + request.getMethod() + " " + request.getRequestURL() +
+ " does not have the expected requestURI",
+ request.getRequestURI(), is(requestURI) );
+ }
if( requestURL != null ) {
assertThat(
"Request " + request.getMethod() + " " + request.getRequestURL() +
@@ -319,7 +331,7 @@
@Override
public String toString() {
- return "from=" + from + ", pathInfo=" + pathInfo;
+ return "from=" + from + ", pathInfo=" + pathInfo + ", requestURI=" + requestURI;
}
private static List<NameValuePair> parseQueryString( String queryString ) {
diff --git a/gateway-test/src/test/java/org/apache/knox/gateway/GatewayBasicFuncTest.java b/gateway-test/src/test/java/org/apache/knox/gateway/GatewayBasicFuncTest.java
index e79b827..307f007 100644
--- a/gateway-test/src/test/java/org/apache/knox/gateway/GatewayBasicFuncTest.java
+++ b/gateway-test/src/test/java/org/apache/knox/gateway/GatewayBasicFuncTest.java
@@ -28,6 +28,7 @@
import io.restassured.specification.ResponseSpecification;
import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.apache.commons.lang3.ArrayUtils;
+import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
@@ -35,15 +36,19 @@
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.knox.gateway.util.KnoxCLI;
import org.apache.knox.test.TestUtils;
@@ -99,6 +104,7 @@
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.text.IsEmptyString.isEmptyString;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.xmlmatchers.XmlMatchers.isEquivalentTo;
import static org.xmlmatchers.transform.XmlConverters.the;
@@ -1665,6 +1671,74 @@
}
@Test( timeout = TestUtils.MEDIUM_TIMEOUT )
+ public void testEncodedForwardSlash() throws IOException {
+ LOG_ENTER();
+ String username = "hbase";
+ String password = "hbase-password";
+
+ String resourceName = "hbase/table-data";
+ String singleRowPath = "/table/%2F%2Ftestrow";
+
+ //PUT request
+ driver.getMock( "WEBHBASE" )
+ .expect()
+ .method( "PUT" )
+ .requestURI( singleRowPath ) // Need to use requestURI since pathInfo is url decoded
+ .contentType( ContentType.JSON.toString() )
+ .respond()
+ .status( HttpStatus.SC_OK );
+
+ CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+ UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(username, password);
+ credentialsProvider.setCredentials(AuthScope.ANY, credentials);
+ // Show that normalizing from HttpClient doesn't change the behavior of %2F handling
+ // with HttpClient 4.5.8+ - HTTPCLIENT-1968
+ RequestConfig requestConfig = RequestConfig.custom().setNormalizeUri(false).build();
+ try(CloseableHttpClient httpClient = HttpClients.custom()
+ .setDefaultCredentialsProvider(credentialsProvider)
+ .setDefaultRequestConfig(requestConfig)
+ .build()) {
+
+ HttpPut httpPut = new HttpPut(driver.getUrl("WEBHBASE") + singleRowPath);
+ httpPut.setHeader("X-XSRF-Header", "jksdhfkhdsf");
+ httpPut.setHeader(HttpHeaders.CONTENT_TYPE, org.apache.http.entity.ContentType.APPLICATION_JSON.getMimeType());
+ httpPut.setEntity(new ByteArrayEntity(driver.getResourceBytes(resourceName + ".json")));
+
+ HttpResponse response = httpClient.execute(httpPut);
+ assertEquals(200, response.getStatusLine().getStatusCode());
+ }
+ driver.assertComplete();
+
+ driver.getMock( "WEBHBASE" )
+ .expect()
+ .method( "PUT" )
+ .requestURI( singleRowPath ) // Need to use requestURI since pathInfo is url decoded
+ .contentType( ContentType.JSON.toString() )
+ .respond()
+ .status( HttpStatus.SC_OK );
+
+ // There is no way to change the normalization behavior of the
+ // HttpClient created by rest-assured since RequestConfig isn't
+ // exposed. Instead the above test uses HttpClient directly,
+ // this shows that url normalization doesn't matter with 4.5.8+.
+ // If this view changes, don't use rest-assured for this type of
+ // testing.
+ // See: https://github.com/rest-assured/rest-assured/issues/497
+ given()
+ .urlEncodingEnabled(false) // make sure to avoid rest-assured automatic url encoding
+ .auth().preemptive().basic( username, password )
+ .header("X-XSRF-Header", "jksdhfkhdsf")
+ .body( driver.getResourceBytes( resourceName + ".json" ) )
+ .contentType( ContentType.JSON.toString() )
+ .then()
+ .statusCode( HttpStatus.SC_OK )
+ .when().put(driver.getUrl("WEBHBASE") + singleRowPath);
+ driver.assertComplete();
+
+ LOG_EXIT();
+ }
+
+ @Test( timeout = TestUtils.MEDIUM_TIMEOUT )
public void testHBaseInsertDataIntoTable() throws IOException {
LOG_ENTER();
String username = "hbase";
diff --git a/pom.xml b/pom.xml
index 6ffe6e4..ffa1465 100644
--- a/pom.xml
+++ b/pom.xml
@@ -186,7 +186,7 @@
<hadoop.version>3.2.1</hadoop.version>
<hamcrest.version>2.2</hamcrest.version>
<hamcrest-json.version>0.2</hamcrest-json.version>
- <httpclient.version>4.5.6</httpclient.version>
+ <httpclient.version>4.5.10</httpclient.version>
<httpcore.version>4.4.12</httpcore.version>
<j2e-pac4j.version>4.1.0</j2e-pac4j.version>
<jackson.version>2.10.0</jackson.version>