diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/util/SecurityUtils.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/util/SecurityUtils.java
new file mode 100644
index 0000000000..ae3ad76ecc
--- /dev/null
+++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/util/SecurityUtils.java
@@ -0,0 +1,221 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.testing.util;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.security.PrivilegedAction;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletionException;
+import javax.security.auth.Subject;
+
+import org.apache.hc.core5.annotation.Internal;
+
+/**
+ * This class is based on SecurityUtils in Apache Avatica which is loosely based on SecurityUtils in
+ * Jetty 12.0
+ *
+ * Collections of utility methods to deal with the scheduled removal of the security classes defined
+ * by JEP 411.
+ *
+ */
+@Internal
+public class SecurityUtils {
+ private static final MethodHandle CALL_AS = lookupCallAs();
+ private static final MethodHandle CURRENT = lookupCurrent();
+ private static final MethodHandle DO_PRIVILEGED = lookupDoPrivileged();
+
+ private SecurityUtils() {
+ }
+
+ private static MethodHandle lookupCallAs() {
+ final MethodHandles.Lookup lookup = MethodHandles.lookup();
+ try {
+ try {
+ // Subject.doAs() is deprecated for removal and replaced by Subject.callAs().
+ // Lookup first the new API, since for Java versions where both exist, the
+ // new API delegates to the old API (for example Java 18, 19 and 20).
+ // Otherwise (Java 17), lookup the old API.
+ return lookup.findStatic(Subject.class, "callAs",
+ MethodType.methodType(Object.class, Subject.class, Callable.class));
+ } catch (final NoSuchMethodException x) {
+ try {
+ // Lookup the old API.
+ final MethodType oldSignature =
+ MethodType.methodType(Object.class, Subject.class,
+ PrivilegedExceptionAction.class);
+ final MethodHandle doAs =
+ lookup.findStatic(Subject.class, "doAs", oldSignature);
+ // Convert the Callable used in the new API to the PrivilegedAction used in the
+ // old
+ // API.
+ final MethodType convertSignature =
+ MethodType.methodType(PrivilegedExceptionAction.class, Callable.class);
+ final MethodHandle converter =
+ lookup.findStatic(SecurityUtils.class,
+ "callableToPrivilegedExceptionAction", convertSignature);
+ return MethodHandles.filterArguments(doAs, 1, converter);
+ } catch (final NoSuchMethodException e) {
+ throw new AssertionError(e);
+ }
+ }
+ } catch (final IllegalAccessException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private static MethodHandle lookupDoPrivileged() {
+ try {
+ // Use reflection to work with Java versions that have and don't have AccessController.
+ final Class> klass =
+ ClassLoader.getSystemClassLoader().loadClass("java.security.AccessController");
+ final MethodHandles.Lookup lookup = MethodHandles.lookup();
+ return lookup.findStatic(klass, "doPrivileged",
+ MethodType.methodType(Object.class, PrivilegedAction.class));
+ } catch (final NoSuchMethodException | IllegalAccessException x) {
+ // Assume that single methods won't be removed from AcessController
+ throw new AssertionError(x);
+ } catch (final ClassNotFoundException e) {
+ return null;
+ }
+ }
+
+ private static MethodHandle lookupCurrent() {
+ final MethodHandles.Lookup lookup = MethodHandles.lookup();
+ try {
+ // Subject.getSubject(AccessControlContext) is deprecated for removal and replaced by
+ // Subject.current().
+ // Lookup first the new API, since for Java versions where both exists, the
+ // new API delegates to the old API (for example Java 18, 19 and 20).
+ // Otherwise (Java 17), lookup the old API.
+ return lookup.findStatic(Subject.class, "current",
+ MethodType.methodType(Subject.class));
+ } catch (final NoSuchMethodException e) {
+ final MethodHandle getContext = lookupGetContext();
+ final MethodHandle getSubject = lookupGetSubject();
+ return MethodHandles.filterReturnValue(getContext, getSubject);
+ } catch (final IllegalAccessException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private static MethodHandle lookupGetSubject() {
+ final MethodHandles.Lookup lookup = MethodHandles.lookup();
+ try {
+ final Class> contextklass =
+ ClassLoader.getSystemClassLoader()
+ .loadClass("java.security.AccessControlContext");
+ return lookup.findStatic(Subject.class, "getSubject",
+ MethodType.methodType(Subject.class, contextklass));
+ } catch (final ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private static MethodHandle lookupGetContext() {
+ try {
+ // Use reflection to work with Java versions that have and don't have AccessController.
+ final Class> controllerKlass =
+ ClassLoader.getSystemClassLoader().loadClass("java.security.AccessController");
+ final Class> contextklass =
+ ClassLoader.getSystemClassLoader()
+ .loadClass("java.security.AccessControlContext");
+
+ final MethodHandles.Lookup lookup = MethodHandles.lookup();
+ return lookup.findStatic(controllerKlass, "getContext",
+ MethodType.methodType(contextklass));
+ } catch (final ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Maps to AccessController#doPrivileged if available, otherwise returns action.run().
+ * @param action the action to run
+ * @return the result of running the action
+ * @param the type of the result
+ */
+ public static T doPrivileged(final PrivilegedAction action) {
+ // Keep this method short and inlineable.
+ if (DO_PRIVILEGED == null) {
+ return action.run();
+ }
+ return doPrivileged(DO_PRIVILEGED, action);
+ }
+
+ private static T doPrivileged(final MethodHandle doPrivileged, final PrivilegedAction action) {
+ try {
+ return (T) doPrivileged.invoke(action);
+ } catch (final Throwable t) {
+ throw sneakyThrow(t);
+ }
+ }
+
+ /**
+ * Maps to Subject.callAs() if available, otherwise maps to Subject.doAs()
+ * @param subject the subject this action runs as
+ * @param action the action to run
+ * @return the result of the action
+ * @param the type of the result
+ * @throws CompletionException
+ */
+ public static T callAs(final Subject subject, final Callable action) throws CompletionException {
+ try {
+ return (T) CALL_AS.invoke(subject, action);
+ } catch (final PrivilegedActionException e) {
+ throw new CompletionException(e.getCause());
+ } catch (final Throwable t) {
+ throw sneakyThrow(t);
+ }
+ }
+
+ /**
+ * Maps to Subject.currect() is available, otherwise maps to Subject.getSubject()
+ * @return the current subject
+ */
+ public static Subject currentSubject() {
+ try {
+ return (Subject) CURRENT.invoke();
+ } catch (final Throwable t) {
+ throw sneakyThrow(t);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static PrivilegedExceptionAction
+ callableToPrivilegedExceptionAction(final Callable callable) {
+ return callable::call;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static RuntimeException sneakyThrow(final Throwable e) throws E {
+ throw (E) e;
+ }
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ApacheHTTPDSquidCompatibilityIT.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ApacheHTTPDSquidCompatibilityIT.java
index 68ed99e54b..1600311d84 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ApacheHTTPDSquidCompatibilityIT.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ApacheHTTPDSquidCompatibilityIT.java
@@ -26,11 +26,20 @@
*/
package org.apache.hc.client5.testing.compatibility;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+import javax.security.auth.Subject;
+
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.testing.compatibility.async.CachingHttpAsyncClientCompatibilityTest;
import org.apache.hc.client5.testing.compatibility.async.HttpAsyncClientCompatibilityTest;
import org.apache.hc.client5.testing.compatibility.async.HttpAsyncClientHttp1CompatibilityTest;
import org.apache.hc.client5.testing.compatibility.async.HttpAsyncClientProxyCompatibilityTest;
+import org.apache.hc.client5.testing.compatibility.spnego.SpnegoTestUtil;
import org.apache.hc.client5.testing.compatibility.sync.CachingHttpClientCompatibilityTest;
import org.apache.hc.client5.testing.compatibility.sync.HttpClientCompatibilityTest;
import org.apache.hc.client5.testing.compatibility.sync.HttpClientProxyCompatibilityTest;
@@ -38,6 +47,7 @@
import org.apache.hc.core5.http.URIScheme;
import org.apache.hc.core5.http2.HttpVersionPolicy;
import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.testcontainers.containers.GenericContainer;
@@ -49,10 +59,23 @@
class ApacheHTTPDSquidCompatibilityIT {
private static Network NETWORK = Network.newNetwork();
+ private static final Path KEYTAB_DIR = SpnegoTestUtil.createKeytabDir();
+
+ @Container
+ static final GenericContainer> KDC = ContainerImages.KDC(NETWORK, KEYTAB_DIR);
@Container
- static final GenericContainer> HTTPD_CONTAINER = ContainerImages.apacheHttpD(NETWORK);
+ static final GenericContainer> HTTPD_CONTAINER = ContainerImages.apacheHttpD(NETWORK, KEYTAB_DIR, KDC);
@Container
- static final GenericContainer> SQUID = ContainerImages.squid(NETWORK);
+ static final GenericContainer> SQUID = ContainerImages.squid(NETWORK, KEYTAB_DIR, KDC);
+
+ private static Path KRB5_CONF_PATH;
+ private static Subject spnegoSubject;
+
+ @BeforeAll
+ static void init() throws IOException {
+ KRB5_CONF_PATH = SpnegoTestUtil.prepareKrb5Conf(KDC.getHost() + ":" + KDC.getMappedPort(ContainerImages.KDC_PORT));
+ spnegoSubject = SpnegoTestUtil.loginFromKeytab("testclient", KEYTAB_DIR.resolve("testclient.keytab"));
+ }
static HttpHost targetContainerHost() {
return new HttpHost(URIScheme.HTTP.id, HTTPD_CONTAINER.getHost(), HTTPD_CONTAINER.getMappedPort(ContainerImages.HTTP_PORT));
@@ -74,193 +97,411 @@ static HttpHost proxyContainerHost() {
return new HttpHost(URIScheme.HTTP.id, SQUID.getHost(), SQUID.getMappedPort(ContainerImages.PROXY_PORT));
}
- static HttpHost proxyPwProtectedContainerHost() {
+ static HttpHost proxyAuthenticatedContainerHost() {
return new HttpHost(URIScheme.HTTP.id, SQUID.getHost(), SQUID.getMappedPort(ContainerImages.PROXY_PW_PROTECTED_PORT));
}
@AfterAll
static void cleanup() {
+ try {
+ // Let tail -F for squid logs catch up
+ Thread.sleep(5 * 1000);
+ } catch (final InterruptedException e) {
+ // Do nothring
+ }
SQUID.close();
HTTPD_CONTAINER.close();
+ KDC.close();
NETWORK.close();
+ try {
+ Files.delete(KRB5_CONF_PATH);
+ Files.delete(KRB5_CONF_PATH.getParent());
+ try ( Stream dirStream = Files.walk(KEYTAB_DIR)) {
+ dirStream
+ .filter(Files::isRegularFile)
+ .map(Path::toFile)
+ .forEach(File::delete);
+ }
+ } catch (final IOException e) {
+ //We leave some files around in tmp
+ }
}
@Nested
- @DisplayName("Classic client: HTTP/1.1, plain, direct connection")
- class ClassicDirectHttp extends HttpClientCompatibilityTest {
+ @DisplayName("Classic client: HTTP/1.1, plain, password, direct connection")
+ class ClassicDirectHttpPw extends HttpClientCompatibilityTest {
+
+ public ClassicDirectHttpPw() throws Exception {
+ super(targetContainerHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ null,
+ null,
+ null);
+ }
- public ClassicDirectHttp() throws Exception {
- super(targetContainerHost(), null, null);
+ }
+
+ @Nested
+ @DisplayName("Classic client: HTTP/1.1, plain, SPNEGO, direct connection")
+ class ClassicDirectHttpSpnego extends HttpClientCompatibilityTest {
+
+ public ClassicDirectHttpSpnego() throws Exception {
+ super(targetContainerHost(),
+ SpnegoTestUtil.createCredentials(spnegoSubject),
+ null,
+ null,
+ spnegoSubject);
}
}
@Nested
- @DisplayName("Classic client: HTTP/1.1, plain, connection via proxy")
+ @DisplayName("Classic client: HTTP/1.1, plain, password, connection via proxy")
class ClassicViaProxyHttp extends HttpClientCompatibilityTest {
public ClassicViaProxyHttp() throws Exception {
- super(targetInternalHost(), proxyContainerHost(), null);
+ super(targetInternalHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ proxyContainerHost(),
+ null,
+ null);
}
}
@Nested
- @DisplayName("Classic client: HTTP/1.1, plain, connection via password protected proxy")
- class ClassicViaPwProtectedProxyHttp extends HttpClientCompatibilityTest {
-
- public ClassicViaPwProtectedProxyHttp() throws Exception {
- super(targetInternalHost(), proxyPwProtectedContainerHost(), new UsernamePasswordCredentials("squid", "nopassword".toCharArray()));
+ @DisplayName("Classic client: HTTP/1.1, TLS, password, direct connection")
+ class ClassicDirectHttpTlsPw extends HttpClientCompatibilityTest {
+
+ public ClassicDirectHttpTlsPw() throws Exception {
+ super(targetContainerTlsHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ null,
+ null,
+ null);
}
}
@Nested
- @DisplayName("Classic client: HTTP/1.1, TLS, direct connection")
- class ClassicDirectHttpTls extends HttpClientCompatibilityTest {
-
- public ClassicDirectHttpTls() throws Exception {
- super(targetContainerTlsHost(), null, null);
+ @DisplayName("Classic client: HTTP/1.1, TLS, SPNEGO, direct connection")
+ class ClassicDirectHttpTlsSpnego extends HttpClientCompatibilityTest {
+
+ public ClassicDirectHttpTlsSpnego() throws Exception {
+ super(targetContainerTlsHost(),
+ SpnegoTestUtil.createCredentials(spnegoSubject),
+ null,
+ null,
+ spnegoSubject);
}
}
@Nested
- @DisplayName("Classic client: HTTP/1.1, TLS, connection via proxy (tunnel)")
+ @DisplayName("Classic client: HTTP/1.1, TLS, password, connection via proxy (tunnel)")
class ClassicViaProxyHttpTls extends HttpClientCompatibilityTest {
public ClassicViaProxyHttpTls() throws Exception {
- super(targetInternalTlsHost(), proxyContainerHost(), null);
+ super(targetInternalTlsHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ proxyContainerHost(),
+ null,
+ null);
}
}
@Nested
- @DisplayName("Classic client: HTTP/1.1, TLS, connection via password protected proxy (tunnel)")
+ @DisplayName("Classic client: HTTP/1.1, TLS, password, connection via password protected proxy (tunnel)")
class ClassicViaPwProtectedProxyHttpTls extends HttpClientCompatibilityTest {
public ClassicViaPwProtectedProxyHttpTls() throws Exception {
- super(targetInternalTlsHost(), proxyPwProtectedContainerHost(), new UsernamePasswordCredentials("squid", "nopassword".toCharArray()));
+ super(targetInternalTlsHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ proxyAuthenticatedContainerHost(),
+ new UsernamePasswordCredentials("squid", "nopassword".toCharArray()),
+ null);
}
}
@Nested
- @DisplayName("Async client: HTTP/1.1, plain, direct connection")
- class AsyncDirectHttp1 extends HttpAsyncClientHttp1CompatibilityTest {
+ @DisplayName("Classic client: HTTP/1.1, TLS, password, connection via SPNEGO protected proxy (tunnel)")
+ class ClassicViaSpnegoProtectedProxyHttpTls extends HttpClientCompatibilityTest {
+
+ public ClassicViaSpnegoProtectedProxyHttpTls() throws Exception {
+ super(targetInternalTlsHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ proxyAuthenticatedContainerHost(),
+ SpnegoTestUtil.createCredentials(spnegoSubject),
+ null);
+ }
+
+ }
+
+ @Nested
+ @DisplayName("Async client: HTTP/1.1, plain, password, direct connection")
+ class AsyncDirectHttp1Pw extends HttpAsyncClientHttp1CompatibilityTest {
+
+ public AsyncDirectHttp1Pw() throws Exception {
+ super(targetContainerHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ null,
+ null);
+ }
- public AsyncDirectHttp1() throws Exception {
- super(targetContainerHost(), null, null);
+ }
+
+ @Nested
+ @DisplayName("Async client: HTTP/1.1, plain, SPNEGO, direct connection")
+ class AsyncDirectHttp1Spnego extends HttpAsyncClientHttp1CompatibilityTest {
+
+ public AsyncDirectHttp1Spnego() throws Exception {
+ super(targetContainerHost(),
+ SpnegoTestUtil.createCredentials(spnegoSubject),
+ null,
+ null);
}
}
@Nested
- @DisplayName("Async client: HTTP/1.1, plain, connection via proxy")
+ @DisplayName("Async client: HTTP/1.1, plain, password, connection via proxy")
class AsyncViaProxyHttp1 extends HttpAsyncClientHttp1CompatibilityTest {
public AsyncViaProxyHttp1() throws Exception {
- super(targetInternalHost(), proxyContainerHost(), null);
+ super(targetInternalHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ proxyContainerHost(),
+ null);
}
}
@Nested
- @DisplayName("Async client: HTTP/1.1, plain, connection via password protected proxy")
- class AsyncViaPwProtectedProxyHttp1 extends HttpAsyncClientHttp1CompatibilityTest {
+ @DisplayName("Async client: HTTP/1.1, plain, password, connection via password protected proxy")
+ class AsyncViaPwProtectedProxyHttp1Pw extends HttpAsyncClientHttp1CompatibilityTest {
+
+ public AsyncViaPwProtectedProxyHttp1Pw() throws Exception {
+ super(targetInternalHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ proxyAuthenticatedContainerHost(),
+ new UsernamePasswordCredentials("squid", "nopassword".toCharArray()));
+ }
- public AsyncViaPwProtectedProxyHttp1() throws Exception {
- super(targetInternalHost(), proxyPwProtectedContainerHost(), new UsernamePasswordCredentials("squid", "nopassword".toCharArray()));
+ }
+
+ @Nested
+ @DisplayName("Async client: HTTP/1.1, plain, passwsord, connection via SPNEGO protected proxy")
+ class AsyncViaPwProtectedProxyHttp1Spnego extends HttpAsyncClientHttp1CompatibilityTest {
+
+ public AsyncViaPwProtectedProxyHttp1Spnego() throws Exception {
+ super(targetInternalHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ proxyAuthenticatedContainerHost(),
+ SpnegoTestUtil.createCredentials(spnegoSubject));
}
}
@Nested
- @DisplayName("Async client: HTTP/1.1, TLS, direct connection")
- class AsyncDirectHttp1Tls extends HttpAsyncClientHttp1CompatibilityTest {
+ @DisplayName("Async client: HTTP/1.1, TLS, password, direct connection")
+ class AsyncDirectHttp1TlsPw extends HttpAsyncClientHttp1CompatibilityTest {
+
+ public AsyncDirectHttp1TlsPw() throws Exception {
+ super(targetContainerTlsHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ null,
+ null);
+ }
+
+ }
- public AsyncDirectHttp1Tls() throws Exception {
- super(targetContainerTlsHost(), null, null);
+ @Nested
+ @DisplayName("Async client: HTTP/1.1, TLS, SPNEGO, direct connection")
+ class AsyncDirectHttp1TlsSpnego extends HttpAsyncClientHttp1CompatibilityTest {
+
+ public AsyncDirectHttp1TlsSpnego() throws Exception {
+ super(targetContainerTlsHost(),
+ SpnegoTestUtil.createCredentials(spnegoSubject),
+ null,
+ null);
}
}
@Nested
- @DisplayName("Async client: HTTP/1.1, TLS, connection via proxy (tunnel)")
+ @DisplayName("Async client: HTTP/1.1, TLS, password, connection via proxy (tunnel)")
class AsyncViaProxyHttp1Tls extends HttpAsyncClientHttp1CompatibilityTest {
public AsyncViaProxyHttp1Tls() throws Exception {
- super(targetInternalTlsHost(), proxyContainerHost(), null);
+ super(targetInternalTlsHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ proxyContainerHost(),
+ null);
}
}
@Nested
- @DisplayName("Async client: HTTP/1.1, TLS, connection via password protected proxy (tunnel)")
- class AsyncViaPwProtectedProxyHttp1Tls extends HttpAsyncClientHttp1CompatibilityTest {
-
- public AsyncViaPwProtectedProxyHttp1Tls() throws Exception {
- super(targetInternalTlsHost(), proxyPwProtectedContainerHost(), new UsernamePasswordCredentials("squid", "nopassword".toCharArray()));
+ @DisplayName("Async client: HTTP/1.1, TLS, password, connection via password protected proxy (tunnel)")
+ class AsyncViaPwProtectedProxyHttp1TlsPw extends HttpAsyncClientHttp1CompatibilityTest {
+
+ public AsyncViaPwProtectedProxyHttp1TlsPw() throws Exception {
+ super(targetInternalTlsHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ proxyAuthenticatedContainerHost(),
+ new UsernamePasswordCredentials("squid", "nopassword".toCharArray()));
}
}
@Nested
- @DisplayName("Async client: HTTP/2, plain, direct connection")
- class AsyncDirectHttp2 extends HttpAsyncClientCompatibilityTest {
+ @DisplayName("Async client: HTTP/1.1, TLS, password, connection via SPNEGO protected proxy (tunnel)")
+ class AsyncViaPwProtectedProxyHttp1TlsSpnego extends HttpAsyncClientHttp1CompatibilityTest {
+
+ public AsyncViaPwProtectedProxyHttp1TlsSpnego() throws Exception {
+ super(targetInternalTlsHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ proxyAuthenticatedContainerHost(),
+ SpnegoTestUtil.createCredentials(spnegoSubject));
+ }
+
+ }
- public AsyncDirectHttp2() throws Exception {
- super(HttpVersionPolicy.FORCE_HTTP_2, targetContainerHost(), null, null);
+ @Nested
+ @DisplayName("Async client: HTTP/2, plain, password, direct connection")
+ class AsyncDirectHttp2Pw extends HttpAsyncClientCompatibilityTest {
+
+ public AsyncDirectHttp2Pw() throws Exception {
+ super(HttpVersionPolicy.FORCE_HTTP_2, targetContainerHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ null,
+ null);
}
}
@Nested
- @DisplayName("Async client: HTTP/2, TLS, direct connection")
- class AsyncDirectHttp2Tls extends HttpAsyncClientCompatibilityTest {
+ @DisplayName("Async client: HTTP/2, plain, SPNEGO, direct connection")
+ class AsyncDirectHttp2Spnego extends HttpAsyncClientCompatibilityTest {
+
+ public AsyncDirectHttp2Spnego() throws Exception {
+ super(HttpVersionPolicy.FORCE_HTTP_2,
+ targetContainerHost(),
+ SpnegoTestUtil.createCredentials(spnegoSubject),
+ null,
+ null);
+ }
- public AsyncDirectHttp2Tls() throws Exception {
- super(HttpVersionPolicy.FORCE_HTTP_2, targetContainerTlsHost(), null, null);
+ }
+
+ @Nested
+ @DisplayName("Async client: HTTP/2, TLS, password, direct connection")
+ class AsyncDirectHttp2TlsPw extends HttpAsyncClientCompatibilityTest {
+
+ public AsyncDirectHttp2TlsPw() throws Exception {
+ super(HttpVersionPolicy.FORCE_HTTP_2,
+ targetContainerTlsHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ null,
+ null);
}
}
@Nested
- @DisplayName("Async client: HTTP/2, TLS, connection via proxy (tunnel)")
+ @DisplayName("Async client: HTTP/2, TLS, SPNEGO, direct connection")
+ class AsyncDirectHttp2TlsSpnego extends HttpAsyncClientCompatibilityTest {
+
+ public AsyncDirectHttp2TlsSpnego() throws Exception {
+ super(HttpVersionPolicy.FORCE_HTTP_2,
+ targetContainerTlsHost(),
+ SpnegoTestUtil.createCredentials(spnegoSubject),
+ null,
+ null);
+ }
+
+ }
+ @Nested
+ @DisplayName("Async client: HTTP/2, TLS, password, connection via proxy (tunnel)")
class AsyncViaProxyHttp2Tls extends HttpAsyncClientCompatibilityTest {
public AsyncViaProxyHttp2Tls() throws Exception {
- super(HttpVersionPolicy.FORCE_HTTP_2, targetInternalTlsHost(), proxyContainerHost(), null);
+ super(HttpVersionPolicy.FORCE_HTTP_2,
+ targetInternalTlsHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ proxyContainerHost(), null);
}
}
@Nested
- @DisplayName("Async client: HTTP/2, TLS, connection via password protected proxy (tunnel)")
+ @DisplayName("Async client: HTTP/2, TLS, password, connection via password protected proxy (tunnel)")
class AsyncViaPwProtectedProxyHttp2Tls extends HttpAsyncClientCompatibilityTest {
public AsyncViaPwProtectedProxyHttp2Tls() throws Exception {
- super(HttpVersionPolicy.FORCE_HTTP_2, targetInternalTlsHost(), proxyPwProtectedContainerHost(), new UsernamePasswordCredentials("squid", "nopassword".toCharArray()));
+ super(HttpVersionPolicy.FORCE_HTTP_2,
+ targetInternalTlsHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ proxyAuthenticatedContainerHost(),
+ new UsernamePasswordCredentials("squid", "nopassword".toCharArray()));
}
}
@Nested
- @DisplayName("Async client: protocol negotiate, TLS, connection via proxy (tunnel)")
+ @DisplayName("Async client: HTTP/2, TLS, password, connection via SPNEGO protected proxy (tunnel)")
+ class AsyncViaPwProtectedProxyHttp2TlsSpnego extends HttpAsyncClientCompatibilityTest {
+
+ public AsyncViaPwProtectedProxyHttp2TlsSpnego() throws Exception {
+ super(HttpVersionPolicy.FORCE_HTTP_2,
+ targetInternalTlsHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ proxyAuthenticatedContainerHost(),
+ SpnegoTestUtil.createCredentials(spnegoSubject));
+ }
+
+ }
+
+ @Nested
+ @DisplayName("Async client: protocol negotiate, TLS, password, connection via proxy (tunnel)")
class AsyncViaProxyHttpNegotiateTls extends HttpAsyncClientCompatibilityTest {
public AsyncViaProxyHttpNegotiateTls() throws Exception {
- super(HttpVersionPolicy.NEGOTIATE, targetInternalTlsHost(), proxyContainerHost(), null);
+ super(HttpVersionPolicy.NEGOTIATE,
+ targetInternalTlsHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ proxyContainerHost(),
+ null);
}
}
@Nested
- @DisplayName("Async client: protocol negotiate, TLS, connection via password protected proxy (tunnel)")
+ @DisplayName("Async client: protocol negotiate, TLS, password, connection via password protected proxy (tunnel)")
class AsyncViaPwProtectedProxyHttpNegotiateTls extends HttpAsyncClientCompatibilityTest {
public AsyncViaPwProtectedProxyHttpNegotiateTls() throws Exception {
- super(HttpVersionPolicy.NEGOTIATE, targetInternalTlsHost(), proxyPwProtectedContainerHost(), new UsernamePasswordCredentials("squid", "nopassword".toCharArray()));
+ super(HttpVersionPolicy.NEGOTIATE,
+ targetInternalTlsHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ proxyAuthenticatedContainerHost(),
+ new UsernamePasswordCredentials("squid", "nopassword".toCharArray()));
+ }
+
+ }
+
+ @Nested
+ @DisplayName("Async client: protocol negotiate, TLS, password, connection via SPNEGO protected proxy (tunnel)")
+ class AsyncViaPwProtectedProxyHttpNegotiateTlsSpnego extends HttpAsyncClientCompatibilityTest {
+
+ public AsyncViaPwProtectedProxyHttpNegotiateTlsSpnego() throws Exception {
+ super(HttpVersionPolicy.NEGOTIATE,
+ targetInternalTlsHost(),
+ new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()),
+ proxyAuthenticatedContainerHost(),
+ SpnegoTestUtil.createCredentials(spnegoSubject));
}
}
@@ -330,7 +571,7 @@ public AsyncCachingHttp2Tls() throws Exception {
class HttpClientProxy extends HttpClientProxyCompatibilityTest {
public HttpClientProxy() throws Exception {
- super(targetInternalTlsHost(), proxyPwProtectedContainerHost());
+ super(targetInternalTlsHost(), proxyAuthenticatedContainerHost());
}
}
@@ -340,7 +581,7 @@ public HttpClientProxy() throws Exception {
class AsyncClientProxy extends HttpAsyncClientProxyCompatibilityTest {
public AsyncClientProxy() throws Exception {
- super(targetInternalTlsHost(), proxyPwProtectedContainerHost());
+ super(targetInternalTlsHost(), proxyAuthenticatedContainerHost());
}
}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ContainerImages.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ContainerImages.java
index 0cb835b1f6..8de58e9ae0 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ContainerImages.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ContainerImages.java
@@ -27,6 +27,7 @@
package org.apache.hc.client5.testing.compatibility;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
import java.util.Random;
import org.apache.hc.client5.http.utils.ByteArrayBuilder;
@@ -45,6 +46,8 @@ public final class ContainerImages {
public final static String WEB_SERVER = "test-httpd";
public final static int HTTP_PORT = 8080;
public final static int HTTPS_PORT = 8443;
+ public final static String KDC_SERVER = "test-kdc";
+ public final static int KDC_PORT = 88;
public final static String PROXY = "test-proxy";
public final static int PROXY_PORT = 8888;
public final static int PROXY_PW_PROTECTED_PORT = 8889;
@@ -62,12 +65,13 @@ static byte[] randomData(final int max) {
return builder.toByteArray();
}
- public static GenericContainer> apacheHttpD(final Network network) {
+ public static GenericContainer> apacheHttpD(final Network network, final Path keytabsHostPath, final GenericContainer dependsOn) {
return new GenericContainer<>(new ImageFromDockerfile()
.withFileFromClasspath("server-cert.pem", "docker/server-cert.pem")
.withFileFromClasspath("server-key.pem", "docker/server-key.pem")
.withFileFromClasspath("httpd.conf", "docker/httpd/httpd.conf")
.withFileFromClasspath("httpd-ssl.conf", "docker/httpd/httpd-ssl.conf")
+ .withFileFromClasspath("krb5.conf", "docker/kdc/krb5.conf")
.withFileFromTransferable("111", Transferable.of(randomData(10240)))
.withFileFromTransferable("222", Transferable.of(randomData(10240)))
.withFileFromTransferable("333", Transferable.of(randomData(10240)))
@@ -78,6 +82,7 @@ public static GenericContainer> apacheHttpD(final Network network) {
.env("var_dir", "/var/httpd")
.env("www_dir", "${var_dir}/www")
.env("private_dir", "${www_dir}/private")
+ .env("private_spnego_dir", "${www_dir}/private_spnego")
.run("mkdir ${httpd_home}/ssl")
.copy("server-cert.pem", "${httpd_home}/ssl/")
.copy("server-key.pem", "${httpd_home}/ssl/")
@@ -87,33 +92,76 @@ public static GenericContainer> apacheHttpD(final Network network) {
.copy("222", "${www_dir}/")
.copy("333", "${www_dir}/")
.run("mkdir -p ${private_dir}")
+ .run("mkdir -p ${private_spnego_dir}")
//# user: testuser; pwd: nopassword
- .run("echo \"testuser:{SHA}0Ybo2sSKJNARW1aNCrLJ6Lguats=\" > ${private_dir}/.htpasswd")
- .run("echo \"testuser:Restricted Files:73deccd22e07066db8c405e5364335f5\" > ${private_dir}/.htpasswd_digest")
- .run("echo \"Big Secret\" > ${private_dir}/big-secret.txt")
+ .run("echo \"testuser:{SHA}0Ybo2sSKJNARW1aNCrLJ6Lguats=\" > ${private_dir}/.htpasswd;"
+ + "echo \"testuser:Restricted Files:73deccd22e07066db8c405e5364335f5\" > ${private_dir}/.htpasswd_digest;"
+ + "echo \"Big Secret\" > ${private_dir}/big-secret.txt;"
+ + "echo \"Big Secret\" > ${private_spnego_dir}/big-secret.txt")
+ .env("MOD_AUTH_GSSAPI_PREFIX", "/usr/local/mod_auth_gssapi")
+ .run("mkdir -p \"$MOD_AUTH_GSSAPI_PREFIX\"")
+ .workDir("$MOD_AUTH_GSSAPI_PREFIX")
+ .run("apt-get update; apt-get install -y krb5-user libkrb5-dev "
+ + " wget automake libtool pkg-config bison flex "
+ + " libapr1-dev libaprutil1-dev libssl-dev make;"
+ + " wget https://github.com/gssapi/mod_auth_gssapi/releases/download/v1.6.5/mod_auth_gssapi-1.6.5.tar.gz;"
+ + " mkdir src; cd src; tar xfvz ../mod_auth_gssapi-1.6.5.tar.gz")
+ .run("cd src/mod_auth_gssapi-1.6.5;"
+ + " autoreconf -fi; ./configure; make; make install")
+ .copy("krb5.conf", "/etc/krb5.conf")
.build()))
.withNetwork(network)
.withNetworkAliases(WEB_SERVER)
+ .withFileSystemBind(keytabsHostPath.toString(), "/keytabs")
+ .dependsOn(dependsOn)
.withLogConsumer(new Slf4jLogConsumer(LOG))
.withExposedPorts(HTTP_PORT, HTTPS_PORT);
}
- public static GenericContainer> squid(final Network network) {
+ public static GenericContainer> squid(final Network network, final Path keytabsHostPath, final GenericContainer dependsOn) {
return new GenericContainer<>(new ImageFromDockerfile()
.withFileFromClasspath("squid.conf", "docker/squid/squid.conf")
+ .withFileFromClasspath("krb5.conf", "docker/kdc/krb5.conf")
.withDockerfileFromBuilder(builder ->
builder
.from("ubuntu/squid:5.2-22.04_beta")
.env("conf_dir", "/etc/squid")
.copy("squid.conf", "${conf_dir}/")
+ .copy("krb5.conf", "/etc/krb5.conf")
//# user: squid; pwd: nopassword
.run("echo \"squid:\\$apr1\\$.5saX63T\\$cMSoCJPqEfUw9br6zBdSO0\" > ${conf_dir}/htpasswd")
.build()))
.withNetwork(network)
.withNetworkAliases(PROXY)
+ .dependsOn(dependsOn)
+ .withFileSystemBind(keytabsHostPath.toString(), "/keytabs")
.withLogConsumer(new Slf4jLogConsumer(LOG))
.withExposedPorts(PROXY_PORT, PROXY_PW_PROTECTED_PORT);
}
+ // This image builds on Ubuntu 24.04 and uses the included KDC
+ public static GenericContainer> KDC(final Network network, final Path keytabsHostPath) {
+ return new GenericContainer<>(new ImageFromDockerfile()
+ .withFileFromClasspath("krb5.conf", "docker/kdc/krb5.conf")
+ .withFileFromClasspath("start.sh", "docker/kdc/start.sh")
+ .withDockerfileFromBuilder(builder ->
+ builder
+ .from("ubuntu:noble")
+ .workDir("/workdir")
+ .volume("/keytabs")
+ .expose(KDC_PORT)
+ .copy("krb5.conf", "/etc/krb5.conf")
+ .copy("start.sh", ".")
+ .run("mkdir /var/log/kerberos && apt-get update"
+ + " && apt-get -y install krb5-kdc krb5-admin-server")
+ .cmd("/bin/sh", "start.sh")
+ .build()))
+ .withNetwork(network)
+ .withNetworkAliases(KDC_SERVER)
+ .withLogConsumer(new Slf4jLogConsumer(LOG))
+ .withExposedPorts(KDC_PORT)
+ .withFileSystemBind(keytabsHostPath.toString(), "/keytabs");
+ }
+
}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java
index e6019d9952..49ff8a45f2 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java
@@ -38,10 +38,13 @@
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.Credentials;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
+import org.apache.hc.client5.http.auth.gss.GssCredentials;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.testing.Result;
+import org.apache.hc.client5.testing.compatibility.spnego.SpnegoAuthenticationStrategy;
+import org.apache.hc.client5.testing.compatibility.spnego.SpnegoTestUtil;
import org.apache.hc.client5.testing.extension.async.HttpAsyncClientResource;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.HttpHost;
@@ -60,23 +63,46 @@ public abstract class HttpAsyncClientCompatibilityTest {
static final Timeout LONG_TIMEOUT = Timeout.ofSeconds(30);
private final HttpVersionPolicy versionPolicy;
- private final HttpHost target;
+ protected final HttpHost target;
@RegisterExtension
private final HttpAsyncClientResource clientResource;
private final BasicCredentialsProvider credentialsProvider;
+ protected final Credentials targetCreds;
+ protected String secretPath = "/private/big-secret.txt";
public HttpAsyncClientCompatibilityTest(
final HttpVersionPolicy versionPolicy,
final HttpHost target,
+ final Credentials targetCreds,
final HttpHost proxy,
final Credentials proxyCreds) throws Exception {
this.versionPolicy = versionPolicy;
this.target = target;
- this.clientResource = new HttpAsyncClientResource(versionPolicy);
- this.clientResource.configure(builder -> builder.setProxy(proxy));
+ this.targetCreds = targetCreds;
this.credentialsProvider = new BasicCredentialsProvider();
- if (proxy != null && proxyCreds != null) {
- this.credentialsProvider.setCredentials(new AuthScope(proxy), proxyCreds);
+ this.clientResource = new HttpAsyncClientResource(versionPolicy);
+ if (targetCreds != null) {
+ //this.setCredentials(new AuthScope(target), targetCreds);
+ if (targetCreds instanceof GssCredentials) {
+ secretPath = "/private_spnego/big-secret.txt";
+ this.clientResource.configure(builder -> builder
+ .setTargetAuthenticationStrategy(new SpnegoAuthenticationStrategy())
+ .setDefaultAuthSchemeRegistry(SpnegoTestUtil.getDefaultSpnegoSchemeRegistry()));
+ }
+ }
+ if (proxy != null) {
+ this.clientResource.configure(builder -> builder.setProxy(proxy));
+ if (proxyCreds != null) {
+ this.setCredentials(new AuthScope(proxy), proxyCreds);
+ if (proxyCreds instanceof GssCredentials) {
+ // We disable Mutual Auth, because Squid does not support it.
+ // There is no way to set separate scheme registry for target/proxy,
+ // but that's not a problem as SPNEGO cannot be proxied anyway.
+ this.clientResource.configure(builder ->
+ builder.setProxyAuthenticationStrategy(new SpnegoAuthenticationStrategy())
+ .setDefaultAuthSchemeRegistry(SpnegoTestUtil.getLegacySpnegoSchemeRegistry()));
+ }
+ }
}
}
@@ -90,7 +116,7 @@ HttpClientContext context() {
.build();
}
- void addCredentials(final AuthScope authScope, final Credentials credentials) {
+ void setCredentials(final AuthScope authScope, final Credentials credentials) {
credentialsProvider.setCredentials(authScope, credentials);
}
@@ -176,15 +202,15 @@ public void cancelled() {
@Test
void test_auth_failure_wrong_auth_scope() throws Exception {
- addCredentials(
+ setCredentials(
new AuthScope("http", "otherhost", -1, "Restricted Files", null),
- new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()));
+ targetCreds);
final CloseableHttpAsyncClient client = client();
final HttpClientContext context = context();
final SimpleHttpRequest httpGetSecret = SimpleRequestBuilder.get()
.setHttpHost(target)
- .setPath("/private/big-secret.txt")
+ .setPath(secretPath)
.build();
final Future future = client.execute(httpGetSecret, context, null);
final SimpleHttpResponse response = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
@@ -194,7 +220,7 @@ void test_auth_failure_wrong_auth_scope() throws Exception {
@Test
void test_auth_failure_wrong_auth_credentials() throws Exception {
- addCredentials(
+ setCredentials(
new AuthScope(target),
new UsernamePasswordCredentials("testuser", "wrong password".toCharArray()));
final CloseableHttpAsyncClient client = client();
@@ -202,7 +228,7 @@ void test_auth_failure_wrong_auth_credentials() throws Exception {
final SimpleHttpRequest httpGetSecret = SimpleRequestBuilder.get()
.setHttpHost(target)
- .setPath("/private/big-secret.txt")
+ .setPath(secretPath)
.build();
final Future future = client.execute(httpGetSecret, context, null);
final SimpleHttpResponse response = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
@@ -212,15 +238,15 @@ void test_auth_failure_wrong_auth_credentials() throws Exception {
@Test
void test_auth_success() throws Exception {
- addCredentials(
+ setCredentials(
new AuthScope(target),
- new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()));
+ targetCreds);
final CloseableHttpAsyncClient client = client();
final HttpClientContext context = context();
final SimpleHttpRequest httpGetSecret = SimpleRequestBuilder.get()
.setHttpHost(target)
- .setPath("/private/big-secret.txt")
+ .setPath(secretPath)
.build();
final Future future = client.execute(httpGetSecret, context, null);
final SimpleHttpResponse response = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientHttp1CompatibilityTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientHttp1CompatibilityTest.java
index 901e655c4b..503d915eb5 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientHttp1CompatibilityTest.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientHttp1CompatibilityTest.java
@@ -33,7 +33,6 @@
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.Credentials;
-import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.HeaderElements;
@@ -46,27 +45,26 @@
public abstract class HttpAsyncClientHttp1CompatibilityTest extends HttpAsyncClientCompatibilityTest {
- private final HttpHost target;
public HttpAsyncClientHttp1CompatibilityTest(
final HttpHost target,
+ final Credentials targetCreds,
final HttpHost proxy,
final Credentials proxyCreds) throws Exception {
- super(HttpVersionPolicy.FORCE_HTTP_1, target, proxy, proxyCreds);
- this.target = target;
+ super(HttpVersionPolicy.FORCE_HTTP_1, target, targetCreds, proxy, proxyCreds);
}
@Test
void test_auth_success_no_keep_alive() throws Exception {
- addCredentials(
+ setCredentials(
new AuthScope(target),
- new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()));
+ targetCreds);
final CloseableHttpAsyncClient client = client();
final HttpClientContext context = context();
final SimpleHttpRequest httpGetSecret = SimpleRequestBuilder.get()
.setHttpHost(target)
- .setPath("/private/big-secret.txt")
+ .setPath(secretPath)
.addHeader(HttpHeaders.CONNECTION, HeaderElements.CLOSE)
.build();
final Future future = client.execute(httpGetSecret, context, null);
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/KeytabConfiguration.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/KeytabConfiguration.java
new file mode 100644
index 0000000000..9206b8de57
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/KeytabConfiguration.java
@@ -0,0 +1,73 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.testing.compatibility.spnego;
+
+import java.nio.file.Path;
+import java.util.HashMap;
+
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+
+public class KeytabConfiguration extends Configuration {
+ private static final String IBM_KRB5_LOGIN_MODULE =
+ "com.ibm.security.auth.module.Krb5LoginModule";
+ private static final String SUN_KRB5_LOGIN_MODULE =
+ "com.sun.security.auth.module.Krb5LoginModule";
+
+ private static final String JAVA_VENDOR_NAME = System.getProperty("java.vendor");
+ private static final boolean IS_IBM_JAVA = JAVA_VENDOR_NAME.contains("IBM");
+
+ private final String principal;
+ private final Path keytabFilePath;
+
+ public KeytabConfiguration(final String principal, final Path keyTabFilePath) {
+ this.principal = principal;
+ this.keytabFilePath = keyTabFilePath;
+ }
+
+ @Override
+ public AppConfigurationEntry[] getAppConfigurationEntry(final String name) {
+ final HashMap options = new HashMap<>();
+
+ if (IS_IBM_JAVA) {
+ options.put("principal", principal);
+ options.put("useKeyTab", "true");
+ options.put("useKeytab", "file://" + keytabFilePath.normalize().toString());
+ return new AppConfigurationEntry[] { new AppConfigurationEntry(
+ IBM_KRB5_LOGIN_MODULE, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
+ options) };
+ } else {
+ options.put("principal", principal);
+ options.put("doNotPrompt", "true");
+ options.put("useKeyTab", "true");
+ options.put("keyTab", keytabFilePath.normalize().toString());
+ return new AppConfigurationEntry[] { new AppConfigurationEntry(
+ SUN_KRB5_LOGIN_MODULE, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
+ options) };
+ }
+ }
+}
\ No newline at end of file
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoAuthenticationStrategy.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoAuthenticationStrategy.java
new file mode 100644
index 0000000000..4c48f3ea9e
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoAuthenticationStrategy.java
@@ -0,0 +1,50 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.testing.compatibility.spnego;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.hc.client5.http.auth.StandardAuthScheme;
+import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
+
+public class SpnegoAuthenticationStrategy extends DefaultAuthenticationStrategy {
+
+ private static final List SPNEGO_SCHEME_PRIORITY =
+ Collections.unmodifiableList(
+ Arrays.asList(
+ StandardAuthScheme.BEARER,
+ StandardAuthScheme.DIGEST,
+ StandardAuthScheme.BASIC,
+ StandardAuthScheme.SPNEGO));
+
+ @Override
+ protected final List getSchemePriority() {
+ return SPNEGO_SCHEME_PRIORITY;
+ }
+}
\ No newline at end of file
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java
new file mode 100644
index 0000000000..492a9f10f0
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java
@@ -0,0 +1,156 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.testing.compatibility.spnego;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.concurrent.Callable;
+import java.util.stream.Collectors;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.apache.hc.client5.http.auth.AuthSchemeFactory;
+import org.apache.hc.client5.http.auth.StandardAuthScheme;
+import org.apache.hc.client5.http.auth.gss.GssCredentials;
+import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory;
+import org.apache.hc.client5.http.impl.auth.BearerSchemeFactory;
+import org.apache.hc.client5.http.impl.auth.DigestSchemeFactory;
+import org.apache.hc.client5.http.impl.auth.gss.SpnegoSchemeFactory;
+import org.apache.hc.client5.testing.compatibility.ContainerImages;
+import org.apache.hc.client5.testing.util.SecurityUtils;
+import org.apache.hc.core5.http.config.Registry;
+import org.apache.hc.core5.http.config.RegistryBuilder;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSManager;
+
+
+public class SpnegoTestUtil {
+
+ public static GssCredentials createCredentials(final Subject subject) {
+ return SecurityUtils.callAs(subject, new Callable() {
+ @Override
+ public GssCredentials call() throws Exception {
+ return new GssCredentials(
+ GSSManager.getInstance().createCredential(GSSCredential.INITIATE_ONLY));
+ }
+ });
+ }
+
+ public static Path createKeytabDir() {
+ try {
+ return Files.createTempDirectory("keytabs",
+ PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("r-xr-xr-x")));
+ } catch (final IOException e) {
+ return Paths.get("/tmp/keytabs");
+ }
+ }
+
+ public static Registry getDefaultSpnegoSchemeRegistry() {
+ return RegistryBuilder.create()
+ .register(StandardAuthScheme.BEARER, BearerSchemeFactory.INSTANCE)
+ .register(StandardAuthScheme.BASIC, BasicSchemeFactory.INSTANCE)
+ .register(StandardAuthScheme.DIGEST, DigestSchemeFactory.INSTANCE)
+ .register(StandardAuthScheme.SPNEGO, SpnegoSchemeFactory.DEFAULT)
+ // register other schemes as needed
+ .build();
+ }
+
+ //Squid does not support mutual auth
+ public static Registry getLegacySpnegoSchemeRegistry() {
+ return RegistryBuilder.create()
+ .register(StandardAuthScheme.BEARER, BearerSchemeFactory.INSTANCE)
+ .register(StandardAuthScheme.BASIC, BasicSchemeFactory.INSTANCE)
+ .register(StandardAuthScheme.DIGEST, DigestSchemeFactory.INSTANCE)
+ .register(StandardAuthScheme.SPNEGO, SpnegoSchemeFactory.LEGACY)
+ // register other schemes as needed
+ .build();
+ }
+
+ public static Subject loginFromKeytab(final String principal, final Path keytabFilePath) {
+ final Configuration kerberosConfig = new KeytabConfiguration(principal, keytabFilePath);
+ final Subject subject = new Subject();
+
+ final LoginContext lc;
+ try {
+ lc = new LoginContext("SPNEGOTest", subject, new CallbackHandler() {
+ @Override
+ public void handle(final Callback[] callbacks)
+ throws IOException, UnsupportedCallbackException {
+ throw new UnsupportedCallbackException(callbacks[0],
+ "Only keytab supported");
+ }
+ }, kerberosConfig);
+ lc.login();
+ return subject;
+ } catch (final LoginException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Updates the krb5.conf file with the specified host and port,
+ * writes it to a tmp file,
+ * and sets the java.security.krb5.conf system property to point to it.
+ *
+ * @param KdcHostPort
+ * @return Path to the updated krb5.conf file
+ * @throws IOException
+ */
+ public static Path prepareKrb5Conf(final String KdcHostPort) throws IOException {
+ // Copy krb5.conf to filesystem
+ final InputStream krb5 = SpnegoTestUtil.class.getResourceAsStream(
+ "/docker/kdc/krb5.conf");
+ // replace KDC address
+ final String krb5In;
+ try (final BufferedReader reader = new BufferedReader(
+ new InputStreamReader(krb5, StandardCharsets.UTF_8))) {
+ krb5In = reader.lines()
+ .collect(Collectors.joining("\n"));
+ }
+ final String krb5Out = krb5In.replaceAll(ContainerImages.KDC_SERVER, KdcHostPort);
+ final Path tmpKrb5 = Files.createTempDirectory("test_krb_config_dir")
+ .resolve("krb5.conf");
+ Files.write(tmpKrb5, krb5Out.getBytes(StandardCharsets.UTF_8));
+ // Set the copied krb5.conf for java
+ System.setProperty("java.security.krb5.conf", tmpKrb5.toString());
+ return tmpKrb5;
+ }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/UseJaasCredentials.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/UseJaasCredentials.java
new file mode 100644
index 0000000000..272d370d38
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/UseJaasCredentials.java
@@ -0,0 +1,45 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.testing.compatibility.spnego;
+
+import java.security.Principal;
+
+import org.apache.hc.client5.http.auth.Credentials;
+
+public class UseJaasCredentials implements Credentials {
+
+ @Override
+ public char[] getPassword() {
+ return null;
+ }
+
+ @Override
+ public Principal getUserPrincipal() {
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java
index 6db3469524..fca601db07 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java
@@ -26,17 +26,26 @@
*/
package org.apache.hc.client5.testing.compatibility.sync;
+
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
+
+import javax.security.auth.Subject;
+
import org.apache.hc.client5.http.ContextBuilder;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.Credentials;
import org.apache.hc.client5.http.auth.CredentialsStore;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
+import org.apache.hc.client5.http.auth.gss.GssCredentials;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpOptions;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.testing.compatibility.spnego.SpnegoAuthenticationStrategy;
+import org.apache.hc.client5.testing.compatibility.spnego.SpnegoTestUtil;
import org.apache.hc.client5.testing.extension.sync.HttpClientResource;
+import org.apache.hc.client5.testing.util.SecurityUtils;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpHeaders;
@@ -54,14 +63,38 @@ public abstract class HttpClientCompatibilityTest {
@RegisterExtension
private final HttpClientResource clientResource;
private final CredentialsStore credentialsProvider;
+ private final Credentials targetCreds;
+ private Subject callAs;
+ private String secretPath = "/private/big-secret.txt";
- public HttpClientCompatibilityTest(final HttpHost target, final HttpHost proxy, final Credentials proxyCreds) throws Exception {
+ public HttpClientCompatibilityTest(final HttpHost target, final Credentials targetCreds, final HttpHost proxy, final Credentials proxyCreds, final Subject callAs) throws Exception {
this.target = target;
- this.clientResource = new HttpClientResource();
- this.clientResource.configure(builder -> builder.setProxy(proxy));
+ this.targetCreds = targetCreds;
+ this.callAs = callAs;
this.credentialsProvider = new BasicCredentialsProvider();
- if (proxy != null && proxyCreds != null) {
- this.addCredentials(new AuthScope(proxy), proxyCreds);
+ this.clientResource = new HttpClientResource();
+ if (targetCreds != null || callAs != null) {
+ //this.setCredentials(new AuthScope(target), targetCreds);
+ if (targetCreds instanceof GssCredentials || callAs != null) {
+ secretPath = "/private_spnego/big-secret.txt";
+ this.clientResource.configure(builder -> builder
+ .setTargetAuthenticationStrategy(new SpnegoAuthenticationStrategy())
+ .setDefaultAuthSchemeRegistry(SpnegoTestUtil.getDefaultSpnegoSchemeRegistry()));
+ }
+ }
+ if (proxy != null) {
+ this.clientResource.configure(builder -> builder.setProxy(proxy));
+ if (proxyCreds != null) {
+ this.setCredentials(new AuthScope(proxy), proxyCreds);
+ if (proxyCreds instanceof GssCredentials) {
+ // We disable Mutual Auth, because Squid does not support it.
+ // There is no way to set separate scheme registry for target/proxy,
+ // but that's not a problem as SPNEGO cannot be proxied anyway.
+ this.clientResource.configure(builder ->
+ builder.setProxyAuthenticationStrategy(new SpnegoAuthenticationStrategy())
+ .setDefaultAuthSchemeRegistry(SpnegoTestUtil.getLegacySpnegoSchemeRegistry()));
+ }
+ }
}
}
@@ -75,7 +108,7 @@ HttpClientContext context() {
.build();
}
- void addCredentials(final AuthScope authScope, final Credentials credentials) {
+ void setCredentials(final AuthScope authScope, final Credentials credentials) {
credentialsProvider.setCredentials(authScope, credentials);
}
@@ -123,14 +156,14 @@ void test_get_connection_close() throws Exception {
@Test
void test_wrong_target_auth_scope() throws Exception {
- addCredentials(
+ setCredentials(
new AuthScope("http", "otherhost", -1, "Restricted Files", null),
new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()));
final CloseableHttpClient client = client();
final HttpClientContext context = context();
- final ClassicHttpRequest request = new HttpGet("/private/big-secret.txt");
+ final ClassicHttpRequest request = new HttpGet(secretPath);
try (ClassicHttpResponse response = client.executeOpen(target, request, context)) {
Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
EntityUtils.consume(response.getEntity());
@@ -139,14 +172,14 @@ void test_wrong_target_auth_scope() throws Exception {
@Test
void test_wrong_target_credentials() throws Exception {
- addCredentials(
+ setCredentials(
new AuthScope(target),
new UsernamePasswordCredentials("testuser", "wrong password".toCharArray()));
final CloseableHttpClient client = client();
final HttpClientContext context = context();
- final ClassicHttpRequest request = new HttpGet("/private/big-secret.txt");
+ final ClassicHttpRequest request = new HttpGet(secretPath);
try (ClassicHttpResponse response = client.executeOpen(target, request, context)) {
Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
EntityUtils.consume(response.getEntity());
@@ -155,13 +188,25 @@ void test_wrong_target_credentials() throws Exception {
@Test
void test_correct_target_credentials() throws Exception {
- addCredentials(
- new AuthScope(target),
- new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()));
+ setCredentials(
+ new AuthScope(target), targetCreds);
+ test_correct_target_credentials_int();
+ }
+
+ @Test
+ void test_correct_target_credentials_call_as() throws Exception {
+ assumeFalse(callAs == null);
+ SecurityUtils.callAs(callAs, () -> {
+ test_correct_target_credentials_int();
+ return 0;
+ });
+ }
+
+ private void test_correct_target_credentials_int() throws Exception {
final CloseableHttpClient client = client();
final HttpClientContext context = context();
- final ClassicHttpRequest request = new HttpGet("/private/big-secret.txt");
+ final ClassicHttpRequest request = new HttpGet(secretPath);
try (ClassicHttpResponse response = client.executeOpen(target, request, context)) {
Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
EntityUtils.consume(response.getEntity());
@@ -170,13 +215,25 @@ void test_correct_target_credentials() throws Exception {
@Test
void test_correct_target_credentials_no_keep_alive() throws Exception {
- addCredentials(
- new AuthScope(target),
- new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()));
+ setCredentials(
+ new AuthScope(target), targetCreds);
+ test_correct_target_credentials_no_keep_alive_int();
+ }
+
+ @Test
+ void test_correct_target_credentials_no_keep_alive_call_as() throws Exception {
+ assumeFalse(callAs == null);
+ SecurityUtils.callAs(callAs, () -> {
+ test_correct_target_credentials_int();
+ return 0;
+ });
+ }
+
+ private void test_correct_target_credentials_no_keep_alive_int() throws Exception {
final CloseableHttpClient client = client();
final HttpClientContext context = context();
- final ClassicHttpRequest request = ClassicRequestBuilder.get("/private/big-secret.txt")
+ final ClassicHttpRequest request = ClassicRequestBuilder.get(secretPath)
.addHeader(HttpHeaders.CONNECTION, "close")
.build();
try (ClassicHttpResponse response = client.executeOpen(target, request, context)) {
@@ -184,5 +241,4 @@ void test_correct_target_credentials_no_keep_alive() throws Exception {
EntityUtils.consume(response.getEntity());
}
}
-
}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSpnegoScheme.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSpnegoScheme.java
new file mode 100644
index 0000000000..70325b5bfb
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSpnegoScheme.java
@@ -0,0 +1,525 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.testing.sync;
+
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.hc.client5.http.AuthenticationStrategy;
+import org.apache.hc.client5.http.ClientProtocolException;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.auth.AuthScheme;
+import org.apache.hc.client5.http.auth.AuthSchemeFactory;
+import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.AuthenticationException;
+import org.apache.hc.client5.http.auth.Credentials;
+import org.apache.hc.client5.http.auth.CredentialsProvider;
+import org.apache.hc.client5.http.auth.StandardAuthScheme;
+import org.apache.hc.client5.http.auth.gss.GssConfig;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
+import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder;
+import org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.http.utils.Base64;
+import org.apache.hc.client5.testing.extension.sync.ClientProtocolLevel;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.config.Registry;
+import org.apache.hc.core5.http.config.RegistryBuilder;
+import org.apache.hc.core5.http.io.HttpRequestHandler;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.http.message.BasicHeader;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.util.Timeout;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.AdditionalMatchers;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+
+/**
+ * Tests for {@link org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme}.
+ */
+public class TestSpnegoScheme extends AbstractIntegrationTestBase {
+
+ protected TestSpnegoScheme() {
+ super(URIScheme.HTTP, ClientProtocolLevel.STANDARD);
+ }
+
+ public static final Timeout TIMEOUT = Timeout.ofMinutes(1);
+
+ private static final String GOOD_TOKEN = "GOOD_TOKEN";
+ private static final byte[] GOOD_TOKEN_BYTES = GOOD_TOKEN.getBytes(StandardCharsets.UTF_8);
+ private static final byte[] GOOD_TOKEN_B64_BYTES = Base64.encodeBase64(GOOD_TOKEN_BYTES);
+ private static final String GOOD_TOKEN_B64 = new String(GOOD_TOKEN_B64_BYTES);
+
+ private static final String NO_TOKEN = "";
+ private static final byte[] NO_TOKEN_BYTES = NO_TOKEN.getBytes(StandardCharsets.UTF_8);
+
+ private static final String GOOD_MUTUAL_AUTH_TOKEN = "GOOD_MUTUAL_AUTH_TOKEN";
+ private static final byte[] GOOD_MUTUAL_AUTH_TOKEN_BYTES = GOOD_MUTUAL_AUTH_TOKEN.getBytes(StandardCharsets.UTF_8);
+ private static final byte[] GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES = Base64.encodeBase64(GOOD_MUTUAL_AUTH_TOKEN_BYTES);
+
+ private static final String BAD_MUTUAL_AUTH_TOKEN = "BAD_MUTUAL_AUTH_TOKEN";
+ private static final byte[] BAD_MUTUAL_AUTH_TOKEN_BYTES = BAD_MUTUAL_AUTH_TOKEN.getBytes(StandardCharsets.UTF_8);
+ private static final byte[] BAD_MUTUAL_AUTH_TOKEN_B64_BYTES = Base64.encodeBase64(BAD_MUTUAL_AUTH_TOKEN_BYTES);
+
+ static GssConfig MUTUAL_KERBEROS_CONFIG = GssConfig.DEFAULT;
+
+ private static class SpnegoAuthenticationStrategy extends DefaultAuthenticationStrategy {
+
+ private static final List SPNEGO_SCHEME_PRIORITY =
+ Collections.unmodifiableList(
+ Arrays.asList(StandardAuthScheme.SPNEGO,
+ StandardAuthScheme.BEARER,
+ StandardAuthScheme.DIGEST,
+ StandardAuthScheme.BASIC));
+
+ @Override
+ protected final List getSchemePriority() {
+ return SPNEGO_SCHEME_PRIORITY;
+ }
+ }
+
+ final AuthenticationStrategy spnegoAuthenticationStrategy = new SpnegoAuthenticationStrategy();
+
+ final CredentialsProvider jaasCredentialsProvider = CredentialsProviderBuilder.create()
+ .add(new AuthScope(null, null, -1, null, null), new UseJaasCredentials())
+ .build();
+
+ /**
+ * This service will continue to ask for authentication.
+ */
+ private static class PleaseNegotiateService implements HttpRequestHandler {
+
+ @Override
+ public void handle(
+ final ClassicHttpRequest request,
+ final ClassicHttpResponse response,
+ final HttpContext context) throws HttpException, IOException {
+ response.setCode(HttpStatus.SC_UNAUTHORIZED);
+ response.addHeader(new BasicHeader("WWW-Authenticate", StandardAuthScheme.SPNEGO + " blablabla"));
+ response.addHeader(new BasicHeader("Connection", "Keep-Alive"));
+ response.setEntity(new StringEntity("auth required "));
+ }
+ }
+
+ /**
+ * This service implements a normal mutualAuth flow
+ */
+ private static class SpnegoService implements HttpRequestHandler {
+
+ int callCount = 1;
+ final boolean sendMutualToken;
+ final byte[] encodedMutualAuthToken;
+
+ SpnegoService (final boolean sendMutualToken, final byte[] encodedMutualAuthToken) {
+ this.sendMutualToken = sendMutualToken;
+ this.encodedMutualAuthToken = encodedMutualAuthToken;
+ }
+
+ @Override
+ public void handle(
+ final ClassicHttpRequest request,
+ final ClassicHttpResponse response,
+ final HttpContext context) throws HttpException, IOException {
+ if (callCount == 1) {
+ callCount++;
+ // Send the empty challenge
+ response.setCode(HttpStatus.SC_UNAUTHORIZED);
+ response.addHeader(new BasicHeader("WWW-Authenticate", StandardAuthScheme.SPNEGO));
+ response.addHeader(new BasicHeader("Connection", "Keep-Alive"));
+ response.setEntity(new StringEntity("auth required "));
+ } else if (callCount == 2) {
+ callCount++;
+ if (request.getHeader("Authorization").getValue().contains(GOOD_TOKEN_B64)) {
+ response.setCode(HttpStatus.SC_OK);
+ if (sendMutualToken) {
+ response.addHeader(new BasicHeader("WWW-Authenticate", StandardAuthScheme.SPNEGO + " " + new String(encodedMutualAuthToken)));
+ }
+ response.addHeader(new BasicHeader("Connection", "Keep-Alive"));
+ response.setEntity(new StringEntity("auth successful "));
+ } else {
+ response.setCode(HttpStatus.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
+ }
+ }
+
+ /**
+ * NegotatieScheme with a custom GSSManager that does not require any Jaas or
+ * Kerberos configuration.
+ *
+ */
+ private static class NegotiateSchemeWithMockGssManager extends SpnegoScheme {
+
+ final GSSManager manager = Mockito.mock(GSSManager.class);
+ final GSSName name = Mockito.mock(GSSName.class);
+ final GSSContext context = Mockito.mock(GSSContext.class);
+
+ NegotiateSchemeWithMockGssManager() throws Exception {
+ super(GssConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE);
+ Mockito.when(context.initSecContext(
+ ArgumentMatchers.any(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt()))
+ .thenReturn("12345678".getBytes());
+ Mockito.when(manager.createName(
+ ArgumentMatchers.anyString(), ArgumentMatchers.any()))
+ .thenReturn(name);
+ Mockito.when(manager.createContext(
+ ArgumentMatchers.any(), ArgumentMatchers.any(),
+ ArgumentMatchers.any(), ArgumentMatchers.anyInt()))
+ .thenReturn(context);
+ }
+
+ @Override
+ protected GSSManager getManager() {
+ return manager;
+ }
+
+ }
+
+ private static class MutualNegotiateSchemeWithMockGssManager extends SpnegoScheme {
+
+ final GSSManager manager = Mockito.mock(GSSManager.class);
+ final GSSName name = Mockito.mock(GSSName.class);
+ final GSSContext context = Mockito.mock(GSSContext.class);
+
+ MutualNegotiateSchemeWithMockGssManager(final boolean established, final boolean mutual) throws Exception {
+ super(MUTUAL_KERBEROS_CONFIG, SystemDefaultDnsResolver.INSTANCE);
+ // Initial empty WWW-Authenticate response header
+ Mockito.when(context.initSecContext(
+ AdditionalMatchers.aryEq(NO_TOKEN_BYTES), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt()))
+ .thenReturn(GOOD_TOKEN_BYTES);
+ // Valid mutual token
+ Mockito.when(context.initSecContext(
+ AdditionalMatchers.aryEq(GOOD_MUTUAL_AUTH_TOKEN_BYTES), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt()))
+ .thenReturn(NO_TOKEN_BYTES);
+ // Invalid mutual token
+ Mockito.when(context.initSecContext(
+ AdditionalMatchers.aryEq(BAD_MUTUAL_AUTH_TOKEN_BYTES), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt()))
+ .thenThrow(new GSSException(GSSException.DEFECTIVE_CREDENTIAL));
+ // It's hard to mock state, so instead we specify the complete and mutualAuth states
+ // in the constructor
+ Mockito.when(context.isEstablished()).thenReturn(established);
+ Mockito.when(context.getMutualAuthState()).thenReturn(mutual);
+ Mockito.when(manager.createName(
+ ArgumentMatchers.anyString(), ArgumentMatchers.any()))
+ .thenReturn(name);
+ Mockito.when(manager.createContext(
+ ArgumentMatchers.any(), ArgumentMatchers.any(),
+ ArgumentMatchers.any(), ArgumentMatchers.anyInt()))
+ .thenReturn(context);
+ }
+
+ @Override
+ protected GSSManager getManager() {
+ return manager;
+ }
+
+ }
+
+ private static class UseJaasCredentials implements Credentials {
+
+ @Override
+ public char[] getPassword() {
+ return null;
+ }
+
+ @Override
+ public Principal getUserPrincipal() {
+ return null;
+ }
+
+ }
+
+ private static class TestAuthSchemeFactory implements AuthSchemeFactory {
+
+ AuthScheme scheme;
+
+ TestAuthSchemeFactory(final AuthScheme scheme) throws Exception {
+ this.scheme = scheme;
+ }
+
+ @Override
+ public AuthScheme create(final HttpContext context) {
+ return scheme;
+ }
+
+ }
+
+
+ /**
+ * Tests that the client will stop connecting to the server if
+ * the server still keep asking for a valid ticket.
+ */
+ @Test
+ void testDontTryToAuthenticateEndlessly() throws Exception {
+ configureServer(t -> {
+ t.register("*", new PleaseNegotiateService());
+ });
+
+ final AuthSchemeFactory nsf = new TestAuthSchemeFactory(new NegotiateSchemeWithMockGssManager());
+ final Registry authSchemeRegistry = RegistryBuilder.create()
+ .register(StandardAuthScheme.SPNEGO, nsf)
+ .build();
+ configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
+ t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
+ t.setDefaultCredentialsProvider(jaasCredentialsProvider);
+ });
+
+ final HttpHost target = startServer();
+ final String s = "/path";
+ final HttpGet httpget = new HttpGet(s);
+ try {
+ client().execute(target, httpget, response -> {
+ EntityUtils.consume(response.getEntity());
+ Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
+ return null;
+ });
+ Assertions.fail();
+ } catch (final IllegalStateException e) {
+ // Expected
+ }
+ }
+
+ /**
+ * Test the success case for mutual auth
+ */
+ @Test
+ void testMutualSuccess() throws Exception {
+ configureServer(t -> {
+ t.register("*", new SpnegoService(true, GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES));
+ });
+ final HttpHost target = startServer();
+
+ final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(true, true);
+ final AuthSchemeFactory nsf = new TestAuthSchemeFactory(mockAuthScheme);
+ final Registry authSchemeRegistry = RegistryBuilder.create()
+ .register(StandardAuthScheme.SPNEGO, nsf)
+ .build();
+
+ configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
+ t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
+ t.setDefaultCredentialsProvider(jaasCredentialsProvider);
+ });
+
+ final String s = "/path";
+ final HttpGet httpget = new HttpGet(s);
+ client().execute(target, httpget, response -> {
+ EntityUtils.consume(response.getEntity());
+ Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
+ return null;
+ });
+
+ Mockito.verify(mockAuthScheme.context, Mockito.atLeastOnce()).isEstablished();
+ Mockito.verify(mockAuthScheme.context, Mockito.atLeastOnce()).getMutualAuthState();
+ }
+
+ /**
+ * No mutual auth response token sent by server.
+ */
+ @Test
+ void testMutualFailureNoToken() throws Exception {
+ configureServer(t -> {
+ t.register("*", new SpnegoService(false, null));
+ });
+
+ final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(false, false);
+ final AuthSchemeFactory nsf = new TestAuthSchemeFactory(mockAuthScheme);
+ final Registry authSchemeRegistry = RegistryBuilder.create()
+ .register(StandardAuthScheme.SPNEGO, nsf)
+ .build();
+
+ configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
+ t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
+ });
+
+ final HttpClientContext context = new HttpClientContext();
+ context.setCredentialsProvider(jaasCredentialsProvider);
+
+ final HttpHost target = startServer();
+ final String s = "/path";
+ final HttpGet httpget = new HttpGet(s);
+ try {
+ client().execute(target, httpget, context, response -> {
+ EntityUtils.consume(response.getEntity());
+ Assertions.fail();
+ return null;
+ });
+ Assertions.fail();
+ } catch (final Exception e) {
+ Assertions.assertTrue(e instanceof ClientProtocolException);
+ Assertions.assertTrue(e.getCause() instanceof AuthenticationException);
+ }
+
+ Mockito.verify(mockAuthScheme.context, Mockito.never()).isEstablished();
+ Mockito.verify(mockAuthScheme.context, Mockito.never()).getMutualAuthState();
+ }
+
+ /**
+ * Server sends a "valid" token, but we mock the established status to false
+ */
+ @Test
+ void testMutualFailureEstablishedStatusFalse() throws Exception {
+ configureServer(t -> {
+ t.register("*", new SpnegoService(true, GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES));
+ });
+
+ final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(false, false);
+ final AuthSchemeFactory nsf = new TestAuthSchemeFactory(mockAuthScheme);
+ final Registry authSchemeRegistry = RegistryBuilder.create()
+ .register(StandardAuthScheme.SPNEGO, nsf)
+ .build();
+ configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
+ t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
+ });
+
+ final HttpClientContext context = new HttpClientContext();
+ context.setCredentialsProvider(jaasCredentialsProvider);
+
+ final HttpHost target = startServer();
+ final String s = "/path";
+ final HttpGet httpget = new HttpGet(s);
+ try {
+ client().execute(target, httpget, context, response -> {
+ EntityUtils.consume(response.getEntity());
+ Assertions.fail();
+ return null;
+ });
+ Assertions.fail();
+ } catch (final Exception e) {
+ Assertions.assertTrue(e instanceof ClientProtocolException);
+ Assertions.assertTrue(e.getCause() instanceof AuthenticationException);
+ }
+
+ Mockito.verify(mockAuthScheme.context, Mockito.atLeastOnce()).isEstablished();
+ Mockito.verify(mockAuthScheme.context, Mockito.never()).getMutualAuthState();
+ }
+
+ /**
+ * Server sends a "valid" token, but we mock the mutual auth status to false
+ */
+ @Test
+ void testMutualFailureMutualStatusFalse() throws Exception {
+ configureServer(t -> {
+ t.register("*", new SpnegoService(true, GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES));
+ });
+
+ final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(true, false);
+ final AuthSchemeFactory nsf = new TestAuthSchemeFactory(mockAuthScheme);
+ final Registry authSchemeRegistry = RegistryBuilder.create()
+ .register(StandardAuthScheme.SPNEGO, nsf)
+ .build();
+ configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
+ t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
+ });
+
+ final HttpClientContext context = new HttpClientContext();
+ context.setCredentialsProvider(jaasCredentialsProvider);
+
+ final HttpHost target = startServer();
+ final String s = "/path";
+ final HttpGet httpget = new HttpGet(s);
+ try {
+ client().execute(target, httpget, context, response -> {
+ EntityUtils.consume(response.getEntity());
+ Assertions.fail();
+ return null;
+ });
+ Assertions.fail();
+ } catch (final Exception e) {
+ Assertions.assertTrue(e instanceof ClientProtocolException);
+ Assertions.assertTrue(e.getCause() instanceof AuthenticationException);
+ }
+
+ Mockito.verify(mockAuthScheme.context, Mockito.atLeastOnce()).isEstablished();
+ Mockito.verify(mockAuthScheme.context, Mockito.atLeastOnce()).getMutualAuthState();
+ }
+
+ /**
+ * Server sends a "bad" token, and GSS throws an exception.
+ */
+ @Test
+ void testMutualFailureBadToken() throws Exception {
+ configureServer(t -> {
+ t.register("*", new SpnegoService(true, BAD_MUTUAL_AUTH_TOKEN_B64_BYTES));
+ });
+
+ // We except that the initSecContent throws an exception, so the status is irrelevant
+ final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(true, true);
+ final AuthSchemeFactory nsf = new TestAuthSchemeFactory(mockAuthScheme);
+ final Registry authSchemeRegistry = RegistryBuilder.create()
+ .register(StandardAuthScheme.SPNEGO, nsf)
+ .build();
+
+ configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
+ t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
+ });
+
+ final HttpClientContext context = new HttpClientContext();
+ context.setCredentialsProvider(jaasCredentialsProvider);
+
+ final HttpHost target = startServer();
+ final String s = "/path";
+ final HttpGet httpget = new HttpGet(s);
+ try {
+ client().execute(target, httpget, context, response -> {
+ EntityUtils.consume(response.getEntity());
+ Assertions.fail();
+ return null;
+ });
+ Assertions.fail();
+ } catch (final Exception e) {
+ Assertions.assertTrue(e instanceof ClientProtocolException);
+ Assertions.assertTrue(e.getCause() instanceof AuthenticationException);
+ }
+
+ Mockito.verify(mockAuthScheme.context, Mockito.never()).isEstablished();
+ Mockito.verify(mockAuthScheme.context, Mockito.never()).getMutualAuthState();
+ }
+}
diff --git a/httpclient5-testing/src/test/resources/docker/httpd/httpd.conf b/httpclient5-testing/src/test/resources/docker/httpd/httpd.conf
index f9931ea71b..cbb11f761a 100644
--- a/httpclient5-testing/src/test/resources/docker/httpd/httpd.conf
+++ b/httpclient5-testing/src/test/resources/docker/httpd/httpd.conf
@@ -77,6 +77,7 @@ Listen 8080
# Example:
# LoadModule foo_module modules/mod_foo.so
#
+LoadModule auth_gssapi_module modules/mod_auth_gssapi.so
LoadModule mpm_event_module modules/mod_mpm_event.so
#LoadModule mpm_prefork_module modules/mod_mpm_prefork.so
#LoadModule mpm_worker_module modules/mod_mpm_worker.so
@@ -340,7 +341,7 @@ DocumentRoot "/var/httpd/www"
# logged here. If you *do* define an error logfile for a
# container, that host's errors will be logged there and not here.
#
-ErrorLog /proc/self/fd/2
+ErrorLog /proc/1/fd/2
#
# LogLevel: Control the number of messages logged to the error_log.
@@ -369,7 +370,7 @@ LogLevel warn
# define per- access logfiles, transactions will be
# logged therein and *not* in this file.
#
- CustomLog /proc/self/fd/1 common
+ CustomLog /proc/1/fd/1 common
#
# If you prefer a logfile with access, agent, and referer information
@@ -595,3 +596,15 @@ SSLRandomSeed connect builtin
Require valid-user
+
+
+
+
+ AuthType GSSAPI
+ AuthName "GSSAPI Single Sign On Login"
+ GssapiCredStore keytab:/keytabs/HTTP.keytab
+ GssapiAcceptorName HTTP
+ Require valid-user
+
+
+
diff --git a/httpclient5-testing/src/test/resources/docker/kdc/krb5.conf b/httpclient5-testing/src/test/resources/docker/kdc/krb5.conf
new file mode 100644
index 0000000000..a9ce6acc50
--- /dev/null
+++ b/httpclient5-testing/src/test/resources/docker/kdc/krb5.conf
@@ -0,0 +1,35 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ==========================================================================
+#
+[libdefaults]
+ default_realm = EXAMPLE.ORG
+ forwardable = true
+ udp_preference_limit = 1
+
+
+[realms]
+ EXAMPLE.ORG = {
+ kdc = test-kdc
+ }
+
+[domain_realm]
+ .example.org = EXAMPLE.ORG
+ example.org = EXAMPLE.ORG
+
+[logging]
+ kdc = FILE:/var/log/kerberos/krb5kdc.log
+ admin_server = FILE:/var/log/kerberos/kadmin.log
+ default = FILE:/var/log/kerberos/krb5lib.log
diff --git a/httpclient5-testing/src/test/resources/docker/kdc/start.sh b/httpclient5-testing/src/test/resources/docker/kdc/start.sh
new file mode 100644
index 0000000000..3156d67ff8
--- /dev/null
+++ b/httpclient5-testing/src/test/resources/docker/kdc/start.sh
@@ -0,0 +1,36 @@
+#!/bin/sh
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ==========================================================================
+#
+# The default image has no init
+kdb5_util -P unsafe create -s
+echo Kerberos DB created
+krb5kdc
+echo KDC started
+useradd testclient
+echo testclient:testclient | chpasswd
+kadmin.local addprinc -pw HTTP HTTP/localhost@EXAMPLE.ORG
+kadmin.local addprinc -pw testclient testclient@EXAMPLE.ORG
+kadmin.local addprinc -pw testpwclient testpwclient@EXAMPLE.ORG
+rm -f /keytabs/testclient.keytab
+rm -f /keytabs/HTTP.keytab
+kadmin.local ktadd -k /keytabs/testclient.keytab testclient@EXAMPLE.ORG
+kadmin.local ktadd -k /keytabs/HTTP.keytab HTTP/localhost@EXAMPLE.ORG
+chmod 666 /keytabs/testclient.keytab
+chmod 666 /keytabs/HTTP.keytab
+echo keytabs written
+sleep 3600
+
diff --git a/httpclient5-testing/src/test/resources/docker/squid/squid.conf b/httpclient5-testing/src/test/resources/docker/squid/squid.conf
index 0f476df6c3..f1fdb56b67 100644
--- a/httpclient5-testing/src/test/resources/docker/squid/squid.conf
+++ b/httpclient5-testing/src/test/resources/docker/squid/squid.conf
@@ -17,12 +17,18 @@
http_port 8888
http_port 8889
+debug_options ALL,1
+
coredump_dir /var/spool/squid
auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/htpasswd
auth_param basic children 5
auth_param basic realm test-proxy
+auth_param negotiate program /usr/lib/squid/negotiate_kerberos_auth -k /keytabs/HTTP.keytab -s HTTP/localhost -d -i
+auth_param negotiate children 5
+auth_param negotiate keep_alive on
+
acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN)
acl localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN)
acl localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN)
@@ -71,4 +77,4 @@ refresh_pattern \/(Packages|Sources)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims
refresh_pattern \/Release(|\.gpg)$ 0 0% 0 refresh-ims
refresh_pattern \/InRelease$ 0 0% 0 refresh-ims
refresh_pattern \/(Translation-.*)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims
-refresh_pattern . 0 20% 4320
\ No newline at end of file
+refresh_pattern . 0 20% 4320
diff --git a/httpclient5-testing/src/test/resources/log4j2-debug.xml.template b/httpclient5-testing/src/test/resources/log4j2-debug.xml.template
index 3386294619..8d540913a7 100644
--- a/httpclient5-testing/src/test/resources/log4j2-debug.xml.template
+++ b/httpclient5-testing/src/test/resources/log4j2-debug.xml.template
@@ -25,6 +25,8 @@
+
+
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosConfig.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosConfig.java
index 508eeb9b0e..6b405bbe5d 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosConfig.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosConfig.java
@@ -35,9 +35,10 @@
*
* @since 4.6
*
- * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
- * supported. Consider using Basic or Bearer authentication with TLS instead.
- *
+ * @deprecated Do not use. The old GGS based experimental authentication schemes are no longer
+ * supported.
+ * @see org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme
+ * @see org.apache.hc.client5.http.auth.gss.GssConfig
*/
@Deprecated
@Contract(threading = ThreadingBehavior.IMMUTABLE)
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosCredentials.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosCredentials.java
index 92bab8d4f3..beb5fe7ed1 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosCredentials.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosCredentials.java
@@ -26,9 +26,7 @@
*/
package org.apache.hc.client5.http.auth;
-import java.io.Serializable;
-import java.security.Principal;
-
+import org.apache.hc.client5.http.auth.gss.GssCredentials;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.ietf.jgss.GSSCredential;
@@ -38,42 +36,23 @@
*
* @since 4.4
*
- * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
- * supported. Consider using Basic or Bearer authentication with TLS instead.
+ * The original KerberosCredentials class has been renamed to
+ * org.apache.hc.client5.http.auth.gss.GssCredentials.
*
- * @see UsernamePasswordCredentials
- * @see BearerToken
+ * @deprecated Do not use. The old GGS based experimental authentication schemes are no longer
+ * supported.
+ * Use org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme, or consider using Basic or Bearer
+ * authentication with TLS instead.
+ * @see org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme
+ * @see org.apache.hc.client5.http.auth.gss.GssConfig
+ * @see org.apache.hc.client5.http.auth.gss.GssCredentials
*/
@Deprecated
@Contract(threading = ThreadingBehavior.IMMUTABLE)
-public class KerberosCredentials implements Credentials, Serializable {
-
- private static final long serialVersionUID = 487421613855550713L;
-
- /** GSSCredential */
- private final GSSCredential gssCredential;
+public class KerberosCredentials extends GssCredentials {
- /**
- * Constructor with GSSCredential argument
- *
- * @param gssCredential
- */
public KerberosCredentials(final GSSCredential gssCredential) {
- this.gssCredential = gssCredential;
- }
-
- public GSSCredential getGSSCredential() {
- return gssCredential;
- }
-
- @Override
- public Principal getUserPrincipal() {
- return null;
- }
-
- @Override
- public char[] getPassword() {
- return null;
+ super(gssCredential);
}
}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/StandardAuthScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/StandardAuthScheme.java
index 4d7d34b7ea..7ccb6e9c88 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/StandardAuthScheme.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/StandardAuthScheme.java
@@ -67,16 +67,15 @@ private StandardAuthScheme() {
/**
* SPNEGO authentication scheme as defined in RFC 4559 and RFC 4178.
*
- * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
- * supported. Consider using Basic or Bearer authentication with TLS instead.
+ * Use {@link org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme} instead of the old
+ * deprecated {@link org.apache.hc.client5.http.impl.auth.SPNegoScheme}
*/
- @Deprecated
public static final String SPNEGO = "Negotiate";
/**
* Kerberos authentication scheme as defined in RFC 4120.
*
- * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
+ * @deprecated Do not use. The old GGS based experimental authentication schemes are no longer
* supported. Consider using Basic or Bearer authentication with TLS instead.
*/
@Deprecated
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java
new file mode 100644
index 0000000000..614589de59
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java
@@ -0,0 +1,204 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+
+package org.apache.hc.client5.http.auth.gss;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+
+/**
+ * Immutable class encapsulating GSS configuration options for the new mutual auth capable
+ * {@link SpnegoScheme}.
+ *
+ * Unlike the deprecated {@link KerberosConfig}, this class uses explicit defaults, and
+ * primitive booleans.
+ *
+ * Compared to {@link KerberosConfig} stripPort has been changed to addPort, and the default is now
+ * false (same effect). The default for useCanonicalHostname has been changed to false from true.
+ *
+ * @since 5.6
+ *
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public class GssConfig implements Cloneable {
+
+
+ public static final GssConfig DEFAULT = new Builder().build();
+ public static final GssConfig LEGACY =
+ new Builder().setIgnoreIncompleteSecurityContext(true).setRequireMutualAuth(false).build();
+
+ private final boolean addPort;
+ private final boolean useCanonicalHostname;
+ private final boolean requestMutualAuth;
+ private final boolean requireMutualAuth;
+ private final boolean requestDelegCreds;
+ private final boolean ignoreIncompleteSecurityContext;
+
+ /**
+ * Intended for CDI compatibility
+ */
+ protected GssConfig() {
+ this(false, false, true, true, false, false);
+ }
+
+ GssConfig(
+ final boolean addPort,
+ final boolean useCanonicalHostname,
+ final boolean requestMutualAuth,
+ final boolean requireMutualAuth,
+ final boolean requestDelegCreds,
+ final boolean ignoreIncompleteSecurityContext) {
+ super();
+ this.addPort = addPort;
+ this.useCanonicalHostname = useCanonicalHostname;
+ this.requestMutualAuth = requestMutualAuth;
+ this.requireMutualAuth = requireMutualAuth;
+ this.requestDelegCreds = requestDelegCreds;
+ this.ignoreIncompleteSecurityContext = ignoreIncompleteSecurityContext;
+ }
+
+ public boolean isAddPort() {
+ return addPort;
+ }
+
+ public boolean isUseCanonicalHostname() {
+ return useCanonicalHostname;
+ }
+
+ public boolean isRequestDelegCreds() {
+ return requestDelegCreds;
+ }
+
+ public boolean isRequestMutualAuth() {
+ return requestMutualAuth;
+ }
+
+ public boolean isRequireMutualAuth() {
+ return requireMutualAuth;
+ }
+
+ public boolean isIgnoreIncompleteSecurityContext() {
+ return ignoreIncompleteSecurityContext;
+ }
+
+ @Override
+ protected GssConfig clone() throws CloneNotSupportedException {
+ return (GssConfig) super.clone();
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("[");
+ builder.append("addPort=").append(addPort);
+ builder.append(", useCanonicalHostname=").append(useCanonicalHostname);
+ builder.append(", requestDelegCreds=").append(requestDelegCreds);
+ builder.append(", requestMutualAuth=").append(requestMutualAuth);
+ builder.append(", requireMutualAuth=").append(requireMutualAuth);
+ builder.append(", ignoreIncompleteSecurityContext=").append(ignoreIncompleteSecurityContext);
+ builder.append("]");
+ return builder.toString();
+ }
+
+ public static GssConfig.Builder custom() {
+ return new Builder();
+ }
+
+ public static GssConfig.Builder copy(final GssConfig config) {
+ return new Builder()
+ .setAddPort(config.isAddPort())
+ .setUseCanonicalHostname(config.isUseCanonicalHostname())
+ .setRequestDelegCreds(config.isRequestDelegCreds())
+ .setRequireMutualAuth(config.isRequireMutualAuth())
+ .setRequestMutualAuth(config.isRequestMutualAuth())
+ .setIgnoreIncompleteSecurityContext(config.isIgnoreIncompleteSecurityContext());
+ }
+
+ public static class Builder {
+
+ private boolean addPort = false;
+ private boolean useCanonicalHostname = false;
+ private boolean requestMutualAuth = true;
+ private boolean requireMutualAuth = true;
+ private boolean requestDelegCreds = false;
+ private boolean ignoreIncompleteSecurityContext = false;
+
+
+ Builder() {
+ super();
+ }
+
+ public Builder setAddPort(final boolean addPort) {
+ this.addPort = addPort;
+ return this;
+ }
+
+ public Builder setUseCanonicalHostname(final boolean useCanonicalHostname) {
+ this.useCanonicalHostname = useCanonicalHostname;
+ return this;
+ }
+
+ public Builder setRequestMutualAuth(final boolean requestMutualAuth) {
+ this.requestMutualAuth = requestMutualAuth;
+ return this;
+ }
+
+ public Builder setRequireMutualAuth(final boolean requireMutualAuth) {
+ this.requireMutualAuth = requireMutualAuth;
+ return this;
+ }
+
+ public Builder setRequestDelegCreds(final boolean requuestDelegCreds) {
+ this.requestDelegCreds = requuestDelegCreds;
+ return this;
+ }
+
+ public Builder setIgnoreIncompleteSecurityContext(final boolean ignoreIncompleteSecurityContext) {
+ this.ignoreIncompleteSecurityContext = ignoreIncompleteSecurityContext;
+ return this;
+ }
+
+ public GssConfig build() {
+ if (requireMutualAuth && ignoreIncompleteSecurityContext) {
+ throw new IllegalArgumentException("If requireMutualAuth is set then ignoreIncompleteSecurityContext must not be set");
+ }
+ if (requireMutualAuth && !requestMutualAuth) {
+ throw new IllegalArgumentException("If requireMutualAuth is set then requestMutualAuth must also be set");
+ }
+ return new GssConfig(
+ addPort,
+ useCanonicalHostname,
+ requestMutualAuth,
+ requireMutualAuth,
+ requestDelegCreds,
+ ignoreIncompleteSecurityContext
+ );
+ }
+
+ }
+
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssCredentials.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssCredentials.java
new file mode 100644
index 0000000000..90b5752a43
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssCredentials.java
@@ -0,0 +1,75 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.auth.gss;
+
+import java.io.Serializable;
+import java.security.Principal;
+
+import org.apache.hc.client5.http.auth.Credentials;
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.ietf.jgss.GSSCredential;
+
+/**
+ * Kerberos specific {@link Credentials} representation based on {@link GSSCredential}.
+ *
+ * @since 5.6
+ *
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public class GssCredentials implements Credentials, Serializable {
+
+ private static final long serialVersionUID = 487421613855550713L;
+
+ /** GSSCredential */
+ private final GSSCredential gssCredential;
+
+ /**
+ * Constructor with GSSCredential argument
+ *
+ * @param gssCredential
+ */
+ public GssCredentials(final GSSCredential gssCredential) {
+ this.gssCredential = gssCredential;
+ }
+
+ public GSSCredential getGSSCredential() {
+ return gssCredential;
+ }
+
+ @Override
+ public Principal getUserPrincipal() {
+ // TODO obtain from gssCredential
+ return null;
+ }
+
+ @Override
+ public char[] getPassword() {
+ return null;
+ }
+
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/GGSSchemeBase.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/GGSSchemeBase.java
index 773746b612..700a961ffa 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/GGSSchemeBase.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/GGSSchemeBase.java
@@ -61,7 +61,11 @@
* @since 4.2
*
* @deprecated Do not use. The GGS based experimental authentication schemes are no longer
- * supported. Consider using Basic or Bearer authentication with TLS instead.
+ * supported. Use org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme, or consider using Basic or
+ * Bearer authentication with TLS instead.
+ * @see org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme
+ * @see BasicScheme
+ * @see BearerScheme
*/
@Deprecated
public abstract class GGSSchemeBase implements AuthScheme {
@@ -182,8 +186,8 @@ public boolean isResponseReady(
final Credentials credentials = credentialsProvider.getCredentials(
new AuthScope(host, null, getName()), context);
- if (credentials instanceof org.apache.hc.client5.http.auth.KerberosCredentials) {
- this.gssCredential = ((org.apache.hc.client5.http.auth.KerberosCredentials) credentials).getGSSCredential();
+ if (credentials instanceof org.apache.hc.client5.http.auth.gss.GssCredentials) {
+ this.gssCredential = ((org.apache.hc.client5.http.auth.gss.GssCredentials) credentials).getGSSCredential();
} else {
this.gssCredential = null;
}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java
index 656f29633a..deb8316179 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java
@@ -41,9 +41,11 @@
*
* @since 4.2
*
- * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
- * supported. Consider using Basic or Bearer authentication with TLS instead.
- *
+ * @deprecated Do not use. The old GGS based experimental authentication schemes are no longer
+ * supported. Use org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme, or consider using Basic or
+ * Bearer authentication with TLS
+ * instead.
+ * @see org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme
* @see BasicScheme
* @see BearerScheme
*/
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosSchemeFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosSchemeFactory.java
index 25930f0997..161f12629c 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosSchemeFactory.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosSchemeFactory.java
@@ -45,9 +45,10 @@
*
* @since 4.2
*
- * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
- * supported. Consider using Basic or Bearer authentication with TLS instead.
- *
+ * @deprecated Do not use. The old GGS based experimental authentication schemes are no longer
+ * supported. Use org.apache.hc.client5.http.impl.auth.gss.SpnegoSchemeFactory, or consider using
+ * Basic or Bearer authentication with TLS instead.
+ * @see org.apache.hc.client5.http.impl.auth.gss.SpnegoSchemeFactory
* @see BasicSchemeFactory
* @see BearerSchemeFactory
*/
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java
index 7971ff935d..2516588dc3 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java
@@ -42,9 +42,10 @@
*
* @since 4.2
*
- * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
- * supported. Consider using Basic or Bearer authentication with TLS instead.
- *
+ * @deprecated Do not use. The old GGS based experimental authentication schemes are no longer
+ * supported. Use org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme, or consider using Basic or
+ * Bearer authentication with TLS instead.
+ * @see org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme
* @see BasicScheme
* @see BearerScheme
*/
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoSchemeFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoSchemeFactory.java
index 14d8528c5e..d45d9dfe1a 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoSchemeFactory.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoSchemeFactory.java
@@ -45,9 +45,10 @@
*
* @since 4.2
*
- * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
- * supported. Consider using Basic or Bearer authentication with TLS instead.
- *
+ * @deprecated Do not use. The old GGS based experimental authentication schemes are no longer
+ * supported. Use org.apache.hc.client5.http.impl.auth.gss.SpnegoSchemeFactory, or consider using
+ * Basic or Bearer authentication with TLS instead.
+ * @see org.apache.hc.client5.http.impl.auth.gss.SpnegoSchemeFactory
* @see BasicSchemeFactory
* @see BearerSchemeFactory
*/
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java
new file mode 100644
index 0000000000..91fdfa091e
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java
@@ -0,0 +1,414 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.impl.auth.gss;
+
+import java.net.UnknownHostException;
+import java.security.Principal;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.auth.AuthChallenge;
+import org.apache.hc.client5.http.auth.AuthScheme;
+import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.AuthenticationException;
+import org.apache.hc.client5.http.auth.Credentials;
+import org.apache.hc.client5.http.auth.CredentialsProvider;
+import org.apache.hc.client5.http.auth.InvalidCredentialsException;
+import org.apache.hc.client5.http.auth.StandardAuthScheme;
+import org.apache.hc.client5.http.auth.gss.GssConfig;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.http.utils.Base64;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.util.Args;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Common behaviour for the new mutual authentication capable {@code GSS} based authentication
+ * schemes.
+ *
+ * This class is derived from the old {@link org.apache.hc.client5.http.impl.auth.GGSSchemeBase}
+ * class, which was deprecated in 5.3.
+ *
+ * @since 5.6
+ *
+ * @see GGSSchemeBase
+ */
+public abstract class GssSchemeBase implements AuthScheme {
+
+ enum State {
+ UNINITIATED,
+ TOKEN_READY,
+ TOKEN_SENT,
+ SUCCEEDED,
+ FAILED,
+ }
+
+ private static final Logger LOG = LoggerFactory.getLogger(GssSchemeBase.class);
+ private static final String PEER_SERVICE_NAME = "HTTP";
+
+ // The GSS spec does not specify how long the conversation can be. This should be plenty.
+ // Realistically, we get one initial token, then one maybe one more for mutual authentication.
+ // TODO In the future this might need to be configurable with the upcoming IAKerb support
+ private static final int MAX_GSS_CHALLENGES = 3;
+ private final GssConfig config;
+ private final DnsResolver dnsResolver;
+ private final boolean requireMutualAuth;
+ private final boolean ignoreIncompleteSecurityContext;
+ private int challengesLeft = MAX_GSS_CHALLENGES;
+
+ /** Authentication process state */
+ private State state;
+ private GSSCredential gssCredential;
+ private GSSContext gssContext;
+ private String challenge;
+ private byte[] queuedToken = new byte[0];
+
+ GssSchemeBase(final GssConfig config, final DnsResolver dnsResolver) {
+ super();
+ this.config = config != null ? config : GssConfig.DEFAULT;
+ this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
+ this.requireMutualAuth = config.isRequireMutualAuth();
+ this.ignoreIncompleteSecurityContext = config.isIgnoreIncompleteSecurityContext();
+ this.state = State.UNINITIATED;
+ }
+
+ private void dispose() {
+ // remove sensitive information from memory
+ // cleaning up the credential is the caller's job
+ try {
+ if (gssContext != null) {
+ gssContext.dispose();
+ }
+ } catch (final Exception e) {
+ if (LOG.isWarnEnabled()) {
+ LOG.warn("Exception caught while calling gssContext.dispose()", e);
+ }
+ }
+ }
+
+ GssSchemeBase(final GssConfig config) {
+ this(config, SystemDefaultDnsResolver.INSTANCE);
+ }
+
+ GssSchemeBase() {
+ this(GssConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE);
+ }
+
+ @Override
+ public String getRealm() {
+ return null;
+ }
+
+ // Required by AuthScheme for backwards compatibility
+ @Override
+ public void processChallenge(final AuthChallenge authChallenge,
+ final HttpContext context ) {
+ // If this gets called, then AuthScheme was changed in an incompatible way
+ throw new UnsupportedOperationException();
+ }
+
+ // The AuthScheme API maps awkwardly to GSSAPI, where proccessChallange and generateAuthResponse
+ // map to the same single method call. Hence the generated token is only stored in this method.
+ @Override
+ public void processChallenge(
+ final HttpHost host,
+ final boolean challenged,
+ final AuthChallenge authChallenge,
+ final HttpContext context
+ ) throws AuthenticationException {
+
+ if (challengesLeft-- <= 0 ) {
+ if (LOG.isWarnEnabled()) {
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ LOG.warn("{} GSS error: too many challenges received. Infinite loop ?", exchangeId);
+ }
+ state = State.FAILED;
+ return;
+ }
+
+ final byte[] challengeToken = (authChallenge == null) ? null : Base64.decodeBase64(authChallenge.getValue());
+
+ final String gssHostname;
+ String hostname = host.getHostName();
+ if (config.isUseCanonicalHostname()) {
+ try {
+ hostname = dnsResolver.resolveCanonicalHostname(host.getHostName());
+ } catch (final UnknownHostException ignore) {
+ if (LOG.isWarnEnabled()) {
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ LOG.warn("{} Could not canonicalize hostname {}, using as is.", exchangeId, host.getHostName());
+ }
+ }
+ }
+ if (config.isAddPort()) {
+ gssHostname = hostname + ":" + host.getPort();
+ } else {
+ gssHostname = hostname;
+ }
+
+ if (LOG.isDebugEnabled()) {
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ LOG.debug("{} GSS init {}", exchangeId, gssHostname);
+ }
+ try {
+ switch (state) {
+ case UNINITIATED:
+ setGssCredential(HttpClientContext.cast(context).getCredentialsProvider(), host, context);
+ if (challengeToken == null) {
+ queuedToken = generateToken(challengeToken, PEER_SERVICE_NAME, gssHostname);
+ state = State.TOKEN_READY;
+ } else {
+ if (LOG.isDebugEnabled()) {
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ LOG.debug("{} Internal GSS error: token received when none was sent yet: {}", exchangeId, challengeToken);
+ }
+ state = State.FAILED;
+ }
+ break;
+ case TOKEN_SENT:
+ if (challengeToken == null) {
+ if (!challenged && ignoreIncompleteSecurityContext) {
+ // Got a Non 401/407 code without a challenge. Old non RFC compliant server.
+ if (LOG.isWarnEnabled()) {
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ LOG.warn("{} GSS Context is not established, but continuing because GssConfig.ignoreIncompleteSecurityContext is true.", exchangeId);
+ }
+ state = State.SUCCEEDED;
+ break;
+ } else {
+ if (LOG.isDebugEnabled()) {
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ LOG.debug("{} Did not receive required challenge.",
+ exchangeId);
+ }
+ state = State.FAILED;
+ throw new AuthenticationException(
+ "Did not receive required challenge.");
+ }
+ }
+ queuedToken = generateToken(challengeToken, PEER_SERVICE_NAME, gssHostname);
+ if (challenged) {
+ state = State.TOKEN_READY;
+ } else if (!gssContext.isEstablished()) {
+ if (LOG.isDebugEnabled()) {
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ LOG.debug("{} GSSContext is not established.", exchangeId);
+ }
+ state = State.FAILED;
+ // TODO should we have specific exception(s) for these ?
+ throw new AuthenticationException(
+ "GSSContext is not established.");
+ } else if (!gssContext.getMutualAuthState()) {
+ if (requireMutualAuth) {
+ if (LOG.isDebugEnabled()) {
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ LOG.debug("{} requireMutualAuth is true but GSSContext mutualAuthState is false",
+ exchangeId);
+ }
+ state = State.FAILED;
+ throw new AuthenticationException(
+ "requireMutualAuth is true but GSSContext mutualAuthState is false");
+ } else {
+ if (LOG.isDebugEnabled()) {
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ LOG.debug("{} GSSContext MutualAuthState is false, but continuing because GssConfig.requireMutualAuth is false.",
+ exchangeId);
+ }
+ state = State.SUCCEEDED;
+ }
+ } else {
+ state = State.SUCCEEDED;
+ }
+ break;
+ default:
+ final State prevState = state;
+ state = State.FAILED;
+ throw new IllegalStateException("Illegal state: " + prevState);
+ }
+ } catch (final GSSException gsse) {
+ state = State.FAILED;
+ if (gsse.getMajor() == GSSException.DEFECTIVE_CREDENTIAL
+ || gsse.getMajor() == GSSException.CREDENTIALS_EXPIRED) {
+ throw new InvalidCredentialsException(gsse.getMessage(), gsse);
+ }
+ if (gsse.getMajor() == GSSException.NO_CRED) {
+ throw new InvalidCredentialsException(gsse.getMessage(), gsse);
+ }
+ if (gsse.getMajor() == GSSException.DEFECTIVE_TOKEN
+ || gsse.getMajor() == GSSException.DUPLICATE_TOKEN
+ || gsse.getMajor() == GSSException.OLD_TOKEN) {
+ throw new AuthenticationException(gsse.getMessage(), gsse);
+ }
+ // other error
+ throw new AuthenticationException(gsse.getMessage(), gsse);
+ } finally {
+ if ((state == State.FAILED || state == State.SUCCEEDED) && gssContext != null) {
+ dispose();
+ }
+ }
+ }
+
+ protected GSSManager getManager() {
+ return GSSManager.getInstance();
+ }
+
+ /**
+ * @since 4.4
+ */
+ protected byte[] generateGSSToken(
+ final byte[] input, final Oid oid, final String gssServiceName, final String gssHostname) throws GSSException {
+ final GSSManager manager = getManager();
+ final GSSName peerName = manager.createName(gssServiceName + "@" + gssHostname, GSSName.NT_HOSTBASED_SERVICE);
+
+ if (gssContext == null) {
+ gssContext = createGSSContext(manager, oid, peerName, gssCredential);
+ }
+ if (input != null) {
+ return gssContext.initSecContext(input, 0, input.length);
+ }
+ return gssContext.initSecContext(new byte[] {}, 0, 0);
+ }
+
+ /**
+ * @since 5.0
+ */
+ protected GSSContext createGSSContext(
+ final GSSManager manager,
+ final Oid oid,
+ final GSSName peerName,
+ final GSSCredential gssCredential) throws GSSException {
+ final GSSContext gssContext = manager.createContext(peerName.canonicalize(oid), oid, gssCredential,
+ GSSContext.DEFAULT_LIFETIME);
+ gssContext.requestMutualAuth(config.isRequestMutualAuth());
+ gssContext.requestCredDeleg(config.isRequestDelegCreds());
+ return gssContext;
+ }
+
+ /**
+ * @since 4.4
+ */
+ protected abstract byte[] generateToken(byte[] input, String gssServiceName, String gssHostname) throws GSSException;
+
+ @Override
+ public boolean isChallengeComplete() {
+ // For the mutual authentication response, this is should technically return true.
+ // However, the HttpAuthenticator immediately fails the authentication
+ // process if we return true, so we only return true here if the authentication has failed.
+ return this.state == State.FAILED;
+ }
+
+ @Override
+ public boolean isChallengeExpected() {
+ return state == State.TOKEN_SENT;
+ }
+
+ @Override
+ public boolean isResponseReady(
+ final HttpHost host,
+ final CredentialsProvider credentialsProvider,
+ final HttpContext context) throws AuthenticationException {
+
+ Args.notNull(host, "Auth host");
+ Args.notNull(credentialsProvider, "CredentialsProvider");
+
+ return true;
+ }
+
+ protected void setGssCredential(final CredentialsProvider credentialsProvider,
+ final HttpHost host,
+ final HttpContext context) {
+ final Credentials credentials =
+ credentialsProvider.getCredentials(new AuthScope(host, null, getName()), context);
+ if (credentials instanceof org.apache.hc.client5.http.auth.gss.GssCredentials) {
+ this.gssCredential =
+ ((org.apache.hc.client5.http.auth.gss.GssCredentials) credentials)
+ .getGSSCredential();
+ } else {
+ this.gssCredential = null;
+ }
+ }
+
+ @Override
+ public Principal getPrincipal() {
+ return null;
+ }
+
+ // Format the queued token and update the state.
+ // All token processing is done in processChallenge()
+ @Override
+ public String generateAuthResponse(
+ final HttpHost host,
+ final HttpRequest request,
+ final HttpContext context) throws AuthenticationException {
+ Args.notNull(host, "HTTP host");
+ Args.notNull(request, "HTTP request");
+ switch (state) {
+ case UNINITIATED:
+ throw new AuthenticationException(getName() + " authentication has not been initiated");
+ case FAILED:
+ throw new AuthenticationException(getName() + " authentication has failed");
+ case SUCCEEDED:
+ return null;
+ case TOKEN_READY:
+ state = State.TOKEN_SENT;
+ final Base64 codec = new Base64(0);
+ final String tokenstr = new String(codec.encode(queuedToken));
+ if (LOG.isDebugEnabled()) {
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ LOG.debug("{} Sending GSS response '{}' back to the auth server", exchangeId, tokenstr);
+ }
+ return StandardAuthScheme.SPNEGO + " " + tokenstr;
+ default:
+ throw new IllegalStateException("Illegal state: " + state);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getName() + "{" + this.state + " " + challenge + '}';
+ }
+
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoScheme.java
new file mode 100644
index 0000000000..c552c8ab8a
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoScheme.java
@@ -0,0 +1,121 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.impl.auth.gss;
+
+import org.apache.hc.client5.http.AuthenticationStrategy;
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.auth.StandardAuthScheme;
+import org.apache.hc.client5.http.impl.auth.SPNegoScheme;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.Oid;
+
+/**
+ * SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) authentication
+ * scheme.
+ *
+ * This is the new mutual authentication capable Scheme which replaces the old deprecated
+ * {@link SPNegoScheme}
+ *
+ *
+ *
+ * Note that this scheme is not enabled by default. To use it, you need create a custom
+ * {@link AuthenticationStrategy} and a custom
+ * {@link org.apache.hc.client5.http.auth.AuthSchemeFactory}
+ * {@link org.apache.hc.core5.http.config.Registry},
+ * and set them on the HttpClientBuilder.
+ *
+ *
+ *
+ * {@code
+ * private static class SpnegoAuthenticationStrategy extends DefaultAuthenticationStrategy {
+ * private static final List SPNEGO_SCHEME_PRIORITY =
+ * Collections.unmodifiableList(
+ * Arrays.asList(StandardAuthScheme.SPNEGO
+ * // Add other Schemes as needed
+ * );
+ *
+ * protected final List getSchemePriority() {
+ * return SPNEGO_SCHEME_PRIORITY;
+ * }
+ * }
+ *
+ * AuthenticationStrategy spnegoStrategy = new SpnegoAuthenticationStrategy();
+ *
+ * AuthSchemeFactory spnegoFactory = new SpnegoSchemeFactory();
+ * Registry mutualSchemeRegistry = RegistryBuilder.create()
+ * .register(StandardAuthScheme.SPNEGO, spnegoFactory)
+ * //register other schemes as needed
+ * .build();
+ *
+ * CloseableHttpClient mutualClient = HttpClientBuilder.create()
+ * .setTargetAuthenticationStrategy(spnegoStrategy);
+ * .setDefaultAuthSchemeRegistry(spnegoSchemeRegistry);
+ * .build();
+ * }
+ *
+ *
+ * @since 5.6
+ */
+public class SpnegoScheme extends GssSchemeBase {
+
+ private static final String SPNEGO_OID_STRING = "1.3.6.1.5.5.2";
+ private static final Oid SPNEGO_OID;
+ static {
+ try {
+ SPNEGO_OID = new Oid(SPNEGO_OID_STRING);
+ } catch (final GSSException e) {
+ throw new IllegalStateException("Failed to create OID for SPNEGO mechanism", e);
+ }
+ }
+
+ /**
+ * @since 5.0
+ */
+ public SpnegoScheme(final org.apache.hc.client5.http.auth.gss.GssConfig config, final DnsResolver dnsResolver) {
+ super(config, dnsResolver);
+ }
+
+ public SpnegoScheme() {
+ super();
+ }
+
+ @Override
+ public String getName() {
+ return StandardAuthScheme.SPNEGO;
+ }
+
+ @Override
+ protected byte[] generateToken(final byte[] input, final String gssServiceName, final String gssHostname) throws GSSException {
+ return generateGSSToken(input, SPNEGO_OID, gssServiceName, gssHostname);
+ }
+
+ @Override
+ public boolean isConnectionBased() {
+ return false;
+ }
+
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoSchemeFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoSchemeFactory.java
new file mode 100644
index 0000000000..b36ac93229
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoSchemeFactory.java
@@ -0,0 +1,79 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.impl.auth.gss;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.auth.AuthScheme;
+import org.apache.hc.client5.http.auth.AuthSchemeFactory;
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.Experimental;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.http.protocol.HttpContext;
+
+/**
+ * {@link AuthSchemeFactory} implementation that creates and initialises
+ * {@link SpnegoScheme} instances.
+ *
+ * This replaces the old deprecated {@link org.apache.hc.client5.http.impl.auth.SPNegoSchemeFactory}
+ *
+ *
+ * @since 5.6
+ *
+ * @see SPNegoSchemeFactory
+ */
+@Contract(threading = ThreadingBehavior.STATELESS)
+@Experimental
+public class SpnegoSchemeFactory implements AuthSchemeFactory {
+
+ /**
+ * Singleton instance for the default configuration.
+ */
+ public static final SpnegoSchemeFactory DEFAULT = new SpnegoSchemeFactory(org.apache.hc.client5.http.auth.gss.GssConfig.DEFAULT,
+ SystemDefaultDnsResolver.INSTANCE);
+
+ public static final SpnegoSchemeFactory LEGACY = new SpnegoSchemeFactory(org.apache.hc.client5.http.auth.gss.GssConfig.LEGACY,
+ SystemDefaultDnsResolver.INSTANCE);
+
+ private final org.apache.hc.client5.http.auth.gss.GssConfig config;
+ private final DnsResolver dnsResolver;
+
+ /**
+ * @since 5.6
+ */
+ public SpnegoSchemeFactory(final org.apache.hc.client5.http.auth.gss.GssConfig config, final DnsResolver dnsResolver) {
+ super();
+ this.config = config;
+ this.dnsResolver = dnsResolver;
+ }
+
+ @Override
+ public AuthScheme create(final HttpContext context) {
+ return new SpnegoScheme(this.config, this.dnsResolver);
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index cc21e55cc1..d1ac126717 100644
--- a/pom.xml
+++ b/pom.xml
@@ -77,7 +77,7 @@
1.21.3
2.10.1
5.3
- javax.net.ssl.SSLEngine,javax.net.ssl.SSLParameters,java.nio.ByteBuffer,java.nio.CharBuffer,jdk.net.ExtendedSocketOptions,jdk.net.Sockets
+ javax.net.ssl.SSLEngine,javax.net.ssl.SSLParameters,java.nio.ByteBuffer,java.nio.CharBuffer,jdk.net.ExtendedSocketOptions,jdk.net.Sockets,java.lang.invoke.MethodHandle
1.27.1
1.5.7-4
1.15.2
@@ -336,6 +336,8 @@
com.github.siom79.japicmp
japicmp-maven-plugin
+
+ true
${project.groupId}
@@ -503,6 +505,8 @@
+
+ true
${project.groupId}