diff --git a/CHANGELOG.md b/CHANGELOG.md index 48211487..8972ec3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## 1.1.0 [unreleased] +### Features + +1. [#229](https://github.com/InfluxCommunity/influxdb3-java/pull/229): Support proxy and custom ssl root certificates + ## 1.0.0 [2024-12-11] ### Features diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml new file mode 100644 index 00000000..b820d1bb --- /dev/null +++ b/examples/docker-compose.yml @@ -0,0 +1,34 @@ +# +# The MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +version: "3" + +services: + envoy: + image: envoyproxy/envoy:v1.26-latest + volumes: + - ./envoy.yaml:/etc/envoy/envoy.yaml + ports: + - "10000:10000" + environment: + - ENVOY_UID=0 + diff --git a/examples/envoy.yaml b/examples/envoy.yaml new file mode 100644 index 00000000..ab539c73 --- /dev/null +++ b/examples/envoy.yaml @@ -0,0 +1,84 @@ +# +# The MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +static_resources: + listeners: + - name: listener_0 + address: + socket_address: { address: 0.0.0.0, port_value: 10000 } + filter_chains: + - filter_chain_match: + filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + access_log: + - name: envoy.access_loggers.stdout + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog + http2_protocol_options: + allow_connect: true + upgrade_configs: + - upgrade_type: CONNECT + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: [ "*" ] + routes: + - match: + connect_matcher: { } + route: + cluster: influxdb_cluster + upgrade_configs: + upgrade_type: CONNECT + connect_config: { } + - match: + prefix: "/" + route: + cluster: influxdb_cluster + prefix_rewrite: "/" + auto_host_rewrite: true + timeout: 10s + cors: + allow_origin_string_match: + - prefix: "*" + allow_methods: GET, PUT, DELETE, POST, OPTIONS + clusters: + - name: influxdb_cluster + connect_timeout: 10s + type: STRICT_DNS + load_assignment: + cluster_name: influxdb_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: "us-east-1-1.aws.cloud2.influxdata.com" + port_value: 443 \ No newline at end of file diff --git a/examples/src/main/java/com/influxdb/v3/ProxyExample.java b/examples/src/main/java/com/influxdb/v3/ProxyExample.java new file mode 100644 index 00000000..76a3204d --- /dev/null +++ b/examples/src/main/java/com/influxdb/v3/ProxyExample.java @@ -0,0 +1,72 @@ +/* + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.influxdb.v3; + +import java.util.UUID; +import java.util.stream.Stream; + +import com.influxdb.v3.client.InfluxDBClient; +import com.influxdb.v3.client.Point; +import com.influxdb.v3.client.PointValues; +import com.influxdb.v3.client.config.ClientConfig; + +public final class ProxyExample { + + private ProxyExample() { + } + + public static void main(final String[] args) throws Exception { + // Run docker-compose.yml file to start Envoy proxy + + String proxyUrl = "http://localhost:10000"; + String sslRootsFilePath = "src/test/java/com/influxdb/v3/client/testdata/influxdb-certificate.pem"; + ClientConfig clientConfig = new ClientConfig.Builder() + .host(System.getenv("INFLUXDB_URL")) + .token(System.getenv("INFLUXDB_TOKEN").toCharArray()) + .database(System.getenv("INFLUXDB_DATABASE")) + .proxyUrl(proxyUrl) + .sslRootsFilePath(sslRootsFilePath) + .build(); + + InfluxDBClient influxDBClient = InfluxDBClient.getInstance(clientConfig); + String testId = UUID.randomUUID().toString(); + Point point = Point.measurement("My_Home") + .setTag("room", "Kitchen") + .setField("temp", 12.7) + .setField("hum", 37) + .setField("testId", testId); + influxDBClient.writePoint(point); + + String query = String.format("SELECT * FROM \"My_Home\" WHERE \"testId\" = '%s'", testId); + try (Stream stream = influxDBClient.queryPoints(query)) { + stream.findFirst().ifPresent(values -> { + assert values.getTimestamp() != null; + System.out.printf("room[%s]: %s, temp: %3.2f, hum: %d", + new java.util.Date(values.getTimestamp().longValue() / 1000000), + values.getTag("room"), + (Double) values.getField("temp"), + (Long) values.getField("hum")); + }); + } + } +} + diff --git a/pom.xml b/pom.xml index cc9af3c1..f4cfc172 100644 --- a/pom.xml +++ b/pom.xml @@ -399,7 +399,7 @@ **/target/**, **/*.jar, **/.git/**, **/.*, **/*.png, **/*.iml, **/*.bolt, .idea/**, **/*nightly*/**, **/.m2/**, LICENSE, **/*.md, **/.github/**, license_header.txt, release.properties/, **/pom.xml.releaseBackup, **/pom.xml.tag, **/semantic.yml, - .circleci/config.yml + .circleci/config.yml, **/*.pem diff --git a/src/main/java/com/influxdb/v3/client/config/ClientConfig.java b/src/main/java/com/influxdb/v3/client/config/ClientConfig.java index 55508a01..fc248e30 100644 --- a/src/main/java/com/influxdb/v3/client/config/ClientConfig.java +++ b/src/main/java/com/influxdb/v3/client/config/ClientConfig.java @@ -57,20 +57,23 @@ *
  • disableServerCertificateValidation - * disable server certificate validation for HTTPS connections *
  • - *
  • proxy - HTTP proxy selector
  • + *
  • proxyUrl - Proxy url for query api and write api
  • *
  • authenticator - HTTP proxy authenticator
  • *
  • headers - headers to be added to requests
  • + *
  • sslRootsFilePath - Path to the stored certificates file in PEM format
  • * *

    * If you want to create a client with custom configuration, you can use following code: *

      * ClientConfig config = new ClientConfig.Builder()
    - *     .host("https://us-east-1-1.aws.cloud2.influxdata.com")
    + *     .host("
    + *         https://us-east-1-1.aws.cloud2.influxdata.com
    + *         ")
      *     .token("my-token".toCharArray())
      *     .database("my-database")
      *     .writePrecision(WritePrecision.S)
      *     .gzipThreshold(4096)
    - *     .proxy(ProxySelector.of(new InetSocketAddress("proxy.local", 8888)))
    + *     .proxyUrl("http://localhost:10000")
      *     .build();
      *
      * try (InfluxDBClient client = InfluxDBClient.getInstance(config)) {
    @@ -84,7 +87,6 @@
      * Immutable class.
      */
     public final class ClientConfig {
    -
         private final String host;
         private final char[] token;
         private final String authScheme;
    @@ -96,9 +98,16 @@ public final class ClientConfig {
         private final Duration timeout;
         private final Boolean allowHttpRedirects;
         private final Boolean disableServerCertificateValidation;
    -    private final ProxySelector proxy;
    +    private final String proxyUrl;
         private final Authenticator authenticator;
         private final Map headers;
    +    private final String sslRootsFilePath;
    +
    +    /**
    +     * Deprecated use {@link #proxyUrl}.
    +     */
    +    @Deprecated
    +    private final ProxySelector proxy;
     
         /**
          * Gets URL of the InfluxDB server.
    @@ -213,12 +222,34 @@ public Boolean getDisableServerCertificateValidation() {
          * Gets the proxy.
          *
          * @return the proxy, may be null
    +     * Deprecated use {@link #proxyUrl}
          */
         @Nullable
    +    @Deprecated
         public ProxySelector getProxy() {
             return proxy;
         }
     
    +    /**
    +     * Gets the proxy url.
    +     *
    +     * @return the proxy url, may be null
    +     */
    +    @Nullable
    +    public String getProxyUrl() {
    +        return proxyUrl;
    +    }
    +
    +    /**
    +     * Gets certificates file path.
    +     *
    +     * @return the certificates file path, may be null
    +     */
    +    @Nullable
    +    public String sslRootsFilePath() {
    +        return sslRootsFilePath;
    +    }
    +
         /**
          * Gets the (proxy) authenticator.
          *
    @@ -269,17 +300,19 @@ public boolean equals(final Object o) {
                     && Objects.equals(allowHttpRedirects, that.allowHttpRedirects)
                     && Objects.equals(disableServerCertificateValidation, that.disableServerCertificateValidation)
                     && Objects.equals(proxy, that.proxy)
    +                && Objects.equals(proxyUrl, that.proxyUrl)
                     && Objects.equals(authenticator, that.authenticator)
    -                && Objects.equals(headers, that.headers);
    +                && Objects.equals(headers, that.headers)
    +                && Objects.equals(sslRootsFilePath, that.sslRootsFilePath);
         }
     
         @Override
         public int hashCode() {
             return Objects.hash(host, Arrays.hashCode(token), authScheme, organization,
    -          database, writePrecision, gzipThreshold,
    -          timeout, allowHttpRedirects, disableServerCertificateValidation,
    -          proxy, authenticator, headers,
    -          defaultTags);
    +                database, writePrecision, gzipThreshold,
    +                timeout, allowHttpRedirects, disableServerCertificateValidation,
    +                proxy, proxyUrl, authenticator, headers,
    +                defaultTags, sslRootsFilePath);
         }
     
         @Override
    @@ -294,9 +327,11 @@ public String toString() {
                     .add("allowHttpRedirects=" + allowHttpRedirects)
                     .add("disableServerCertificateValidation=" + disableServerCertificateValidation)
                     .add("proxy=" + proxy)
    +                .add("proxyUrl=" + proxyUrl)
                     .add("authenticator=" + authenticator)
                     .add("headers=" + headers)
                     .add("defaultTags=" + defaultTags)
    +                .add("sslRootsFilePath=" + sslRootsFilePath)
                     .toString();
         }
     
    @@ -318,8 +353,10 @@ public static final class Builder {
             private Boolean allowHttpRedirects;
             private Boolean disableServerCertificateValidation;
             private ProxySelector proxy;
    +        private String proxyUrl;
             private Authenticator authenticator;
             private Map headers;
    +        private String sslRootsFilePath;
     
             /**
              * Sets the URL of the InfluxDB server.
    @@ -472,6 +509,7 @@ public Builder disableServerCertificateValidation(@Nullable final Boolean disabl
              *
              * @param proxy Proxy selector.
              * @return this
    +         * Deprecated use {@link #proxyUrl}
              */
             @Nonnull
             public Builder proxy(@Nullable final ProxySelector proxy) {
    @@ -480,6 +518,19 @@ public Builder proxy(@Nullable final ProxySelector proxy) {
                 return this;
             }
     
    +        /**
    +         * Sets the proxy url. Default is 'null'.
    +         *
    +         * @param proxyUrl Proxy url.
    +         * @return this
    +         */
    +        @Nonnull
    +        public Builder proxyUrl(@Nullable final String proxyUrl) {
    +
    +            this.proxyUrl = proxyUrl;
    +            return this;
    +        }
    +
             /**
              * Sets the proxy authenticator. Default is 'null'.
              *
    @@ -498,7 +549,9 @@ public Builder authenticator(@Nullable final Authenticator authenticator) {
              * such as tracing headers. To add custom headers use following code:
              * 
              * ClientConfig config = new ClientConfig.Builder()
    -         *     .host("https://us-east-1-1.aws.cloud2.influxdata.com")
    +         *     .host("
    +         *         https://us-east-1-1.aws.cloud2.influxdata.com
    +         *         ")
              *     .token("my-token".toCharArray())
              *     .database("my-database")
              *     .headers(Map.of("X-Tracing-Id", "123"))
    @@ -523,6 +576,19 @@ public Builder headers(@Nullable final Map headers) {
                 return this;
             }
     
    +        /**
    +         * Sets certificate file path. Default is 'null'.
    +         *
    +         * @param sslRootsFilePath The certificate file path
    +         * @return this
    +         */
    +        @Nonnull
    +        public Builder sslRootsFilePath(@Nullable final String sslRootsFilePath) {
    +
    +            this.sslRootsFilePath = sslRootsFilePath;
    +            return this;
    +        }
    +
             /**
              * Build an instance of {@code ClientConfig}.
              *
    @@ -658,7 +724,9 @@ private ClientConfig(@Nonnull final Builder builder) {
             disableServerCertificateValidation = builder.disableServerCertificateValidation != null
                     ? builder.disableServerCertificateValidation : false;
             proxy = builder.proxy;
    +        proxyUrl = builder.proxyUrl;
             authenticator = builder.authenticator;
             headers = builder.headers;
    +        sslRootsFilePath = builder.sslRootsFilePath;
         }
     }
    diff --git a/src/main/java/com/influxdb/v3/client/internal/FlightSqlClient.java b/src/main/java/com/influxdb/v3/client/internal/FlightSqlClient.java
    index 5280550c..e1b37298 100644
    --- a/src/main/java/com/influxdb/v3/client/internal/FlightSqlClient.java
    +++ b/src/main/java/com/influxdb/v3/client/internal/FlightSqlClient.java
    @@ -21,6 +21,8 @@
      */
     package com.influxdb.v3.client.internal;
     
    +import java.io.FileInputStream;
    +import java.net.InetSocketAddress;
     import java.net.URI;
     import java.net.URISyntaxException;
     import java.nio.charset.StandardCharsets;
    @@ -39,22 +41,35 @@
     
     import com.fasterxml.jackson.core.JsonProcessingException;
     import com.fasterxml.jackson.databind.ObjectMapper;
    +import io.grpc.HttpConnectProxiedSocketAddress;
     import io.grpc.Metadata;
    +import io.grpc.ProxyDetector;
    +import io.grpc.netty.GrpcSslContexts;
    +import io.grpc.netty.NettyChannelBuilder;
    +import io.netty.handler.ssl.SslContext;
    +import io.netty.handler.ssl.SslContextBuilder;
    +import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
     import org.apache.arrow.flight.FlightClient;
    +import org.apache.arrow.flight.FlightGrpcUtils;
     import org.apache.arrow.flight.FlightStream;
     import org.apache.arrow.flight.HeaderCallOption;
     import org.apache.arrow.flight.Location;
    +import org.apache.arrow.flight.LocationSchemes;
     import org.apache.arrow.flight.Ticket;
     import org.apache.arrow.flight.grpc.MetadataAdapter;
     import org.apache.arrow.memory.RootAllocator;
     import org.apache.arrow.util.AutoCloseables;
     import org.apache.arrow.vector.VectorSchemaRoot;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
     
     import com.influxdb.v3.client.config.ClientConfig;
     import com.influxdb.v3.client.query.QueryType;
     
     final class FlightSqlClient implements AutoCloseable {
     
    +    private static final Logger LOG = LoggerFactory.getLogger(FlightSqlClient.class);
    +
         private final FlightClient client;
     
         private final Map defaultHeaders = new HashMap<>();
    @@ -83,8 +98,6 @@ final class FlightSqlClient implements AutoCloseable {
                 defaultHeaders.putAll(config.getHeaders());
             }
     
    -        Package mainPackage = RestClient.class.getPackage();
    -
             this.client = (client != null) ? client : createFlightClient(config);
         }
     
    @@ -130,11 +143,49 @@ public void close() throws Exception {
         private FlightClient createFlightClient(@Nonnull final ClientConfig config) {
             Location location = createLocation(config);
     
    -        return FlightClient.builder()
    -                .location(location)
    -                .allocator(new RootAllocator(Long.MAX_VALUE))
    -                .verifyServer(!config.getDisableServerCertificateValidation())
    -                .build();
    +        final NettyChannelBuilder nettyChannelBuilder = NettyChannelBuilder.forTarget(location.getUri().getHost());
    +
    +        if (LocationSchemes.GRPC_TLS.equals(location.getUri().getScheme())) {
    +            nettyChannelBuilder.useTransportSecurity();
    +
    +            SslContext nettySslContext = createNettySslContext(config);
    +            nettyChannelBuilder.sslContext(nettySslContext);
    +        } else {
    +            nettyChannelBuilder.usePlaintext();
    +        }
    +
    +        if (config.getProxyUrl() != null) {
    +            ProxyDetector proxyDetector = createProxyDetector(config.getHost(), config.getProxyUrl());
    +            nettyChannelBuilder.proxyDetector(proxyDetector);
    +        }
    +
    +        if (config.getProxy() != null) {
    +            LOG.warn("proxy property in ClientConfig will not work in query api, use proxyUrl property instead");
    +        }
    +
    +        nettyChannelBuilder.maxTraceEvents(0)
    +                .maxInboundMessageSize(Integer.MAX_VALUE)
    +                .maxInboundMetadataSize(Integer.MAX_VALUE);
    +
    +        return FlightGrpcUtils.createFlightClient(new RootAllocator(Long.MAX_VALUE), nettyChannelBuilder.build());
    +    }
    +
    +    @Nonnull
    +    SslContext createNettySslContext(@Nonnull final ClientConfig config) {
    +        try {
    +            SslContextBuilder sslContextBuilder;
    +            sslContextBuilder = GrpcSslContexts.forClient();
    +            if (config.getDisableServerCertificateValidation()) {
    +                sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE);
    +            } else if (config.sslRootsFilePath() != null) {
    +                try (FileInputStream fileInputStream = new FileInputStream(config.sslRootsFilePath())) {
    +                    sslContextBuilder.trustManager(fileInputStream);
    +                }
    +            }
    +            return sslContextBuilder.build();
    +        } catch (Exception e) {
    +            throw new RuntimeException(e);
    +        }
         }
     
         @Nonnull
    @@ -166,6 +217,22 @@ private HeaderCallOption metadataHeader(@Nonnull final Map reque
             return new HeaderCallOption(metadata);
         }
     
    +    ProxyDetector createProxyDetector(@Nonnull final String targetUrl, @Nonnull final String proxyUrl) {
    +        URI targetUri = URI.create(targetUrl);
    +        URI proxyUri = URI.create(proxyUrl);
    +        return (targetServerAddress) -> {
    +            InetSocketAddress targetAddress = (InetSocketAddress) targetServerAddress;
    +            if (targetUri.getHost().equals(targetAddress.getHostString())
    +                    && targetUri.getPort() == targetAddress.getPort()) {
    +                return HttpConnectProxiedSocketAddress.newBuilder()
    +                        .setProxyAddress(new InetSocketAddress(proxyUri.getHost(), proxyUri.getPort()))
    +                        .setTargetAddress(targetAddress)
    +                        .build();
    +            }
    +            return null;
    +        };
    +    }
    +
         private static final class FlightSqlIterator implements Iterator, AutoCloseable {
     
             private final List autoCloseable = new ArrayList<>();
    diff --git a/src/main/java/com/influxdb/v3/client/internal/RestClient.java b/src/main/java/com/influxdb/v3/client/internal/RestClient.java
    index 89607807..971abd24 100644
    --- a/src/main/java/com/influxdb/v3/client/internal/RestClient.java
    +++ b/src/main/java/com/influxdb/v3/client/internal/RestClient.java
    @@ -21,11 +21,19 @@
      */
     package com.influxdb.v3.client.internal;
     
    +import java.io.FileInputStream;
    +import java.net.InetSocketAddress;
    +import java.net.ProxySelector;
    +import java.net.URI;
     import java.net.URISyntaxException;
     import java.net.http.HttpClient;
     import java.net.http.HttpRequest;
     import java.net.http.HttpResponse;
    +import java.security.KeyStore;
     import java.security.SecureRandom;
    +import java.security.cert.Certificate;
    +import java.security.cert.CertificateFactory;
    +import java.util.ArrayList;
     import java.util.List;
     import java.util.Map;
     import java.util.stream.Stream;
    @@ -33,6 +41,7 @@
     import javax.annotation.Nullable;
     import javax.net.ssl.SSLContext;
     import javax.net.ssl.TrustManager;
    +import javax.net.ssl.TrustManagerFactory;
     import javax.net.ssl.X509TrustManager;
     
     import com.fasterxml.jackson.core.JsonProcessingException;
    @@ -97,8 +106,14 @@ public void checkServerTrusted(
             // default headers
             this.defaultHeaders = config.getHeaders() != null ? Map.copyOf(config.getHeaders()) : null;
     
    -        // proxy
    -        if (config.getProxy() != null) {
    +        if (config.getProxyUrl() != null) {
    +            URI proxyUri = URI.create(config.getProxyUrl());
    +            ProxySelector proxy = ProxySelector.of(new InetSocketAddress(proxyUri.getHost(), proxyUri.getPort()));
    +            builder.proxy(proxy);
    +            if (config.getAuthenticator() != null) {
    +                builder.authenticator(config.getAuthenticator());
    +            }
    +        } else if (config.getProxy() != null) {
                 builder.proxy(config.getProxy());
                 if (config.getAuthenticator() != null) {
                     builder.authenticator(config.getAuthenticator());
    @@ -107,11 +122,16 @@ public void checkServerTrusted(
     
             if (baseUrl.startsWith("https")) {
                 try {
    +                SSLContext sslContext = SSLContext.getInstance("TLS");
                     if (config.getDisableServerCertificateValidation()) {
    -                    SSLContext sslContext = SSLContext.getInstance("TLS");
                         sslContext.init(null, TRUST_ALL_CERTS, new SecureRandom());
    -                    builder.sslContext(sslContext);
    +                } else if (config.sslRootsFilePath() != null) {
    +                    X509TrustManager x509TrustManager = getX509TrustManagerFromFile(config.sslRootsFilePath());
    +                    sslContext.init(null, new X509TrustManager[]{x509TrustManager}, new SecureRandom());
    +                } else {
    +                    sslContext.init(null, null, new SecureRandom());
                     }
    +                builder.sslContext(sslContext);
                 } catch (Exception e) {
                     throw new RuntimeException(e);
                 }
    @@ -217,6 +237,38 @@ void request(@Nonnull final String path,
             }
         }
     
    +    private X509TrustManager getX509TrustManagerFromFile(@Nonnull final String filePath) {
    +        try {
    +            KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
    +            trustStore.load(null);
    +
    +            FileInputStream fis = new FileInputStream(filePath);
    +            List certificates = new ArrayList(
    +                    CertificateFactory.getInstance("X.509")
    +                            .generateCertificates(fis)
    +            );
    +
    +            for (int i = 0; i < certificates.size(); i++) {
    +                Certificate cert = certificates.get(i);
    +                trustStore.setCertificateEntry("alias" + i, cert);
    +            }
    +
    +            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
    +                    TrustManagerFactory.getDefaultAlgorithm()
    +            );
    +            trustManagerFactory.init(trustStore);
    +            X509TrustManager x509TrustManager = null;
    +            for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
    +                if (trustManager instanceof X509TrustManager) {
    +                    x509TrustManager = (X509TrustManager) trustManager;
    +                }
    +            }
    +            return x509TrustManager;
    +        } catch (Exception e) {
    +            throw new RuntimeException(e);
    +        }
    +    }
    +
         @Override
         public void close() {
         }
    diff --git a/src/test/java/com/influxdb/v3/client/InfluxDBClientTest.java b/src/test/java/com/influxdb/v3/client/InfluxDBClientTest.java
    index f1e868a2..e1f1667f 100644
    --- a/src/test/java/com/influxdb/v3/client/InfluxDBClientTest.java
    +++ b/src/test/java/com/influxdb/v3/client/InfluxDBClientTest.java
    @@ -21,22 +21,34 @@
      */
     package com.influxdb.v3.client;
     
    -import java.math.BigInteger;
    -import java.time.Instant;
     import java.util.Map;
     import java.util.Properties;
    -import java.util.UUID;
    -import java.util.stream.Stream;
     
     import org.assertj.core.api.Assertions;
     import org.junit.jupiter.api.Test;
    -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
     
    -import com.influxdb.v3.client.write.WriteOptions;
    -import com.influxdb.v3.client.write.WritePrecision;
    +import com.influxdb.v3.client.config.ClientConfig;
     
     public class InfluxDBClientTest {
     
    +    @Test
    +    void withProxyUrl() {
    +        String proxyUrl = "http://localhost:10000";
    +        ClientConfig.Builder builder = new ClientConfig.Builder();
    +        builder.proxyUrl(proxyUrl);
    +        ClientConfig clientConfig = builder.build();
    +        Assertions.assertThat(clientConfig.getProxyUrl()).isEqualTo(proxyUrl);
    +    }
    +
    +    @Test
    +    void withSslRootsFilePath() {
    +        String path = "/path/to/cert";
    +        ClientConfig.Builder builder = new ClientConfig.Builder();
    +        builder.sslRootsFilePath(path);
    +        ClientConfig clientConfig = builder.build();
    +        Assertions.assertThat(clientConfig.sslRootsFilePath()).isEqualTo(path);
    +    }
    +
         @Test
         void requiredHost() {
     
    @@ -125,52 +137,4 @@ public void unsupportedQueryParams() throws Exception {
             }
         }
     
    -    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_URL", matches = ".*")
    -    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_TOKEN", matches = ".*")
    -    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_DATABASE", matches = ".*")
    -    @Test
    -    public void testQuery() throws Exception {
    -        try (InfluxDBClient client = InfluxDBClient.getInstance(
    -                System.getenv("TESTING_INFLUXDB_URL"),
    -                System.getenv("TESTING_INFLUXDB_TOKEN").toCharArray(),
    -                System.getenv("TESTING_INFLUXDB_DATABASE"),
    -                null)) {
    -            String uuid = UUID.randomUUID().toString();
    -            long timestamp = Instant.now().getEpochSecond();
    -            String record = String.format(
    -                    "host10,tag=empty "
    -                            + "name=\"intel\","
    -                            + "mem_total=2048,"
    -                            + "disk_free=100i,"
    -                            + "temperature=100.86,"
    -                            + "isActive=true,"
    -                            + "testId=\"%s\" %d",
    -                    uuid,
    -                    timestamp
    -            );
    -            client.writeRecord(record, new WriteOptions(null, WritePrecision.S, null));
    -
    -            Map parameters = Map.of("testId", uuid);
    -            String sql = "Select * from host10 where \"testId\"=$testId";
    -            try (Stream stream = client.query(sql, parameters)) {
    -                stream.findFirst()
    -                      .ifPresent(objects -> {
    -                          Assertions.assertThat(objects[0].getClass()).isEqualTo(Long.class);
    -                          Assertions.assertThat(objects[0]).isEqualTo(100L);
    -
    -                          Assertions.assertThat(objects[1].getClass()).isEqualTo(Boolean.class);
    -                          Assertions.assertThat(objects[1]).isEqualTo(true);
    -
    -                          Assertions.assertThat(objects[2].getClass()).isEqualTo(Double.class);
    -                          Assertions.assertThat(objects[2]).isEqualTo(2048.0);
    -
    -                          Assertions.assertThat(objects[3].getClass()).isEqualTo(String.class);
    -                          Assertions.assertThat(objects[3]).isEqualTo("intel");
    -
    -                          Assertions.assertThat(objects[7].getClass()).isEqualTo(BigInteger.class);
    -                          Assertions.assertThat(objects[7]).isEqualTo(BigInteger.valueOf(timestamp * 1_000_000_000));
    -                      });
    -            }
    -        }
    -    }
     }
    diff --git a/src/test/java/com/influxdb/v3/client/integration/E2ETest.java b/src/test/java/com/influxdb/v3/client/integration/E2ETest.java
    new file mode 100644
    index 00000000..0eff6cdc
    --- /dev/null
    +++ b/src/test/java/com/influxdb/v3/client/integration/E2ETest.java
    @@ -0,0 +1,210 @@
    +/*
    + * The MIT License
    + *
    + * Permission is hereby granted, free of charge, to any person obtaining a copy
    + * of this software and associated documentation files (the "Software"), to deal
    + * in the Software without restriction, including without limitation the rights
    + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    + * copies of the Software, and to permit persons to whom the Software is
    + * furnished to do so, subject to the following conditions:
    + *
    + * The above copyright notice and this permission notice shall be included in
    + * all copies or substantial portions of the Software.
    + *
    + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    + * THE SOFTWARE.
    + */
    +package com.influxdb.v3.client.integration;
    +
    +import java.math.BigInteger;
    +import java.net.ConnectException;
    +import java.net.URL;
    +import java.net.URLConnection;
    +import java.time.Instant;
    +import java.util.Map;
    +import java.util.UUID;
    +import java.util.logging.Logger;
    +import java.util.stream.Stream;
    +import javax.annotation.Nonnull;
    +
    +import org.assertj.core.api.Assertions;
    +import org.junit.jupiter.api.Test;
    +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
    +
    +import com.influxdb.v3.client.InfluxDBClient;
    +import com.influxdb.v3.client.Point;
    +import com.influxdb.v3.client.PointValues;
    +import com.influxdb.v3.client.config.ClientConfig;
    +import com.influxdb.v3.client.write.WriteOptions;
    +import com.influxdb.v3.client.write.WritePrecision;
    +
    +import static org.junit.jupiter.api.Assumptions.assumeFalse;
    +
    +public class E2ETest {
    +
    +    private static final java.util.logging.Logger LOG = Logger.getLogger(E2ETest.class.getName());
    +
    +    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_URL", matches = ".*")
    +    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_TOKEN", matches = ".*")
    +    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_DATABASE", matches = ".*")
    +    @Test
    +    void testQueryWithProxy() {
    +        String proxyUrl = "http://localhost:10000";
    +
    +        try {
    +            // Continue to run this test only if Envoy proxy is running on this address http://localhost:10000
    +            String url = String.format("http://%s:%d", "localhost", 10000);
    +            URLConnection hpCon = new URL(url).openConnection();
    +            hpCon.connect();
    +        } catch (Exception e) {
    +            if (e instanceof ConnectException && e.getMessage().contains("Connection refused")) {
    +                LOG.warning("Tests with proxy have been skipped because no proxy is running on " + proxyUrl);
    +                assumeFalse(e.getMessage().contains("Connection refused"));
    +                return;
    +            }
    +        }
    +
    +        ClientConfig clientConfig = new ClientConfig.Builder()
    +                .host(System.getenv("TESTING_INFLUXDB_URL"))
    +                .token(System.getenv("TESTING_INFLUXDB_TOKEN").toCharArray())
    +                .database(System.getenv("TESTING_INFLUXDB_DATABASE"))
    +                .proxyUrl(proxyUrl)
    +                .build();
    +
    +        InfluxDBClient influxDBClient = InfluxDBClient.getInstance(clientConfig);
    +        influxDBClient.writePoint(
    +                Point.measurement("test1")
    +                        .setField("field", "field1")
    +        );
    +
    +        try (Stream stream = influxDBClient.queryPoints("SELECT * FROM test1")) {
    +            stream.findFirst()
    +                    .ifPresent(pointValues -> {
    +                        Assertions.assertThat(pointValues.getField("field")).isEqualTo("field1");
    +                    });
    +        }
    +    }
    +
    +    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_URL", matches = ".*")
    +    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_TOKEN", matches = ".*")
    +    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_DATABASE", matches = ".*")
    +    @Test
    +    void correctSslCertificates() throws Exception {
    +        // This is real certificate downloaded from https://cloud2.influxdata.com
    +        String influxDBcertificateFile = "src/test/java/com/influxdb/v3/client/testdata/influxdb-certificate.pem";
    +
    +        ClientConfig clientConfig = new ClientConfig.Builder()
    +                .host(System.getenv("TESTING_INFLUXDB_URL"))
    +                .token(System.getenv("TESTING_INFLUXDB_TOKEN").toCharArray())
    +                .database(System.getenv("TESTING_INFLUXDB_DATABASE"))
    +                .sslRootsFilePath(influxDBcertificateFile)
    +                .build();
    +        InfluxDBClient influxDBClient = InfluxDBClient.getInstance(clientConfig);
    +        assertGetDataSuccess(influxDBClient);
    +    }
    +
    +    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_URL", matches = ".*")
    +    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_TOKEN", matches = ".*")
    +    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_DATABASE", matches = ".*")
    +    @Test
    +    void wrongSslCertificate() {
    +        String certificateFile = "src/test/java/com/influxdb/v3/client/testdata/docker.com.pem";
    +
    +        ClientConfig clientConfig = new ClientConfig.Builder()
    +                .host(System.getenv("TESTING_INFLUXDB_URL"))
    +                .token(System.getenv("TESTING_INFLUXDB_TOKEN").toCharArray())
    +                .database(System.getenv("TESTING_INFLUXDB_DATABASE"))
    +                .sslRootsFilePath(certificateFile)
    +                .build();
    +        InfluxDBClient influxDBClient = InfluxDBClient.getInstance(clientConfig);
    +        Assertions.assertThatThrownBy(() -> assertGetDataSuccess(influxDBClient))
    +                .isInstanceOf(Exception.class);
    +    }
    +
    +    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_URL", matches = ".*")
    +    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_TOKEN", matches = ".*")
    +    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_DATABASE", matches = ".*")
    +    @Test
    +    void disableServerCertificateValidation() {
    +        String wrongCertificateFile = "src/test/java/com/influxdb/v3/client/testdata/docker.com.pem";
    +
    +        ClientConfig clientConfig = new ClientConfig.Builder()
    +                .host(System.getenv("TESTING_INFLUXDB_URL"))
    +                .token(System.getenv("TESTING_INFLUXDB_TOKEN").toCharArray())
    +                .database(System.getenv("TESTING_INFLUXDB_DATABASE"))
    +                .disableServerCertificateValidation(true)
    +                .sslRootsFilePath(wrongCertificateFile)
    +                .build();
    +
    +        // Test succeeded with wrong certificate file because disableServerCertificateValidation is true
    +        InfluxDBClient influxDBClient = InfluxDBClient.getInstance(clientConfig);
    +        assertGetDataSuccess(influxDBClient);
    +    }
    +
    +    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_URL", matches = ".*")
    +    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_TOKEN", matches = ".*")
    +    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_DATABASE", matches = ".*")
    +    @Test
    +    public void testQuery() throws Exception {
    +        try (InfluxDBClient client = InfluxDBClient.getInstance(
    +                System.getenv("TESTING_INFLUXDB_URL"),
    +                System.getenv("TESTING_INFLUXDB_TOKEN").toCharArray(),
    +                System.getenv("TESTING_INFLUXDB_DATABASE"),
    +                null)) {
    +            String uuid = UUID.randomUUID().toString();
    +            long timestamp = Instant.now().getEpochSecond();
    +            String record = String.format(
    +                    "host10,tag=empty "
    +                            + "name=\"intel\","
    +                            + "mem_total=2048,"
    +                            + "disk_free=100i,"
    +                            + "temperature=100.86,"
    +                            + "isActive=true,"
    +                            + "testId=\"%s\" %d",
    +                    uuid,
    +                    timestamp
    +            );
    +            client.writeRecord(record, new WriteOptions(null, WritePrecision.S, null));
    +
    +            Map parameters = Map.of("testId", uuid);
    +            String sql = "Select * from host10 where \"testId\"=$testId";
    +            try (Stream stream = client.query(sql, parameters)) {
    +                stream.findFirst()
    +                        .ifPresent(objects -> {
    +                            Assertions.assertThat(objects[0].getClass()).isEqualTo(Long.class);
    +                            Assertions.assertThat(objects[0]).isEqualTo(100L);
    +
    +                            Assertions.assertThat(objects[1].getClass()).isEqualTo(Boolean.class);
    +                            Assertions.assertThat(objects[1]).isEqualTo(true);
    +
    +                            Assertions.assertThat(objects[2].getClass()).isEqualTo(Double.class);
    +                            Assertions.assertThat(objects[2]).isEqualTo(2048.0);
    +
    +                            Assertions.assertThat(objects[3].getClass()).isEqualTo(String.class);
    +                            Assertions.assertThat(objects[3]).isEqualTo("intel");
    +
    +                            Assertions.assertThat(objects[7].getClass()).isEqualTo(BigInteger.class);
    +                            Assertions.assertThat(objects[7]).isEqualTo(BigInteger.valueOf(timestamp * 1_000_000_000));
    +                        });
    +            }
    +        }
    +    }
    +
    +    private void assertGetDataSuccess(@Nonnull final InfluxDBClient influxDBClient) {
    +        influxDBClient.writePoint(
    +                Point.measurement("test1")
    +                        .setField("field", "field1")
    +        );
    +        try (Stream stream = influxDBClient.queryPoints("SELECT * FROM test1")) {
    +            stream.findFirst()
    +                    .ifPresent(pointValues -> {
    +                        Assertions.assertThat(pointValues.getField("field")).isEqualTo("field1");
    +                    });
    +        }
    +    }
    +}
    diff --git a/src/test/java/com/influxdb/v3/client/internal/FlightSqlClientTest.java b/src/test/java/com/influxdb/v3/client/internal/FlightSqlClientTest.java
    index 19e0ea0b..84a609ad 100644
    --- a/src/test/java/com/influxdb/v3/client/internal/FlightSqlClientTest.java
    +++ b/src/test/java/com/influxdb/v3/client/internal/FlightSqlClientTest.java
    @@ -21,9 +21,12 @@
      */
     package com.influxdb.v3.client.internal;
     
    +import java.net.InetSocketAddress;
     import java.net.URISyntaxException;
     import java.util.Map;
     
    +import io.grpc.HttpConnectProxiedSocketAddress;
    +import io.grpc.ProxyDetector;
     import io.grpc.internal.GrpcUtil;
     import org.apache.arrow.flight.CallHeaders;
     import org.apache.arrow.flight.CallInfo;
    @@ -83,6 +86,31 @@ void tearDown() throws Exception {
             }
         }
     
    +    @Test
    +    void flightSqlClient() throws Exception {
    +        String correctHost = "grpc+unix://tmp/dummy.sock";
    +        ClientConfig clientConfig = new ClientConfig.Builder()
    +                .host(correctHost)
    +                .token("Token".toCharArray())
    +                .build();
    +        try (FlightSqlClient flightSqlClient = new FlightSqlClient(clientConfig)) {
    +            Assertions.assertThat(flightSqlClient).isNotNull();
    +        }
    +
    +        FlightClient.Builder builder = FlightClient.builder(allocator, server.getLocation());
    +        try (FlightClient flightClient = builder.build()) {
    +            FlightSqlClient flightSqlClient = new FlightSqlClient(clientConfig, flightClient);
    +            Assertions.assertThat(flightSqlClient).isNotNull();
    +        }
    +
    +        var inCorrectHost = "grpc+unix://///tmp/dummy.sock";
    +        ClientConfig clientConfig1 = new ClientConfig.Builder()
    +                .host(inCorrectHost)
    +                .token("Token".toCharArray())
    +                .build();
    +        Assertions.assertThatThrownBy(() -> new FlightSqlClient(clientConfig1));
    +    }
    +
         @Test
         public void invalidHost() {
             ClientConfig clientConfig = new ClientConfig.Builder()
    @@ -272,6 +300,31 @@ public void useParamsFromQueryConfig() throws Exception {
             }
         }
     
    +    @Test
    +    void createProxyDetector() {
    +        String targetUrl = "https://localhost:80";
    +        ClientConfig clientConfig = new ClientConfig.Builder()
    +                .host(targetUrl)
    +                .build();
    +        try (FlightSqlClient flightSqlClient = new FlightSqlClient(clientConfig)) {
    +            String proxyUrl = "http://localhost:10000";
    +            ProxyDetector proxyDetector = flightSqlClient.createProxyDetector(targetUrl, proxyUrl);
    +            Assertions.assertThat(proxyDetector.proxyFor(
    +                    new InetSocketAddress("localhost", 80)
    +            )).isEqualTo(HttpConnectProxiedSocketAddress.newBuilder()
    +                    .setProxyAddress(new InetSocketAddress("localhost", 10000))
    +                    .setTargetAddress(new InetSocketAddress("localhost", 80))
    +                    .build());
    +
    +            // Return null case
    +            Assertions.assertThat(proxyDetector.proxyFor(
    +                    new InetSocketAddress("123.2.3.1", 80)
    +            )).isNull();
    +        } catch (Exception e) {
    +            throw new RuntimeException(e);
    +        }
    +    }
    +
         static class CallHeadersMiddleware implements FlightClientMiddleware.Factory {
             CallHeaders headers;
     
    diff --git a/src/test/java/com/influxdb/v3/client/internal/RestClientTest.java b/src/test/java/com/influxdb/v3/client/internal/RestClientTest.java
    index bd53a3ce..47f30acc 100644
    --- a/src/test/java/com/influxdb/v3/client/internal/RestClientTest.java
    +++ b/src/test/java/com/influxdb/v3/client/internal/RestClientTest.java
    @@ -291,6 +291,24 @@ public void proxy() throws InterruptedException {
             Assertions.assertThat(recordedRequest.getRequestLine()).isEqualTo("GET http://foo.com:8086/ping HTTP/1.1");
         }
     
    +    @Test
    +    public void proxyUrl() throws InterruptedException {
    +        mockServer.enqueue(createResponse(200));
    +
    +        restClient = new RestClient(new ClientConfig.Builder()
    +                .host("http://foo.com:8086")
    +                .proxyUrl(String.format("http://%s:%d", mockServer.getHostName(), mockServer.getPort()))
    +                .build());
    +
    +        restClient.request("ping", HttpMethod.GET, null, null, null);
    +
    +        RecordedRequest recordedRequest = mockServer.takeRequest();
    +
    +        Assertions.assertThat(recordedRequest.getRequestUrl()).isNotNull();
    +        Assertions.assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(baseURL); // server is used as proxy
    +        Assertions.assertThat(recordedRequest.getRequestLine()).isEqualTo("GET http://foo.com:8086/ping HTTP/1.1");
    +    }
    +
         @Test
         public void proxyWithAuthentication() throws InterruptedException {
             mockServer.enqueue(createResponseWithHeaders(407, Map.of("Proxy-Authenticate", "Basic")));
    @@ -298,7 +316,7 @@ public void proxyWithAuthentication() throws InterruptedException {
     
             restClient = new RestClient(new ClientConfig.Builder()
                     .host("http://foo.com:8086")
    -                .proxy(ProxySelector.of((InetSocketAddress) mockServer.toProxyAddress().address()))
    +                .proxyUrl(String.format("http://%s:%d", mockServer.getHostName(), mockServer.getPort()))
                     .authenticator(new Authenticator() {
                         @Override
                         protected PasswordAuthentication getPasswordAuthentication() {
    diff --git a/src/test/java/com/influxdb/v3/client/testdata/docker.com.pem b/src/test/java/com/influxdb/v3/client/testdata/docker.com.pem
    new file mode 100644
    index 00000000..ac9ed44c
    --- /dev/null
    +++ b/src/test/java/com/influxdb/v3/client/testdata/docker.com.pem
    @@ -0,0 +1,33 @@
    +-----BEGIN CERTIFICATE-----
    +MIIFxDCCBKygAwIBAgIQDE0Qon/WHglZ2jXwZzD0xjANBgkqhkiG9w0BAQsFADA8
    +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRwwGgYDVQQDExNBbWF6b24g
    +UlNBIDIwNDggTTAyMB4XDTI0MDgxODAwMDAwMFoXDTI1MDkxNjIzNTk1OVowFzEV
    +MBMGA1UEAwwMKi5kb2NrZXIuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
    +CgKCAQEAytONEwLLBXfyepD/iE7N76+xGDWE/7g21rpPRejCobpaKGVKSL4Y8Pf9
    +whjh5pzBLxcpQMpYcg/oW+Cp4scjhXyi9yqrOC2Vf26DRA3ufjbescZUjPP28mPO
    +N1gGQhnr0Sa7mbhNo5JVE7yxLrjhAZFCdEpl1LYdfosYxeBowVOaxBsGfUWCxYuI
    +HbolTUUNWJKaAN7knKmrHF0a2a6BftaTyFK/6N1FV3rXs5oD+5DEYVFN8193fdz5
    +DUcf5p7xzjx9yXmHfdomznUPL5Sja2FSigH+Gm6EG3cBKylVCpafwQhbYbpdkP12
    +p7KtsPGUWIwBAyRr1AUqx0ceIafa9wIDAQABo4IC5TCCAuEwHwYDVR0jBBgwFoAU
    +wDFSzVpQw4J8dHHOy+mc+XrrguIwHQYDVR0OBBYEFFgCnhEuuG5ptVcvPfBJZssU
    +U6jFMBcGA1UdEQQQMA6CDCouZG9ja2VyLmNvbTATBgNVHSAEDDAKMAgGBmeBDAEC
    +ATAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC
    +MDsGA1UdHwQ0MDIwMKAuoCyGKmh0dHA6Ly9jcmwucjJtMDIuYW1hem9udHJ1c3Qu
    +Y29tL3IybTAyLmNybDB1BggrBgEFBQcBAQRpMGcwLQYIKwYBBQUHMAGGIWh0dHA6
    +Ly9vY3NwLnIybTAyLmFtYXpvbnRydXN0LmNvbTA2BggrBgEFBQcwAoYqaHR0cDov
    +L2NydC5yMm0wMi5hbWF6b250cnVzdC5jb20vcjJtMDIuY2VyMAwGA1UdEwEB/wQC
    +MAAwggF+BgorBgEEAdZ5AgQCBIIBbgSCAWoBaAB3AN3cyjSV1+EWBeeVMvrHn/g9
    +HFDf2wA6FBJ2Ciysu8gqAAABkWTCKI4AAAQDAEgwRgIhALzND2kn+PR4aObDiWGv
    +7QJZ/z4i/vhAGjd/vp2hjt4HAiEAytgsAcPUFd/Y98n5EQkMg6GBSUUygBMc7+Xk
    +UVJXFgcAdQDm0jFjQHeMwRBBBtdxuc7B0kD2loSG+7qHMh39HjeOUAAAAZFkwiic
    +AAAEAwBGMEQCIGXbP5ughe+EksczYEErOqY1LZHi9/SmXcp6/vAeuXBmAiB16Sw7
    +1sPB/CpKq75/pEwb5pamTdmeEIaHtGQTkTMZ5QB2AMz7D2qFcQll/pWbU87psnwi
    +6YVcDZeNtql+VMD+TA2wAAABkWTCKIoAAAQDAEcwRQIgX2WsD3ThDwalMCYqmf+X
    +ICS9imyYmwKXJbfkSnMH/HkCIQCEkDrOAJZG1fGfyOzhSPHWPMxbuGwV18jBGe52
    +D9PPQTANBgkqhkiG9w0BAQsFAAOCAQEAcW8Z3+JzqfGTYX+MBk2pQvo8msx+fINU
    +ZzORUcFUNV0467G/Kc780S8GRxF8dr90WaFctMvw8yDOipfj0sacGJFVFFf5XoWu
    +1EqYyx3hgMLCQ5DzjAoY4X1KsfPRBe2DCsqf+Nt/TVzgVjOSglURBWKV2T+Av78H
    +HiMjtEUZbatvuKvUg5S26dvUxXseN/8Jbbt9MCCGascPOf+zXGysoBSwglt0Nz7W
    +hjXnY+BPeLD38ouUGx0R+sZbXICR6BIxtT4GgOcmJJ/KT3fcMObhsihiq6lO1aql
    +Yo4AvWpg2tJtwFrG4ooDRyyARljHMUMIkuTz9Dypl/dnJJfzAQLjVA==
    +-----END CERTIFICATE-----
    diff --git a/src/test/java/com/influxdb/v3/client/testdata/influxdb-certificate.pem b/src/test/java/com/influxdb/v3/client/testdata/influxdb-certificate.pem
    new file mode 100644
    index 00000000..b85c8037
    --- /dev/null
    +++ b/src/test/java/com/influxdb/v3/client/testdata/influxdb-certificate.pem
    @@ -0,0 +1,31 @@
    +-----BEGIN CERTIFICATE-----
    +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
    +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
    +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
    +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
    +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
    +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
    +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
    +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
    +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
    +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
    +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
    +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
    +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
    +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
    +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
    +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
    +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
    +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
    +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
    +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
    +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
    +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
    +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
    +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
    +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
    +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
    +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
    +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
    +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
    +-----END CERTIFICATE-----