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

io.fabric8.kubernetes.client.utils.HttpClientUtils Maven / Gradle / Ivy

There is a newer version: 7.1.0
Show newest version
/*
 * Copyright (C) 2015 Red Hat, Inc.
 *
 * 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.fabric8.kubernetes.client.utils;

import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.http.BasicBuilder;
import io.fabric8.kubernetes.client.http.HttpClient;
import io.fabric8.kubernetes.client.http.HttpRequest;
import io.fabric8.kubernetes.client.http.Interceptor;
import io.fabric8.kubernetes.client.internal.SSLUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.net.ssl.KeyManager;
import javax.net.ssl.TrustManager;

public class HttpClientUtils {

  private static final class HeaderInterceptor implements Interceptor {
    private final Config config;

    private HeaderInterceptor(Config config) {
      this.config = config;
    }

    @Override
    public void before(BasicBuilder builder, HttpRequest request, RequestTags tags) {
      if (config.getCustomHeaders() != null && !config.getCustomHeaders().isEmpty()) {
        for (Map.Entry entry : config.getCustomHeaders().entrySet()) {
          builder.header(entry.getKey(), entry.getValue());
        }
      }
      if (config.getUserAgent() != null && !config.getUserAgent().isEmpty()) {
        builder.setHeader("User-Agent", config.getUserAgent());
      }
    }
  }

  private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientUtils.class);
  private static final String HEADER_INTERCEPTOR = "HEADER";
  private static final String KUBERNETES_BACKWARDS_COMPATIBILITY_INTERCEPTOR_DISABLE = "kubernetes.backwardsCompatibilityInterceptor.disable";
  private static final String BACKWARDS_COMPATIBILITY_DISABLE_DEFAULT = "true";
  private static final Pattern IPV4_PATTERN = Pattern.compile(
      "(http://|https://)?(?(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])(\\/([1-2]\\d|3[0-2]|\\d))?)(\\D+|$)");
  private static final Pattern INVALID_HOST_PATTERN = Pattern.compile("[^\\da-zA-Z.\\-/:]+");

  private HttpClientUtils() {
  }

  static URI getProxyUri(URL master, Config config) throws URISyntaxException {
    String proxy = config.getHttpsProxy();
    if (master.getProtocol().equals("http")) {
      proxy = config.getHttpProxy();
    }
    if (proxy != null) {
      final String completedProxy;
      if (proxy.contains("://")) {
        completedProxy = proxy;
      } else {
        // No protocol specified, default to cluster requirements
        completedProxy = master.getProtocol() + "://" + proxy;
      }
      final URI proxyUrl = new URI(completedProxy);
      if (proxyUrl.getPort() < 0) {
        throw new IllegalArgumentException("Failure in creating proxy URL. Proxy port is required!");
      }
      return proxyUrl;
    }
    return null;
  }

  public static Map createApplicableInterceptors(Config config,
      HttpClient.Factory factory) {
    Map interceptors = new LinkedHashMap<>();

    // Header Interceptor
    interceptors.put(HEADER_INTERCEPTOR, new HeaderInterceptor(config));
    // Impersonator Interceptor
    interceptors.put(ImpersonatorInterceptor.NAME, new ImpersonatorInterceptor(config.getRequestConfig()));
    // Token Refresh Interceptor
    interceptors.put(TokenRefreshInterceptor.NAME, new TokenRefreshInterceptor(config, factory, Instant.now()));
    // Backwards Compatibility Interceptor
    String shouldDisableBackwardsCompatibilityInterceptor = Utils
        .getSystemPropertyOrEnvVar(KUBERNETES_BACKWARDS_COMPATIBILITY_INTERCEPTOR_DISABLE,
            BACKWARDS_COMPATIBILITY_DISABLE_DEFAULT);
    if (!Boolean.parseBoolean(shouldDisableBackwardsCompatibilityInterceptor)) {
      interceptors.put(BackwardsCompatibilityInterceptor.NAME, new BackwardsCompatibilityInterceptor());
    }

    return interceptors;
  }

  public static String basicCredentials(String usernameAndPassword) {
    String encoded = Base64.getEncoder().encodeToString(usernameAndPassword.getBytes(StandardCharsets.UTF_8));
    return "Basic " + encoded;
  }

  public static String basicCredentials(String username, String password) {
    return basicCredentials(username + ":" + password);
  }

  public static String[] decodeBasicCredentials(String basicCredentials) {
    if (basicCredentials == null) {
      return null;
    }
    try {
      final String encodedCredentials = basicCredentials.replaceFirst("Basic ", "");
      final String decodedProxyAuthorization = new String(Base64.getDecoder().decode(encodedCredentials),
          StandardCharsets.UTF_8);
      final String[] userPassword = decodedProxyAuthorization.split(":");
      if (userPassword.length == 2) {
        return userPassword;
      }
    } catch (Exception ignored) {
      // Ignored
    }
    return null;
  }

  /**
   * @deprecated you should not need to call this method directly. Please create your own HttpClient.Factory
   *             should you need to customize your clients.
   */
  @Deprecated
  public static HttpClient createHttpClient(Config config) {
    HttpClient.Factory factory = getHttpClientFactory();
    return factory.newBuilder(config).build();
  }

  public static HttpClient.Factory getHttpClientFactory() {
    HttpClient.Factory factory = getFactory(
        ServiceLoader.load(HttpClient.Factory.class, Thread.currentThread().getContextClassLoader()));
    if (factory == null) {
      factory = getFactory(ServiceLoader.load(HttpClient.Factory.class, HttpClientUtils.class.getClassLoader()));
      if (factory == null) {
        throw new KubernetesClientException(
            "No httpclient implementations found on the context classloader, please ensure your classpath includes an implementation jar");
      }
    }
    LOGGER.debug("Using httpclient {} factory", factory.getClass().getName());
    return factory;
  }

  private static HttpClient.Factory getFactory(ServiceLoader loader) {
    List factories = new ArrayList<>();
    loader.forEach(factories::add);
    if (factories.isEmpty()) {
      return null;
    }
    Collections.sort(factories, (f1, f2) -> Integer.compare(f2.priority(), f1.priority()));
    HttpClient.Factory factory = factories.get(0);
    if (factories.size() > 1) {
      if (factories.get(1).priority() == factory.priority()) {
        LOGGER.warn("The following httpclient factories were detected on your classpath: {}, "
            + "multiple of which had the same priority ({}) so one was chosen randomly. "
            + "You should exclude dependencies that aren't needed or use an explicit association of the HttpClient.Factory.",
            factories.stream().map(f -> f.getClass().getName()).toArray(), factory.priority());
      } else if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("The following httpclient factories were detected on your classpath: {}",
            factories.stream().map(f -> f.getClass().getName()).toArray());
      }
    }
    return factory;
  }

  public static void applyCommonConfiguration(Config config, HttpClient.Builder builder, HttpClient.Factory factory) {
    builder.followAllRedirects();

    builder.tag(config.getRequestConfig());

    if (config.getConnectionTimeout() > 0) {
      builder.connectTimeout(config.getConnectionTimeout(), TimeUnit.MILLISECONDS);
    }

    if (config.isHttp2Disable()) {
      builder.preferHttp11();
    }

    try {
      configureProxy(config, builder);

      TrustManager[] trustManagers = SSLUtils.trustManagers(config);
      KeyManager[] keyManagers = SSLUtils.keyManagers(config);
      builder.sslContext(keyManagers, trustManagers);
    } catch (Exception e) {
      throw KubernetesClientException.launderThrowable(e);
    }

    if (config.getTlsVersions() != null && config.getTlsVersions().length > 0) {
      builder.tlsVersions(config.getTlsVersions());
    }

    HttpClientUtils.createApplicableInterceptors(config, factory).forEach(builder::addOrReplaceInterceptor);
  }

  static void configureProxy(Config config, HttpClient.Builder builder)
      throws URISyntaxException, MalformedURLException {
    URL master;
    try {
      master = new URL(config.getMasterUrl());
    } catch (MalformedURLException e) {
      // Only check proxy if it's a full URL with protocol
      return;
    }
    URI proxyUri = HttpClientUtils.getProxyUri(master, config);
    if (proxyUri == null) {
      // not configured for a proxy
      return;
    }
    String host = master.getHost();
    if (isHostMatchedByNoProxy(host, config.getNoProxy())) {
      builder.proxyType(HttpClient.ProxyType.DIRECT);
    } else {
      builder.proxyAddress(new InetSocketAddress(proxyUri.getHost(), proxyUri.getPort()));

      if (config.getProxyUsername() != null) {
        builder.proxyAuthorization(basicCredentials(config.getProxyUsername(), config.getProxyPassword()));
      }

      String userInfo = proxyUri.getUserInfo();
      if (userInfo != null) {
        builder.proxyAuthorization(basicCredentials(userInfo));
      }

      builder.proxyType(toProxyType(proxyUri.getScheme()));
    }
  }

  static HttpClient.ProxyType toProxyType(String scheme) throws MalformedURLException {
    if (scheme == null) {
      throw new MalformedURLException("No protocol specified on proxy URL");
    }
    scheme = scheme.toLowerCase();
    if (scheme.startsWith("http")) {
      return HttpClient.ProxyType.HTTP;
    }
    if (scheme.equals("socks4")) {
      return HttpClient.ProxyType.SOCKS4;
    }
    if (scheme.equals("socks5")) {
      return HttpClient.ProxyType.SOCKS5;
    }
    throw new MalformedURLException("Unsupported protocol specified on proxy URL");
  }

  static boolean isHostMatchedByNoProxy(String host, String[] noProxies) throws MalformedURLException {
    for (String noProxy : noProxies == null ? new String[0] : noProxies) {
      if (INVALID_HOST_PATTERN.matcher(noProxy).find()) {
        throw new MalformedURLException("NO_PROXY URL contains invalid entry: '" + noProxy + "'");
      }
      final Optional noProxyIpOrSubnet = extractIpAddressOrSubnet(noProxy);
      if (noProxyIpOrSubnet.isPresent()) {
        if (new IpAddressMatcher(noProxyIpOrSubnet.get()).matches(host)) {
          return true;
        }
      } else {
        if (host.endsWith(noProxy)) {
          return true;
        }
      }
    }
    return false;
  }

  private static Optional extractIpAddressOrSubnet(String ipAddressOrSubnet) {
    final Matcher ipMatcher = IPV4_PATTERN.matcher(ipAddressOrSubnet);
    if (ipMatcher.find()) {
      return Optional.of(ipMatcher.group("ipAddressOrSubnet"));
    }
    return Optional.empty();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy