HttpProxyUtil.java

/*
 * Copyright (C) 2023 B3Partners B.V.
 *
 * SPDX-License-Identifier: MIT
 */
package org.tailormap.api.util;

import static org.apache.commons.lang3.StringUtils.isNotEmpty;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;

import jakarta.servlet.http.HttpServletRequest;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.net.http.HttpRequest;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Locale;
import java.util.Set;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;

public class HttpProxyUtil {
  public static void addForwardedForRequestHeaders(HttpRequest.Builder requestBuilder, HttpServletRequest request) {
    try {
      String ip = request.getRemoteAddr();
      InetAddress inetAddress = InetAddress.getByName(ip);

      if (inetAddress instanceof Inet6Address inet6Address) {
        // https://stackoverflow.com/questions/33168783/
        int scopeId = inet6Address.getScopeId();

        if (scopeId > 0) {
          ip = inet6Address.getHostName().replaceAll("%" + scopeId + "$", "");
        }

        // IPv6 address must be bracketed and quoted
        ip = "\"[" + ip + "]\"";
      }
      requestBuilder.header("X-Forwarded-For", ip);
      requestBuilder.header("Forwarded", "for=" + ip);
    } catch (UnknownHostException ignored) {
      // Don't care
    }
  }

  public static void passthroughRequestHeaders(
      HttpRequest.Builder requestBuilder, HttpServletRequest request, Set<String> headers) {
    for (String header : headers) {
      String value = request.getHeader(header);
      if (value != null) {
        requestBuilder.header(header, value);
      }
    }
  }

  public static HttpHeaders passthroughResponseHeaders(
      java.net.http.HttpHeaders upstreamHeaders, Set<String> allowedResponseHeaders) {
    HttpHeaders headers = new HttpHeaders();
    for (String header : allowedResponseHeaders) {
      headers.addAll(header, upstreamHeaders.allValues(header));
    }
    return headers;
  }

  public static void setHttpBasicAuthenticationHeader(
      HttpRequest.Builder requestBuilder, String username, String password) {
    if (username != null && password != null) {
      String toEncode = username + ":" + password;
      String encoded = Base64.getEncoder().encodeToString(toEncode.getBytes(StandardCharsets.UTF_8));
      requestBuilder.header(AUTHORIZATION, "Basic " + encoded);
    }
  }

  /**
   * If the original request was a POST with x-www-urlencoded content type, configure the requestBuilder for a proxy
   * request to do a POST request with all parameters in the body to handle large POST parameters.
   *
   * @param requestBuilder builder for proxy request
   * @param uri URI of the proxy target, including query parameters
   * @param request the original request to be proxied
   */
  public static void configureProxyRequestBuilderForUri(
      HttpRequest.Builder requestBuilder, URI uri, HttpServletRequest request) {
    // When the original request is a POST with x-www-form-urlencoded content type, do the same (for long parameters
    // like CQL_FILTER which may trigger a URI Too Long or Bad Request response)
    if (HttpMethod.POST.matches(request.getMethod())
        && request.getContentType() != null
        && request.getContentType()
            .toLowerCase(Locale.ROOT)
            .contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {

      // The original request could have had some parameters in the URL and some in the body. However, in the
      // proxied request we put all parameters in the body
      // Make sure to not decode the query so '+' stays encoded as "%2B" and does not become a space
      String query = uri.getRawQuery();
      URI uriWithoutQuery = URI.create(uri.toString().split("\\?", 2)[0]);
      requestBuilder.uri(uriWithoutQuery);

      if (isNotEmpty(query)) {
        requestBuilder
            .POST(HttpRequest.BodyPublishers.ofString(query))
            .header(CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
      } else {
        requestBuilder.uri(uri);
      }
    } else {
      requestBuilder.uri(uri);
    }
  }
}