All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.helidon.webclient.WebClientConfiguration Maven / Gradle / Ivy

There is a newer version: 4.1.4
Show newest version
/*
 * Copyright (c) 2019, 2024 Oracle and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.helidon.webclient;

import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.CookieStore;
import java.net.URI;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;

import io.helidon.common.LazyValue;
import io.helidon.common.context.Context;
import io.helidon.config.Config;
import io.helidon.config.DeprecatedConfig;
import io.helidon.config.metadata.Configured;
import io.helidon.config.metadata.ConfiguredOption;
import io.helidon.media.common.MediaContext;
import io.helidon.media.common.MediaContextBuilder;
import io.helidon.media.common.MediaSupport;
import io.helidon.media.common.MessageBodyReader;
import io.helidon.media.common.MessageBodyReaderContext;
import io.helidon.media.common.MessageBodyStreamReader;
import io.helidon.media.common.MessageBodyStreamWriter;
import io.helidon.media.common.MessageBodyWriter;
import io.helidon.media.common.MessageBodyWriterContext;
import io.helidon.media.common.ParentingMediaContextBuilder;
import io.helidon.webclient.spi.WebClientService;

import io.netty.handler.ssl.ClientAuth;
import io.netty.handler.ssl.IdentityCipherSuiteFilter;
import io.netty.handler.ssl.JdkSslContext;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;

/**
 * Configuration of the Helidon web client.
 */
class WebClientConfiguration {

    private final WebClientRequestHeaders clientHeaders;
    private final WebClientCookieManager cookieManager;
    private final CookiePolicy cookiePolicy;
    private final Config config;
    private final Context context;
    private final Duration connectTimeout;
    private final boolean enableAutomaticCookieStore;
    private final Duration readTimeout;
    private final LazyValue userAgent;
    private final List clientServices;
    private final Proxy proxy;
    private final boolean followRedirects;
    private final boolean keepAlive;
    private final int maxRedirects;
    private final MessageBodyReaderContext readerContext;
    private final MessageBodyWriterContext writerContext;
    private final WebClientTls webClientTls;
    private final URI uri;
    private final boolean validateHeaders;
    private final boolean relativeUris;
    private final DnsResolverType dnsResolverType;
    private final boolean mediaTypeParserRelaxed;

    /**
     * Creates a new instance of client configuration.
     *
     * @param builder configuration builder
     */
    WebClientConfiguration(Builder builder) {
        this.connectTimeout = builder.connectTimeout;
        this.readTimeout = builder.readTimeout;
        this.followRedirects = builder.followRedirects;
        this.userAgent = builder.userAgent;
        this.proxy = builder.proxy;
        this.webClientTls = builder.webClientTls;
        this.maxRedirects = builder.maxRedirects;
        this.clientHeaders = builder.clientHeaders;
        this.cookiePolicy = builder.cookiePolicy;
        this.enableAutomaticCookieStore = builder.enableAutomaticCookieStore;
        this.cookieManager = WebClientCookieManager.create(cookiePolicy,
                                                           builder.cookieStore,
                                                           builder.defaultCookies,
                                                           enableAutomaticCookieStore);
        this.config = builder.config;
        this.context = builder.context;
        this.readerContext = builder.readerContext;
        this.writerContext = builder.writerContext;
        this.clientServices = List.copyOf(builder.clientServices);
        this.uri = builder.uri;
        this.keepAlive = builder.keepAlive;
        this.validateHeaders = builder.validateHeaders;
        this.relativeUris = builder.relativeUris;
        this.dnsResolverType = builder.dnsResolverType;
        this.mediaTypeParserRelaxed = builder.mediaTypeParserRelaxed;
    }

    /**
     * Creates new builder to build a new instance of this class.
     *
     * @return a new builder instance
     */
    static Builder builder() {
        return new Builder<>();
    }

    /**
     * Derives a new builder based on current instance of this class.
     *
     * @return a new builder instance
     */
    Builder derive() {
        return new Builder<>().update(this);
    }

    Optional sslContext() {
        SslContext sslContext;
        try {
            if (webClientTls.sslContext().isPresent()) {
                sslContext = nettySslFromJavaNet(webClientTls.sslContext().get());
            } else {
                SslContextBuilder sslContextBuilder = SslContextBuilder
                        .forClient()
                        .sslProvider(SslProvider.JDK);
                if (webClientTls.certificates().size() > 0) {
                    sslContextBuilder.trustManager(webClientTls.certificates().toArray(new X509Certificate[0]));
                }
                if (webClientTls.clientPrivateKey().isPresent()) {
                    sslContextBuilder.keyManager(webClientTls.clientPrivateKey().get(),
                                                 webClientTls.clientCertificateChain().toArray(new X509Certificate[0]));
                }

                if (webClientTls.trustAll()) {
                    sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE);
                }
                if (!webClientTls.allowedCipherSuite().isEmpty()) {
                    sslContextBuilder.ciphers(webClientTls.allowedCipherSuite());
                }

                sslContext = sslContextBuilder.build();
            }
        } catch (SSLException e) {
            throw new WebClientException("An error occurred while creating ssl context.", e);
        }
        return Optional.of(sslContext);
    }

    private SslContext nettySslFromJavaNet(SSLContext javaNetContext) {
        Set allowedCipherSuite = webClientTls.allowedCipherSuite();
        return new JdkSslContext(
                javaNetContext, true, allowedCipherSuite.isEmpty() ? null : allowedCipherSuite,
                IdentityCipherSuiteFilter.INSTANCE, null,
                ClientAuth.OPTIONAL, null, false);
    }

    /**
     * Connection timeout duration.
     *
     * @return connection timeout
     */
    Duration connectTimeout() {
        return connectTimeout;
    }

    /**
     * Read timeout duration.
     *
     * @return read timeout
     */
    Duration readTimout() {
        return readTimeout;
    }

    /**
     * Configured proxy.
     *
     * @return proxy
     */
    Optional proxy() {
        return Optional.ofNullable(proxy);
    }

    /**
     * Returns true if client should follow redirection.
     *
     * @return follow redirection
     */
    boolean followRedirects() {
        return followRedirects;
    }

    /**
     * Max number of followed redirections.
     *
     * @return max redirections
     */
    int maxRedirects() {
        return maxRedirects;
    }

    /**
     * Default client headers.
     *
     * @return default headers
     */
    WebClientRequestHeaders headers() {
        return clientHeaders;
    }

    /**
     * Instance of {@link CookieManager}.
     *
     * @return cookie manager
     */
    CookieManager cookieManager() {
        return cookieManager;
    }

    /**
     * Returns user agent.
     *
     * @return user agent
     */
    String userAgent() {
        return userAgent.get();
    }

    WebClientTls tls() {
        return webClientTls;
    }

    Optional context() {
        return Optional.ofNullable(context);
    }

    Config config() {
        return config;
    }

    List clientServices() {
        return clientServices;
    }

    MessageBodyReaderContext readerContext() {
        return readerContext;
    }

    MessageBodyWriterContext writerContext() {
        return writerContext;
    }

    URI uri() {
        return uri;
    }

    boolean keepAlive() {
        return keepAlive;
    }

    boolean validateHeaders() {
        return validateHeaders;
    }

    boolean relativeUris() {
        return relativeUris;
    }

    DnsResolverType dnsResolverType() {
        return dnsResolverType;
    }

    boolean mediaTypeParserRelaxed() {
        return mediaTypeParserRelaxed;
    }

    /**
     * A fluent API builder for {@link WebClientConfiguration}.
     */
    @Configured(root = true, prefix = "client", description = "Configuration of the HTTP client")
    static class Builder, T extends WebClientConfiguration>
            implements io.helidon.common.Builder,
                       ParentingMediaContextBuilder,
                       MediaContextBuilder {

        private final WebClientRequestHeaders clientHeaders;
        private final Map defaultCookies;
        private final List clientServices;

        private Config config;
        private Context context;
        private CookieStore cookieStore;
        private CookiePolicy cookiePolicy;
        private int maxRedirects;
        private Duration connectTimeout;
        private Duration readTimeout;
        private boolean followRedirects;
        private LazyValue userAgent;
        private Proxy proxy;
        private boolean enableAutomaticCookieStore;
        private boolean keepAlive;
        private WebClientTls webClientTls;
        private URI uri;
        private MessageBodyReaderContext readerContext;
        private MessageBodyWriterContext writerContext;
        private boolean validateHeaders;
        private boolean relativeUris;
        private DnsResolverType dnsResolverType;
        private boolean mediaTypeParserRelaxed;
        @SuppressWarnings("unchecked")
        private B me = (B) this;

        /**
         * Creates new instance of the builder.
         */
        Builder() {
            clientHeaders = new WebClientRequestHeadersImpl();
            defaultCookies = new HashMap<>();
            clientServices = new ArrayList<>();
        }

        @Override
        @SuppressWarnings("unchecked")
        public T build() {
            return (T) new WebClientConfiguration(this);
        }

        /**
         * Sets new connection timeout of the request.
         *
         * @param connectTimeout new connection timeout
         * @return updated builder instance
         */
        @ConfiguredOption(key = "connect-timeout-millis", type = Long.class, value = "60000")
        public B connectTimeout(Duration connectTimeout) {
            this.connectTimeout = connectTimeout;
            return me;
        }

        /**
         * Sets new read timeout of the response.
         *
         * @param readTimeout new read timeout
         * @return updated builder instance
         */
        @ConfiguredOption(key = "read-timeout-millis", type = Long.class, value = "600000")
        public B readTimeout(Duration readTimeout) {
            this.readTimeout = readTimeout;
            return me;
        }

        /**
         * Whether to follow any response redirections or not.
         *
         * @param followRedirects follow redirection
         * @return updated builder instance
         */
        @ConfiguredOption("false")
        public B followRedirects(boolean followRedirects) {
            this.followRedirects = followRedirects;
            return me;
        }

        /**
         * Name of the user agent which should be used.
         *
         * @param userAgent user agent
         * @return updated builder instance
         */
        @ConfiguredOption
        public B userAgent(String userAgent) {
            this.userAgent = LazyValue.create(() -> userAgent);
            return me;
        }

        /**
         * Sets new user agent wrapped by {@link LazyValue}.
         *
         * @param userAgent wrapped user agent
         * @return updated builder instance
         */
        public B userAgent(LazyValue userAgent) {
            this.userAgent = userAgent;
            return me;
        }

        /**
         * Sets new request proxy.
         *
         * @param proxy request proxy
         * @return updated builder instance
         */
        @ConfiguredOption
        public B proxy(Proxy proxy) {
            this.proxy = proxy;
            return me;
        }

        /**
         * New TLS configuration.
         *
         * @param webClientTls tls configuration
         * @return updated builder instance
         */
        @ConfiguredOption
        public B tls(WebClientTls webClientTls) {
            this.webClientTls = webClientTls;
            return me;
        }

        /**
         * Sets max number of followed redirects.
         *
         * @param maxRedirects max redirects
         * @return updated builder instance
         */
        @ConfiguredOption("5")
        public B maxRedirects(int maxRedirects) {
            this.maxRedirects = maxRedirects;
            return me;
        }

        /**
         * Sets default client request headers.
         *
         * Overrides previously set default client headers.
         *
         * @param clientHeaders default request headers
         * @return updated builder instance
         */
        public B clientHeaders(WebClientRequestHeaders clientHeaders) {
            this.clientHeaders.putAll(clientHeaders);
            return me;
        }

        /**
         * Sets new instance of {@link CookieStore} with default cookies.
         *
         * @param cookieStore cookie store
         * @return updated builder instance
         */
        public B cookieStore(CookieStore cookieStore) {
            this.cookieStore = cookieStore;
            return me;
        }

        /**
         * Sets new {@link CookiePolicy}.
         *
         * @param cookiePolicy cookie policy
         * @return updated builder instance
         */
        @ConfiguredOption(key = "cookies.automatic-store-enabled",
                          type = Boolean.class,
                          description = "Whether to allow automatic cookie storing")
        public B cookiePolicy(CookiePolicy cookiePolicy) {
            this.cookiePolicy = cookiePolicy;
            return me;
        }

        /**
         * Adds default cookie to every request.
         *
         * @param key   cookie name
         * @param value cookie value
         * @return updated builder instance
         */
        @ConfiguredOption(key = "cookies.default-cookies",
                          type = Map.class,
                          description = "Default cookies to be used in each request. "
                                  + "Each list entry has to have \"name\" and \"value\" node")
        public B defaultCookie(String key, String value) {
            defaultCookies.put(key, value);
            return me;
        }

        /**
         * Adds default header to every request.
         *
         * @param key    header name
         * @param values header value
         * @return updated builder instance
         */
        @ConfiguredOption(key = "headers",
                          type = Map.class,
                          description = "Default headers to be used in each request. "
                                  + "Each list entry has to have \"name\" and \"value\" node")
        public B defaultHeader(String key, List values) {
            clientHeaders.put(key, values);
            return me;
        }

        /**
         * Can be set to {@code true} to force the use of relative URIs in all requests,
         * regardless of the presence or absence of proxies or no-proxy lists.
         *
         * @param relativeUris relative URIs flag
         * @return updated builder instance
         */
        @ConfiguredOption("false")
        public B relativeUris(boolean relativeUris) {
            this.relativeUris = relativeUris;
            return me;
        }

        @Override
        @ConfiguredOption(key = "media-support")
        public B mediaContext(MediaContext mediaContext) {
            writerContextParent(mediaContext.writerContext());
            readerContextParent(mediaContext.readerContext());
            return me;
        }

        /**
         * Sets specific context in which all the requests will be running.
         *
         * @return updated builder instance
         */
        public B context(Context context) {
            this.context = context;
            return me;
        }

        /**
         * Base uri for each request.
         *
         * @return updated builder instance
         */
        @ConfiguredOption(type = String.class,
                          description = "Base URI for each request")
        public B uri(URI uri) {
            this.uri = uri;
            return me;
        }

        /**
         * Set which type of DNS resolver should be used.
         *
         * @param dnsResolverType dns resolver type to be used
         * @return updated builder instance
         */
        @ConfiguredOption
        public B dnsResolverType(DnsResolverType dnsResolverType) {
            this.dnsResolverType = dnsResolverType;
            return me;
        }

        /**
         * Whether to validate header names.
         * Defaults to {@code true}.
         *
         * @param validate whether to validate the header name contains only allowed characters
         * @return updated builder instance
         */
        B validateHeaders(boolean validate) {
            this.validateHeaders = validate;
            return me;
        }

        @Override
        public B addReader(MessageBodyReader reader) {
            this.readerContext.registerReader(reader);
            return me;
        }

        @Override
        public B addStreamReader(MessageBodyStreamReader streamReader) {
            this.readerContext.registerReader(streamReader);
            return me;
        }

        @Override
        public B addWriter(MessageBodyWriter writer) {
            this.writerContext.registerWriter(writer);
            return me;
        }

        @Override
        public B addStreamWriter(MessageBodyStreamWriter streamWriter) {
            this.writerContext.registerWriter(streamWriter);
            return me;
        }

        @Override
        public B addMediaSupport(MediaSupport mediaSupport) {
            Objects.requireNonNull(mediaSupport);
            mediaSupport.register(readerContext, writerContext);
            return me;
        }

        B enableAutomaticCookieStore(Boolean enableAutomaticCookieStore) {
            this.enableAutomaticCookieStore = enableAutomaticCookieStore;
            return me;
        }

        B readerContextParent(MessageBodyReaderContext readerContext) {
            this.readerContext = MessageBodyReaderContext.create(readerContext);
            return me;
        }

        B writerContextParent(MessageBodyWriterContext writerContext) {
            this.writerContext = MessageBodyWriterContext.create(writerContext);
            return me;
        }

        B readerContext(MessageBodyReaderContext readerContext) {
            this.readerContext = readerContext;
            return me;
        }

        B writerContext(MessageBodyWriterContext writerContext) {
            this.writerContext = writerContext;
            return me;
        }

        B clientServices(List clientServices) {
            this.clientServices.clear();
            this.clientServices.addAll(clientServices);
            return me;
        }

        B mediaTypeParserRelaxed(boolean relaxedMode) {
            this.mediaTypeParserRelaxed = relaxedMode;
            return me;
        }

        /**
         * Enable keep alive option on the connection.
         *
         * @param keepAlive keep alive value
         * @return updated builder instance
         */
        @ConfiguredOption("true")
        public B keepAlive(boolean keepAlive) {
            this.keepAlive = keepAlive;
            return me;
        }

        /**
         * Configures this {@link WebClientConfiguration.Builder} from the supplied {@link Config}.
         * 
         * 
         * 
         *     
         *     
         * 
         * 
         *     
         *     
         * 
         * 
         *     
         *     
         * 
         * 
         *     
         *     
         * 
         * 
         *     
         *     
         * 
         * 
         *     
         *     
         * 
         * 
         *     
         *     
         * 
         * 
         *     
         *     
         * 
         * 
         *     
         *     
         * 
         * 
         *     
         *     
         * 
         * 
         *     
         *     
         * 
         * 
         *     
         *     
         * 
         * 
         *     
         *     
         * 
         * 
Optional configuration parameters
keydescription
uriBasic uri for each client request
connect-timeout-millisRequest connection timeout
read-timeout-millisResponse read timeout
follow-redirectsWhether redirects should be followed or not
max-redirectsMax number of followed redirections
user-agentName of the user agent which should be used
keep-aliveWhether connection should be kept alive
cookiesDefault cookies which should be used
headersDefault headers which should be used
tlsTLS configuration. See {@link WebClientTls.Builder#config(Config)}
proxyProxy configuration. See {@link Proxy.Builder#config(Config)}
media-type-parser-relaxedWhether relaxed media type parsing mode should be used.
* * @param config config * @return updated builder instance */ public B config(Config config) { this.config = config; // now for other options config.get("uri").asString().ifPresent(baseUri -> uri(URI.create(baseUri))); config.get("connect-timeout-millis").asLong().ifPresent(timeout -> connectTimeout(Duration.ofMillis(timeout))); config.get("read-timeout-millis").asLong().ifPresent(timeout -> readTimeout(Duration.ofMillis(timeout))); config.get("follow-redirects").asBoolean().ifPresent(this::followRedirects); config.get("max-redirects").asInt().ifPresent(this::maxRedirects); config.get("user-agent").asString().ifPresent(this::userAgent); config.get("keep-alive").asBoolean().ifPresent(this::keepAlive); config.get("cookies").asNode().ifPresent(this::cookies); config.get("headers").asNode().ifPresent(this::headers); DeprecatedConfig.get(config, "tls", "ssl") .as(WebClientTls.builder()::config) .map(WebClientTls.Builder::build) .ifPresent(this::tls); config.get("proxy") .as(Proxy.builder()::config) .map(Proxy.Builder::build) .ifPresent(this::proxy); config.get("media-support").as(MediaContext::create).ifPresent(this::mediaContext); config.get("relative-uris").asBoolean().ifPresent(this::relativeUris); config.get("dns-resolver-type").asString() .map(s -> DnsResolverType.valueOf(s.toUpperCase())) .ifPresent(this::dnsResolverType); config.get("media-type-parser-relaxed").asBoolean().ifPresent(this::mediaTypeParserRelaxed); return me; } /** * Updates builder existing client configuration. * * @param configuration client configuration * @return updated builder instance */ public B update(WebClientConfiguration configuration) { connectTimeout(configuration.connectTimeout); readTimeout(configuration.readTimeout); followRedirects(configuration.followRedirects); userAgent(configuration.userAgent); proxy(configuration.proxy); tls(configuration.webClientTls); maxRedirects(configuration.maxRedirects); clientHeaders(configuration.clientHeaders); enableAutomaticCookieStore(configuration.enableAutomaticCookieStore); cookieStore(configuration.cookieManager.getCookieStore()); cookiePolicy(configuration.cookiePolicy); clientServices(configuration.clientServices); readerContextParent(configuration.readerContext); writerContextParent(configuration.writerContext); context(configuration.context); keepAlive(configuration.keepAlive); validateHeaders(configuration.validateHeaders); dnsResolverType(configuration.dnsResolverType); mediaTypeParserRelaxed(configuration.mediaTypeParserRelaxed); configuration.cookieManager.defaultCookies().forEach(this::defaultCookie); config = configuration.config; return me; } private void headers(Config configHeaders) { configHeaders.asNodeList() .ifPresent(headers -> headers .forEach(header -> defaultHeader(header.get("name").asString().get(), header.get("value").asList(String.class).get()))); } private void cookies(Config cookies) { cookies.get("automatic-store-enabled").asBoolean().ifPresent(this::enableAutomaticCookieStore); Config map = cookies.get("default-cookies"); map.asNodeList() .ifPresent(headers -> headers .forEach(header -> defaultCookie(header.get("name").asString().get(), header.get("value").asString().get()))); } } }