Skip to content

Commit

Permalink
Add containerized apache httpd + mod_auth_gssapi tests
Browse files Browse the repository at this point in the history
Fix GSSCredential handling
  • Loading branch information
stoty committed Feb 20, 2025
1 parent 47a656b commit be6688d
Show file tree
Hide file tree
Showing 18 changed files with 905 additions and 21 deletions.
16 changes: 16 additions & 0 deletions httpclient5-testing/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.5-alpha1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
Expand Down Expand Up @@ -107,6 +108,21 @@
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.kerby</groupId>
<artifactId>kerb-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.kerby</groupId>
<artifactId>kerb-client</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.kerby</groupId>
<artifactId>kerb-simplekdc</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<profiles>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
* <http://www.apache.org/>.
*
*/
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
* <p>
* Collections of utility methods to deal with the scheduled removal of the security classes defined
* by <a href="https://openjdk.org/jeps/411">JEP 411</a>.
* </p>
*/
@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 <T> the type of the result
*/
public static <T> T doPrivileged(final PrivilegedAction<T> action) {
// Keep this method short and inlineable.
if (DO_PRIVILEGED == null) {
return action.run();
}
return doPrivileged(DO_PRIVILEGED, action);
}

private static <T> T doPrivileged(final MethodHandle doPrivileged, final PrivilegedAction<T> 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 <T> the type of the result
* @throws CompletionException
*/
public static <T> T callAs(final Subject subject, final Callable<T> 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 <T> PrivilegedExceptionAction<T>
callableToPrivilegedExceptionAction(final Callable<T> callable) {
return callable::call;
}

@SuppressWarnings("unchecked")
private static <E extends Throwable> RuntimeException sneakyThrow(final Throwable e) throws E {
throw (E) e;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,28 @@
*/
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;
import org.apache.hc.core5.http.HttpHost;
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;
Expand All @@ -49,11 +59,24 @@
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);
@Container
static final GenericContainer<?> SQUID = ContainerImages.squid(NETWORK);

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));
}
Expand Down Expand Up @@ -82,15 +105,28 @@ static HttpHost proxyPwProtectedContainerHost() {
static void cleanup() {
SQUID.close();
HTTPD_CONTAINER.close();
KDC.close();
NETWORK.close();
try {
Files.delete(KRB5_CONF_PATH);
Files.delete(KRB5_CONF_PATH.getParent());
try ( Stream<Path> 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 {

public ClassicDirectHttp() throws Exception {
super(targetContainerHost(), null, null);
super(targetContainerHost(), null, null, spnegoSubject);
}

}
Expand Down Expand Up @@ -120,7 +156,7 @@ public ClassicViaPwProtectedProxyHttp() throws Exception {
class ClassicDirectHttpTls extends HttpClientCompatibilityTest {

public ClassicDirectHttpTls() throws Exception {
super(targetContainerTlsHost(), null, null);
super(targetContainerTlsHost(), null, null, spnegoSubject);
}

}
Expand Down Expand Up @@ -150,7 +186,7 @@ public ClassicViaPwProtectedProxyHttpTls() throws Exception {
class AsyncDirectHttp1 extends HttpAsyncClientHttp1CompatibilityTest {

public AsyncDirectHttp1() throws Exception {
super(targetContainerHost(), null, null);
super(targetContainerHost(), null, null, ApacheHTTPDSquidCompatibilityIT.spnegoSubject);
}

}
Expand Down Expand Up @@ -180,7 +216,7 @@ public AsyncViaPwProtectedProxyHttp1() throws Exception {
class AsyncDirectHttp1Tls extends HttpAsyncClientHttp1CompatibilityTest {

public AsyncDirectHttp1Tls() throws Exception {
super(targetContainerTlsHost(), null, null);
super(targetContainerTlsHost(), null, null, ApacheHTTPDSquidCompatibilityIT.spnegoSubject);
}

}
Expand Down Expand Up @@ -210,7 +246,7 @@ public AsyncViaPwProtectedProxyHttp1Tls() throws Exception {
class AsyncDirectHttp2 extends HttpAsyncClientCompatibilityTest {

public AsyncDirectHttp2() throws Exception {
super(HttpVersionPolicy.FORCE_HTTP_2, targetContainerHost(), null, null);
super(HttpVersionPolicy.FORCE_HTTP_2, targetContainerHost(), null, null, ApacheHTTPDSquidCompatibilityIT.spnegoSubject);
}

}
Expand All @@ -220,7 +256,7 @@ public AsyncDirectHttp2() throws Exception {
class AsyncDirectHttp2Tls extends HttpAsyncClientCompatibilityTest {

public AsyncDirectHttp2Tls() throws Exception {
super(HttpVersionPolicy.FORCE_HTTP_2, targetContainerTlsHost(), null, null);
super(HttpVersionPolicy.FORCE_HTTP_2, targetContainerTlsHost(), null, null, ApacheHTTPDSquidCompatibilityIT.spnegoSubject);
}

}
Expand Down
Loading

0 comments on commit be6688d

Please sign in to comment.