Skip to content

Commit be6688d

Browse files
committed
Add containerized apache httpd + mod_auth_gssapi tests
Fix GSSCredential handling
1 parent 47a656b commit be6688d

File tree

18 files changed

+905
-21
lines changed

18 files changed

+905
-21
lines changed

httpclient5-testing/pom.xml

+16
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
<dependency>
5353
<groupId>org.apache.httpcomponents.client5</groupId>
5454
<artifactId>httpclient5</artifactId>
55+
<version>5.5-alpha1-SNAPSHOT</version>
5556
</dependency>
5657
<dependency>
5758
<groupId>org.slf4j</groupId>
@@ -107,6 +108,21 @@
107108
<artifactId>junit-jupiter</artifactId>
108109
<scope>test</scope>
109110
</dependency>
111+
<dependency>
112+
<groupId>org.apache.kerby</groupId>
113+
<artifactId>kerb-core</artifactId>
114+
<scope>test</scope>
115+
</dependency>
116+
<dependency>
117+
<groupId>org.apache.kerby</groupId>
118+
<artifactId>kerb-client</artifactId>
119+
<scope>test</scope>
120+
</dependency>
121+
<dependency>
122+
<groupId>org.apache.kerby</groupId>
123+
<artifactId>kerb-simplekdc</artifactId>
124+
<scope>test</scope>
125+
</dependency>
110126
</dependencies>
111127

112128
<profiles>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.client5.testing.util;
28+
29+
import java.lang.invoke.MethodHandle;
30+
import java.lang.invoke.MethodHandles;
31+
import java.lang.invoke.MethodType;
32+
import java.security.PrivilegedAction;
33+
import java.security.PrivilegedActionException;
34+
import java.security.PrivilegedExceptionAction;
35+
import java.util.concurrent.Callable;
36+
import java.util.concurrent.CompletionException;
37+
import javax.security.auth.Subject;
38+
39+
import org.apache.hc.core5.annotation.Internal;
40+
41+
/**
42+
* This class is based on SecurityUtils in Apache Avatica which is loosely based on SecurityUtils in
43+
* Jetty 12.0
44+
* <p>
45+
* Collections of utility methods to deal with the scheduled removal of the security classes defined
46+
* by <a href="https://openjdk.org/jeps/411">JEP 411</a>.
47+
* </p>
48+
*/
49+
@Internal
50+
public class SecurityUtils {
51+
private static final MethodHandle CALL_AS = lookupCallAs();
52+
private static final MethodHandle CURRENT = lookupCurrent();
53+
private static final MethodHandle DO_PRIVILEGED = lookupDoPrivileged();
54+
55+
private SecurityUtils() {
56+
}
57+
58+
private static MethodHandle lookupCallAs() {
59+
final MethodHandles.Lookup lookup = MethodHandles.lookup();
60+
try {
61+
try {
62+
// Subject.doAs() is deprecated for removal and replaced by Subject.callAs().
63+
// Lookup first the new API, since for Java versions where both exist, the
64+
// new API delegates to the old API (for example Java 18, 19 and 20).
65+
// Otherwise (Java 17), lookup the old API.
66+
return lookup.findStatic(Subject.class, "callAs",
67+
MethodType.methodType(Object.class, Subject.class, Callable.class));
68+
} catch (final NoSuchMethodException x) {
69+
try {
70+
// Lookup the old API.
71+
final MethodType oldSignature =
72+
MethodType.methodType(Object.class, Subject.class,
73+
PrivilegedExceptionAction.class);
74+
final MethodHandle doAs =
75+
lookup.findStatic(Subject.class, "doAs", oldSignature);
76+
// Convert the Callable used in the new API to the PrivilegedAction used in the
77+
// old
78+
// API.
79+
final MethodType convertSignature =
80+
MethodType.methodType(PrivilegedExceptionAction.class, Callable.class);
81+
final MethodHandle converter =
82+
lookup.findStatic(SecurityUtils.class,
83+
"callableToPrivilegedExceptionAction", convertSignature);
84+
return MethodHandles.filterArguments(doAs, 1, converter);
85+
} catch (final NoSuchMethodException e) {
86+
throw new AssertionError(e);
87+
}
88+
}
89+
} catch (final IllegalAccessException e) {
90+
throw new AssertionError(e);
91+
}
92+
}
93+
94+
private static MethodHandle lookupDoPrivileged() {
95+
try {
96+
// Use reflection to work with Java versions that have and don't have AccessController.
97+
final Class<?> klass =
98+
ClassLoader.getSystemClassLoader().loadClass("java.security.AccessController");
99+
final MethodHandles.Lookup lookup = MethodHandles.lookup();
100+
return lookup.findStatic(klass, "doPrivileged",
101+
MethodType.methodType(Object.class, PrivilegedAction.class));
102+
} catch (final NoSuchMethodException | IllegalAccessException x) {
103+
// Assume that single methods won't be removed from AcessController
104+
throw new AssertionError(x);
105+
} catch (final ClassNotFoundException e) {
106+
return null;
107+
}
108+
}
109+
110+
private static MethodHandle lookupCurrent() {
111+
final MethodHandles.Lookup lookup = MethodHandles.lookup();
112+
try {
113+
// Subject.getSubject(AccessControlContext) is deprecated for removal and replaced by
114+
// Subject.current().
115+
// Lookup first the new API, since for Java versions where both exists, the
116+
// new API delegates to the old API (for example Java 18, 19 and 20).
117+
// Otherwise (Java 17), lookup the old API.
118+
return lookup.findStatic(Subject.class, "current",
119+
MethodType.methodType(Subject.class));
120+
} catch (final NoSuchMethodException e) {
121+
final MethodHandle getContext = lookupGetContext();
122+
final MethodHandle getSubject = lookupGetSubject();
123+
return MethodHandles.filterReturnValue(getContext, getSubject);
124+
} catch (final IllegalAccessException e) {
125+
throw new AssertionError(e);
126+
}
127+
}
128+
129+
private static MethodHandle lookupGetSubject() {
130+
final MethodHandles.Lookup lookup = MethodHandles.lookup();
131+
try {
132+
final Class<?> contextklass =
133+
ClassLoader.getSystemClassLoader()
134+
.loadClass("java.security.AccessControlContext");
135+
return lookup.findStatic(Subject.class, "getSubject",
136+
MethodType.methodType(Subject.class, contextklass));
137+
} catch (final ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
138+
throw new AssertionError(e);
139+
}
140+
}
141+
142+
private static MethodHandle lookupGetContext() {
143+
try {
144+
// Use reflection to work with Java versions that have and don't have AccessController.
145+
final Class<?> controllerKlass =
146+
ClassLoader.getSystemClassLoader().loadClass("java.security.AccessController");
147+
final Class<?> contextklass =
148+
ClassLoader.getSystemClassLoader()
149+
.loadClass("java.security.AccessControlContext");
150+
151+
final MethodHandles.Lookup lookup = MethodHandles.lookup();
152+
return lookup.findStatic(controllerKlass, "getContext",
153+
MethodType.methodType(contextklass));
154+
} catch (final ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
155+
throw new AssertionError(e);
156+
}
157+
}
158+
159+
/**
160+
* Maps to AccessController#doPrivileged if available, otherwise returns action.run().
161+
* @param action the action to run
162+
* @return the result of running the action
163+
* @param <T> the type of the result
164+
*/
165+
public static <T> T doPrivileged(final PrivilegedAction<T> action) {
166+
// Keep this method short and inlineable.
167+
if (DO_PRIVILEGED == null) {
168+
return action.run();
169+
}
170+
return doPrivileged(DO_PRIVILEGED, action);
171+
}
172+
173+
private static <T> T doPrivileged(final MethodHandle doPrivileged, final PrivilegedAction<T> action) {
174+
try {
175+
return (T) doPrivileged.invoke(action);
176+
} catch (final Throwable t) {
177+
throw sneakyThrow(t);
178+
}
179+
}
180+
181+
/**
182+
* Maps to Subject.callAs() if available, otherwise maps to Subject.doAs()
183+
* @param subject the subject this action runs as
184+
* @param action the action to run
185+
* @return the result of the action
186+
* @param <T> the type of the result
187+
* @throws CompletionException
188+
*/
189+
public static <T> T callAs(final Subject subject, final Callable<T> action) throws CompletionException {
190+
try {
191+
return (T) CALL_AS.invoke(subject, action);
192+
} catch (final PrivilegedActionException e) {
193+
throw new CompletionException(e.getCause());
194+
} catch (final Throwable t) {
195+
throw sneakyThrow(t);
196+
}
197+
}
198+
199+
/**
200+
* Maps to Subject.currect() is available, otherwise maps to Subject.getSubject()
201+
* @return the current subject
202+
*/
203+
public static Subject currentSubject() {
204+
try {
205+
return (Subject) CURRENT.invoke();
206+
} catch (final Throwable t) {
207+
throw sneakyThrow(t);
208+
}
209+
}
210+
211+
@SuppressWarnings("unused")
212+
private static <T> PrivilegedExceptionAction<T>
213+
callableToPrivilegedExceptionAction(final Callable<T> callable) {
214+
return callable::call;
215+
}
216+
217+
@SuppressWarnings("unchecked")
218+
private static <E extends Throwable> RuntimeException sneakyThrow(final Throwable e) throws E {
219+
throw (E) e;
220+
}
221+
}

httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ApacheHTTPDSquidCompatibilityIT.java

+43-7
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,28 @@
2626
*/
2727
package org.apache.hc.client5.testing.compatibility;
2828

29+
import java.io.File;
30+
import java.io.IOException;
31+
import java.nio.file.Files;
32+
import java.nio.file.Path;
33+
import java.util.stream.Stream;
34+
35+
import javax.security.auth.Subject;
36+
2937
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
3038
import org.apache.hc.client5.testing.compatibility.async.CachingHttpAsyncClientCompatibilityTest;
3139
import org.apache.hc.client5.testing.compatibility.async.HttpAsyncClientCompatibilityTest;
3240
import org.apache.hc.client5.testing.compatibility.async.HttpAsyncClientHttp1CompatibilityTest;
3341
import org.apache.hc.client5.testing.compatibility.async.HttpAsyncClientProxyCompatibilityTest;
42+
import org.apache.hc.client5.testing.compatibility.spnego.SpnegoTestUtil;
3443
import org.apache.hc.client5.testing.compatibility.sync.CachingHttpClientCompatibilityTest;
3544
import org.apache.hc.client5.testing.compatibility.sync.HttpClientCompatibilityTest;
3645
import org.apache.hc.client5.testing.compatibility.sync.HttpClientProxyCompatibilityTest;
3746
import org.apache.hc.core5.http.HttpHost;
3847
import org.apache.hc.core5.http.URIScheme;
3948
import org.apache.hc.core5.http2.HttpVersionPolicy;
4049
import org.junit.jupiter.api.AfterAll;
50+
import org.junit.jupiter.api.BeforeAll;
4151
import org.junit.jupiter.api.DisplayName;
4252
import org.junit.jupiter.api.Nested;
4353
import org.testcontainers.containers.GenericContainer;
@@ -49,11 +59,24 @@
4959
class ApacheHTTPDSquidCompatibilityIT {
5060

5161
private static Network NETWORK = Network.newNetwork();
62+
private static final Path KEYTAB_DIR = SpnegoTestUtil.createKeytabDir();
63+
64+
@Container
65+
static final GenericContainer<?> KDC = ContainerImages.KDC(NETWORK, KEYTAB_DIR);
5266
@Container
53-
static final GenericContainer<?> HTTPD_CONTAINER = ContainerImages.apacheHttpD(NETWORK);
67+
static final GenericContainer<?> HTTPD_CONTAINER = ContainerImages.apacheHttpD(NETWORK, KEYTAB_DIR);
5468
@Container
5569
static final GenericContainer<?> SQUID = ContainerImages.squid(NETWORK);
5670

71+
private static Path KRB5_CONF_PATH;
72+
private static Subject spnegoSubject;
73+
74+
@BeforeAll
75+
static void init() throws IOException {
76+
KRB5_CONF_PATH = SpnegoTestUtil.prepareKrb5Conf(KDC.getHost() + ":" + KDC.getMappedPort(ContainerImages.KDC_PORT));
77+
spnegoSubject = SpnegoTestUtil.loginFromKeytab("testclient", KEYTAB_DIR.resolve("testclient.keytab"));
78+
}
79+
5780
static HttpHost targetContainerHost() {
5881
return new HttpHost(URIScheme.HTTP.id, HTTPD_CONTAINER.getHost(), HTTPD_CONTAINER.getMappedPort(ContainerImages.HTTP_PORT));
5982
}
@@ -82,15 +105,28 @@ static HttpHost proxyPwProtectedContainerHost() {
82105
static void cleanup() {
83106
SQUID.close();
84107
HTTPD_CONTAINER.close();
108+
KDC.close();
85109
NETWORK.close();
110+
try {
111+
Files.delete(KRB5_CONF_PATH);
112+
Files.delete(KRB5_CONF_PATH.getParent());
113+
try ( Stream<Path> dirStream = Files.walk(KEYTAB_DIR)) {
114+
dirStream
115+
.filter(Files::isRegularFile)
116+
.map(Path::toFile)
117+
.forEach(File::delete);
118+
}
119+
} catch (final IOException e) {
120+
//We leave some files around in tmp
121+
}
86122
}
87123

88124
@Nested
89125
@DisplayName("Classic client: HTTP/1.1, plain, direct connection")
90126
class ClassicDirectHttp extends HttpClientCompatibilityTest {
91127

92128
public ClassicDirectHttp() throws Exception {
93-
super(targetContainerHost(), null, null);
129+
super(targetContainerHost(), null, null, spnegoSubject);
94130
}
95131

96132
}
@@ -120,7 +156,7 @@ public ClassicViaPwProtectedProxyHttp() throws Exception {
120156
class ClassicDirectHttpTls extends HttpClientCompatibilityTest {
121157

122158
public ClassicDirectHttpTls() throws Exception {
123-
super(targetContainerTlsHost(), null, null);
159+
super(targetContainerTlsHost(), null, null, spnegoSubject);
124160
}
125161

126162
}
@@ -150,7 +186,7 @@ public ClassicViaPwProtectedProxyHttpTls() throws Exception {
150186
class AsyncDirectHttp1 extends HttpAsyncClientHttp1CompatibilityTest {
151187

152188
public AsyncDirectHttp1() throws Exception {
153-
super(targetContainerHost(), null, null);
189+
super(targetContainerHost(), null, null, ApacheHTTPDSquidCompatibilityIT.spnegoSubject);
154190
}
155191

156192
}
@@ -180,7 +216,7 @@ public AsyncViaPwProtectedProxyHttp1() throws Exception {
180216
class AsyncDirectHttp1Tls extends HttpAsyncClientHttp1CompatibilityTest {
181217

182218
public AsyncDirectHttp1Tls() throws Exception {
183-
super(targetContainerTlsHost(), null, null);
219+
super(targetContainerTlsHost(), null, null, ApacheHTTPDSquidCompatibilityIT.spnegoSubject);
184220
}
185221

186222
}
@@ -210,7 +246,7 @@ public AsyncViaPwProtectedProxyHttp1Tls() throws Exception {
210246
class AsyncDirectHttp2 extends HttpAsyncClientCompatibilityTest {
211247

212248
public AsyncDirectHttp2() throws Exception {
213-
super(HttpVersionPolicy.FORCE_HTTP_2, targetContainerHost(), null, null);
249+
super(HttpVersionPolicy.FORCE_HTTP_2, targetContainerHost(), null, null, ApacheHTTPDSquidCompatibilityIT.spnegoSubject);
214250
}
215251

216252
}
@@ -220,7 +256,7 @@ public AsyncDirectHttp2() throws Exception {
220256
class AsyncDirectHttp2Tls extends HttpAsyncClientCompatibilityTest {
221257

222258
public AsyncDirectHttp2Tls() throws Exception {
223-
super(HttpVersionPolicy.FORCE_HTTP_2, targetContainerTlsHost(), null, null);
259+
super(HttpVersionPolicy.FORCE_HTTP_2, targetContainerTlsHost(), null, null, ApacheHTTPDSquidCompatibilityIT.spnegoSubject);
224260
}
225261

226262
}

0 commit comments

Comments
 (0)