|
17 | 17 |
|
18 | 18 | package org.apache.hugegraph.unit.rest; |
19 | 19 |
|
20 | | -import java.security.NoSuchAlgorithmException; |
| 20 | +import java.io.InputStream; |
| 21 | +import java.io.OutputStream; |
| 22 | +import java.net.InetSocketAddress; |
| 23 | +import java.nio.charset.StandardCharsets; |
| 24 | +import java.nio.file.Files; |
| 25 | +import java.nio.file.Path; |
| 26 | +import java.nio.file.Paths; |
| 27 | +import java.security.KeyStore; |
| 28 | +import java.util.ArrayList; |
| 29 | +import java.util.Arrays; |
| 30 | +import java.util.Comparator; |
21 | 31 | import java.util.HashMap; |
| 32 | +import java.util.List; |
22 | 33 | import java.util.Map; |
23 | 34 | import java.util.UUID; |
24 | 35 | import java.util.concurrent.TimeUnit; |
25 | | -import java.util.function.BiFunction; |
| 36 | +import java.util.stream.Stream; |
26 | 37 |
|
| 38 | +import javax.net.ssl.KeyManagerFactory; |
27 | 39 | import javax.net.ssl.SSLContext; |
| 40 | +import javax.net.ssl.SSLHandshakeException; |
| 41 | +import javax.net.ssl.SSLPeerUnverifiedException; |
28 | 42 | import javax.net.ssl.SSLSession; |
29 | | -import javax.net.ssl.SSLSessionContext; |
30 | 43 |
|
31 | 44 | import org.apache.hugegraph.rest.AbstractRestClient; |
32 | 45 | import org.apache.hugegraph.rest.ClientException; |
|
43 | 56 | import com.google.common.collect.ImmutableList; |
44 | 57 | import com.google.common.collect.ImmutableMap; |
45 | 58 | import com.google.common.net.HttpHeaders; |
| 59 | +import com.sun.net.httpserver.HttpsConfigurator; |
| 60 | +import com.sun.net.httpserver.HttpsServer; |
46 | 61 |
|
47 | 62 | import lombok.SneakyThrows; |
48 | 63 | import okhttp3.HttpUrl; |
@@ -145,30 +160,112 @@ public void testPostHttpsWithTokenAndAllParams() { |
145 | 160 | } |
146 | 161 |
|
147 | 162 | @Test |
148 | | - public void testHostNameVerifier() { |
149 | | - BiFunction<String, String, Boolean> verifer = (url, hostname) -> { |
150 | | - AbstractRestClient.HostNameVerifier verifier; |
151 | | - SSLSession session; |
152 | | - try { |
153 | | - SSLSessionContext sc = SSLContext.getDefault() |
154 | | - .getClientSessionContext(); |
155 | | - session = sc.getSession(new byte[]{11}); |
156 | | - verifier = new AbstractRestClient.HostNameVerifier(url); |
157 | | - } catch (NoSuchAlgorithmException e) { |
158 | | - throw new RuntimeException(e); |
| 163 | + public void testHttpsCertificateIdentity() throws Exception { |
| 164 | + Path directory = Files.createTempDirectory("rest-client-tls-"); |
| 165 | + try { |
| 166 | + Path validStore = createServerStore(directory, "valid", "dns:localhost,ip:127.0.0.1"); |
| 167 | + Path wrongStore = createServerStore(directory, "wrong", "dns:other.example"); |
| 168 | + Path noSanStore = createServerStore(directory, "no-san", null); |
| 169 | + assertHttpsRequest(validStore, validStore, "localhost", null); |
| 170 | + assertHttpsRequest(validStore, validStore, "127.0.0.1", null); |
| 171 | + assertHttpsRequest(wrongStore, wrongStore, "localhost", SSLPeerUnverifiedException.class); |
| 172 | + assertHttpsRequest(noSanStore, noSanStore, "localhost", SSLPeerUnverifiedException.class); |
| 173 | + assertHttpsRequest(validStore, wrongStore, "localhost", SSLHandshakeException.class); |
| 174 | + } finally { |
| 175 | + try (Stream<Path> paths = Files.walk(directory)) { |
| 176 | + for (Path path : (Iterable<Path>) paths.sorted(Comparator.reverseOrder())::iterator) { |
| 177 | + Files.deleteIfExists(path); |
| 178 | + } |
159 | 179 | } |
160 | | - return verifier.verify(hostname, session); |
161 | | - }; |
| 180 | + } |
| 181 | + } |
162 | 182 |
|
163 | | - Assert.assertTrue(verifer.apply("http://baidu.com", "baidu.com")); |
164 | | - Assert.assertTrue(verifer.apply("http://test1.baidu.com", "baidu.com")); |
165 | | - Assert.assertTrue(verifer.apply("http://test2.baidu.com", "baidu.com")); |
166 | | - Assert.assertFalse(verifer.apply("http://baidu2.com", "baidu.com")); |
167 | | - Assert.assertTrue(verifer.apply("http://baidu.com", "")); |
168 | | - Assert.assertTrue(verifer.apply("baidu.com", "baidu.com")); |
169 | | - Assert.assertTrue(verifer.apply("http://baidu.com/test", "baidu.com")); |
170 | | - Assert.assertTrue(verifer.apply("baidu.com/test/abc", "baidu.com")); |
171 | | - Assert.assertFalse(verifer.apply("baidu.com.sina.com", "baidu.com")); |
| 183 | + private static Path createServerStore(Path directory, String name, String san) throws Exception { |
| 184 | + Path store = directory.resolve(name + ".p12"); |
| 185 | + List<String> command = new ArrayList<>(Arrays.asList( |
| 186 | + Paths.get(System.getProperty("java.home"), "bin", "keytool").toString(), |
| 187 | + "-genkeypair", "-alias", "server", "-keyalg", "RSA", "-keysize", "2048", |
| 188 | + "-validity", "2", "-dname", "CN=localhost", "-storetype", "PKCS12", |
| 189 | + "-keystore", store.toString(), "-storepass", "test-password", |
| 190 | + "-keypass", "test-password", "-noprompt")); |
| 191 | + if (san != null) { |
| 192 | + command.addAll(Arrays.asList("-ext", "SAN=" + san)); |
| 193 | + } |
| 194 | + Path log = directory.resolve(name + ".log"); |
| 195 | + Process process = new ProcessBuilder(command).redirectErrorStream(true) |
| 196 | + .redirectOutput(log.toFile()).start(); |
| 197 | + try { |
| 198 | + boolean finished = process.waitFor(30, TimeUnit.SECONDS); |
| 199 | + Assert.assertTrue("keytool timed out", finished); |
| 200 | + Assert.assertEquals(Files.readString(log, StandardCharsets.UTF_8), 0, process.exitValue()); |
| 201 | + } finally { |
| 202 | + if (process.isAlive()) { |
| 203 | + process.destroyForcibly(); |
| 204 | + process.waitFor(5, TimeUnit.SECONDS); |
| 205 | + } |
| 206 | + } |
| 207 | + return store; |
| 208 | + } |
| 209 | + |
| 210 | + private static KeyStore readStore(Path path) throws Exception { |
| 211 | + KeyStore store = KeyStore.getInstance("PKCS12"); |
| 212 | + try (InputStream input = Files.newInputStream(path)) { |
| 213 | + store.load(input, "test-password".toCharArray()); |
| 214 | + } |
| 215 | + return store; |
| 216 | + } |
| 217 | + |
| 218 | + private static void assertHttpsRequest(Path serverStore, Path trustedStore, String hostname, |
| 219 | + Class<? extends Throwable> failure) throws Exception { |
| 220 | + KeyStore keys = readStore(serverStore); |
| 221 | + KeyManagerFactory keyManagers = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); |
| 222 | + keyManagers.init(keys, "test-password".toCharArray()); |
| 223 | + SSLContext context = SSLContext.getInstance("TLS"); |
| 224 | + context.init(keyManagers.getKeyManagers(), null, null); |
| 225 | + HttpsServer server = HttpsServer.create(new InetSocketAddress("127.0.0.1", 0), 0); |
| 226 | + server.setHttpsConfigurator(new HttpsConfigurator(context)); |
| 227 | + server.createContext("/probe", exchange -> { |
| 228 | + byte[] response = "{}".getBytes(StandardCharsets.UTF_8); |
| 229 | + exchange.sendResponseHeaders(200, response.length); |
| 230 | + try (OutputStream output = exchange.getResponseBody()) { |
| 231 | + output.write(response); |
| 232 | + } |
| 233 | + }); |
| 234 | + Path trustStore = Files.createTempFile(serverStore.getParent(), "trust-", ".jks"); |
| 235 | + server.start(); |
| 236 | + try { |
| 237 | + KeyStore trusted = KeyStore.getInstance("JKS"); |
| 238 | + trusted.load(null, null); |
| 239 | + trusted.setCertificateEntry("server", readStore(trustedStore).getCertificate("server")); |
| 240 | + try (OutputStream output = Files.newOutputStream(trustStore)) { |
| 241 | + trusted.store(output, "test-password".toCharArray()); |
| 242 | + } |
| 243 | + RestClientConfig config = RestClientConfig.builder().timeout(5000) |
| 244 | + .trustStoreFile(trustStore.toString()).trustStorePassword("test-password").build(); |
| 245 | + String url = "https://" + hostname + ":" + server.getAddress().getPort(); |
| 246 | + RestClient client = new AbstractRestClient(url, config) { |
| 247 | + @Override |
| 248 | + protected void checkStatus(Response response, int... statuses) { |
| 249 | + Assert.assertEquals(200, response.code()); |
| 250 | + } |
| 251 | + }; |
| 252 | + try { |
| 253 | + if (failure == null) { |
| 254 | + Assert.assertEquals(200, client.get("probe").status()); |
| 255 | + } else { |
| 256 | + org.junit.Assert.assertThrows(failure, () -> client.get("probe")); |
| 257 | + } |
| 258 | + } finally { |
| 259 | + client.close(); |
| 260 | + } |
| 261 | + SSLSession session = Mockito.mock(SSLSession.class); |
| 262 | + Mockito.when(session.getPeerCertificates()).thenReturn(keys.getCertificateChain("server")); |
| 263 | + Assert.assertEquals(failure != SSLPeerUnverifiedException.class, |
| 264 | + new AbstractRestClient.HostNameVerifier(url).verify(hostname, session)); |
| 265 | + } finally { |
| 266 | + server.stop(0); |
| 267 | + Files.deleteIfExists(trustStore); |
| 268 | + } |
172 | 269 | } |
173 | 270 |
|
174 | 271 | @Test |
|
0 commit comments