diff --git a/aws-resources/build.gradle.kts b/aws-resources/build.gradle.kts index 8e9471c0c..f16878419 100644 --- a/aws-resources/build.gradle.kts +++ b/aws-resources/build.gradle.kts @@ -16,13 +16,14 @@ dependencies { compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") implementation("com.fasterxml.jackson.core:jackson-core") - implementation("com.squareup.okhttp3:okhttp") + compileOnly("com.squareup.okhttp3:okhttp") testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") testImplementation("io.opentelemetry:opentelemetry-sdk-testing") testImplementation("com.linecorp.armeria:armeria-junit5") testRuntimeOnly("org.bouncycastle:bcpkix-jdk15on") + testRuntimeOnly("com.squareup.okhttp3:okhttp") testImplementation("com.google.guava:guava") testImplementation("org.skyscreamer:jsonassert") } diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/Ec2Resource.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/Ec2Resource.java index 73d6517b1..3b1376ee2 100644 --- a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/Ec2Resource.java +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/Ec2Resource.java @@ -138,7 +138,7 @@ private static String fetchHostname(URL hostnameUrl, String token) { // Generic HTTP fetch function for IMDS. private static String fetchString(String httpMethod, URL url, String token, boolean includeTtl) { - SimpleHttpClient client = new SimpleHttpClient(); + JdkHttpClient client = new JdkHttpClient(); Map headers = new HashMap<>(); if (includeTtl) { diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EcsResource.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EcsResource.java index badc358f4..b68a74e73 100644 --- a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EcsResource.java +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EcsResource.java @@ -44,11 +44,11 @@ public static Resource get() { } private static Resource buildResource() { - return buildResource(System.getenv(), new SimpleHttpClient()); + return buildResource(System.getenv(), new JdkHttpClient()); } // Visible for testing - static Resource buildResource(Map sysEnv, SimpleHttpClient httpClient) { + static Resource buildResource(Map sysEnv, JdkHttpClient httpClient) { // Note: If V4 is set V3 is set as well, so check V4 first. String ecsMetadataUrl = sysEnv.getOrDefault(ECS_METADATA_KEY_V4, sysEnv.getOrDefault(ECS_METADATA_KEY_V3, "")); @@ -64,8 +64,7 @@ static Resource buildResource(Map sysEnv, SimpleHttpClient httpC return Resource.empty(); } - static void fetchMetadata( - SimpleHttpClient httpClient, String url, AttributesBuilder attrBuilders) { + static void fetchMetadata(JdkHttpClient httpClient, String url, AttributesBuilder attrBuilders) { String json = httpClient.fetchString("GET", url, Collections.emptyMap(), null); if (json.isEmpty()) { return; diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EksResource.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EksResource.java index fc268098a..6327d5902 100644 --- a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EksResource.java +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EksResource.java @@ -51,12 +51,12 @@ public static Resource get() { } private static Resource buildResource() { - return buildResource(new SimpleHttpClient(), new DockerHelper(), K8S_TOKEN_PATH, K8S_CERT_PATH); + return buildResource(new JdkHttpClient(), new DockerHelper(), K8S_TOKEN_PATH, K8S_CERT_PATH); } // Visible for testing static Resource buildResource( - SimpleHttpClient httpClient, + JdkHttpClient httpClient, DockerHelper dockerHelper, String k8sTokenPath, String k8sKeystorePath) { @@ -83,7 +83,7 @@ static Resource buildResource( } private static boolean isEks( - String k8sTokenPath, String k8sKeystorePath, SimpleHttpClient httpClient) { + String k8sTokenPath, String k8sKeystorePath, JdkHttpClient httpClient) { if (!isK8s(k8sTokenPath, k8sKeystorePath)) { logger.log(Level.FINE, "Not running on k8s."); return false; @@ -104,7 +104,7 @@ private static boolean isK8s(String k8sTokenPath, String k8sKeystorePath) { return k8sTokeyFile.exists() && k8sKeystoreFile.exists(); } - private static String getClusterName(SimpleHttpClient httpClient) { + private static String getClusterName(JdkHttpClient httpClient) { Map requestProperties = new HashMap<>(); requestProperties.put("Authorization", getK8sCredHeader()); String json = diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/JdkHttpClient.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/JdkHttpClient.java new file mode 100644 index 000000000..4cf9c5977 --- /dev/null +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/JdkHttpClient.java @@ -0,0 +1,122 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; + +/** A simple HTTP client based on JDK HttpURLConnection. Not meant for high throughput. */ +final class JdkHttpClient { + + private static final Logger logger = Logger.getLogger(JdkHttpClient.class.getName()); + + private static final int TIMEOUT = 2000; + + /** Fetch a string from a remote server. */ + public String fetchString( + String httpMethod, String urlStr, Map headers, @Nullable String certPath) { + + try { + // create URL from string + URL url = new URL(urlStr); + // create connection + URLConnection connection = url.openConnection(); + // https + if (connection instanceof HttpsURLConnection) { + // cast + HttpsURLConnection httpsConnection = (HttpsURLConnection) connection; + // check CA cert path is available + if (certPath != null) { + // create trust manager + X509TrustManager trustManager = SslSocketFactoryBuilder.createTrustManager(certPath); + // socket factory + SSLSocketFactory socketFactory = + SslSocketFactoryBuilder.createSocketFactory(trustManager); + if (socketFactory != null) { + // update connection + httpsConnection.setSSLSocketFactory(socketFactory); + } + } + // process request + return processRequest(httpsConnection, httpMethod, urlStr, headers); + } + // http + if (connection instanceof HttpURLConnection) { + // cast + HttpURLConnection httpConnection = (HttpURLConnection) connection; + // process request + return processRequest(httpConnection, httpMethod, urlStr, headers); + } + // not http + logger.log(Level.FINE, "JdkHttpClient only HTTP/HTTPS connections are supported."); + } catch (MalformedURLException e) { + logger.log(Level.FINE, "JdkHttpClient invalid URL.", e); + } catch (IOException e) { + logger.log(Level.FINE, "JdkHttpClient fetch string failed.", e); + } + return ""; + } + + private static String processRequest( + HttpURLConnection httpConnection, + String httpMethod, + String urlStr, + Map headers) + throws IOException { + // set method + httpConnection.setRequestMethod(httpMethod); + // set headers + headers.forEach(httpConnection::setRequestProperty); + // timeouts + httpConnection.setConnectTimeout(TIMEOUT); + httpConnection.setReadTimeout(TIMEOUT); + // connect + httpConnection.connect(); + try { + // status code + int responseCode = httpConnection.getResponseCode(); + if (responseCode != 200) { + logger.log( + Level.FINE, + "Error response from " + + urlStr + + " code (" + + responseCode + + ") text " + + httpConnection.getResponseMessage()); + return ""; + } + // read response + try (InputStream inputStream = httpConnection.getInputStream()) { + // store read data in byte array + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + // read all bytes + int b; + while ((b = inputStream.read()) != -1) { + outputStream.write(b); + } + // result + return outputStream.toString("UTF-8"); + } + } + } finally { + // disconnect, no need for persistent connections + httpConnection.disconnect(); + } + } +} diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/SimpleHttpClient.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/SimpleHttpClient.java index 12bc6e34e..0dba9d648 100644 --- a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/SimpleHttpClient.java +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/SimpleHttpClient.java @@ -5,21 +5,13 @@ package io.opentelemetry.contrib.aws.resource; -import java.io.FileInputStream; import java.io.IOException; -import java.security.KeyStore; -import java.security.cert.Certificate; -import java.security.cert.CertificateFactory; import java.time.Duration; -import java.util.Collection; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -47,9 +39,8 @@ public String fetchString( .readTimeout(TIMEOUT); if (urlStr.startsWith("https") && certPath != null) { - KeyStore keyStore = getKeystoreForTrustedCert(certPath); - X509TrustManager trustManager = buildTrustManager(keyStore); - SSLSocketFactory socketFactory = buildSslSocketFactory(trustManager); + X509TrustManager trustManager = SslSocketFactoryBuilder.createTrustManager(certPath); + SSLSocketFactory socketFactory = SslSocketFactoryBuilder.createSocketFactory(trustManager); if (socketFactory != null) { clientBuilder.sslSocketFactory(socketFactory, trustManager); } @@ -89,57 +80,4 @@ public String fetchString( return ""; } - - @Nullable - private static X509TrustManager buildTrustManager(@Nullable KeyStore keyStore) { - if (keyStore == null) { - return null; - } - try { - String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); - TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); - tmf.init(keyStore); - return (X509TrustManager) tmf.getTrustManagers()[0]; - } catch (Exception e) { - logger.log(Level.WARNING, "Build SslSocketFactory for K8s restful client exception.", e); - return null; - } - } - - @Nullable - private static SSLSocketFactory buildSslSocketFactory(@Nullable TrustManager trustManager) { - if (trustManager == null) { - return null; - } - try { - SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, new TrustManager[] {trustManager}, null); - return context.getSocketFactory(); - - } catch (Exception e) { - logger.log(Level.WARNING, "Build SslSocketFactory for K8s restful client exception.", e); - } - return null; - } - - @Nullable - private static KeyStore getKeystoreForTrustedCert(String certPath) { - try (FileInputStream fis = new FileInputStream(certPath)) { - KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); - trustStore.load(null, null); - CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - - Collection certificates = certificateFactory.generateCertificates(fis); - - int i = 0; - for (Certificate certificate : certificates) { - trustStore.setCertificateEntry("cert_" + i, certificate); - i++; - } - return trustStore; - } catch (Exception e) { - logger.log(Level.WARNING, "Cannot load KeyStore from " + certPath); - return null; - } - } } diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/SslSocketFactoryBuilder.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/SslSocketFactoryBuilder.java new file mode 100644 index 000000000..38596b1be --- /dev/null +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/SslSocketFactoryBuilder.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import java.io.FileInputStream; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.util.Collection; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +class SslSocketFactoryBuilder { + + private static final Logger logger = Logger.getLogger(SslSocketFactoryBuilder.class.getName()); + + private SslSocketFactoryBuilder() {} + + @Nullable + private static KeyStore getKeystoreForTrustedCert(String certPath) { + try (FileInputStream fis = new FileInputStream(certPath)) { + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(null, null); + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + + Collection certificates = certificateFactory.generateCertificates(fis); + + int i = 0; + for (Certificate certificate : certificates) { + trustStore.setCertificateEntry("cert_" + i, certificate); + i++; + } + return trustStore; + } catch (Exception e) { + logger.log(Level.WARNING, "Cannot load KeyStore from " + certPath); + return null; + } + } + + @Nullable + static X509TrustManager createTrustManager(@Nullable String certPath) { + if (certPath == null) { + return null; + } + try { + // create keystore + KeyStore keyStore = getKeystoreForTrustedCert(certPath); + String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); + tmf.init(keyStore); + return (X509TrustManager) tmf.getTrustManagers()[0]; + } catch (Exception e) { + logger.log(Level.WARNING, "Build SslSocketFactory for K8s restful client exception.", e); + return null; + } + } + + @Nullable + static SSLSocketFactory createSocketFactory(@Nullable TrustManager trustManager) { + if (trustManager == null) { + return null; + } + try { + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, new TrustManager[] {trustManager}, null); + return context.getSocketFactory(); + + } catch (Exception e) { + logger.log(Level.WARNING, "Build SslSocketFactory for K8s restful client exception.", e); + } + return null; + } +} diff --git a/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/EcsResourceTest.java b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/EcsResourceTest.java index e822d88b0..7f5112bdf 100644 --- a/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/EcsResourceTest.java +++ b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/EcsResourceTest.java @@ -31,7 +31,7 @@ class EcsResourceTest { private static final String ECS_METADATA_KEY_V4 = "ECS_CONTAINER_METADATA_URI_V4"; private static final String ECS_METADATA_KEY_V3 = "ECS_CONTAINER_METADATA_URI"; - @Mock private SimpleHttpClient mockHttpClient; + @Mock private JdkHttpClient mockHttpClient; @Test void testCreateAttributesV3() throws IOException { diff --git a/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/EksResourceTest.java b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/EksResourceTest.java index 7649836bb..ad7185e5c 100644 --- a/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/EksResourceTest.java +++ b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/EksResourceTest.java @@ -34,7 +34,7 @@ public class EksResourceTest { @Mock private DockerHelper mockDockerHelper; - @Mock private SimpleHttpClient httpClient; + @Mock private JdkHttpClient httpClient; @Test void testEks(@TempDir File tempFolder) throws IOException { diff --git a/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/JdkHttpClientTest.java b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/JdkHttpClientTest.java new file mode 100644 index 000000000..561a60289 --- /dev/null +++ b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/JdkHttpClientTest.java @@ -0,0 +1,115 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import com.linecorp.armeria.common.AggregatedHttpRequest; +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.server.ServerBuilder; +import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import com.linecorp.armeria.testing.junit5.server.mock.MockWebServerExtension; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.io.TempDir; + +public class JdkHttpClientTest { + + @RegisterExtension + public static final MockWebServerExtension server = new MockWebServerExtension(); + + @Test + void testFetchString() { + server.enqueue(HttpResponse.of("expected result")); + + ImmutableMap requestPropertyMap = + ImmutableMap.of("key1", "value1", "key2", "value2"); + String urlStr = String.format("http://localhost:%s%s", server.httpPort(), "/path"); + JdkHttpClient httpClient = new JdkHttpClient(); + String result = httpClient.fetchString("GET", urlStr, requestPropertyMap, null); + + assertThat(result).isEqualTo("expected result"); + + AggregatedHttpRequest request1 = server.takeRequest().request(); + assertThat(request1.path()).isEqualTo("/path"); + assertThat(request1.headers().get("key1")).isEqualTo("value1"); + assertThat(request1.headers().get("key2")).isEqualTo("value2"); + } + + @Test + void testFailedFetchString() { + ImmutableMap requestPropertyMap = + ImmutableMap.of("key1", "value1", "key2", "value2"); + String urlStr = String.format("http://localhost:%s%s", server.httpPort(), "/path"); + JdkHttpClient httpClient = new JdkHttpClient(); + String result = httpClient.fetchString("GET", urlStr, requestPropertyMap, null); + assertThat(result).isEmpty(); + } + + static class HttpsServerTest { + @RegisterExtension + @Order(1) + public static final SelfSignedCertificateExtension certificate = + new SelfSignedCertificateExtension(); + + @RegisterExtension + @Order(2) + public static ServerExtension server = + new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + sb.tls(certificate.certificateFile(), certificate.privateKeyFile()); + + sb.service("/", (ctx, req) -> HttpResponse.of("Thanks for trusting me")); + } + }; + + @Test + void goodCert() { + JdkHttpClient httpClient = new JdkHttpClient(); + String result = + httpClient.fetchString( + "GET", + "https://localhost:" + server.httpsPort() + "/", + Collections.emptyMap(), + certificate.certificateFile().getAbsolutePath()); + assertThat(result).isEqualTo("Thanks for trusting me"); + } + + @Test + void missingCert() { + JdkHttpClient httpClient = new JdkHttpClient(); + String result = + httpClient.fetchString( + "GET", + "https://localhost:" + server.httpsPort() + "/", + Collections.emptyMap(), + "/foo/bar/bad"); + assertThat(result).isEmpty(); + } + + @Test + void badCert(@TempDir Path tempDir) throws Exception { + Path certFile = tempDir.resolve("test.crt"); + Files.write(certFile, "bad cert".getBytes(StandardCharsets.UTF_8)); + JdkHttpClient httpClient = new JdkHttpClient(); + String result = + httpClient.fetchString( + "GET", + "https://localhost:" + server.httpsPort() + "/", + Collections.emptyMap(), + certFile.toString()); + assertThat(result).isEmpty(); + } + } +}