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

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

There is a newer version: 4.1.4
Show newest version
/*
 * Copyright (c) 2020, 2023 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.InetSocketAddress;
import java.net.ProxySelector;
import java.net.URI;
import java.util.Arrays;
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.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import io.helidon.common.configurable.LruCache;
import io.helidon.config.Config;
import io.helidon.config.metadata.Configured;
import io.helidon.config.metadata.ConfiguredOption;

import io.netty.channel.ChannelHandler;
import io.netty.handler.proxy.HttpProxyHandler;
import io.netty.handler.proxy.Socks4ProxyHandler;
import io.netty.handler.proxy.Socks5ProxyHandler;

/**
 * A definition of a proxy server to use for outgoing requests.
 */
public class Proxy {

    private static final Logger LOGGER = Logger.getLogger(Proxy.class.getName());

    /**
     * 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 systemSelector;
    private final boolean useSystemSelector;

    private Proxy(Proxy.Builder builder) {
        this.type = builder.type();
        this.systemSelector = builder.systemSelector();
        this.host = builder.host();
        this.useSystemSelector = ((null == host) && (null != systemSelector));

        this.port = builder.port();
        this.username = builder.username();
        this.password = builder.password();

        if (useSystemSelector) {
            this.noProxy = inetSocketAddress -> true;
        } else {
            this.noProxy = prepareNoProxy(builder.noProxyHosts());
        }
    }

    /**
     * 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() {
        return builder()
                .useSystemSelector(true)
                .build();
    }

    Function noProxyPredicate() {
        return noProxy;
    }

    /**
     * Get proxy type. For testing purposes.
     *
     * @return the proxy type
     */
    ProxyType type() {
        return type;
    }

    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.getHost()) || noProxyHosts
                    .contains(address.getHost() + ":" + 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 -> {
            String host = resolveHost(address.getHost());
            int port = address.getPort();

            // 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.finest(() -> "IP Address " + host + " bypasses proxy");
                        return true;
                    }
                }
                LOGGER.finest(() -> "IP Address " + host + " uses proxy");
            } else {
                // we have a host name
                for (BiFunction hostMatcher : hostMatchers) {
                    if (hostMatcher.apply(host, port)) {
                        LOGGER.finest(() -> "Host " + host + " bypasses proxy");
                        return true;
                    }
                }
                LOGGER.finest(() -> "Host " + host + " uses proxy");
            }

            return false;
        };
    }

    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());
    }

    /**
     * Creates new proxy channel handler.
     *
     * @param address request address
     * @return channel handler
     */
    public Optional handler(URI address) {
        if (type == ProxyType.NONE) {
            return Optional.empty();
        }

        if (useSystemSelector) {
            return systemSelectorHandler(address);
        }

        if (noProxy.apply(address)) {
            return Optional.empty();
        }

        return Optional.of(handler());
    }

    private Optional systemSelectorHandler(URI address) {
        // this is hardcoded to http protocol and no path - a bit limited use case, though better than none
        List selected = systemSelector
                .select(URI.create("http://" + address.getHost() + ":" + address.getPort()));

        if (selected.isEmpty()) {
            return Optional.empty();
        }

        java.net.Proxy systemProxy = selected.iterator().next();

        switch (systemProxy.type()) {
        case DIRECT:
            return Optional.empty();
        case HTTP:
            return Optional.of(httpProxy(systemProxy));
        case SOCKS:
            return Optional.of(socksProxy(systemProxy));
        default:
            throw new IllegalStateException("Unexpected proxy type: " + systemProxy.type());
        }
    }

    private ChannelHandler handler() {
        switch (type) {
        case HTTP:
            return httpProxy();
        case SOCKS_4:
            return socks4Proxy();
        case SOCKS_5:
            return socks5Proxy();
        default:
            throw new IllegalArgumentException("Unsupported proxy type: " + type);
        }
    }

    private ChannelHandler socks5Proxy() {
        return username.map(s -> new Socks5ProxyHandler(address(), s, password.map(String::new).orElse("")))
                .orElseGet(() -> new Socks5ProxyHandler(address()));
    }

    private ChannelHandler socks4Proxy() {
        return username.map(s -> new Socks4ProxyHandler(address(), s))
                .orElseGet(() -> new Socks4ProxyHandler(address()));
    }

    private ChannelHandler httpProxy() {
        return username.map(s -> new HttpProxyHandler(address(), s, password.map(String::new).orElse("")))
                .orElseGet(() -> new HttpProxyHandler(address()));

    }

    private ChannelHandler httpProxy(java.net.Proxy systemProxy) {
        return new HttpProxyHandler(systemProxy.address());
    }

    private ChannelHandler socksProxy(java.net.Proxy systemProxy) {
        return new Socks5ProxyHandler(systemProxy.address());
    }

    private InetSocketAddress address() {
        return new InetSocketAddress(host, port);
    }

    @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
                && useSystemSelector == proxy.useSystemSelector
                && type == proxy.type
                && Objects.equals(host, proxy.host)
                && Objects.equals(noProxy, proxy.noProxy)
                && Objects.equals(username, proxy.username)
                && Objects.equals(password, proxy.password)
                && Objects.equals(systemSelector, proxy.systemSelector);
    }

    @Override
    public int hashCode() {
        return Objects.hash(type, host, port, noProxy, username, password, systemSelector, useSystemSelector);
    }

    /**
     * Fluent API builder for {@link Proxy}.
     */
    @Configured
    public static class Builder implements io.helidon.common.Builder {
        private final Set noProxyHosts = new HashSet<>();

        private ProxyType type;
        private String host;
        private int port = 80;
        private String username;
        private char[] password;
        private ProxySelector systemSelector;

        private Builder() {
        }

        @Override
        public Proxy build() {
            if ((host == null || host.isEmpty()) && systemSelector == null) {
                useSystemSelector(true);
            }
            return new Proxy(this);
        }

        /**
         * Configure a metric from configuration.
         * The following configuration key are used:
         * 
         * 
         * 
         *     
         *     
         *     
         * 
         * 
         *     
         *     
         *     
         * 
         * 
         *     
         *     
         *     
         * 
         * 
         *     
         *     
         *     
         * 
         * 
         *     
         *     
         *     
         * 
         * 
         *     
         *     
         *     
         * 
         * 
         *     
         *     
         *     
         * 
         * 
         *     
         *     
         *     
         * 
         * 
Client Metric configuration options
keydefaultdescription
use-system-selector{@code false}Whether system proxy selector should be used
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
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("use-system-selector").asBoolean().ifPresent(this::useSystemSelector); if (this.type != ProxyType.SYSTEM) { config.get("type").asString().map(ProxyType::valueOf).ifPresentOrElse(this::type, () -> type(ProxyType.HTTP)); 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 */ @ConfiguredOption("HTTP") public Builder type(ProxyType type) { this.type = type; return this; } /** * Sets a new host value. * * @param host host * @return updated builder instance */ @ConfiguredOption public Builder host(String host) { this.host = 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; } /** * Configure proxy from environment variables and system properties. * * @param useIt use system selector * @return updated builder instance */ @ConfiguredOption("false") public Builder useSystemSelector(boolean useIt) { if (useIt) { this.type = ProxyType.SYSTEM; this.systemSelector = ProxySelector.getDefault(); } else { if (this.type == ProxyType.SYSTEM) { this.type = ProxyType.NONE; } this.systemSelector = null; } return this; } ProxyType type() { return type; } 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); } ProxySelector systemSelector() { return systemSelector; } } /** * Type of the proxy. */ public enum ProxyType { /** * No proxy. */ NONE, /** * Proxy obtained from system. */ SYSTEM, /** * HTTP proxy. */ HTTP, /** * SOCKS V4 proxy. */ SOCKS_4, /** * SOCKS V5 proxy. */ SOCKS_5 } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy