cc600b2e7aa26fe2e37aa9557cc63a0e77975690
[netconf.git] / transport / transport-http / src / test / java / org / opendaylight / netconf / transport / http / HttpClientServerTest.java
1 /*
2  * Copyright (c) 2024 PANTHEON.tech s.r.o. and others. All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.netconf.transport.http;
9
10 import static io.netty.buffer.Unpooled.wrappedBuffer;
11 import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
12 import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
13 import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
14 import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
15 import static io.netty.handler.codec.http.HttpHeaderValues.TEXT_PLAIN;
16 import static io.netty.handler.codec.http.HttpResponseStatus.OK;
17 import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
18 import static org.junit.jupiter.api.Assertions.assertEquals;
19 import static org.junit.jupiter.api.Assertions.assertNotNull;
20 import static org.mockito.ArgumentMatchers.any;
21 import static org.mockito.Mockito.doReturn;
22 import static org.mockito.Mockito.timeout;
23 import static org.mockito.Mockito.verify;
24 import static org.opendaylight.netconf.transport.http.ConfigUtils.clientTransportTcp;
25 import static org.opendaylight.netconf.transport.http.ConfigUtils.clientTransportTls;
26 import static org.opendaylight.netconf.transport.http.ConfigUtils.serverTransportTcp;
27 import static org.opendaylight.netconf.transport.http.ConfigUtils.serverTransportTls;
28
29 import com.google.common.util.concurrent.SettableFuture;
30 import io.netty.handler.codec.http.DefaultFullHttpRequest;
31 import io.netty.handler.codec.http.DefaultFullHttpResponse;
32 import io.netty.handler.codec.http.FullHttpResponse;
33 import io.netty.handler.codec.http.HttpMethod;
34 import java.io.IOException;
35 import java.math.BigInteger;
36 import java.net.InetAddress;
37 import java.net.ServerSocket;
38 import java.nio.charset.StandardCharsets;
39 import java.security.KeyPair;
40 import java.security.KeyPairGenerator;
41 import java.security.PrivateKey;
42 import java.security.SecureRandom;
43 import java.security.cert.X509Certificate;
44 import java.security.spec.ECGenParameterSpec;
45 import java.security.spec.RSAKeyGenParameterSpec;
46 import java.time.Duration;
47 import java.time.Instant;
48 import java.util.Date;
49 import java.util.Map;
50 import java.util.concurrent.Executors;
51 import java.util.concurrent.ScheduledExecutorService;
52 import java.util.concurrent.TimeUnit;
53 import java.util.concurrent.atomic.AtomicInteger;
54 import org.bouncycastle.asn1.x500.X500Name;
55 import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
56 import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
57 import org.bouncycastle.jce.provider.BouncyCastleProvider;
58 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
59 import org.junit.jupiter.api.AfterAll;
60 import org.junit.jupiter.api.BeforeAll;
61 import org.junit.jupiter.api.extension.ExtendWith;
62 import org.junit.jupiter.params.ParameterizedTest;
63 import org.junit.jupiter.params.provider.ValueSource;
64 import org.mockito.Mock;
65 import org.mockito.junit.jupiter.MockitoExtension;
66 import org.opendaylight.netconf.transport.api.TransportChannelListener;
67 import org.opendaylight.netconf.transport.tcp.BootstrapFactory;
68 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.client.rev240208.HttpClientStackGrouping;
69 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208.HttpServerStackGrouping;
70
71 @ExtendWith(MockitoExtension.class)
72 public class HttpClientServerTest {
73
74     private static final String USERNAME = "username";
75     private static final String PASSWORD = "pa$$W0rd";
76     private static final Map<String, String> USER_HASHES_MAP = Map.of(USERNAME, "$0$" + PASSWORD);
77     private static final AtomicInteger COUNTER = new AtomicInteger(0);
78     private static final SecureRandom SECURE_RANDOM = new SecureRandom();
79     private static final String[] METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE"};
80     private static final String RESPONSE_TEMPLATE = "Method: %s URI: %s Payload: %s";
81
82     private static ScheduledExecutorService scheduledExecutor;
83     private static RequestDispatcher requestDispatcher;
84     private static BootstrapFactory bootstrapFactory;
85     private static String localAddress;
86
87     @Mock
88     private HttpServerStackGrouping serverConfig;
89     @Mock
90     private HttpClientStackGrouping clientConfig;
91     @Mock
92     private TransportChannelListener serverTransportListener;
93     @Mock
94     private TransportChannelListener clientTransportListener;
95
96     @BeforeAll
97     static void beforeAll() {
98         bootstrapFactory = new BootstrapFactory("IntegrationTest", 0);
99         localAddress = InetAddress.getLoopbackAddress().getHostAddress();
100         scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
101
102         requestDispatcher = request -> {
103             final var future = SettableFuture.<FullHttpResponse>create();
104             // emulate asynchronous server request processing - run in separate thread with 100 millis delay
105             scheduledExecutor.schedule(() -> {
106                 // return 200 response with a content built from request parameters
107                 final var method = request.method().name();
108                 final var uri = request.uri();
109                 final var payload = request.content().readableBytes() > 0
110                     ? request.content().toString(StandardCharsets.UTF_8) : "";
111                 final var responseMessage = RESPONSE_TEMPLATE.formatted(method, uri, payload);
112                 final var response = new DefaultFullHttpResponse(request.protocolVersion(), OK,
113                     wrappedBuffer(responseMessage.getBytes(StandardCharsets.UTF_8)));
114                 response.headers().set(CONTENT_TYPE, TEXT_PLAIN)
115                     .setInt(CONTENT_LENGTH, response.content().readableBytes());
116                 return future.set(response);
117             }, 100, TimeUnit.MILLISECONDS);
118             return future;
119         };
120     }
121
122     @AfterAll
123     static void afterAll() {
124         bootstrapFactory.close();
125         scheduledExecutor.shutdown();
126     }
127
128     @ParameterizedTest(name = "TCP with no authorization, HTTP/2: {0}")
129     @ValueSource(booleans = {false, true})
130     void noAuthTcp(final boolean http2) throws Exception {
131         final var localPort = freePort();
132         doReturn(serverTransportTcp(localAddress, localPort)).when(serverConfig).getTransport();
133         doReturn(clientTransportTcp(localAddress, localPort)).when(clientConfig).getTransport();
134         integrationTest(http2);
135     }
136
137     @ParameterizedTest(name = "TCP with Basic authorization, HTTP/2: {0}")
138     @ValueSource(booleans = {false, true})
139     void basicAuthTcp(final boolean http2) throws Exception {
140         final var localPort = freePort();
141         doReturn(serverTransportTcp(localAddress, localPort, USER_HASHES_MAP))
142             .when(serverConfig).getTransport();
143         doReturn(clientTransportTcp(localAddress, localPort, USERNAME, PASSWORD))
144             .when(clientConfig).getTransport();
145         integrationTest(http2);
146     }
147
148     @ParameterizedTest(name = "TLS with no authorization, HTTP/2: {0}")
149     @ValueSource(booleans = {false, true})
150     void noAuthTls(final boolean http2) throws Exception {
151         final var certData = generateX509CertData("RSA");
152         final var localPort = freePort();
153         doReturn(serverTransportTls(localAddress, localPort, certData.certificate(), certData.privateKey()))
154             .when(serverConfig).getTransport();
155         doReturn(clientTransportTls(localAddress, localPort, certData.certificate())).when(clientConfig).getTransport();
156         integrationTest(http2);
157     }
158
159     @ParameterizedTest(name = "TLS with Basic authorization, HTTP/2: {0}")
160     @ValueSource(booleans = {false, true})
161     void basicAuthTls(final boolean http2) throws Exception {
162         final var certData = generateX509CertData("EC");
163         final var localPort = freePort();
164         doReturn(serverTransportTls(localAddress, localPort, certData.certificate(), certData.privateKey(),
165             USER_HASHES_MAP)).when(serverConfig).getTransport();
166         doReturn(clientTransportTls(localAddress, localPort, certData.certificate(), USERNAME, PASSWORD))
167             .when(clientConfig).getTransport();
168         integrationTest(http2);
169     }
170
171     private void integrationTest(final boolean http2) throws Exception {
172         final var server = HTTPServer.listen(serverTransportListener, bootstrapFactory.newServerBootstrap(),
173             serverConfig, requestDispatcher).get(2, TimeUnit.SECONDS);
174         try {
175             final var client = HTTPClient.connect(clientTransportListener, bootstrapFactory.newBootstrap(),
176                     clientConfig, http2).get(2, TimeUnit.SECONDS);
177             try {
178                 verify(serverTransportListener, timeout(2000)).onTransportChannelEstablished(any());
179                 verify(clientTransportListener, timeout(2000)).onTransportChannelEstablished(any());
180
181                 for (var method : METHODS) {
182                     final var uri = nextValue("URI");
183                     final var payload = nextValue("PAYLOAD");
184                     final var request = new DefaultFullHttpRequest(HTTP_1_1, HttpMethod.valueOf(method),
185                         uri, wrappedBuffer(payload.getBytes(StandardCharsets.UTF_8)));
186                     request.headers().set(CONTENT_TYPE, TEXT_PLAIN)
187                         .setInt(CONTENT_LENGTH, request.content().readableBytes())
188                         // allow multiple requests on same connections
189                         .set(CONNECTION, KEEP_ALIVE);
190
191                     final var response = client.invoke(request).get(2, TimeUnit.SECONDS);
192                     assertNotNull(response);
193                     assertEquals(OK, response.status());
194                     final var expected = RESPONSE_TEMPLATE.formatted(method, uri, payload);
195                     assertEquals(expected, response.content().toString(StandardCharsets.UTF_8));
196                 }
197             } finally {
198                 client.shutdown().get(2, TimeUnit.SECONDS);
199             }
200         } finally {
201             server.shutdown().get(2, TimeUnit.SECONDS);
202         }
203     }
204
205     private static int freePort() throws IOException {
206         // find free port
207         final var socket = new ServerSocket(0);
208         final var localPort = socket.getLocalPort();
209         socket.close();
210         return localPort;
211     }
212
213     private static String nextValue(final String prefix) {
214         return prefix + COUNTER.incrementAndGet();
215     }
216
217     private static X509CertData generateX509CertData(final String algorithm) throws Exception {
218         final var keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
219         if (isRSA(algorithm)) {
220             keyPairGenerator.initialize(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4), SECURE_RANDOM);
221         } else {
222             keyPairGenerator.initialize(new ECGenParameterSpec("secp256r1"), SECURE_RANDOM);
223         }
224         final var keyPair = keyPairGenerator.generateKeyPair();
225         final var certificate = generateCertificate(keyPair, isRSA(algorithm) ? "SHA256withRSA" : "SHA256withECDSA");
226         return new X509CertData(certificate, keyPair.getPrivate());
227     }
228
229     private static X509Certificate generateCertificate(final KeyPair keyPair, final String hashAlgorithm)
230             throws Exception {
231         final var now = Instant.now();
232         final var contentSigner = new JcaContentSignerBuilder(hashAlgorithm).build(keyPair.getPrivate());
233
234         final var x500Name = new X500Name("CN=TestCertificate");
235         final var certificateBuilder = new JcaX509v3CertificateBuilder(x500Name,
236             BigInteger.valueOf(now.toEpochMilli()),
237             Date.from(now), Date.from(now.plus(Duration.ofDays(365))),
238             x500Name,
239             keyPair.getPublic());
240         return new JcaX509CertificateConverter()
241             .setProvider(new BouncyCastleProvider()).getCertificate(certificateBuilder.build(contentSigner));
242     }
243
244     private static boolean isRSA(final String algorithm) {
245         return "RSA".equals(algorithm);
246     }
247
248     private record X509CertData(X509Certificate certificate, PrivateKey privateKey) {
249     }
250 }