Introduced Response.HeaderListener to allow applications to control processing of headers.
This commit is contained in:
parent
08a7cd86e3
commit
0682af3502
|
@ -1,192 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.client;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.http.HttpCookie;
|
||||
|
||||
public class HttpCookieParser
|
||||
{
|
||||
private static final String[] DATE_PATTERNS = new String[]
|
||||
{
|
||||
"EEE',' dd-MMM-yyyy HH:mm:ss 'GMT'",
|
||||
"EEE',' dd MMM yyyy HH:mm:ss 'GMT'",
|
||||
"EEE MMM dd yyyy HH:mm:ss 'GMT'Z"
|
||||
};
|
||||
|
||||
private HttpCookieParser()
|
||||
{
|
||||
}
|
||||
|
||||
public static List<HttpCookie> parseCookies(String headerValue)
|
||||
{
|
||||
if (headerValue.toLowerCase(Locale.ENGLISH).contains("expires="))
|
||||
{
|
||||
HttpCookie cookie = parseCookie(headerValue, 0);
|
||||
if (cookie != null)
|
||||
return Collections.singletonList(cookie);
|
||||
else
|
||||
return Collections.emptyList();
|
||||
}
|
||||
else
|
||||
{
|
||||
List<HttpCookie> result = new ArrayList<>();
|
||||
List<String> cookieStrings = splitCookies(headerValue);
|
||||
for (String cookieString : cookieStrings)
|
||||
{
|
||||
HttpCookie cookie = parseCookie(cookieString, 1);
|
||||
if (cookie != null)
|
||||
result.add(cookie);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> splitCookies(String headerValue)
|
||||
{
|
||||
// The comma is the separator, but only if it's outside double quotes
|
||||
List<String> result = new ArrayList<>();
|
||||
int start = 0;
|
||||
int quotes = 0;
|
||||
for (int i = 0; i < headerValue.length(); ++i)
|
||||
{
|
||||
char c = headerValue.charAt(i);
|
||||
if (c == ',' && quotes % 2 == 0)
|
||||
{
|
||||
result.add(headerValue.substring(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
else if (c == '"')
|
||||
{
|
||||
++quotes;
|
||||
}
|
||||
}
|
||||
result.add(headerValue.substring(start));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static HttpCookie parseCookie(String cookieString, int version)
|
||||
{
|
||||
String[] cookieParts = cookieString.split(";");
|
||||
|
||||
String nameValue = cookieParts[0];
|
||||
int equal = nameValue.indexOf('=');
|
||||
if (equal < 1)
|
||||
return null;
|
||||
|
||||
String name = nameValue.substring(0, equal).trim();
|
||||
String value = nameValue.substring(equal + 1);
|
||||
String domain = null;
|
||||
String path = "/";
|
||||
long maxAge = -1;
|
||||
boolean httpOnly = false;
|
||||
boolean secure = false;
|
||||
String comment = null;
|
||||
for (int i = 1; i < cookieParts.length; ++i)
|
||||
{
|
||||
try
|
||||
{
|
||||
String[] attributeParts = cookieParts[i].split("=", 2);
|
||||
String attributeName = attributeParts[0].trim().toLowerCase(Locale.ENGLISH);
|
||||
String attributeValue = attributeParts.length < 2 ? "" : attributeParts[1].trim();
|
||||
switch (attributeName)
|
||||
{
|
||||
case "domain":
|
||||
{
|
||||
domain = attributeValue;
|
||||
break;
|
||||
}
|
||||
case "path":
|
||||
{
|
||||
path = attributeValue;
|
||||
break;
|
||||
}
|
||||
case "max-age":
|
||||
{
|
||||
maxAge = Long.parseLong(attributeValue);
|
||||
break;
|
||||
}
|
||||
case "expires":
|
||||
{
|
||||
maxAge = parseDate(attributeValue);
|
||||
break;
|
||||
}
|
||||
case "secure":
|
||||
{
|
||||
secure = true;
|
||||
break;
|
||||
}
|
||||
case "httponly":
|
||||
{
|
||||
httpOnly = true;
|
||||
}
|
||||
case "comment":
|
||||
{
|
||||
comment = attributeValue;
|
||||
break;
|
||||
}
|
||||
case "version":
|
||||
{
|
||||
version = Integer.parseInt(attributeValue);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
// Ignore
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException x)
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
return new HttpCookie(name, value, domain, path, maxAge, httpOnly, secure, comment, version);
|
||||
}
|
||||
|
||||
private static long parseDate(String attributeValue)
|
||||
{
|
||||
for (String pattern : DATE_PATTERNS)
|
||||
{
|
||||
try
|
||||
{
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat(pattern, Locale.US);
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||
long result = TimeUnit.MILLISECONDS.toSeconds(dateFormat.parse(attributeValue).getTime() - System.currentTimeMillis());
|
||||
if (result < 0)
|
||||
return 0;
|
||||
}
|
||||
catch (ParseException x)
|
||||
{
|
||||
// Ignore and continue
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
|
@ -193,21 +193,27 @@ public class HttpReceiver implements HttpParser.ResponseHandler<ByteBuffer>
|
|||
// The exchange may be null if it failed concurrently
|
||||
if (exchange != null)
|
||||
{
|
||||
exchange.getResponse().getHeaders().add(field);
|
||||
HttpHeader fieldHeader = field.getHeader();
|
||||
if (fieldHeader != null)
|
||||
HttpConversation conversation = exchange.getConversation();
|
||||
HttpResponse response = exchange.getResponse();
|
||||
boolean process = responseNotifier.notifyHeader(conversation.getResponseListeners(), response, field);
|
||||
if (process)
|
||||
{
|
||||
switch (fieldHeader)
|
||||
response.getHeaders().add(field);
|
||||
HttpHeader fieldHeader = field.getHeader();
|
||||
if (fieldHeader != null)
|
||||
{
|
||||
case SET_COOKIE:
|
||||
case SET_COOKIE2:
|
||||
switch (fieldHeader)
|
||||
{
|
||||
storeCookie(exchange.getRequest().getURI(), field);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
break;
|
||||
case SET_COOKIE:
|
||||
case SET_COOKIE2:
|
||||
{
|
||||
storeCookie(exchange.getRequest().getURI(), field);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -300,6 +300,13 @@ public class HttpRequest implements Request
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Request onResponseHeader(Response.HeaderListener listener)
|
||||
{
|
||||
this.responseListeners.add(listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Request onResponseHeaders(Response.HeadersListener listener)
|
||||
{
|
||||
|
|
|
@ -19,12 +19,14 @@
|
|||
package org.eclipse.jetty.client;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.api.Response;
|
||||
import org.eclipse.jetty.client.api.Result;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
|
@ -57,6 +59,28 @@ public class ResponseNotifier
|
|||
}
|
||||
}
|
||||
|
||||
public boolean notifyHeader(List<Response.ResponseListener> listeners, Response response, HttpField field)
|
||||
{
|
||||
boolean result = true;
|
||||
for (Response.ResponseListener listener : listeners)
|
||||
if (listener instanceof Response.HeaderListener)
|
||||
result &= notifyHeader((Response.HeaderListener)listener, response, field);
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean notifyHeader(Response.HeaderListener listener, Response response, HttpField field)
|
||||
{
|
||||
try
|
||||
{
|
||||
return listener.onHeader(response, field);
|
||||
}
|
||||
catch (Exception x)
|
||||
{
|
||||
LOG.info("Exception while notifying listener " + listener, x);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyHeaders(List<Response.ResponseListener> listeners, Response response)
|
||||
{
|
||||
for (Response.ResponseListener listener : listeners)
|
||||
|
@ -156,6 +180,12 @@ public class ResponseNotifier
|
|||
public void forwardSuccess(List<Response.ResponseListener> listeners, Response response)
|
||||
{
|
||||
notifyBegin(listeners, response);
|
||||
for (Iterator<HttpField> iterator = response.getHeaders().iterator(); iterator.hasNext();)
|
||||
{
|
||||
HttpField field = iterator.next();
|
||||
if (!notifyHeader(listeners, response, field))
|
||||
iterator.remove();
|
||||
}
|
||||
notifyHeaders(listeners, response);
|
||||
if (response instanceof ContentResponse)
|
||||
notifyContent(listeners, response, ByteBuffer.wrap(((ContentResponse)response).getContent()));
|
||||
|
@ -173,6 +203,12 @@ public class ResponseNotifier
|
|||
public void forwardFailure(List<Response.ResponseListener> listeners, Response response, Throwable failure)
|
||||
{
|
||||
notifyBegin(listeners, response);
|
||||
for (Iterator<HttpField> iterator = response.getHeaders().iterator(); iterator.hasNext();)
|
||||
{
|
||||
HttpField field = iterator.next();
|
||||
if (!notifyHeader(listeners, response, field))
|
||||
iterator.remove();
|
||||
}
|
||||
notifyHeaders(listeners, response);
|
||||
if (response instanceof ContentResponse)
|
||||
notifyContent(listeners, response, ByteBuffer.wrap(((ContentResponse)response).getContent()));
|
||||
|
|
|
@ -26,6 +26,7 @@ import java.util.Map;
|
|||
import java.util.concurrent.Future;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.util.InputStreamResponseListener;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
|
@ -36,7 +37,7 @@ import org.eclipse.jetty.util.Fields;
|
|||
* various attributes such as the path, the headers, the content, etc.</p>
|
||||
* <p>You can create {@link Request} objects via {@link HttpClient#newRequest(String)} and
|
||||
* you can send them using either {@link #send()} for a blocking semantic, or
|
||||
* {@link #send(Response.Listener)} for an asynchronous semantic.</p>
|
||||
* {@link #send(Response.CompleteListener)} for an asynchronous semantic.</p>
|
||||
*
|
||||
* @see Response
|
||||
*/
|
||||
|
@ -260,6 +261,12 @@ public interface Request
|
|||
*/
|
||||
Request onResponseBegin(Response.BeginListener listener);
|
||||
|
||||
/**
|
||||
* @param listener a listener for response header event
|
||||
* @return this request object
|
||||
*/
|
||||
Request onResponseHeader(Response.HeaderListener listener);
|
||||
|
||||
/**
|
||||
* @param listener a listener for response headers event
|
||||
* @return this request object
|
||||
|
@ -291,7 +298,7 @@ public interface Request
|
|||
* This method should be used when a simple blocking semantic is needed, and when it is known
|
||||
* that the response content can be buffered without exceeding memory constraints.
|
||||
* For example, this method is not appropriate to download big files from a server; consider using
|
||||
* {@link #send(Response.Listener)} instead, passing your own {@link Response.Listener} or a utility
|
||||
* {@link #send(Response.CompleteListener)} instead, passing your own {@link Response.Listener} or a utility
|
||||
* listener such as {@link InputStreamResponseListener}.
|
||||
* <p />
|
||||
* The future will return when {@link Response.Listener#onComplete(Result)} is invoked.
|
||||
|
|
|
@ -23,6 +23,7 @@ import java.util.EventListener;
|
|||
import java.util.List;
|
||||
|
||||
import org.eclipse.jetty.client.util.BufferingResponseListener;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
|
||||
|
@ -99,6 +100,22 @@ public interface Response
|
|||
public void onBegin(Response response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for a response header event.
|
||||
*/
|
||||
public interface HeaderListener extends ResponseListener
|
||||
{
|
||||
/**
|
||||
* Callback method invoked when a response header has been received,
|
||||
* returning whether the header should be processed or not.
|
||||
*
|
||||
* @param response the response containing the response line data and the headers so far
|
||||
* @param field the header received
|
||||
* @return true to process the header, false to skip processing of the header
|
||||
*/
|
||||
public boolean onHeader(Response response, HttpField field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for the response headers event.
|
||||
*/
|
||||
|
@ -180,7 +197,7 @@ public interface Response
|
|||
/**
|
||||
* Listener for all response events.
|
||||
*/
|
||||
public interface Listener extends BeginListener, HeadersListener, ContentListener, SuccessListener, FailureListener, CompleteListener
|
||||
public interface Listener extends BeginListener, HeaderListener, HeadersListener, ContentListener, SuccessListener, FailureListener, CompleteListener
|
||||
{
|
||||
/**
|
||||
* An empty implementation of {@link Listener}
|
||||
|
@ -192,6 +209,12 @@ public interface Response
|
|||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onHeader(Response response, HttpField field)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeaders(Response response)
|
||||
{
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.eclipse.jetty.client.Schedulable;
|
|||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.api.Response;
|
||||
import org.eclipse.jetty.client.api.Result;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
import org.eclipse.jetty.util.thread.Scheduler;
|
||||
|
@ -89,6 +90,14 @@ public class TimedResponseListener implements Response.Listener, Schedulable, Ru
|
|||
((Response.BeginListener)delegate).onBegin(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onHeader(Response response, HttpField field)
|
||||
{
|
||||
if (delegate instanceof Response.HeaderListener)
|
||||
return ((Response.HeaderListener)delegate).onHeader(response, field);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeaders(Response response)
|
||||
{
|
||||
|
|
|
@ -48,6 +48,7 @@ import org.eclipse.jetty.client.api.Request;
|
|||
import org.eclipse.jetty.client.api.Response;
|
||||
import org.eclipse.jetty.client.api.Result;
|
||||
import org.eclipse.jetty.client.util.BytesContentProvider;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.server.handler.AbstractHandler;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.toolchain.test.annotation.Slow;
|
||||
|
@ -638,4 +639,36 @@ public class HttpClientTest extends AbstractHttpClientServerTest
|
|||
Assert.assertNotNull(response);
|
||||
Assert.assertEquals(200, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHeaderProcessing() throws Exception
|
||||
{
|
||||
final String headerName = "X-Header-Test";
|
||||
start(new AbstractHandler()
|
||||
{
|
||||
@Override
|
||||
public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
{
|
||||
baseRequest.setHandled(true);
|
||||
response.setHeader(headerName, "X");
|
||||
}
|
||||
});
|
||||
|
||||
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
|
||||
.scheme(scheme)
|
||||
.onResponseHeader(new Response.HeaderListener()
|
||||
{
|
||||
@Override
|
||||
public boolean onHeader(Response response, HttpField field)
|
||||
{
|
||||
return !field.getName().equals(headerName);
|
||||
}
|
||||
})
|
||||
.send()
|
||||
.get(5, TimeUnit.SECONDS);
|
||||
|
||||
Assert.assertNotNull(response);
|
||||
Assert.assertEquals(200, response.getStatus());
|
||||
Assert.assertFalse(response.getHeaders().containsKey(headerName));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.client;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jetty.http.HttpCookie;
|
||||
import org.eclipse.jetty.toolchain.test.TestTracker;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
public class HttpCookieParserTest
|
||||
{
|
||||
@Rule
|
||||
public final TestTracker tracker = new TestTracker();
|
||||
|
||||
@Test
|
||||
public void testParseCookie1() throws Exception
|
||||
{
|
||||
String value = "wd=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/; domain=.domain.com; httponly; secure";
|
||||
List<HttpCookie> cookies = HttpCookieParser.parseCookies(value);
|
||||
Assert.assertEquals(1, cookies.size());
|
||||
HttpCookie cookie = cookies.get(0);
|
||||
Assert.assertEquals("wd", cookie.getName());
|
||||
Assert.assertEquals("deleted", cookie.getValue());
|
||||
Assert.assertEquals(".domain.com", cookie.getDomain());
|
||||
Assert.assertEquals("/", cookie.getPath());
|
||||
Assert.assertTrue(cookie.isHttpOnly());
|
||||
Assert.assertTrue(cookie.isSecure());
|
||||
Assert.assertTrue(cookie.isExpired(System.nanoTime()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseCookie2() throws Exception
|
||||
{
|
||||
String value = "wd=deleted; max-Age=0; path=/; domain=.domain.com; httponly; version=3";
|
||||
List<HttpCookie> cookies = HttpCookieParser.parseCookies(value);
|
||||
Assert.assertEquals(1, cookies.size());
|
||||
HttpCookie cookie = cookies.get(0);
|
||||
Assert.assertEquals("wd", cookie.getName());
|
||||
Assert.assertEquals("deleted", cookie.getValue());
|
||||
Assert.assertEquals(".domain.com", cookie.getDomain());
|
||||
Assert.assertEquals("/", cookie.getPath());
|
||||
Assert.assertTrue(cookie.isHttpOnly());
|
||||
Assert.assertEquals(3, cookie.getVersion());
|
||||
Assert.assertTrue(cookie.isExpired(System.nanoTime()));
|
||||
}
|
||||
}
|
|
@ -34,6 +34,7 @@ import javax.servlet.http.HttpServletResponse;
|
|||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Response;
|
||||
import org.eclipse.jetty.client.api.Result;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.handler.AbstractHandler;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
|
@ -75,6 +76,35 @@ public class HttpResponseAbortTest extends AbstractHttpClientServerTest
|
|||
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAbortOnHeader() throws Exception
|
||||
{
|
||||
start(new EmptyServerHandler());
|
||||
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
client.newRequest("localhost", connector.getLocalPort())
|
||||
.scheme(scheme)
|
||||
.onResponseHeader(new Response.HeaderListener()
|
||||
{
|
||||
@Override
|
||||
public boolean onHeader(Response response, HttpField field)
|
||||
{
|
||||
response.abort(new Exception());
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.send(new Response.CompleteListener()
|
||||
{
|
||||
@Override
|
||||
public void onComplete(Result result)
|
||||
{
|
||||
Assert.assertTrue(result.isFailed());
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAbortOnHeaders() throws Exception
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue