HTTPCLIENT-985: cache module should populate Via header to capture upstream and downstream protocols
Contributed by Jonathan Moore <jonathan_moore at comcast.com> git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@992117 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
ebb54d79b9
commit
b368f90913
|
@ -35,11 +35,13 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
import org.apache.http.HttpHost;
|
import org.apache.http.HttpHost;
|
||||||
|
import org.apache.http.HttpMessage;
|
||||||
import org.apache.http.HttpRequest;
|
import org.apache.http.HttpRequest;
|
||||||
import org.apache.http.HttpResponse;
|
import org.apache.http.HttpResponse;
|
||||||
import org.apache.http.HttpStatus;
|
import org.apache.http.HttpStatus;
|
||||||
import org.apache.http.HttpVersion;
|
import org.apache.http.HttpVersion;
|
||||||
import org.apache.http.ProtocolException;
|
import org.apache.http.ProtocolException;
|
||||||
|
import org.apache.http.ProtocolVersion;
|
||||||
import org.apache.http.RequestLine;
|
import org.apache.http.RequestLine;
|
||||||
import org.apache.http.annotation.ThreadSafe;
|
import org.apache.http.annotation.ThreadSafe;
|
||||||
import org.apache.http.client.ClientProtocolException;
|
import org.apache.http.client.ClientProtocolException;
|
||||||
|
@ -55,6 +57,7 @@ import org.apache.http.impl.client.DefaultHttpClient;
|
||||||
import org.apache.http.message.BasicHttpResponse;
|
import org.apache.http.message.BasicHttpResponse;
|
||||||
import org.apache.http.params.HttpParams;
|
import org.apache.http.params.HttpParams;
|
||||||
import org.apache.http.protocol.HttpContext;
|
import org.apache.http.protocol.HttpContext;
|
||||||
|
import org.apache.http.util.VersionInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 4.1
|
* @since 4.1
|
||||||
|
@ -358,6 +361,8 @@ public class CachingHttpClient implements HttpClient {
|
||||||
// default response context
|
// default response context
|
||||||
setResponseStatus(context, CacheResponseStatus.CACHE_MISS);
|
setResponseStatus(context, CacheResponseStatus.CACHE_MISS);
|
||||||
|
|
||||||
|
String via = generateViaHeader(request);
|
||||||
|
|
||||||
if (clientRequestsOurOptions(request)) {
|
if (clientRequestsOurOptions(request)) {
|
||||||
setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
|
setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
|
||||||
return new OptionsHttp11Response();
|
return new OptionsHttp11Response();
|
||||||
|
@ -375,6 +380,7 @@ public class CachingHttpClient implements HttpClient {
|
||||||
} catch (ProtocolException e) {
|
} catch (ProtocolException e) {
|
||||||
throw new ClientProtocolException(e);
|
throw new ClientProtocolException(e);
|
||||||
}
|
}
|
||||||
|
request.addHeader("Via",via);
|
||||||
|
|
||||||
responseCache.flushInvalidatedCacheEntriesFor(target, request);
|
responseCache.flushInvalidatedCacheEntriesFor(target, request);
|
||||||
|
|
||||||
|
@ -429,6 +435,19 @@ public class CachingHttpClient implements HttpClient {
|
||||||
return callBackend(target, request, context);
|
return callBackend(target, request, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String generateViaHeader(HttpMessage msg) {
|
||||||
|
final VersionInfo vi = VersionInfo.loadVersionInfo("org.apache.http.client", getClass().getClassLoader());
|
||||||
|
final String release = (vi != null) ? vi.getRelease() : VersionInfo.UNAVAILABLE;
|
||||||
|
final ProtocolVersion pv = msg.getProtocolVersion();
|
||||||
|
if ("http".equalsIgnoreCase(pv.getProtocol())) {
|
||||||
|
return String.format("%d.%d localhost (Apache-HttpClient/%s (cache))",
|
||||||
|
pv.getMajor(), pv.getMinor(), release);
|
||||||
|
} else {
|
||||||
|
return String.format("%s/%d.%d localhost (Apache-HttpClient/%s (cache))",
|
||||||
|
pv.getProtocol(), pv.getMajor(), pv.getMinor(), release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void setResponseStatus(final HttpContext context, final CacheResponseStatus value) {
|
private void setResponseStatus(final HttpContext context, final CacheResponseStatus value) {
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
context.setAttribute(CACHE_RESPONSE_STATUS, value);
|
context.setAttribute(CACHE_RESPONSE_STATUS, value);
|
||||||
|
@ -469,6 +488,7 @@ public class CachingHttpClient implements HttpClient {
|
||||||
|
|
||||||
log.debug("Calling the backend");
|
log.debug("Calling the backend");
|
||||||
HttpResponse backendResponse = backend.execute(target, request, context);
|
HttpResponse backendResponse = backend.execute(target, request, context);
|
||||||
|
backendResponse.addHeader("Via", generateViaHeader(backendResponse));
|
||||||
return handleBackendResponse(target, request, requestDate, getCurrentDate(),
|
return handleBackendResponse(target, request, requestDate, getCurrentDate(),
|
||||||
backendResponse);
|
backendResponse);
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ import org.apache.http.params.BasicHttpParams;
|
||||||
import org.apache.http.params.HttpParams;
|
import org.apache.http.params.HttpParams;
|
||||||
import org.apache.http.protocol.BasicHttpContext;
|
import org.apache.http.protocol.BasicHttpContext;
|
||||||
import org.apache.http.protocol.HttpContext;
|
import org.apache.http.protocol.HttpContext;
|
||||||
|
import org.easymock.Capture;
|
||||||
import org.easymock.classextension.EasyMock;
|
import org.easymock.classextension.EasyMock;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
@ -371,10 +372,11 @@ public class TestCachingHttpClient {
|
||||||
@Test
|
@Test
|
||||||
public void testCallBackendMakesBackEndRequestAndHandlesResponse() throws Exception {
|
public void testCallBackendMakesBackEndRequestAndHandlesResponse() throws Exception {
|
||||||
mockImplMethods(GET_CURRENT_DATE, HANDLE_BACKEND_RESPONSE);
|
mockImplMethods(GET_CURRENT_DATE, HANDLE_BACKEND_RESPONSE);
|
||||||
|
HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
|
||||||
getCurrentDateReturns(requestDate);
|
getCurrentDateReturns(requestDate);
|
||||||
backendCallWasMadeWithRequest(request);
|
backendCallWasMade(request, resp);
|
||||||
getCurrentDateReturns(responseDate);
|
getCurrentDateReturns(responseDate);
|
||||||
handleBackendResponseReturnsResponse(request, mockBackendResponse);
|
handleBackendResponseReturnsResponse(request, resp);
|
||||||
|
|
||||||
replayMocks();
|
replayMocks();
|
||||||
|
|
||||||
|
@ -755,6 +757,30 @@ public class TestCachingHttpClient {
|
||||||
context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS));
|
context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRecordsClientProtocolInViaHeaderIfRequestNotServableFromCache()
|
||||||
|
throws Exception {
|
||||||
|
impl = new CachingHttpClient(mockBackend);
|
||||||
|
HttpRequest req = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_0);
|
||||||
|
req.setHeader("Cache-Control","no-cache");
|
||||||
|
HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content");
|
||||||
|
Capture<HttpRequest> cap = new Capture<HttpRequest>();
|
||||||
|
|
||||||
|
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
|
||||||
|
EasyMock.capture(cap), EasyMock.isA(HttpContext.class)))
|
||||||
|
.andReturn(resp);
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
impl.execute(host, req, context);
|
||||||
|
verifyMocks();
|
||||||
|
|
||||||
|
HttpRequest captured = cap.getValue();
|
||||||
|
String via = captured.getFirstHeader("Via").getValue();
|
||||||
|
String proto = via.split("\\s+")[0];
|
||||||
|
Assert.assertTrue("http/1.0".equalsIgnoreCase(proto) ||
|
||||||
|
"1.0".equalsIgnoreCase(proto));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSetsCacheMissContextIfRequestNotServableFromCache()
|
public void testSetsCacheMissContextIfRequestNotServableFromCache()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
@ -774,6 +800,46 @@ public class TestCachingHttpClient {
|
||||||
context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS));
|
context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetsViaHeaderOnResponseIfRequestNotServableFromCache()
|
||||||
|
throws Exception {
|
||||||
|
impl = new CachingHttpClient(mockBackend);
|
||||||
|
HttpRequest req = new HttpGet("http://foo.example.com/");
|
||||||
|
req.setHeader("Cache-Control","no-cache");
|
||||||
|
HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content");
|
||||||
|
|
||||||
|
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
|
||||||
|
EasyMock.isA(HttpRequest.class), (HttpContext)EasyMock.isNull()))
|
||||||
|
.andReturn(resp);
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
HttpResponse result = impl.execute(host, req);
|
||||||
|
verifyMocks();
|
||||||
|
Assert.assertNotNull(result.getFirstHeader("Via"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetsViaHeaderOnResponseForCacheMiss()
|
||||||
|
throws Exception {
|
||||||
|
impl = new CachingHttpClient(mockBackend);
|
||||||
|
HttpRequest req1 = new HttpGet("http://foo.example.com/");
|
||||||
|
HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
|
||||||
|
resp1.setEntity(HttpTestUtils.makeBody(128));
|
||||||
|
resp1.setHeader("Content-Length","128");
|
||||||
|
resp1.setHeader("ETag","\"etag\"");
|
||||||
|
resp1.setHeader("Date", DateUtils.formatDate(new Date()));
|
||||||
|
resp1.setHeader("Cache-Control","public, max-age=3600");
|
||||||
|
|
||||||
|
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
|
||||||
|
EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class)))
|
||||||
|
.andReturn(resp1);
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
HttpResponse result = impl.execute(host, req1, new BasicHttpContext());
|
||||||
|
verifyMocks();
|
||||||
|
Assert.assertNotNull(result.getFirstHeader("Via"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSetsCacheHitContextIfRequestServedFromCache()
|
public void testSetsCacheHitContextIfRequestServedFromCache()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
@ -799,6 +865,30 @@ public class TestCachingHttpClient {
|
||||||
context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS));
|
context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetsViaHeaderOnResponseIfRequestServedFromCache()
|
||||||
|
throws Exception {
|
||||||
|
impl = new CachingHttpClient(mockBackend);
|
||||||
|
HttpRequest req1 = new HttpGet("http://foo.example.com/");
|
||||||
|
HttpRequest req2 = new HttpGet("http://foo.example.com/");
|
||||||
|
HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
|
||||||
|
resp1.setEntity(HttpTestUtils.makeBody(128));
|
||||||
|
resp1.setHeader("Content-Length","128");
|
||||||
|
resp1.setHeader("ETag","\"etag\"");
|
||||||
|
resp1.setHeader("Date", DateUtils.formatDate(new Date()));
|
||||||
|
resp1.setHeader("Cache-Control","public, max-age=3600");
|
||||||
|
|
||||||
|
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
|
||||||
|
EasyMock.isA(HttpRequest.class), (HttpContext)EasyMock.isNull()))
|
||||||
|
.andReturn(resp1);
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
impl.execute(host, req1);
|
||||||
|
HttpResponse result = impl.execute(host, req2);
|
||||||
|
verifyMocks();
|
||||||
|
Assert.assertNotNull(result.getFirstHeader("Via"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSetsValidatedContextIfRequestWasSuccessfullyValidated()
|
public void testSetsValidatedContextIfRequestWasSuccessfullyValidated()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
@ -838,6 +928,45 @@ public class TestCachingHttpClient {
|
||||||
context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS));
|
context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetsViaHeaderIfRequestWasSuccessfullyValidated()
|
||||||
|
throws Exception {
|
||||||
|
Date now = new Date();
|
||||||
|
Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
|
||||||
|
|
||||||
|
impl = new CachingHttpClient(mockBackend);
|
||||||
|
HttpRequest req1 = new HttpGet("http://foo.example.com/");
|
||||||
|
HttpRequest req2 = new HttpGet("http://foo.example.com/");
|
||||||
|
|
||||||
|
HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
|
||||||
|
resp1.setEntity(HttpTestUtils.makeBody(128));
|
||||||
|
resp1.setHeader("Content-Length","128");
|
||||||
|
resp1.setHeader("ETag","\"etag\"");
|
||||||
|
resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
|
||||||
|
resp1.setHeader("Cache-Control","public, max-age=5");
|
||||||
|
|
||||||
|
HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
|
||||||
|
resp2.setEntity(HttpTestUtils.makeBody(128));
|
||||||
|
resp2.setHeader("Content-Length","128");
|
||||||
|
resp2.setHeader("ETag","\"etag\"");
|
||||||
|
resp2.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
|
||||||
|
resp2.setHeader("Cache-Control","public, max-age=5");
|
||||||
|
|
||||||
|
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
|
||||||
|
EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class)))
|
||||||
|
.andReturn(resp1);
|
||||||
|
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
|
||||||
|
EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class)))
|
||||||
|
.andReturn(resp2);
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
impl.execute(host, req1, new BasicHttpContext());
|
||||||
|
HttpResponse result = impl.execute(host, req2, context);
|
||||||
|
verifyMocks();
|
||||||
|
Assert.assertNotNull(result.getFirstHeader("Via"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSetsModuleResponseContextIfValidationRequiredButFailed()
|
public void testSetsModuleResponseContextIfValidationRequiredButFailed()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
@ -902,6 +1031,38 @@ public class TestCachingHttpClient {
|
||||||
context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS));
|
context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetViaHeaderIfValidationFailsButNotRequired()
|
||||||
|
throws Exception {
|
||||||
|
Date now = new Date();
|
||||||
|
Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
|
||||||
|
|
||||||
|
impl = new CachingHttpClient(mockBackend);
|
||||||
|
HttpRequest req1 = new HttpGet("http://foo.example.com/");
|
||||||
|
HttpRequest req2 = new HttpGet("http://foo.example.com/");
|
||||||
|
|
||||||
|
HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
|
||||||
|
resp1.setEntity(HttpTestUtils.makeBody(128));
|
||||||
|
resp1.setHeader("Content-Length","128");
|
||||||
|
resp1.setHeader("ETag","\"etag\"");
|
||||||
|
resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
|
||||||
|
resp1.setHeader("Cache-Control","public, max-age=5");
|
||||||
|
|
||||||
|
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
|
||||||
|
EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class)))
|
||||||
|
.andReturn(resp1);
|
||||||
|
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
|
||||||
|
EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class)))
|
||||||
|
.andThrow(new IOException());
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
impl.execute(host, req1, new BasicHttpContext());
|
||||||
|
HttpResponse result = impl.execute(host, req2, context);
|
||||||
|
verifyMocks();
|
||||||
|
Assert.assertNotNull(result.getFirstHeader("Via"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testIsSharedCache() {
|
public void testIsSharedCache() {
|
||||||
Assert.assertTrue(impl.isSharedCache());
|
Assert.assertTrue(impl.isSharedCache());
|
||||||
|
@ -967,6 +1128,13 @@ public class TestCachingHttpClient {
|
||||||
EasyMock.<HttpContext>anyObject())).andReturn(mockBackendResponse);
|
EasyMock.<HttpContext>anyObject())).andReturn(mockBackendResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void backendCallWasMade(HttpRequest request, HttpResponse response) throws IOException {
|
||||||
|
EasyMock.expect(mockBackend.execute(
|
||||||
|
EasyMock.<HttpHost>anyObject(),
|
||||||
|
EasyMock.same(request),
|
||||||
|
EasyMock.<HttpContext>anyObject())).andReturn(response);
|
||||||
|
}
|
||||||
|
|
||||||
private void responsePolicyAllowsCaching(boolean allow) {
|
private void responsePolicyAllowsCaching(boolean allow) {
|
||||||
EasyMock.expect(
|
EasyMock.expect(
|
||||||
mockResponsePolicy.isResponseCacheable(
|
mockResponsePolicy.isResponseCacheable(
|
||||||
|
|
|
@ -31,6 +31,8 @@ import java.io.InputStream;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import org.apache.http.Header;
|
import org.apache.http.Header;
|
||||||
import org.apache.http.HeaderElement;
|
import org.apache.http.HeaderElement;
|
||||||
|
@ -5463,5 +5465,147 @@ public class TestProtocolRequirements extends AbstractProtocolTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* "The Via general-header field MUST be used by gateways and proxies
|
||||||
|
* to indicate the intermediate protocols and recipients between the
|
||||||
|
* user agent and the server on requests, and between the origin server
|
||||||
|
* and the client on responses."
|
||||||
|
*
|
||||||
|
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testProperlyFormattedViaHeaderIsAddedToRequests() throws Exception {
|
||||||
|
Capture<HttpRequest> cap = new Capture<HttpRequest>();
|
||||||
|
request.removeHeaders("Via");
|
||||||
|
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
|
||||||
|
EasyMock.capture(cap), (HttpContext)EasyMock.isNull()))
|
||||||
|
.andReturn(originResponse);
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
impl.execute(host, request);
|
||||||
|
verifyMocks();
|
||||||
|
|
||||||
|
HttpRequest captured = cap.getValue();
|
||||||
|
String via = captured.getFirstHeader("Via").getValue();
|
||||||
|
assertValidViaHeader(via);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProperlyFormattedViaHeaderIsAddedToResponses() throws Exception {
|
||||||
|
originResponse.removeHeaders("Via");
|
||||||
|
backendExpectsAnyRequest().andReturn(originResponse);
|
||||||
|
replayMocks();
|
||||||
|
HttpResponse result = impl.execute(host, request);
|
||||||
|
verifyMocks();
|
||||||
|
assertValidViaHeader(result.getFirstHeader("Via").getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void assertValidViaHeader(String via) {
|
||||||
|
// Via = "Via" ":" 1#( received-protocol received-by [ comment ] )
|
||||||
|
// received-protocol = [ protocol-name "/" ] protocol-version
|
||||||
|
// protocol-name = token
|
||||||
|
// protocol-version = token
|
||||||
|
// received-by = ( host [ ":" port ] ) | pseudonym
|
||||||
|
// pseudonym = token
|
||||||
|
|
||||||
|
String[] parts = via.split("\\s+");
|
||||||
|
Assert.assertTrue(parts.length >= 2);
|
||||||
|
|
||||||
|
// received protocol
|
||||||
|
String receivedProtocol = parts[0];
|
||||||
|
String[] protocolParts = receivedProtocol.split("/");
|
||||||
|
Assert.assertTrue(protocolParts.length >= 1);
|
||||||
|
Assert.assertTrue(protocolParts.length <= 2);
|
||||||
|
|
||||||
|
final String tokenRegexp = "[^\\p{Cntrl}()<>@,;:\\\\\"/\\[\\]?={} \\t]+";
|
||||||
|
for(String protocolPart : protocolParts) {
|
||||||
|
Assert.assertTrue(Pattern.matches(tokenRegexp, protocolPart));
|
||||||
|
}
|
||||||
|
|
||||||
|
// received-by
|
||||||
|
if (!Pattern.matches(tokenRegexp, parts[1])) {
|
||||||
|
// host : port
|
||||||
|
new HttpHost(parts[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// comment
|
||||||
|
if (parts.length > 2) {
|
||||||
|
StringBuilder buf = new StringBuilder(parts[2]);
|
||||||
|
for(int i=3; i<parts.length; i++) {
|
||||||
|
buf.append(" "); buf.append(parts[i]);
|
||||||
|
}
|
||||||
|
Assert.assertTrue(isValidComment(buf.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidComment(String s) {
|
||||||
|
final String leafComment = "^\\(([^\\p{Cntrl}()]|\\\\\\p{ASCII})*\\)$";
|
||||||
|
final String nestedPrefix = "^\\(([^\\p{Cntrl}()]|\\\\\\p{ASCII})*\\(";
|
||||||
|
final String nestedSuffix = "\\)([^\\p{Cntrl}()]|\\\\\\p{ASCII})*\\)$";
|
||||||
|
|
||||||
|
if (Pattern.matches(leafComment,s)) return true;
|
||||||
|
Matcher pref = Pattern.compile(nestedPrefix).matcher(s);
|
||||||
|
Matcher suff = Pattern.compile(nestedSuffix).matcher(s);
|
||||||
|
if (!pref.find()) return false;
|
||||||
|
if (!suff.find()) return false;
|
||||||
|
return isValidComment(s.substring(pref.end() - 1, suff.start() + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* "The received-protocol indicates the protocol version of the message
|
||||||
|
* received by the server or client along each segment of the request/
|
||||||
|
* response chain. The received-protocol version is appended to the Via
|
||||||
|
* field value when the message is forwarded so that information about
|
||||||
|
* the protocol capabilities of upstream applications remains visible
|
||||||
|
* to all recipients."
|
||||||
|
*
|
||||||
|
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testViaHeaderOnRequestProperlyRecordsClientProtocol()
|
||||||
|
throws Exception {
|
||||||
|
request = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_0);
|
||||||
|
request.removeHeaders("Via");
|
||||||
|
Capture<HttpRequest> cap = new Capture<HttpRequest>();
|
||||||
|
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
|
||||||
|
EasyMock.capture(cap), (HttpContext)EasyMock.isNull()))
|
||||||
|
.andReturn(originResponse);
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
impl.execute(host, request);
|
||||||
|
verifyMocks();
|
||||||
|
|
||||||
|
HttpRequest captured = cap.getValue();
|
||||||
|
String via = captured.getFirstHeader("Via").getValue();
|
||||||
|
String protocol = via.split("\\s+")[0];
|
||||||
|
String[] protoParts = protocol.split("/");
|
||||||
|
if (protoParts.length > 1) {
|
||||||
|
Assert.assertTrue("http".equalsIgnoreCase(protoParts[0]));
|
||||||
|
}
|
||||||
|
Assert.assertEquals("1.0",protoParts[protoParts.length-1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testViaHeaderOnResponseProperlyRecordsOriginProtocol()
|
||||||
|
throws Exception {
|
||||||
|
|
||||||
|
originResponse = new BasicHttpResponse(HttpVersion.HTTP_1_0, HttpStatus.SC_NO_CONTENT, "No Content");
|
||||||
|
|
||||||
|
backendExpectsAnyRequest().andReturn(originResponse);
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
HttpResponse result = impl.execute(host, request);
|
||||||
|
verifyMocks();
|
||||||
|
|
||||||
|
String via = result.getFirstHeader("Via").getValue();
|
||||||
|
String protocol = via.split("\\s+")[0];
|
||||||
|
String[] protoParts = protocol.split("/");
|
||||||
|
Assert.assertTrue(protoParts.length >= 1);
|
||||||
|
Assert.assertTrue(protoParts.length <= 2);
|
||||||
|
if (protoParts.length > 1) {
|
||||||
|
Assert.assertTrue("http".equalsIgnoreCase(protoParts[0]));
|
||||||
|
}
|
||||||
|
Assert.assertEquals("1.0", protoParts[protoParts.length - 1]);
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue