diff --git a/.circleci/config.yml b/.circleci/config.yml index 3dca04ff..6862a72b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -18,6 +18,14 @@ jobs: keys: - &cache-key maven-cache_v1-<< parameters.maven-image >>-{{ checksum "pom.xml" }} - maven-cache_v3-<< parameters.maven-image >>- + - run: + name: "Run Envoy" + command: | + wget -O- https://apt.envoyproxy.io/signing.key | sudo gpg --dearmor -o /etc/apt/keyrings/envoy-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/envoy-keyring.gpg] https://apt.envoyproxy.io focal main" | sudo tee /etc/apt/sources.list.d/envoy.list + sudo apt-get update + sudo apt-get install envoy + envoy -c .circleci/envoy.yaml - run: name: "Running tests" command: | diff --git a/.circleci/envoy.yaml b/.circleci/envoy.yaml new file mode 100644 index 00000000..ab539c73 --- /dev/null +++ b/.circleci/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/CHANGELOG.md b/CHANGELOG.md index 48211487..48ed94b0 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 ssl + ## 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..dd43333b --- /dev/null +++ b/examples/src/main/java/com/influxdb/v3/ProxyExample.java @@ -0,0 +1,62 @@ +/* + * 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.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://127.0.0.1:10000"; + String certificateFilePath = "src/test/java/com/influxdb/v3/client/testdata/valid-certificates.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")) + .proxyUrl(proxyUrl) + .certificateFilePath(certificateFilePath) + .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"); + }); + } + } +} + 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..a2df4a6d 100644 --- a/src/main/java/com/influxdb/v3/client/config/ClientConfig.java +++ b/src/main/java/com/influxdb/v3/client/config/ClientConfig.java @@ -23,7 +23,6 @@ import java.net.Authenticator; import java.net.MalformedURLException; -import java.net.ProxySelector; import java.net.URL; import java.time.Duration; import java.util.Arrays; @@ -57,15 +56,19 @@ *
  • disableServerCertificateValidation - * disable server certificate validation for HTTPS connections *
  • - *
  • proxy - HTTP proxy selector
  • + *
  • proxyUrl - Proxy url for query api and write api
  • + *
  • queryApiProxy - HTTP query detector
  • *
  • authenticator - HTTP proxy authenticator
  • *
  • headers - headers to be added to requests
  • + *
  • certificateFilePath - Path to the stored certificates file
  • * *

    * 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)
    @@ -84,7 +87,8 @@
      * Immutable class.
      */
     public final class ClientConfig {
    -
    +    //todo check if main use proxySelector for backward compality
    +    //todo check comments
         private final String host;
         private final char[] token;
         private final String authScheme;
    @@ -96,9 +100,10 @@ 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 certificateFilePath;
     
         /**
          * Gets URL of the InfluxDB server.
    @@ -210,13 +215,23 @@ public Boolean getDisableServerCertificateValidation() {
         }
     
         /**
    -     * Gets the proxy.
    +     * Gets the proxy url.
    +     *
    +     * @return the proxy url, may be null
    +     */
    +    @Nullable
    +    public String getProxyUrl() {
    +        return proxyUrl;
    +    }
    +
    +    /**
    +     * Gets certificates file path.
          *
    -     * @return the proxy, may be null
    +     * @return the certificates file path, may be null
          */
         @Nullable
    -    public ProxySelector getProxy() {
    -        return proxy;
    +    public String certificateFilePath() {
    +        return certificateFilePath;
         }
     
         /**
    @@ -268,18 +283,19 @@ public boolean equals(final Object o) {
                     && Objects.equals(timeout, that.timeout)
                     && 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(certificateFilePath, that.certificateFilePath);
         }
     
         @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,
    +                proxyUrl, authenticator, headers,
    +                defaultTags, certificateFilePath);
         }
     
         @Override
    @@ -293,10 +309,11 @@ public String toString() {
                     .add("timeout=" + timeout)
                     .add("allowHttpRedirects=" + allowHttpRedirects)
                     .add("disableServerCertificateValidation=" + disableServerCertificateValidation)
    -                .add("proxy=" + proxy)
    +                .add("proxy=" + proxyUrl)
                     .add("authenticator=" + authenticator)
                     .add("headers=" + headers)
                     .add("defaultTags=" + defaultTags)
    +                .add("certificateFilePath=" + certificateFilePath)
                     .toString();
         }
     
    @@ -317,9 +334,10 @@ public static final class Builder {
             private Duration timeout;
             private Boolean allowHttpRedirects;
             private Boolean disableServerCertificateValidation;
    -        private ProxySelector proxy;
    +        private String proxyUrl;
             private Authenticator authenticator;
             private Map headers;
    +        private String certificateFilePath;
     
             /**
              * Sets the URL of the InfluxDB server.
    @@ -468,15 +486,15 @@ public Builder disableServerCertificateValidation(@Nullable final Boolean disabl
             }
     
             /**
    -         * Sets the proxy selector. Default is 'null'.
    +         * Sets the proxy url. Default is 'null'.
              *
    -         * @param proxy Proxy selector.
    +         * @param proxyUrl Proxy url.
              * @return this
              */
             @Nonnull
    -        public Builder proxy(@Nullable final ProxySelector proxy) {
    +        public Builder proxyUrl(@Nullable final String proxyUrl) {
     
    -            this.proxy = proxy;
    +            this.proxyUrl = proxyUrl;
                 return this;
             }
     
    @@ -498,7 +516,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 +543,19 @@ public Builder headers(@Nullable final Map headers) {
                 return this;
             }
     
    +        /**
    +         * Sets certificate file path. Default is 'null'.
    +         *
    +         * @param certificateFilePath The certificate file path
    +         * @return this
    +         */
    +        @Nonnull
    +        public Builder certificateFilePath(@Nullable final String certificateFilePath) {
    +
    +            this.certificateFilePath = certificateFilePath;
    +            return this;
    +        }
    +
             /**
              * Build an instance of {@code ClientConfig}.
              *
    @@ -657,8 +690,9 @@ private ClientConfig(@Nonnull final Builder builder) {
             allowHttpRedirects = builder.allowHttpRedirects != null ? builder.allowHttpRedirects : false;
             disableServerCertificateValidation = builder.disableServerCertificateValidation != null
                     ? builder.disableServerCertificateValidation : false;
    -        proxy = builder.proxy;
    +        proxyUrl = builder.proxyUrl;
             authenticator = builder.authenticator;
             headers = builder.headers;
    +        certificateFilePath = builder.certificateFilePath;
         }
     }
    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..ed1769d9 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,10 @@
      */
     package com.influxdb.v3.client.internal;
     
    +import java.io.FileInputStream;
    +import java.io.IOException;
    +import java.lang.reflect.InvocationTargetException;
    +import java.net.InetSocketAddress;
     import java.net.URI;
     import java.net.URISyntaxException;
     import java.nio.charset.StandardCharsets;
    @@ -36,14 +40,25 @@
     import java.util.stream.StreamSupport;
     import javax.annotation.Nonnull;
     import javax.annotation.Nullable;
    +import javax.net.ssl.SSLException;
     
     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.channel.EventLoopGroup;
    +import io.netty.channel.ServerChannel;
    +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;
    @@ -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,59 @@ 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());
    +        var validSchemas = List.of(
    +                LocationSchemes.GRPC,
    +                LocationSchemes.GRPC_INSECURE,
    +                LocationSchemes.GRPC_TLS,
    +                LocationSchemes.GRPC_DOMAIN_SOCKET
    +        );
    +        if (!validSchemas.contains(location.getUri().getScheme())) {
    +            throw new IllegalArgumentException(
    +                    "Scheme is not supported: " + location.getUri().getScheme());
    +        }
    +
    +        if (location.getUri().getScheme().equals(LocationSchemes.GRPC_DOMAIN_SOCKET)) {
    +            setChannelTypeAndEventLoop(nettyChannelBuilder);
    +        }
    +
    +        if (LocationSchemes.GRPC_TLS.equals(location.getUri().getScheme())) {
    +            nettyChannelBuilder.useTransportSecurity();
    +
    +            SslContextBuilder sslContextBuilder;
    +            sslContextBuilder = GrpcSslContexts.forClient();
    +            if (!config.getDisableServerCertificateValidation()) {
    +                if (config.certificateFilePath() != null) {
    +                    try (FileInputStream fileInputStream = new FileInputStream(config.certificateFilePath())) {
    +                        sslContextBuilder.trustManager(fileInputStream);
    +                    } catch (IOException e) {
    +                        throw new RuntimeException(e);
    +                    }
    +                }
    +            } else {
    +                sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE);
    +            }
    +
    +            try {
    +                nettyChannelBuilder.sslContext(sslContextBuilder.build());
    +            } catch (SSLException e) {
    +                throw new RuntimeException(e);
    +            }
    +
    +        } else {
    +            nettyChannelBuilder.usePlaintext();
    +        }
    +
    +        if (config.getProxyUrl() != null) {
    +            ProxyDetector proxyDetector = createProxyDetector(config.getProxyUrl());
    +            nettyChannelBuilder.proxyDetector(proxyDetector);
    +        }
    +
    +        nettyChannelBuilder.maxTraceEvents(0)
    +                .maxInboundMessageSize(Integer.MAX_VALUE)
    +                .maxInboundMetadataSize(Integer.MAX_VALUE);
    +
    +        return FlightGrpcUtils.createFlightClient(new RootAllocator(Long.MAX_VALUE), nettyChannelBuilder.build());
         }
     
         @Nonnull
    @@ -166,6 +227,56 @@ private HeaderCallOption metadataHeader(@Nonnull final Map reque
             return new HeaderCallOption(metadata);
         }
     
    +    private void setChannelTypeAndEventLoop(@Nonnull final NettyChannelBuilder nettyChannelBuilder) {
    +        // The implementation is platform-specific, so we have to find the classes at runtime
    +        try {
    +            try {
    +                // Linux
    +                nettyChannelBuilder.channelType(
    +                        Class.forName("io.netty.channel.epoll.EpollDomainSocketChannel")
    +                                .asSubclass(ServerChannel.class));
    +                final EventLoopGroup elg =
    +                        Class.forName("io.netty.channel.epoll.EpollEventLoopGroup")
    +                                .asSubclass(EventLoopGroup.class)
    +                                .getDeclaredConstructor()
    +                                .newInstance();
    +                nettyChannelBuilder.eventLoopGroup(elg);
    +            } catch (ClassNotFoundException e) {
    +                // BSD
    +                nettyChannelBuilder.channelType(
    +                        Class.forName("io.netty.channel.kqueue.KQueueDomainSocketChannel")
    +                                .asSubclass(ServerChannel.class));
    +                final EventLoopGroup elg =
    +                        Class.forName("io.netty.channel.kqueue.KQueueEventLoopGroup")
    +                                .asSubclass(EventLoopGroup.class)
    +                                .getDeclaredConstructor()
    +                                .newInstance();
    +                nettyChannelBuilder.eventLoopGroup(elg);
    +            }
    +        } catch (ClassNotFoundException
    +                 | InstantiationException
    +                 | IllegalAccessException
    +                 | NoSuchMethodException
    +                 | InvocationTargetException e) {
    +            throw new UnsupportedOperationException(
    +                    "Could not find suitable Netty native transport implementation for domain socket address.");
    +        }
    +    }
    +
    +    private ProxyDetector createProxyDetector(@Nonnull final String url) {
    +        URI proxyUri = URI.create(url);
    +        return (targetServerAddress) -> {
    +            InetSocketAddress targetAddress = (InetSocketAddress) targetServerAddress;
    +            if (proxyUri.getHost().equals(targetAddress.getHostString())) {
    +                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..be6c33f5 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;
    @@ -98,8 +107,10 @@ public void checkServerTrusted(
             this.defaultHeaders = config.getHeaders() != null ? Map.copyOf(config.getHeaders()) : null;
     
             // proxy
    -        if (config.getProxy() != null) {
    -            builder.proxy(config.getProxy());
    +        if (config.getProxyUrl() != null) {
    +            URI uri = URI.create(config.getProxyUrl());
    +            ProxySelector proxy = ProxySelector.of(new InetSocketAddress(uri.getHost(), uri.getPort()));
    +            builder.proxy(proxy);
                 if (config.getAuthenticator() != null) {
                     builder.authenticator(config.getAuthenticator());
                 }
    @@ -107,11 +118,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);
                     }
    +
    +                if (config.certificateFilePath() != null && !config.getDisableServerCertificateValidation()) {
    +                    X509TrustManager x509TrustManager = getX509TrustManagerFromFile(config.certificateFilePath());
    +                    sslContext.init(null, new X509TrustManager[]{x509TrustManager}, new SecureRandom());
    +                }
                 } catch (Exception e) {
                     throw new RuntimeException(e);
                 }
    @@ -217,6 +233,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..66e6a6dd 100644
    --- a/src/test/java/com/influxdb/v3/client/InfluxDBClientTest.java
    +++ b/src/test/java/com/influxdb/v3/client/InfluxDBClientTest.java
    @@ -32,11 +32,62 @@
     import org.junit.jupiter.api.Test;
     import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
     
    +import com.influxdb.v3.client.config.ClientConfig;
     import com.influxdb.v3.client.write.WriteOptions;
     import com.influxdb.v3.client.write.WritePrecision;
     
     public class InfluxDBClientTest {
     
    +    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_URL", matches = ".*")
    +    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_TOKEN", matches = ".*")
    +    @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_DATABASE", matches = ".*")
    +    @Test
    +    void testQueryProxyAndSslCertificate() {
    +        String proxyUrl = "http://127.0.0.1:10000";
    +
    +        // This is real certificate downloaded from https://cloud2.influxdata.com
    +        String certificateFilePath = "src/test/java/com/influxdb/v3/client/testdata/valid-certificates.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"))
    +                .proxyUrl(proxyUrl)
    +                .certificateFilePath(certificateFilePath)
    +                .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");
    +                    });
    +        }
    +    }
    +
    +    @Test
    +    void withProxyUrl() {
    +        String proxyUrl = "http://127.0.0.1:10000";
    +        ClientConfig.Builder builder = new ClientConfig.Builder();
    +        builder.proxyUrl(proxyUrl);
    +        ClientConfig clientConfig = builder.build();
    +        Assertions.assertThat(clientConfig.getProxyUrl()).isEqualTo(proxyUrl);
    +    }
    +
    +    @Test
    +    void withCertificateFilePath() {
    +        String path = "/path/to/cert";
    +        ClientConfig.Builder builder = new ClientConfig.Builder();
    +        builder.certificateFilePath(path);
    +        ClientConfig clientConfig = builder.build();
    +        Assertions.assertThat(clientConfig.certificateFilePath()).isEqualTo(path);
    +    }
    +
         @Test
         void requiredHost() {
     
    @@ -102,10 +153,10 @@ void withDefaultTags() throws Exception {
             Map defaultTags = Map.of("unit", "U2", "model", "M1");
     
             try (InfluxDBClient client = InfluxDBClient.getInstance(
    -          "http://localhost:8086",
    -          "MY-TOKEN".toCharArray(),
    -          "MY-DATABASE",
    -          defaultTags)) {
    +                "http://localhost:8086",
    +                "MY-TOKEN".toCharArray(),
    +                "MY-DATABASE",
    +                defaultTags)) {
                 Assertions.assertThat(client).isNotNull();
             }
         }
    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..16aa6fe2 100644
    --- a/src/test/java/com/influxdb/v3/client/internal/RestClientTest.java
    +++ b/src/test/java/com/influxdb/v3/client/internal/RestClientTest.java
    @@ -22,9 +22,7 @@
     package com.influxdb.v3.client.internal;
     
     import java.net.Authenticator;
    -import java.net.InetSocketAddress;
     import java.net.PasswordAuthentication;
    -import java.net.ProxySelector;
     import java.net.http.HttpClient;
     import java.net.http.HttpHeaders;
     import java.time.Duration;
    @@ -279,7 +277,7 @@ public void proxy() 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:%s", mockServer.getHostName(), mockServer.getPort()))
                     .build());
     
             restClient.request("ping", HttpMethod.GET, null, null, null);
    @@ -298,7 +296,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:%s", mockServer.getHostName(), mockServer.getPort()))
                     .authenticator(new Authenticator() {
                         @Override
                         protected PasswordAuthentication getPasswordAuthentication() {
    diff --git a/src/test/java/com/influxdb/v3/client/testdata/valid-certificates.pem b/src/test/java/com/influxdb/v3/client/testdata/valid-certificates.pem
    new file mode 100644
    index 00000000..1613beac
    --- /dev/null
    +++ b/src/test/java/com/influxdb/v3/client/testdata/valid-certificates.pem
    @@ -0,0 +1,138 @@
    +-----BEGIN CERTIFICATE-----
    +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
    +-----END CERTIFICATE-----
    +
    +-----BEGIN CERTIFICATE-----
    +MIINYTCCDQagAwIBAgIRALQPM3zi5SVWCcng/hZpShIwCgYIKoZIzj0EAwIwOzEL
    +MAkGA1UEBhMCVVMxHjAcBgNVBAoTFUdvb2dsZSBUcnVzdCBTZXJ2aWNlczEMMAoG
    +A1UEAxMDV0UyMB4XDTI1MDIyNjE1MzMwM1oXDTI1MDUyMTE1MzMwMlowFzEVMBMG
    +A1UEAwwMKi5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgY2u
    +jvbPkuQiKZw9XHFP0GrdujA0tw6feXg2eVb3bWPQ7M0lJ/of3dlc9J/NQ8BjBrrn
    +bObLpB/4C3PjqziyQqOCDA0wggwJMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK
    +BggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTKaaMWpKmNeZC6ky5y
    +0iHRZX20szAfBgNVHSMEGDAWgBR1vsR3ron2RDd9z7FoHx0a69w0WTBYBggrBgEF
    +BQcBAQRMMEowIQYIKwYBBQUHMAGGFWh0dHA6Ly9vLnBraS5nb29nL3dlMjAlBggr
    +BgEFBQcwAoYZaHR0cDovL2kucGtpLmdvb2cvd2UyLmNydDCCCeQGA1UdEQSCCdsw
    +ggnXggwqLmdvb2dsZS5jb22CFiouYXBwZW5naW5lLmdvb2dsZS5jb22CCSouYmRu
    +LmRldoIVKi5vcmlnaW4tdGVzdC5iZG4uZGV2ghIqLmNsb3VkLmdvb2dsZS5jb22C
    +GCouY3Jvd2Rzb3VyY2UuZ29vZ2xlLmNvbYIYKi5kYXRhY29tcHV0ZS5nb29nbGUu
    +Y29tggsqLmdvb2dsZS5jYYILKi5nb29nbGUuY2yCDiouZ29vZ2xlLmNvLmlugg4q
    +Lmdvb2dsZS5jby5qcIIOKi5nb29nbGUuY28udWuCDyouZ29vZ2xlLmNvbS5hcoIP
    +Ki5nb29nbGUuY29tLmF1gg8qLmdvb2dsZS5jb20uYnKCDyouZ29vZ2xlLmNvbS5j
    +b4IPKi5nb29nbGUuY29tLm14gg8qLmdvb2dsZS5jb20udHKCDyouZ29vZ2xlLmNv
    +bS52boILKi5nb29nbGUuZGWCCyouZ29vZ2xlLmVzggsqLmdvb2dsZS5mcoILKi5n
    +b29nbGUuaHWCCyouZ29vZ2xlLml0ggsqLmdvb2dsZS5ubIILKi5nb29nbGUucGyC
    +CyouZ29vZ2xlLnB0gg8qLmdvb2dsZWFwaXMuY26CESouZ29vZ2xldmlkZW8uY29t
    +ggwqLmdzdGF0aWMuY26CECouZ3N0YXRpYy1jbi5jb22CD2dvb2dsZWNuYXBwcy5j
    +boIRKi5nb29nbGVjbmFwcHMuY26CEWdvb2dsZWFwcHMtY24uY29tghMqLmdvb2ds
    +ZWFwcHMtY24uY29tggxna2VjbmFwcHMuY26CDiouZ2tlY25hcHBzLmNughJnb29n
    +bGVkb3dubG9hZHMuY26CFCouZ29vZ2xlZG93bmxvYWRzLmNughByZWNhcHRjaGEu
    +bmV0LmNughIqLnJlY2FwdGNoYS5uZXQuY26CEHJlY2FwdGNoYS1jbi5uZXSCEiou
    +cmVjYXB0Y2hhLWNuLm5ldIILd2lkZXZpbmUuY26CDSoud2lkZXZpbmUuY26CEWFt
    +cHByb2plY3Qub3JnLmNughMqLmFtcHByb2plY3Qub3JnLmNughFhbXBwcm9qZWN0
    +Lm5ldC5jboITKi5hbXBwcm9qZWN0Lm5ldC5jboIXZ29vZ2xlLWFuYWx5dGljcy1j
    +bi5jb22CGSouZ29vZ2xlLWFuYWx5dGljcy1jbi5jb22CF2dvb2dsZWFkc2Vydmlj
    +ZXMtY24uY29tghkqLmdvb2dsZWFkc2VydmljZXMtY24uY29tghFnb29nbGV2YWRz
    +LWNuLmNvbYITKi5nb29nbGV2YWRzLWNuLmNvbYIRZ29vZ2xlYXBpcy1jbi5jb22C
    +EyouZ29vZ2xlYXBpcy1jbi5jb22CFWdvb2dsZW9wdGltaXplLWNuLmNvbYIXKi5n
    +b29nbGVvcHRpbWl6ZS1jbi5jb22CEmRvdWJsZWNsaWNrLWNuLm5ldIIUKi5kb3Vi
    +bGVjbGljay1jbi5uZXSCGCouZmxzLmRvdWJsZWNsaWNrLWNuLm5ldIIWKi5nLmRv
    +dWJsZWNsaWNrLWNuLm5ldIIOZG91YmxlY2xpY2suY26CECouZG91YmxlY2xpY2su
    +Y26CFCouZmxzLmRvdWJsZWNsaWNrLmNughIqLmcuZG91YmxlY2xpY2suY26CEWRh
    +cnRzZWFyY2gtY24ubmV0ghMqLmRhcnRzZWFyY2gtY24ubmV0gh1nb29nbGV0cmF2
    +ZWxhZHNlcnZpY2VzLWNuLmNvbYIfKi5nb29nbGV0cmF2ZWxhZHNlcnZpY2VzLWNu
    +LmNvbYIYZ29vZ2xldGFnc2VydmljZXMtY24uY29tghoqLmdvb2dsZXRhZ3NlcnZp
    +Y2VzLWNuLmNvbYIXZ29vZ2xldGFnbWFuYWdlci1jbi5jb22CGSouZ29vZ2xldGFn
    +bWFuYWdlci1jbi5jb22CGGdvb2dsZXN5bmRpY2F0aW9uLWNuLmNvbYIaKi5nb29n
    +bGVzeW5kaWNhdGlvbi1jbi5jb22CJCouc2FmZWZyYW1lLmdvb2dsZXN5bmRpY2F0
    +aW9uLWNuLmNvbYIWYXBwLW1lYXN1cmVtZW50LWNuLmNvbYIYKi5hcHAtbWVhc3Vy
    +ZW1lbnQtY24uY29tggtndnQxLWNuLmNvbYINKi5ndnQxLWNuLmNvbYILZ3Z0Mi1j
    +bi5jb22CDSouZ3Z0Mi1jbi5jb22CCzJtZG4tY24ubmV0gg0qLjJtZG4tY24ubmV0
    +ghRnb29nbGVmbGlnaHRzLWNuLm5ldIIWKi5nb29nbGVmbGlnaHRzLWNuLm5ldIIM
    +YWRtb2ItY24uY29tgg4qLmFkbW9iLWNuLmNvbYIUZ29vZ2xlc2FuZGJveC1jbi5j
    +b22CFiouZ29vZ2xlc2FuZGJveC1jbi5jb22CHiouc2FmZW51cC5nb29nbGVzYW5k
    +Ym94LWNuLmNvbYINKi5nc3RhdGljLmNvbYIUKi5tZXRyaWMuZ3N0YXRpYy5jb22C
    +CiouZ3Z0MS5jb22CESouZ2NwY2RuLmd2dDEuY29tggoqLmd2dDIuY29tgg4qLmdj
    +cC5ndnQyLmNvbYIQKi51cmwuZ29vZ2xlLmNvbYIWKi55b3V0dWJlLW5vY29va2ll
    +LmNvbYILKi55dGltZy5jb22CC2FuZHJvaWQuY29tgg0qLmFuZHJvaWQuY29tghMq
    +LmZsYXNoLmFuZHJvaWQuY29tggRnLmNuggYqLmcuY26CBGcuY2+CBiouZy5jb4IG
    +Z29vLmdsggp3d3cuZ29vLmdsghRnb29nbGUtYW5hbHl0aWNzLmNvbYIWKi5nb29n
    +bGUtYW5hbHl0aWNzLmNvbYIKZ29vZ2xlLmNvbYISZ29vZ2xlY29tbWVyY2UuY29t
    +ghQqLmdvb2dsZWNvbW1lcmNlLmNvbYIIZ2dwaHQuY26CCiouZ2dwaHQuY26CCnVy
    +Y2hpbi5jb22CDCoudXJjaGluLmNvbYIIeW91dHUuYmWCC3lvdXR1YmUuY29tgg0q
    +LnlvdXR1YmUuY29tghFtdXNpYy55b3V0dWJlLmNvbYITKi5tdXNpYy55b3V0dWJl
    +LmNvbYIUeW91dHViZWVkdWNhdGlvbi5jb22CFioueW91dHViZWVkdWNhdGlvbi5j
    +b22CD3lvdXR1YmVraWRzLmNvbYIRKi55b3V0dWJla2lkcy5jb22CBXl0LmJlggcq
    +Lnl0LmJlghphbmRyb2lkLmNsaWVudHMuZ29vZ2xlLmNvbYITKi5hbmRyb2lkLmdv
    +b2dsZS5jboISKi5jaHJvbWUuZ29vZ2xlLmNughYqLmRldmVsb3BlcnMuZ29vZ2xl
    +LmNughUqLmFpc3R1ZGlvLmdvb2dsZS5jb20wEwYDVR0gBAwwCjAIBgZngQwBAgEw
    +NgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2MucGtpLmdvb2cvd2UyLzY0T1VJVnpw
    +WlY0LmNybDCCAQMGCisGAQQB1nkCBAIEgfQEgfEA7wB1AM8RVu7VLnyv84db2Wku
    +m+kacWdKsBfsrAHSW3fOzDsIAAABlUMa3KQAAAQDAEYwRAIgbFT96GaW9xdF/H3T
    +R9A8aKomiq6jRak+HuHJKeYZKbICIFj0lmIw2MOCmAFoPML8Do+XUopVVJGpvKum
    +UZ3/H1ZBAHYAfVkeEuF4KnscYWd8Xv340IdcFKBOlZ65Ay/ZDowuebgAAAGVQxrc
    +mAAABAMARzBFAiA1pA/zxlqWAUktHGVu/3MlUzPsjEcaRHOEqw4qFdXIcgIhALwo
    +K02gj5Eep6saapUuV9BRJ8S5T4iydYXSoKuINbtcMAoGCCqGSM49BAMCA0kAMEYC
    +IQCe7kXY4zHS22OUlCThOBy44kLyCaMS20ylMx0JWcjHqQIhAIiNQSfvChn4LD5B
    +TtlK9da3ocqcq85/6pDQ4Cx7a2Ej
    +-----END CERTIFICATE-----
    +
    +-----BEGIN CERTIFICATE-----
    +MIIFSzCCBDOgAwIBAgISA+oZlVmojyzdwsUR47BXXUkOMA0GCSqGSIb3DQEBCwUA
    +MDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQD
    +EwNSMTAwHhcNMjUwMzA2MDUxNDM2WhcNMjUwNjA0MDUxNDM1WjAuMSwwKgYDVQQD
    +EyNjbi1wcm9kMzAxLXVzLWVhc3QtNC5pbmZsdXhkYXRhLmNvbTCCASIwDQYJKoZI
    +hvcNAQEBBQADggEPADCCAQoCggEBANufjJuDVC5JX7FkgtJTsTY4BgXKhJ2CNr7S
    +Y0JRZWEMGiPE1LIusHYTIgSMR7DqkQubXTIst+GQCd1I0TaPq0JOgUcElPJ/Hroc
    +UeDXUmkwPB9oyUqAEZE7N1QhZPfrzKtizvt3y+lcMLSrD2npFnSQXDotUonnCgnV
    +/wviPowV3rEBqUkEmfeNaPqWwNhS5vGAS9nHq+VhJl63i4MV6VVUhsmXKNJpwj+S
    +6pferlhLRkPFO7T+IOLMOoM50lrZZk2D359+JVG2UVQknlWzPljteikY2GRahIam
    +mbQwAtdCX0dcz5x5EXtN4/Au4SFzHZAWuhIm0i3VMcvs36RcW18CAwEAAaOCAlww
    +ggJYMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH
    +AwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUi2nfPdxKxh481jUurQRTJ01g8JQw
    +HwYDVR0jBBgwFoAUu7zDR6XkvKnGw6RyDBCNojXhyOgwVwYIKwYBBQUHAQEESzBJ
    +MCIGCCsGAQUFBzABhhZodHRwOi8vcjEwLm8ubGVuY3Iub3JnMCMGCCsGAQUFBzAC
    +hhdodHRwOi8vcjEwLmkubGVuY3Iub3JnLzBkBgNVHREEXTBbghVjbG91ZDIuaW5m
    +bHV4ZGF0YS5jb22CI2NuLXByb2QzMDEtdXMtZWFzdC00LmluZmx1eGRhdGEuY29t
    +gh1zdGFnaW5nLmNsb3VkMi5pbmZsdXhkYXRhLmNvbTATBgNVHSAEDDAKMAgGBmeB
    +DAECATCCAQMGCisGAQQB1nkCBAIEgfQEgfEA7wB1AKLjCuRF772tm3447Udnd1PX
    +gluElNcrXhssxLlQpEfnAAABlWoWE1IAAAQDAEYwRAIgQNybPpI1UMVzFtTaaZAz
    +7m/XcSglDuT/mvEv6nrdt3YCIGfx58c5llPJGjWNUSPbbEpL22sKFPbXuLEO78nB
    +rHPTAHYAE0rfGrWYQgl4DG/vTHqRpBa3I0nOWFdq367ap8Kr4CIAAAGVahYUxwAA
    +BAMARzBFAiBHSneDjE19jyyftoe1V5J1XQBVcm7ftufbswguI4qB/QIhAOegRql3
    +XPu0n8tq7/dfFeI1ChN7WhqqZxXZinDcr9rCMA0GCSqGSIb3DQEBCwUAA4IBAQAC
    +dgqfe0okFZ1g08EI4c8VMwYE8ii9z5VtM9bDf+2ukubahIlOBQ61glKuFkwr6yqU
    +yS5x/FUooq7yWYqMb/7zo/Xgb5/vpDfJblGi2oeuI3NBW1vu8Zkxa4rO1D8ypAPM
    +4JgsgtjYA4XOtTRoRmltlet3KvILPwFh0pTn9iDuLv0G8qf4eqezXpVj9QeZJknp
    +cr+tx8H7/0Kesy8coOzhkTXx3rjYbGce6JYlXHkyhMtppNcgHP8ly7CQCJjr4784
    ++mVdGOYN+rswgsnwMDwGEjbQfvErSqKS1DZNxIpy8CJLwfWJUNU3pqHqKM9lp4dY
    +ZWSUzpxgWgtYEdvcvDIi
    +-----END CERTIFICATE-----
    \ No newline at end of file