File: HttpClientLocalAddrTest.java

package info (click to toggle)
openjdk-21 21.0.8%2B9-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 823,976 kB
  • sloc: java: 5,613,338; xml: 1,643,607; cpp: 1,296,296; ansic: 420,291; asm: 404,850; objc: 20,994; sh: 15,271; javascript: 11,245; python: 6,895; makefile: 2,362; perl: 357; awk: 351; sed: 172; jsp: 24; csh: 3
file content (422 lines) | stat: -rw-r--r-- 17,881 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
/*
 * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

import jdk.test.lib.net.IPSupport;
import jdk.test.lib.net.SimpleSSLContext;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import javax.net.ssl.SSLContext;
import java.io.Closeable;
import java.net.InetAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.function.Supplier;

import jdk.httpclient.test.lib.common.HttpServerAdapters;
import static java.net.http.HttpClient.Version.HTTP_1_1;
import static java.net.http.HttpClient.Version.HTTP_2;

/*
 * @test
 * @summary Tests HttpClient usage when configured with a local address to bind
 *          to, when sending requests
 * @bug 8209137 8316031
 * @library /test/lib /test/jdk/java/net/httpclient/lib
 *
 * @build jdk.test.lib.net.SimpleSSLContext jdk.test.lib.net.IPSupport
 *        jdk.httpclient.test.lib.common.HttpServerAdapters
 *
 * @run testng/othervm
 *      -Djdk.httpclient.HttpClient.log=frames,ssl,requests,responses,errors
 *      -Djdk.internal.httpclient.debug=true
 *      -Dsun.net.httpserver.idleInterval=50000
 *      HttpClientLocalAddrTest
 *
 * @run testng/othervm/java.security.policy=httpclient-localaddr-security.policy
 *      -Djdk.httpclient.HttpClient.log=frames,ssl,requests,responses,errors
 *      -Djdk.internal.httpclient.debug=true
 *      -Dsun.net.httpserver.idleInterval=50000
 *      -Djdk.tracePinnedThreads=full
 *      HttpClientLocalAddrTest
 *
 */
public class HttpClientLocalAddrTest implements HttpServerAdapters {

    private static SSLContext sslContext;
    private static HttpServerAdapters.HttpTestServer http1_1_Server;
    private static URI httpURI;
    private static HttpServerAdapters.HttpTestServer https_1_1_Server;
    private static URI httpsURI;
    private static HttpServerAdapters.HttpTestServer http2_Server;
    private static URI http2URI;
    private static HttpServerAdapters.HttpTestServer https2_Server;
    private static URI https2URI;
    private static final AtomicInteger IDS = new AtomicInteger();

    // start various HTTP/HTTPS servers that will be invoked against in the tests
    @BeforeClass
    public static void beforeClass() throws Exception {
        sslContext = new SimpleSSLContext().get();
        Assert.assertNotNull(sslContext, "Unexpected null sslContext");

        HttpServerAdapters.HttpTestHandler handler = (exchange) -> {
            // the handler receives a request and sends back a 200 response with the
            // response body containing the raw IP address (in byte[] form) of the client from whom
            // the request was received
            var clientAddr = exchange.getRemoteAddress();
            System.out.println("Received a request from client address " + clientAddr);
            var responseContent = clientAddr.getAddress().getAddress();
            exchange.sendResponseHeaders(200, responseContent.length);
            try (var os = exchange.getResponseBody()) {
                // write out the client address as a response
                os.write(responseContent);
            }
            exchange.close();
        };

        // HTTP/1.1 - create servers with http and https
        http1_1_Server = HttpServerAdapters.HttpTestServer.create(HTTP_1_1);
        http1_1_Server.addHandler(handler, "/");
        http1_1_Server.start();
        System.out.println("Started HTTP v1.1 server at " + http1_1_Server.serverAuthority());
        httpURI = new URI("http://" + http1_1_Server.serverAuthority() + "/");

        https_1_1_Server = HttpServerAdapters.HttpTestServer.create(HTTP_1_1, sslContext);
        https_1_1_Server.addHandler(handler, "/");
        https_1_1_Server.start();
        System.out.println("Started HTTPS v1.1 server at " + https_1_1_Server.serverAuthority());
        httpsURI = new URI("https://" + https_1_1_Server.serverAuthority() + "/");

        // HTTP/2 - create servers with http and https
        http2_Server = HttpServerAdapters.HttpTestServer.create(HTTP_2);
        http2_Server.addHandler(handler, "/");
        http2_Server.start();
        System.out.println("Started HTTP v2 server at " + http2_Server.serverAuthority());
        http2URI = new URI("http://" + http2_Server.serverAuthority() + "/");

        https2_Server = HttpServerAdapters.HttpTestServer.create(HTTP_2, sslContext);
        https2_Server.addHandler(handler, "/");
        https2_Server.start();
        System.out.println("Started HTTPS v2 server at " + https2_Server.serverAuthority());
        https2URI = new URI("https://" + https2_Server.serverAuthority() + "/");
    }

    // stop each of the started servers
    @AfterClass
    public static void afterClass() throws Exception {
        // stop each of the server and accumulate any exception
        // that might happen during stop and finally throw
        // the accumulated exception(s)
        var e = safeStop(http1_1_Server, null);
        e = safeStop(https_1_1_Server, e);
        e = safeStop(http2_Server, e);
        e = safeStop(https2_Server, e);
        // throw any exception that happened during stop
        if (e != null) {
            throw e;
        }
    }

    /**
     * Stops the server and returns (instead of throwing) any exception that might
     * have occurred during stop. If {@code prevException} is not null then any
     * exception during stop of the {@code server} will be added as a suppressed
     * exception to the {@code prevException} and the {@code prevException} will be
     * returned.
     */
    private static Exception safeStop(HttpServerAdapters.HttpTestServer server, Exception prevException) {
        if (server == null) {
            return null;
        }
        var serverAuthority = server.serverAuthority();
        try {
            server.stop();
        } catch (Exception e) {
            System.err.println("Failed to stop server " + serverAuthority);
            if (prevException == null) {
                return e;
            }
            prevException.addSuppressed(e);
            return prevException;
        }
        return prevException;
    }

    @DataProvider(name = "params")
    private Object[][] paramsProvider() throws Exception {
        final List<Object[]> testMethodParams = new ArrayList();
        final URI[] requestURIs = new URI[]{httpURI, httpsURI, http2URI, https2URI};
        final Predicate<URI> requiresSSLContext = (uri) -> uri.getScheme().equals("https");
        for (var requestURI : requestURIs) {
            final var configureClientSSL = requiresSSLContext.test(requestURI);
            // no localAddr set
            testMethodParams.add(new Object[]{
                    newBuilder(configureClientSSL).provider(),
                    requestURI,
                    null
            });
            // null localAddr set
            testMethodParams.add(new Object[]{
                    newBuilder(configureClientSSL).localAddress(null).provider(),
                    requestURI,
                    null
            });
            // localAddr set to loopback address
            final var loopbackAddr = InetAddress.getLoopbackAddress();
            testMethodParams.add(new Object[]{
                    newBuilder(configureClientSSL)
                            .localAddress(loopbackAddr)
                            .provider(),
                    requestURI,
                    loopbackAddr
            });
            // anyAddress
            if (IPSupport.hasIPv6()) {
                // ipv6 wildcard
                final var localAddr = InetAddress.getByName("::");
                testMethodParams.add(new Object[]{
                        newBuilder(configureClientSSL)
                                .localAddress(localAddr)
                                .provider(),
                        requestURI,
                        localAddr
                });
            }
            if (IPSupport.hasIPv4()) {
                // ipv4 wildcard
                final var localAddr = InetAddress.getByName("0.0.0.0");
                testMethodParams.add(new Object[]{
                        newBuilder(configureClientSSL)
                                .localAddress(localAddr)
                                .provider(),
                        requestURI,
                        localAddr
                });
            }
        }
        return testMethodParams.stream().toArray(Object[][]::new);
    }

    // An object that holds a client and that can be closed
    // Used when closing the client might require closing additional
    // resources, such as an executor
    sealed interface ClientCloseable extends Closeable {

        HttpClient client();

        @Override
        void close();

        // a reusable client that does nothing when close() is called,
        // so that the underlying client can be reused
        record ReusableClient(HttpClient client) implements ClientCloseable {
            // do not close the client so that it can be reused
            @Override
            public void close() { }
        }

        // a client configured with an executor, that closes both the client
        // and the executor when close() is called
        record ClientWithExecutor(HttpClient client, ExecutorService service)
                implements ClientCloseable {
            // close both the client and executor
            @Override
            public void close() {
                client.close();
                service.close();
            }
        }

        static ReusableClient reusable(HttpClient client) {
            return new ReusableClient(client);
        }

        static ClientWithExecutor withExecutor(HttpClient client, ExecutorService service) {
            return new ClientWithExecutor(client, service);
        }
    }

    // A supplier of ClientCloseable
    sealed interface ClientProvider extends Supplier<ClientCloseable> {

        ClientCloseable get();

        // a ClientProvider that returns reusable clients wrapping the given clieny
        record ReusableClientProvider(HttpClient client) implements ClientProvider {
            @Override
            public ClientCloseable get() {
                return ClientCloseable.reusable(client);
            }
        }

        // A ClientProvider that builds a new ClientWithExecutor for every call to get()
        record ClientBuilder(HttpClient.Builder builder) implements ClientProvider {
            ClientCloseable build() {
                int id = IDS.getAndIncrement();
                ExecutorService virtualExecutor = Executors.newThreadPerTaskExecutor(Thread.ofVirtual()
                        .name("HttpClient-" + id + "-Worker", 0).factory());
                builder.executor(virtualExecutor);
                return ClientCloseable.withExecutor(builder.build(), virtualExecutor);
            }

            public ClientBuilder localAddress(InetAddress localAddress) {
                builder.localAddress(localAddress);
                return this;
            }

            public ClientProvider provider() { return this; }

            @Override
            public ClientCloseable get() { return build(); }
        }

        static ReusableClientProvider reusable(HttpClient client) {
            return new ReusableClientProvider(client);
        }

        static ClientBuilder builder(HttpClient.Builder builder) {
            return new ClientBuilder(builder);
        }
    }




    private static ClientProvider.ClientBuilder newBuilder(boolean configureClientSSL) {
        var builder = HttpClient.newBuilder();
        // don't let proxies interfere with the client addresses received on the
        // HTTP request, by the server side handler used in this test.
        builder.proxy(HttpClient.Builder.NO_PROXY);
        if (configureClientSSL) {
            builder.sslContext(sslContext);
        }
        return ClientProvider.builder(builder);
    }

    /**
     * Sends a GET request using the {@code client} and expects a 200 response.
     * The returned response body is then tested to see if the client address
     * seen by the server side handler is the same one as that is set on the
     * {@code client}
     */
    @Test(dataProvider = "params")
    public void testSend(ClientProvider clientProvider, URI requestURI, InetAddress localAddress) throws Exception {
        try (var c = clientProvider.get()) {
            HttpClient client = c.client();
            System.out.println("Testing using a HTTP client " + client.version() + " with local address " + localAddress
                    + " against request URI " + requestURI);
            // GET request
            var req = HttpRequest.newBuilder(requestURI).build();
            var resp = client.send(req, HttpResponse.BodyHandlers.ofByteArray());
            Assert.assertEquals(resp.statusCode(), 200, "Unexpected status code");
            // verify the address only if a specific one was set on the client
            if (localAddress != null && !localAddress.isAnyLocalAddress()) {
                Assert.assertEquals(resp.body(), localAddress.getAddress(),
                        "Unexpected client address seen by the server handler");
            }
        }
    }

    /**
     * Sends a GET request using the {@code sendAsync} method on the {@code client} and
     * expects a 200 response. The returned response body is then tested to see if the client address
     * seen by the server side handler is the same one as that is set on the
     * {@code client}
     */
    @Test(dataProvider = "params")
    public void testSendAsync(ClientProvider clientProvider, URI requestURI, InetAddress localAddress) throws Exception {
        try (var c = clientProvider.get()) {
            HttpClient client = c.client();
            System.out.println("Testing using a HTTP client " + client.version()
                    + " with local address " + localAddress
                    + " against request URI " + requestURI);
            // GET request
            var req = HttpRequest.newBuilder(requestURI).build();
            var cf = client.sendAsync(req,
                    HttpResponse.BodyHandlers.ofByteArray());
            var resp = cf.get();
            Assert.assertEquals(resp.statusCode(), 200, "Unexpected status code");
            // verify the address only if a specific one was set on the client
            if (localAddress != null && !localAddress.isAnyLocalAddress()) {
                Assert.assertEquals(resp.body(), localAddress.getAddress(),
                        "Unexpected client address seen by the server handler");
            }
        }
    }

    /**
     * Invokes the {@link #testSend} and {@link #testSendAsync}
     * tests, concurrently in multiple threads to verify that the correct local address
     * is used when multiple concurrent threads are involved in sending requests from
     * the {@code client}
     */
    @Test(dataProvider = "params")
    public void testMultiSendRequests(ClientProvider clientProvider,
                                      URI requestURI,
                                      InetAddress localAddress) throws Exception {
        int numThreads = 4;
        ExecutorService executor = Executors.newFixedThreadPool(numThreads);
        List<Future<Void>> taskResults = new ArrayList<>();
        try (var c = clientProvider.get()) {
            // prevents testSend/testSendAsync from closing the client
            ClientProvider client = ClientProvider.reusable(c.client());
            for (int i = 0; i < numThreads; i++) {
                final var currentIdx = i;
                var f = executor.submit(new Callable<Void>() {
                    @Override
                    public Void call() throws Exception {
                        // test some for send and some for sendAsync
                        if (currentIdx % 2 == 0) {
                            testSend(client, requestURI, localAddress);
                        } else {
                            testSendAsync(client, requestURI, localAddress);
                        }
                        return null;
                    }
                });
                taskResults.add(f);
            }
            // wait for results
            for (var r : taskResults) {
                r.get();
            }
        } finally {
            executor.shutdownNow();
        }
    }
}