From a4686bc6a4364910d250c4ce6b175cfea41a3844 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Sat, 5 Mar 2016 13:29:31 +0100 Subject: [PATCH] Issue #353 (Jetty Client doesn't forward authentication headers with redirects when using proxy) Fixed by making sure that URI matches take into account default HTTP ports. --- .../jetty/client/HttpAuthenticationStore.java | 3 +- .../org/eclipse/jetty/client/HttpClient.java | 2 +- .../org/eclipse/jetty/client/HttpRequest.java | 2 +- .../client/util/AbstractAuthentication.java | 80 ++++++++++++++ .../client/util/BasicAuthentication.java | 32 ++---- .../client/util/DigestAuthentication.java | 29 ++--- .../client/HttpAuthenticationStoreTest.java | 100 ++++++++++++++++++ 7 files changed, 204 insertions(+), 44 deletions(-) create mode 100644 jetty-client/src/main/java/org/eclipse/jetty/client/util/AbstractAuthentication.java create mode 100644 jetty-client/src/test/java/org/eclipse/jetty/client/HttpAuthenticationStoreTest.java diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpAuthenticationStore.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpAuthenticationStore.java index fd58166003a..4b73c941ec6 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpAuthenticationStore.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpAuthenticationStore.java @@ -26,6 +26,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import org.eclipse.jetty.client.api.Authentication; import org.eclipse.jetty.client.api.AuthenticationStore; +import org.eclipse.jetty.client.util.AbstractAuthentication; public class HttpAuthenticationStore implements AuthenticationStore { @@ -85,7 +86,7 @@ public class HttpAuthenticationStore implements AuthenticationStore // TODO: I should match the longest URI for (Map.Entry entry : results.entrySet()) { - if (uri.toString().startsWith(entry.getKey().toString())) + if (AbstractAuthentication.matchesURI(entry.getKey(), uri)) return entry.getValue(); } return null; diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java index 697b30a292c..57d8c3bde4f 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java @@ -1000,7 +1000,7 @@ public class HttpClient extends ContainerLifeCycle return host; } - protected int normalizePort(String scheme, int port) + public static int normalizePort(String scheme, int port) { return port > 0 ? port : HttpScheme.HTTPS.is(scheme) ? 443 : 80; } 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 ef2a209d993..4c4648d5cf9 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 @@ -88,7 +88,7 @@ public class HttpRequest implements Request this.conversation = conversation; scheme = uri.getScheme(); host = client.normalizeHost(uri.getHost()); - port = client.normalizePort(scheme, uri.getPort()); + port = HttpClient.normalizePort(scheme, uri.getPort()); path = uri.getRawPath(); query = uri.getRawQuery(); extractParams(query); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/AbstractAuthentication.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/AbstractAuthentication.java new file mode 100644 index 00000000000..7d5f9738b86 --- /dev/null +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/AbstractAuthentication.java @@ -0,0 +1,80 @@ +// +// ======================================================================== +// Copyright (c) 1995-2016 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.util; + +import java.net.URI; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.Authentication; + +public abstract class AbstractAuthentication implements Authentication +{ + private final URI uri; + private final String realm; + + public AbstractAuthentication(URI uri, String realm) + { + this.uri = uri; + this.realm = realm; + } + + public abstract String getType(); + + public URI getURI() + { + return uri; + } + + public String getRealm() + { + return realm; + } + + @Override + public boolean matches(String type, URI uri, String realm) + { + if (!getType().equalsIgnoreCase(type)) + return false; + + if (!this.realm.equals(realm)) + return false; + + return matchesURI(this.uri, uri); + } + + public static boolean matchesURI(URI uri1, URI uri2) + { + String scheme = uri1.getScheme(); + if (uri1.getScheme().equalsIgnoreCase(scheme)) + { + if (uri1.getHost().equalsIgnoreCase(uri2.getHost())) + { + // Handle default HTTP ports. + int thisPort = HttpClient.normalizePort(scheme, uri1.getPort()); + int thatPort = HttpClient.normalizePort(scheme, uri2.getPort()); + if (thisPort == thatPort) + { + // Use decoded URI paths. + return uri2.getPath().startsWith(uri1.getPath()); + } + } + } + return false; + } +} diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java index e37f82b66d3..256ce071ca9 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java @@ -21,7 +21,8 @@ package org.eclipse.jetty.client.util; import java.net.URI; import java.nio.charset.StandardCharsets; -import org.eclipse.jetty.client.api.Authentication; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.AuthenticationStore; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.http.HttpHeader; @@ -35,10 +36,8 @@ import org.eclipse.jetty.util.B64Code; * {@link AuthenticationStore} retrieved from the {@link HttpClient} * via {@link HttpClient#getAuthenticationStore()}. */ -public class BasicAuthentication implements Authentication +public class BasicAuthentication extends AbstractAuthentication { - private final URI uri; - private final String realm; private final String user; private final String password; @@ -50,48 +49,39 @@ public class BasicAuthentication implements Authentication */ public BasicAuthentication(URI uri, String realm, String user, String password) { - this.uri = uri; - this.realm = realm; + super(uri, realm); this.user = user; this.password = password; } @Override - public boolean matches(String type, URI uri, String realm) + public String getType() { - if (!"basic".equalsIgnoreCase(type)) - return false; - - if (!uri.toString().startsWith(this.uri.toString())) - return false; - - return this.realm.equals(realm); + return "Basic"; } @Override public Result authenticate(Request request, ContentResponse response, HeaderInfo headerInfo, Attributes context) { String value = "Basic " + B64Code.encode(user + ":" + password, StandardCharsets.ISO_8859_1); - return new BasicResult(headerInfo.getHeader(), uri, value); + return new BasicResult(headerInfo.getHeader(), value); } - private static class BasicResult implements Result + private class BasicResult implements Result { private final HttpHeader header; - private final URI uri; private final String value; - public BasicResult(HttpHeader header, URI uri, String value) + public BasicResult(HttpHeader header, String value) { this.header = header; - this.uri = uri; this.value = value; } @Override public URI getURI() { - return uri; + return BasicAuthentication.this.getURI(); } @Override @@ -103,7 +93,7 @@ public class BasicAuthentication implements Authentication @Override public String toString() { - return String.format("Basic authentication result for %s", uri); + return String.format("Basic authentication result for %s", getURI()); } } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java index 0a131b7dfcf..89dda602d13 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java @@ -23,7 +23,6 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -33,7 +32,8 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.eclipse.jetty.client.api.Authentication; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.AuthenticationStore; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.http.HttpHeader; @@ -48,12 +48,10 @@ import org.eclipse.jetty.util.TypeUtil; * {@link AuthenticationStore} retrieved from the {@link HttpClient} * via {@link HttpClient#getAuthenticationStore()}. */ -public class DigestAuthentication implements Authentication +public class DigestAuthentication extends AbstractAuthentication { private static final Pattern PARAM_PATTERN = Pattern.compile("([^=]+)=(.*)"); - private final URI uri; - private final String realm; private final String user; private final String password; @@ -65,22 +63,15 @@ public class DigestAuthentication implements Authentication */ public DigestAuthentication(URI uri, String realm, String user, String password) { - this.uri = uri; - this.realm = realm; + super(uri, realm); this.user = user; this.password = password; } @Override - public boolean matches(String type, URI uri, String realm) + public String getType() { - if (!"digest".equalsIgnoreCase(type)) - return false; - - if (!uri.toString().startsWith(this.uri.toString())) - return false; - - return this.realm.equals(realm); + return "Digest"; } @Override @@ -108,7 +99,7 @@ public class DigestAuthentication implements Authentication clientQOP = "auth-int"; } - return new DigestResult(headerInfo.getHeader(), uri, response.getContent(), realm, user, password, algorithm, nonce, clientQOP, opaque); + return new DigestResult(headerInfo.getHeader(), response.getContent(), getRealm(), user, password, algorithm, nonce, clientQOP, opaque); } private Map parseParameters(String wwwAuthenticate) @@ -179,7 +170,6 @@ public class DigestAuthentication implements Authentication { private final AtomicInteger nonceCount = new AtomicInteger(); private final HttpHeader header; - private final URI uri; private final byte[] content; private final String realm; private final String user; @@ -189,10 +179,9 @@ public class DigestAuthentication implements Authentication private final String qop; private final String opaque; - public DigestResult(HttpHeader header, URI uri, byte[] content, String realm, String user, String password, String algorithm, String nonce, String qop, String opaque) + public DigestResult(HttpHeader header, byte[] content, String realm, String user, String password, String algorithm, String nonce, String qop, String opaque) { this.header = header; - this.uri = uri; this.content = content; this.realm = realm; this.user = user; @@ -206,7 +195,7 @@ public class DigestAuthentication implements Authentication @Override public URI getURI() { - return uri; + return DigestAuthentication.this.getURI(); } @Override diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpAuthenticationStoreTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpAuthenticationStoreTest.java new file mode 100644 index 00000000000..062986f1c5a --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpAuthenticationStoreTest.java @@ -0,0 +1,100 @@ +// +// ======================================================================== +// Copyright (c) 1995-2016 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.net.URI; + +import org.eclipse.jetty.client.api.Authentication; +import org.eclipse.jetty.client.api.AuthenticationStore; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.BasicAuthentication; +import org.eclipse.jetty.client.util.DigestAuthentication; +import org.junit.Assert; +import org.junit.Test; + +public class HttpAuthenticationStoreTest +{ + @Test + public void testFindAuthenticationWithDefaultHTTPPort() throws Exception + { + AuthenticationStore store = new HttpAuthenticationStore(); + + URI uri1 = URI.create("http://host:80"); + URI uri2 = URI.create("http://host"); + String realm = "realm"; + store.addAuthentication(new BasicAuthentication(uri1, realm, "user", "password")); + + Authentication result = store.findAuthentication("Basic", uri2, realm); + Assert.assertNotNull(result); + + store.clearAuthentications(); + + // Flip the URIs. + uri1 = URI.create("https://server/"); + uri2 = URI.create("https://server:443/path"); + store.addAuthentication(new DigestAuthentication(uri1, realm, "user", "password")); + result = store.findAuthentication("Digest", uri2, realm); + Assert.assertNotNull(result); + } + + @Test + public void testFindAuthenticationResultWithDefaultHTTPPort() throws Exception + { + AuthenticationStore store = new HttpAuthenticationStore(); + + store.addAuthenticationResult(new Authentication.Result() + { + @Override + public URI getURI() + { + return URI.create("http://host:80"); + } + + @Override + public void apply(Request request) + { + } + }); + + URI uri2 = URI.create("http://host"); + Authentication.Result result = store.findAuthenticationResult(uri2); + Assert.assertNotNull(result); + + store.clearAuthenticationResults(); + + // Flip the URIs. + store.addAuthenticationResult(new Authentication.Result() + { + @Override + public URI getURI() + { + return URI.create("https://server/"); + } + + @Override + public void apply(Request request) + { + } + }); + + uri2 = URI.create("https://server:443/path"); + result = store.findAuthenticationResult(uri2); + Assert.assertNotNull(result); + } +}