aensley/sonar-teams-notifier

View on GitHub
src/main/java/com/andrewensley/sonarteamsnotifier/extension/TeamsHttpClient.java

Summary

Maintainability
A
0 mins
Test Coverage
package com.andrewensley.sonarteamsnotifier.extension;

import com.andrewensley.sonarteamsnotifier.domain.InvalidHttpResponseException;
import com.google.gson.Gson;

import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.Optional;

import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.TrustAllStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;

class TeamsHttpClient {

  /**
   * Logger.
   */
  private static final Logger LOG = Loggers.get(TeamsHttpClient.class);

  /**
   * String value indicating a setting is not set.
   */
  private static final String NOT_SET = "NOT_SET";

  /**
   * The URL for the webhook.
   */
  private URL hook;

  /**
   * The port to be used for the connection.
   */
  private int port;

  /**
   * The full path of the URL including query string and anchor reference.
   */
  private String path;

  /**
   * The payload to send with the request.
   */
  private Payload payload;

  /**
   * Internal Apache HTTP Client.
   */
  private CloseableHttpClient httpClient;

  /**
   * Whether or not to bypass HTTPS validation.
   */
  private boolean bypassHttpsValidation = false;

  /**
   * The target host of the webhook.
   */
  private HttpHost target;

  /**
   * The HTTP POST request to send.
   */
  private HttpPost httpPost;

  /**
   * The proxy host name or IP.
   */
  private Optional<String> proxyIp;

  /**
   * The proxy port.
   */
  private Optional<Integer> proxyPort;

  /**
   * The username for proxy authentication.
   */
  private Optional<String> proxyUser;

  /**
   * The password for proxy authentication.
   */
  private Optional<String> proxyPass;

  /**
   * Constructor.
   *
   * @param url     The URL of the webhook.
   * @param payload The payload to send to the webhook.
   *
   * @throws MalformedURLException If the URL is malformed.
   */
  private TeamsHttpClient(String url, Payload payload) throws MalformedURLException {
    this.hook = new URL(url);
    this.payload = payload;
  }

  /**
   * Static pattern constructor.
   *
   * @param url     The URL of the webhook.
   * @param payload The payload to send to the webhook.
   *
   * @return The TeamsHttpClient
   *
   * @throws MalformedURLException If the URL is malformed.
   */
  static TeamsHttpClient of(String url, Payload payload) throws MalformedURLException {
    return new TeamsHttpClient(url, payload);
  }

  /**
   * Sets the bypass HTTPS validation state to enabled or disabled.
   *
   * @param bypass Set to true to enable the bypass. False to disable.
   *
   * @return The TeamsHttpClient
   */
  TeamsHttpClient bypassHttpsValidation(boolean bypass) {
    this.bypassHttpsValidation = bypass;
    return this;
  }

  /**
   * Sets proxy settings on the TeamsHttpClient.
   *
   * @param ip   The proxy host name or IP.
   * @param port The proxy port.
   *
   * @return The TeamsHttpClient.
   */
  TeamsHttpClient proxy(Optional<String> ip, Optional<Integer> port) {
    this.proxyIp = ip;
    this.proxyPort = port;
    return this;
  }

  /**
   * Sets proxy auth settings on the TeamsHttpClient.
   *
   * @param username The proxy username.
   * @param password The proxy password.
   *
   * @return The TeamsHttpClient
   */
  TeamsHttpClient proxyAuth(Optional<String> username, Optional<String> password) {
    this.proxyUser = username;
    this.proxyPass = password;
    return this;
  }

  /**
   * Builds the TeamsHttpClient, preparing it to make the request.
   *
   * @return The TeamsHttpClient
   *
   * @throws UnsupportedEncodingException If the payload is malformed.
   */
  TeamsHttpClient build() throws UnsupportedEncodingException {
    port = getPort();
    path = getPath();
    httpClient = getHttpClient();
    target = new HttpHost(hook.getHost(), port, hook.getProtocol());
    httpPost = getHttpPost();

    LOG.debug(
        "TeamsHttpClient BUILT"
        + " | Host: " + hook.getHost()
        + " | Port: " + port
        + " | Path: " + path
        + " | Bypass HTTPS Validation: " + bypassHttpsValidation
        + " | ProxyEnabled: " + proxyEnabled()
        + " | ProxyAuthEnabled: " + proxyAuthEnabled()
        + " | Proxy IP: " + proxyIp.orElse(NOT_SET)
        + " | Proxy Port: " + (proxyPort.isPresent() ? proxyPort.get() : NOT_SET)
        + " | Proxy User: " + proxyUser.orElse(NOT_SET)
        + " | Proxy Pass (length): " + proxyPass.orElse("").length()
    );

    return this;
  }

  /**
   * Posts the message to the webhook.
   *
   * @return True on success. False on failure.
   */
  boolean post() {
    boolean success = false;
    try {
      CloseableHttpResponse response = httpClient.execute(target, httpPost);
      int responseCode = response.getStatusLine().getStatusCode();
      if (responseCode < 200 || responseCode > 299) {
        throw new InvalidHttpResponseException("Invalid HTTP Response Code: " + responseCode);
      }

      LOG.info("POST Successful!");
      success = true;
    } catch (Exception e) {
      LOG.error("Failed to send teams message", e);
    } finally {
      try {
        httpClient.close();
      } catch (Exception e) {
        LOG.error("Unable to close HTTP Client", e);
      }
    }

    return success;
  }

  /**
   * Gets the HttpPost request object.
   *
   * @return The HttpPost.
   *
   * @throws UnsupportedEncodingException If the payload is malformed.
   */
  private HttpPost getHttpPost() throws UnsupportedEncodingException {
    Gson gson = new Gson();
    HttpPost tempHttpPost = new HttpPost(path);
    tempHttpPost.setEntity(new StringEntity(gson.toJson(payload)));
    tempHttpPost.setHeader("Accept", "application/json");
    tempHttpPost.setHeader("Content-type", "application/json");

    if (proxyEnabled()) {
      //noinspection OptionalGetWithoutIsPresent
      HttpHost proxy = new HttpHost(proxyIp.get(), proxyPort.get());
      RequestConfig config = RequestConfig.custom()
          .setProxy(proxy)
          .build();
      tempHttpPost.setConfig(config);
    }

    return tempHttpPost;
  }

  /**
   * Checks if the HttpClient should use a proxy.
   *
   * @return True if proxy is enabled. False if not.
   */
  private boolean proxyEnabled() {
    return (proxyIp.isPresent() && proxyPort.isPresent());
  }

  /**
   * Checks if the HttpClient should use proxy authentication.
   *
   * @return True if proxy auth is enabled. False if not.
   */
  private boolean proxyAuthEnabled() {
    return (proxyEnabled() && proxyUser.isPresent() && proxyPass.isPresent());
  }

  /**
   * Gets the internal HTTP Client to be used for the request.
   *
   * @return The HTTP Client.
   */
  private CloseableHttpClient getHttpClient() {
    CloseableHttpClient tempHttpClient = HttpClients.createDefault();
    if (proxyAuthEnabled() || bypassHttpsValidation) {
      tempHttpClient = getCustomHttpClient();
    }

    return tempHttpClient;
  }

  /**
   * Gets a custom HTTP client for proxy auth and/or HTTPS validation bypass.
   *
   * @return The custom HTTP client.
   */
  private CloseableHttpClient getCustomHttpClient() {
    HttpClientBuilder httpClientBuilder = HttpClients.custom();
    if (bypassHttpsValidation) {
      httpClientBypassHttpsValidation(httpClientBuilder);
    }

    if (proxyAuthEnabled()) {
      httpClientProxyAuth(httpClientBuilder);
    }

    return httpClientBuilder.build();
  }

  /**
   * Sets necessary properties on the HTTP Client to bypass HTTPS validation.
   *
   * @param builder The HttpClientBuilder being used to build the client.
   */
  private void httpClientBypassHttpsValidation(HttpClientBuilder builder) {
    try {
      builder
        .setSSLContext(
            new SSLContextBuilder().loadTrustMaterial(null, TrustAllStrategy.INSTANCE).build()
        )
        .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
    } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
      LOG.error("Error bypassing HTTPS Validation", e);
    }
  }

  /**
   * Sets necessary properties on the HTTP Client to add proxy authentication.
   *
   * @param builder The HttpClientBuilder being used to build the client.
   */
  private void httpClientProxyAuth(HttpClientBuilder builder) {
    CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
    //noinspection OptionalGetWithoutIsPresent
    credentialsProvider.setCredentials(
        new AuthScope(proxyIp.get(), proxyPort.get()),
        new UsernamePasswordCredentials(proxyUser.get(), proxyPass.get()));
    credentialsProvider.setCredentials(
        new AuthScope(hook.getHost(), port),
        new UsernamePasswordCredentials(proxyUser.get(), proxyPass.get()));
    builder.setDefaultCredentialsProvider(credentialsProvider);
  }

  /**
   * Gets the port of the webhook URL.
   *
   * @return The port of the webhook URL.
   */
  private int getPort() {
    int tempPort = hook.getPort();
    if (tempPort == -1) {
      tempPort = (hook.getProtocol().equals("https") ? 443 : 80);
    }

    return tempPort;
  }

  /**
   * Gets the full path of the webhook URL, including query string and anchor reference.
   *
   * @return The full path of the webhook URL.
   */
  private String getPath() {
    String tempPath = hook.getPath();
    String query = hook.getQuery();
    if (query != null && !query.isEmpty()) {
      tempPath += "?" + query;
    }

    String ref = hook.getRef();
    if (ref != null && !ref.isEmpty()) {
      tempPath += "#" + ref;
    }

    return tempPath;
  }

}