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

io.helidon.webclient.api.Proxy Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2023, 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.api;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.System.Logger.Level;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.Socket;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import io.helidon.common.config.Config;
import io.helidon.common.configurable.LruCache;
import io.helidon.common.media.type.MediaTypes;
import io.helidon.common.socket.SocketOptions;
import io.helidon.common.tls.Tls;
import io.helidon.config.metadata.Configured;
import io.helidon.config.metadata.ConfiguredOption;
import io.helidon.http.Header;
import io.helidon.http.HeaderNames;
import io.helidon.http.HeaderValues;
import io.helidon.http.Method;
import io.helidon.http.Status;

/**
 * A definition of a proxy server to use for outgoing requests.
 */
public class Proxy {
    private static final System.Logger LOGGER = System.getLogger(Proxy.class.getName());
    private static final Tls NO_TLS = Tls.builder().enabled(false).build();

    /**
     * No proxy instance.
     */
    private static final Proxy NO_PROXY = new Proxy(builder().type(ProxyType.NONE));
    private static final Pattern PORT_PATTERN = Pattern.compile(".*:(\\d+)");
    private static final Pattern IP_V4 = Pattern.compile("^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\."
                                                                 + "(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}$");
    private static final Pattern IP_V6_IDENTIFIER = Pattern.compile("^\\[(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}]$");
    private static final Pattern IP_V6_HEX_IDENTIFIER = Pattern
            .compile("^\\[((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)]$");
    private static final Pattern IP_V6_HOST = Pattern.compile("^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$");
    private static final Pattern IP_V6_HEX_HOST = Pattern
            .compile("^((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)$");

    private static final LruCache IVP6_HOST_MATCH_RESULTS = LruCache.builder()
            .capacity(100)
            .build();
    private static final LruCache IVP6_IDENTIFIER_MATCH_RESULTS = LruCache.builder()
            .capacity(100)
            .build();

    private final ProxyType type;
    private final String host;
    private final int port;
    private final Function noProxy;
    private final Optional username;
    private final Optional password;
    private final ProxySelector systemProxySelector;
    private final Optional
proxyAuthHeader; private final boolean forceHttpConnect; private Proxy(Proxy.Builder builder) { this.host = builder.host(); if (this.host != null) { this.type = ProxyType.HTTP; } else { this.type = builder.type(); } this.port = builder.port(); this.username = builder.username(); this.password = builder.password(); this.forceHttpConnect = builder.forceHttpConnect(); if (type == ProxyType.SYSTEM) { this.noProxy = inetSocketAddress -> true; this.systemProxySelector = ProxySelector.getDefault(); } else { this.noProxy = prepareNoProxy(builder.noProxyHosts()); this.systemProxySelector = null; } if (username.isPresent()) { char[] pass = password.orElse(new char[0]); // Making the password char[] to String looks not correct, but it is done in the same way in HttpBasicAuthProvider String b64 = Base64.getEncoder().encodeToString((username.get() + ":" + new String(pass)) .getBytes(StandardCharsets.UTF_8)); this.proxyAuthHeader = Optional.of(HeaderValues.create(HeaderNames.PROXY_AUTHORIZATION, "Basic " + b64)); } else { this.proxyAuthHeader = Optional.empty(); } } /** * Fluent API builder for new instances. * * @return a new builder */ public static Builder builder() { return new Builder(); } /** * A Proxy instance that does not proxy requests. * * @return a new instance with no proxy definition */ public static Proxy noProxy() { return NO_PROXY; } /** * Create a new proxy instance from configuration. * {@code * proxy: * http: * uri: https://www.example.org * https: * uri: https://www.example.org * no-proxy: ["*.example.org", "localhost"] * } * * @param config configuration, should be located on a key that has proxy as a subkey * @return proxy instance */ public static Proxy create(Config config) { return builder() .config(config) .build(); } /** * Create from environment and system properties. * * @return a proxy instance configured based on this system settings */ public static Proxy create() { // we must create a new instance, as the system proxy may be reset return builder().type(ProxyType.SYSTEM).build(); } static Function prepareNoProxy(Set noProxyHosts) { if (noProxyHosts.isEmpty()) { // if no exceptions, then simple return address -> false; } boolean simple = true; for (String noProxyHost : noProxyHosts) { // go through all - if none start with *. then simple contains is sufficient if (noProxyHost.startsWith(".")) { simple = false; break; } } if (simple) { return address -> noProxyHosts.contains(address.getHostName()) || noProxyHosts.contains(address.getHostName() + ":" + address.getPort()); } List> hostMatchers = new LinkedList<>(); List> ipMatchers = new LinkedList<>(); for (String noProxyHost : noProxyHosts) { String hostPart = noProxyHost; Integer portPart = null; Matcher portMatcher = PORT_PATTERN.matcher(noProxyHost); if (portMatcher.matches()) { // we have a port portPart = Integer.parseInt(portMatcher.group(1)); int index = noProxyHost.lastIndexOf(':'); hostPart = noProxyHost.substring(0, index); } if (isIpV4(hostPart)) { //this is going to be an IP matcher - IP matchers only support full IP addresses exactMatch(ipMatchers, hostPart, portPart); } else if (isIpV6Identifier(hostPart)) { if ("[::1]".equals(hostPart)) { exactMatch(ipMatchers, "0:0:0:0:0:0:0:1", portPart); } exactMatch(ipMatchers, hostPart.substring(1, hostPart.length() - 1), portPart); } else { // for host names, we must honor . prefix to handle all sub-domains if (hostPart.charAt(0) == '.') { prefixedMatch(hostMatchers, hostPart, portPart); } else { // exact match exactMatch(hostMatchers, hostPart, portPart); } } } // complicated - must check for . prefixes return address -> { InetAddress inetAddress = address.getAddress(); Set toCheck; if (inetAddress == null) { toCheck = Set.of(address.getHostString()); } else { toCheck = new HashSet<>(); // if the address was created with an IP address, both may be the same toCheck.add(resolveHost(inetAddress.getHostName())); toCheck.add(resolveHost(inetAddress.getHostAddress())); } int port = address.getPort(); // we need to check both IP address and host name (if set) for (String host : toCheck) { // first need to make sure whether I have an IP address or a hostname if (isIpV4(host) || isIpV6Host(host)) { // we have an IP address for (BiFunction ipMatcher : ipMatchers) { if (ipMatcher.apply(host, port)) { LOGGER.log(Level.TRACE, () -> "IP Address " + host + " bypasses proxy"); return true; } } LOGGER.log(Level.TRACE, () -> "IP Address " + host + " uses proxy"); } else { // we have a host name for (BiFunction hostMatcher : hostMatchers) { if (hostMatcher.apply(host, port)) { LOGGER.log(Level.TRACE, () -> "Host " + host + " bypasses proxy"); return true; } } LOGGER.log(Level.TRACE, () -> "Host " + host + " uses proxy"); } } return false; }; } /** * Create a socket for TCP, connected through the proxy. * * @param webClient web client to use if HTTP requests must be done * @param inetSocketAddress target address of the request (proxy address is configured as part Proxy instance) * @param socketOptions options for creating sockets * @param tls whether to use TLS * @return a new connected socket */ public Socket tcpSocket(WebClient webClient, InetSocketAddress inetSocketAddress, SocketOptions socketOptions, boolean tls) { return type.connect(webClient, this, inetSocketAddress, socketOptions, tls); } /** * Get proxy type. * * @return the proxy type */ public ProxyType type() { return type; } /** * Verifies whether the current host is inside noHosts. * * @param uri the uri * @return true if it is in no hosts, otherwise false */ public boolean isNoHosts(InetSocketAddress uri) { return noProxy.apply(uri); } /** * Verifies whether the specified Uri is using system proxy. * * @param uri the uri * @return true if the uri resource will be proxied */ public boolean isUsingSystemProxy(String uri) { if (systemProxySelector != null) { List proxies = systemProxySelector .select(URI.create(uri)); return !proxies.isEmpty() && !proxies.get(0).equals(java.net.Proxy.NO_PROXY); } return false; } /** * Creates an Optional with the InetSocketAddress of the server proxy for the specified uri. * * @param uri the uri * @return the InetSocketAddress */ private Optional address(InetSocketAddress uri) { if (type == null || type == ProxyType.NONE || type == ProxyType.SYSTEM) { return Optional.empty(); } if (isNoHosts(uri)) { return Optional.empty(); } return Optional.of(new InetSocketAddress(host, port)); } /** * Returns the port. * * @return proxy port */ public int port() { return port; } /** * Returns the host. * * @return proxy host */ public String host() { return host; } /** * Returns an Optional with the username. * * @return the username */ public Optional username() { return username; } /** * Returns an Optional with the password. * * @return the password */ public Optional password() { return password; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Proxy proxy = (Proxy) o; return port == proxy.port && type == proxy.type && Objects.equals(systemProxySelector, proxy.systemProxySelector) && Objects.equals(host, proxy.host) && Objects.equals(noProxy, proxy.noProxy) && Objects.equals(username, proxy.username) && Objects.equals(password, proxy.password); } @Override public int hashCode() { return Objects.hash(type, host, port, noProxy, username, password); } private static String resolveHost(String host) { if (host != null && isIpV6Identifier(host)) { return host.substring(1, host.length() - 1); } return host; } private static void prefixedMatch(List> matchers, String hostPart, Integer portPart) { if (null == portPart) { matchers.add((host, port) -> prefixHostMatch(hostPart, host)); } else { matchers.add((host, port) -> portPart.equals(port) && prefixHostMatch(hostPart, host)); } } private static boolean prefixHostMatch(String hostPart, String host) { if (host.endsWith(hostPart)) { return true; } return host.equals(hostPart.substring(1)); } private static void exactMatch(List> matchers, String hostPart, Integer portPart) { if (null == portPart) { matchers.add((host, port) -> hostPart.equals(host)); } else { matchers.add((host, port) -> portPart.equals(port) && hostPart.equals(host)); } } private static boolean isIpV4(String host) { return IP_V4.matcher(host).matches(); } private static boolean isIpV6Identifier(String host) { return IVP6_IDENTIFIER_MATCH_RESULTS.computeValue(host, () -> isIpV6IdentifierRegExp(host)).orElse(false); } private static Optional isIpV6IdentifierRegExp(String host) { return Optional.of(IP_V6_IDENTIFIER.matcher(host).matches() || IP_V6_HEX_IDENTIFIER.matcher(host).matches()); } private static boolean isIpV6Host(String host) { return IVP6_HOST_MATCH_RESULTS.computeValue(host, () -> isIpV6HostRegExp(host)).orElse(false); } private static Optional isIpV6HostRegExp(String host) { return Optional.of(IP_V6_HOST.matcher(host).matches() || IP_V6_HEX_HOST.matcher(host).matches()); } private static Socket connectToProxy(WebClient webClient, InetSocketAddress proxyAddress, InetSocketAddress targetAddress, Proxy proxy, boolean tls) { WebClientConfig clientConfig = webClient.prototype(); TcpClientConnection connection = TcpClientConnection.create(webClient, new ConnectionKey("http", proxyAddress.getHostName(), proxyAddress.getPort(), clientConfig.readTimeout() .orElse(Duration.ZERO), NO_TLS, clientConfig.dnsResolver(), clientConfig.dnsAddressLookup(), NO_PROXY), List.of(), it -> false, it -> { }) .connect(); if (proxy.forceHttpConnect || tls || proxy.username.isPresent()) { HttpClientRequest request = webClient.method(Method.CONNECT) .followRedirects(false) // do not follow redirects for proxy connect itself .connection(connection) .uri("http://" + proxyAddress.getHostName() + ":" + proxyAddress.getPort()) .protocolId("http/1.1") // MUST be 1.1, if not available, proxy connection will fail .header(HeaderNames.HOST, targetAddress.getHostName() + ":" + targetAddress.getPort()) .accept(MediaTypes.WILDCARD); if (clientConfig.keepAlive()) { request.header(HeaderValues.CONNECTION_KEEP_ALIVE) .header(ClientRequestBase.PROXY_CONNECTION); } proxy.proxyAuthHeader.ifPresent(request::header); // we cannot close the response, as that would close the connection HttpClientResponse response = request.request(); if (response.status().family() != Status.Family.SUCCESSFUL) { response.close(); throw new IllegalStateException("Proxy sent wrong HTTP response code: " + response.status()); } } return connection.socket(); } /** * Type of the proxy. */ public enum ProxyType { /** * No proxy. */ NONE { @Override Socket connect(WebClient webClient, Proxy proxy, InetSocketAddress targetAddress, SocketOptions socketOptions, boolean tls) { try { Socket socket = new Socket(); socketOptions.configureSocket(socket); socket.connect(targetAddress, (int) socketOptions.connectTimeout().toMillis()); return socket; } catch (IOException e) { throw new UncheckedIOException(e); } } }, /** * Proxy obtained from system. */ SYSTEM { @Override Socket connect(WebClient webClient, Proxy proxy, InetSocketAddress targetAddress, SocketOptions socketOptions, boolean tls) { String scheme = tls ? "https" : "http"; if (proxy.systemProxySelector == null) { return NONE.connect(webClient, proxy, targetAddress, socketOptions, tls); } List proxies = proxy.systemProxySelector .select(URI.create(scheme + "://" + targetAddress.getHostName() + ":" + targetAddress.getPort())); if (proxies.isEmpty()) { return NONE.connect(webClient, proxy, targetAddress, socketOptions, tls); } try { Socket socket = new Socket(proxies.get(0)); socketOptions.configureSocket(socket); socket.connect(targetAddress, (int) socketOptions.connectTimeout().toMillis()); return socket; } catch (IOException e) { throw new UncheckedIOException(e); } } }, /** * HTTP proxy. */ HTTP { @Override Socket connect(WebClient webClient, Proxy proxy, InetSocketAddress targetAddress, SocketOptions socketOptions, boolean tls) { return proxy.address(targetAddress) .map(proxyAddress -> connectToProxy(webClient, proxyAddress, targetAddress, proxy, tls)) .orElseGet(() -> NONE.connect(webClient, proxy, targetAddress, socketOptions, tls)); } }; abstract Socket connect(WebClient webClient, Proxy proxy, InetSocketAddress targetAddress, SocketOptions socketOptions, boolean tls); } /** * Fluent API builder for {@link Proxy}. */ @Configured public static class Builder implements io.helidon.common.Builder { private final Set noProxyHosts = new HashSet<>(); // Defaults to system private ProxyType type = ProxyType.SYSTEM; private String host; private int port = 80; private String username; private char[] password; private boolean forceHttpConnect = false; private Builder() { } @Override public Proxy build() { return new Proxy(this); } /** * Configure a metric from configuration. * The following configuration key are used: * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Client Metric configuration options
keydefaultdescription
type{@code no default}Sets which type is this proxy. See {@link Proxy.ProxyType}
host{@code no default}Host of the proxy
port{@code 80}Port of the proxy
username{@code no default}Proxy username
password{@code no default}Proxy password
httpConnect{@code false}Specify whether the HTTP client will always execute HTTP CONNECT.
no-proxy{@code no default}Contains list of the hosts which should be excluded from using proxy
* * @param config configuration to configure this proxy * @return updated builder instance */ public Builder config(Config config) { config.get("type").asString().map(ProxyType::valueOf).ifPresent(this::type); if (this.type != ProxyType.SYSTEM && this.type != ProxyType.NONE) { config.get("host").asString().ifPresent(this::host); config.get("port").asInt().ifPresent(this::port); config.get("username").asString().ifPresent(this::username); config.get("password").asString().map(String::toCharArray).ifPresent(this::password); config.get("no-proxy").asList(String.class).ifPresent(hosts -> hosts.forEach(this::addNoProxy)); } return this; } /** * Sets a new proxy type. * * @param type proxy type * @return updated builder instance * @throws NullPointerException when type is null */ @ConfiguredOption("HTTP") public Builder type(ProxyType type) { this.type = Objects.requireNonNull(type); return this; } /** * Forces HTTP CONNECT with the proxy server. * Otherwise it will not execute HTTP CONNECT when the request is * plain HTTP with no authentication. * * @param forceHttpConnect HTTP CONNECT * @return updated builder instance */ @ConfiguredOption public Builder forceHttpConnect(boolean forceHttpConnect) { this.forceHttpConnect = forceHttpConnect; return this; } /** * Sets a new host value. * * @param host host * @return updated builder instance * @throws NullPointerException when host is null */ @ConfiguredOption public Builder host(String host) { this.host = Objects.requireNonNull(host); return this; } /** * Sets a port value. * * @param port port * @return updated builder instance */ @ConfiguredOption public Builder port(int port) { this.port = port; return this; } /** * Sets a new username for the proxy. * * @param username proxy username * @return updated builder instance */ @ConfiguredOption public Builder username(String username) { this.username = username; return this; } /** * Sets a new password for the proxy. * * @param password proxy password * @return updated builder instance */ @ConfiguredOption(type = String.class) public Builder password(char[] password) { this.password = Arrays.copyOf(password, password.length); return this; } /** * Configure a host pattern that is not going through a proxy. *

* Options are: *

    *
  • IP Address, such as {@code 192.168.1.1}
  • *
  • IP V6 Address, such as {@code [2001:db8:85a3:8d3:1319:8a2e:370:7348]}
  • *
  • Hostname, such as {@code localhost}
  • *
  • Domain name, such as {@code helidon.io}
  • *
  • Domain name and all sub-domains, such as {@code .helidon.io} (leading dot)
  • *
  • Combination of all options from above with a port, such as {@code .helidon.io:80}
  • *
* * @param noProxyHost to exclude from proxying * @return updated builder instance */ @ConfiguredOption(key = "no-proxy", kind = ConfiguredOption.Kind.LIST) public Builder addNoProxy(String noProxyHost) { noProxyHosts.add(noProxyHost); return this; } ProxyType type() { return type; } boolean forceHttpConnect() { return forceHttpConnect; } String host() { return host; } int port() { return port; } Set noProxyHosts() { return new HashSet<>(noProxyHosts); } Optional username() { return Optional.ofNullable(username); } Optional password() { return Optional.ofNullable(password); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy