diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpCookieParser.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpCookieParser.java deleted file mode 100644 index 0651fe2636c..00000000000 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpCookieParser.java +++ /dev/null @@ -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 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 result = new ArrayList<>(); - List cookieStrings = splitCookies(headerValue); - for (String cookieString : cookieStrings) - { - HttpCookie cookie = parseCookie(cookieString, 1); - if (cookie != null) - result.add(cookie); - } - return result; - } - } - - private static List splitCookies(String headerValue) - { - // The comma is the separator, but only if it's outside double quotes - List 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; - } -} diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java index 1f5b9f1abb1..941f9a7357f 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java @@ -193,21 +193,27 @@ public class HttpReceiver implements HttpParser.ResponseHandler // 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; + } } } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java index 8ed4147d294..0656abbc639 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java @@ -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) { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/ResponseNotifier.java b/jetty-client/src/main/java/org/eclipse/jetty/client/ResponseNotifier.java index a1b64c6d2ee..a332bbf683c 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/ResponseNotifier.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/ResponseNotifier.java @@ -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 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 listeners, Response response) { for (Response.ResponseListener listener : listeners) @@ -156,6 +180,12 @@ public class ResponseNotifier public void forwardSuccess(List listeners, Response response) { notifyBegin(listeners, response); + for (Iterator 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 listeners, Response response, Throwable failure) { notifyBegin(listeners, response); + for (Iterator 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())); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java index 1a386bf2c7d..f52c00fdb5b 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java @@ -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.

*

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.

+ * {@link #send(Response.CompleteListener)} for an asynchronous semantic.

* * @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}. *

* The future will return when {@link Response.Listener#onComplete(Result)} is invoked. diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java index e28e95628f3..c9ab661f5ed 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java @@ -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) { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/TimedResponseListener.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/TimedResponseListener.java index bae72759296..3a18bf46381 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/TimedResponseListener.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/TimedResponseListener.java @@ -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) { diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java index d560e69b74d..a8af2d05345 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java @@ -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)); + } } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieParserTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieParserTest.java deleted file mode 100644 index 744fd437451..00000000000 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieParserTest.java +++ /dev/null @@ -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 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 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())); - } -} diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpResponseAbortTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpResponseAbortTest.java index ec97ac48528..69c2e1326fb 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpResponseAbortTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpResponseAbortTest.java @@ -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 {