hazelcast-master/hazelcast/src/main/java/com/hazelcast/internal/util/AddressUtil.java
/*
 * Copyright (c) 2008-2023, Hazelcast, Inc. All Rights Reserved.
 *
 * 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 com.hazelcast.internal.util;

import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import static com.hazelcast.internal.util.EmptyStatement.ignore;

/**
 * AddressUtil contains Address helper methods.
 */
public final class AddressUtil {

    private static final int NUMBER_OF_ADDRESSES = 255;
    private static final int IPV4_LENGTH = 4;
    private static final int IPV6_LENGTH = 8;
    private static final int IPV6_MAX_THRESHOLD = 65535;
    private static final int IPV4_MAX_THRESHOLD = 255;
    private static final int HEXADECIMAL_RADIX = 16;
    private static final int DECIMAL_RADIX = 10;

    private static NetworkInterfacesEnumerator networkInterfacesEnumerator = NetworkInterfacesEnumerator.defaultEnumerator();

    private AddressUtil() {
    }

    static void setNetworkInterfacesEnumerator(NetworkInterfacesEnumerator networkInterfacesEnumerator) {
        AddressUtil.networkInterfacesEnumerator = networkInterfacesEnumerator;
    }

    public static boolean matchAnyInterface(String address, Collection<String> interfaces) {
        if (interfaces == null || interfaces.isEmpty()) {
            return false;
        }
        for (String interfaceMask : interfaces) {
            if (matchInterface(address, interfaceMask)) {
                return true;
            }
        }
        return false;
    }

    public static boolean matchInterface(String address, String interfaceMask) {
        final AddressMatcher mask;
        try {
            mask = getAddressMatcher(interfaceMask);
        } catch (Exception e) {
            return false;
        }
        return mask.match(address);
    }

    public static boolean matchAnyDomain(String name, Collection<String> patterns) {
        if (patterns == null || patterns.isEmpty()) {
            return false;
        }
        for (String pattern : patterns) {
            if (matchDomain(name, pattern)) {
                return true;
            }
        }
        return false;
    }

    public static boolean matchDomain(String name, String pattern) {
        final int index = pattern.indexOf('*');
        if (index == -1) {
            return name.equals(pattern);
        } else {
            String[] names = name.split("\\.");
            String[] patterns = pattern.split("\\.");
            if (patterns.length > names.length) {
                return false;
            }
            int nameIndexDiff = names.length - patterns.length;
            for (int i = patterns.length - 1; i > -1; i--) {
                if ("*".equals(patterns[i])) {
                    continue;
                }
                if (!patterns[i].equals(names[i + nameIndexDiff])) {
                    return false;
                }
            }
            return true;
        }
    }

    public static AddressHolder getAddressHolder(String address) {
        return getAddressHolder(address, -1);
    }

    public static AddressHolder getAddressHolder(final String address, int defaultPort) {
        final int indexBracketStart = address.indexOf('[');
        final int indexBracketEnd = address.indexOf(']', indexBracketStart);
        final int indexColon = address.indexOf(':');
        final int lastIndexColon = address.lastIndexOf(':');
        String host;
        int port = defaultPort;
        String scopeId = null;
        if (indexColon > -1 && lastIndexColon > indexColon) {
            // IPv6
            if (indexBracketStart == 0 && indexBracketEnd > indexBracketStart) {
                host = address.substring(indexBracketStart + 1, indexBracketEnd);
                if (lastIndexColon == indexBracketEnd + 1) {
                    port = Integer.parseInt(address.substring(lastIndexColon + 1));
                }
            } else {
                host = address;
            }
            final int indexPercent = host.indexOf('%');
            if (indexPercent != -1) {
                scopeId = host.substring(indexPercent + 1);
                host = host.substring(0, indexPercent);
            }
        } else if (indexColon > 0 && indexColon == lastIndexColon) {
            host = address.substring(0, indexColon);
            port = Integer.parseInt(address.substring(indexColon + 1));
        } else {
            host = address;
        }
        return new AddressHolder(host, port, scopeId);
    }

    public static boolean isIpAddress(String address) {
        try {
            getAddressMatcher(address);
            return true;
        } catch (InvalidAddressException e) {
            return false;
        }
    }

    public static InetAddress fixScopeIdAndGetInetAddress(final InetAddress inetAddress) throws SocketException {

        if (!(inetAddress instanceof Inet6Address)) {
            return inetAddress;
        }
        if (!inetAddress.isLinkLocalAddress() && !inetAddress.isSiteLocalAddress()) {
            return inetAddress;
        }

        final Inet6Address inet6Address = (Inet6Address) inetAddress;
        if (inet6Address.getScopeId() > 0 || inet6Address.getScopedInterface() != null) {
            return inetAddress;

        }
        final Inet6Address resultInetAddress = findRealInet6Address(inet6Address);
        return resultInetAddress == null ? inetAddress : resultInetAddress;
    }

    private static Inet6Address findRealInet6Address(Inet6Address inet6Address) throws SocketException {
        Inet6Address resultInetAddress = null;
        List<NetworkInterfaceInfo> interfaces = networkInterfacesEnumerator.getNetworkInterfaces();
        for (NetworkInterfaceInfo ni : interfaces) {
            List<InetAddress> addresses = ni.getInetAddresses();
            for (InetAddress address : addresses) {
                if (!isInet6Compatible(address, inet6Address)) {
                    continue;
                }

                if (resultInetAddress != null) {
                    throw new IllegalArgumentException("This address "
                            + inet6Address
                            + " is bound to more than one network interface!");
                }
                resultInetAddress = (Inet6Address) address;
            }
        }
        return resultInetAddress;
    }

    private static boolean isInet6Compatible(InetAddress address, Inet6Address inet6Address) {
        if (!(address instanceof Inet6Address)) {
            return false;
        }
        if (!Arrays.equals(address.getAddress(), inet6Address.getAddress())) {
            return false;
        }

        return true;
    }

    public static Inet6Address getInetAddressFor(final Inet6Address inetAddress, String scope)
            throws UnknownHostException, SocketException {
        if (inetAddress.isLinkLocalAddress() || inetAddress.isSiteLocalAddress()) {
            final char[] chars = scope.toCharArray();
            boolean numeric = true;
            for (char c : chars) {
                if (!Character.isDigit(c)) {
                    numeric = false;
                    break;
                }
            }
            if (numeric) {
                return Inet6Address.getByAddress(null, inetAddress.getAddress(), Integer.parseInt(scope));
            } else {
                return Inet6Address.getByAddress(null, inetAddress.getAddress(), NetworkInterface.getByName(scope));
            }
        }
        return inetAddress;
    }

    public static Collection<Inet6Address> getPossibleInetAddressesFor(final Inet6Address inet6Address) {
        if ((!inet6Address.isSiteLocalAddress() && !inet6Address.isLinkLocalAddress())
                || inet6Address.getScopeId() > 0 || inet6Address.getScopedInterface() != null) {
            return Collections.singleton(inet6Address);
        }

        LinkedList<Inet6Address> possibleAddresses = new LinkedList<>();
        try {
            List<NetworkInterfaceInfo> interfaces = networkInterfacesEnumerator.getNetworkInterfaces();
            for (NetworkInterfaceInfo ni : interfaces) {
                addPossibleAddress(inet6Address, possibleAddresses, ni);
            }
        } catch (IOException ignored) {
            ignore(ignored);
        }

        if (possibleAddresses.isEmpty()) {
            throw new IllegalArgumentException("Could not find a proper network interface"
                    + " to connect to "
                    + inet6Address);
        }
        return possibleAddresses;
    }

    private static void addPossibleAddress(Inet6Address inet6Address, Deque<Inet6Address> possibleAddresses,
                                           NetworkInterfaceInfo ni) throws UnknownHostException {
        List<InetAddress> addresses = ni.getInetAddresses();
        for (InetAddress address : addresses) {
            if (address instanceof Inet4Address) {
                continue;
            }

            if (inet6Address.isLinkLocalAddress() && address.isLinkLocalAddress()
                    || inet6Address.isSiteLocalAddress() && address.isSiteLocalAddress()) {
                final Inet6Address newAddress = Inet6Address.getByAddress(null, inet6Address.getAddress(),
                        ((Inet6Address) address).getScopeId());
                possibleAddresses.addFirst(newAddress);
            }
        }
    }

    public static Collection<String> getMatchingIpv4Addresses(final AddressMatcher addressMatcher) {
        if (addressMatcher.isIPv6()) {
            throw new IllegalArgumentException("Cannot wildcard matching for IPv6: "
                    + addressMatcher);
        }
        final Collection<String> addresses = new HashSet<String>();
        final String first3 = addressMatcher.address[0] + '.'
                + addressMatcher.address[1]
                + '.'
                + addressMatcher.address[2];
        final String lastPart = addressMatcher.address[3];
        final int dashPos;
        if ("*".equals(lastPart)) {
            for (int j = 0; j <= NUMBER_OF_ADDRESSES; j++) {
                addresses.add(first3 + '.' + j);
            }
        } else if (lastPart.indexOf('-') > 0) {
            dashPos = lastPart.indexOf('-');
            final int start = Integer.parseInt(lastPart.substring(0, dashPos));
            final int end = Integer.parseInt(lastPart.substring(dashPos + 1));
            for (int j = start; j <= end; j++) {
                addresses.add(first3 + '.' + j);
            }
        } else {
            addresses.add(addressMatcher.getAddress());
        }
        return addresses;
    }

    /**
     * Gets an AddressMatcher for a given addresses.
     *
     * @param address the address
     * @return the returned AddressMatcher. The returned value will never be null.
     * @throws com.hazelcast.internal.util.AddressUtil.InvalidAddressException if the address is not valid.
     */
    public static AddressMatcher getAddressMatcher(String address) {
        final AddressMatcher matcher;
        final int indexColon = address.indexOf(':');
        final int lastIndexColon = address.lastIndexOf(':');
        final int indexDot = address.indexOf('.');
        final int lastIndexDot = address.lastIndexOf('.');

        if (indexColon > -1 && lastIndexColon > indexColon) {
            if (indexDot == -1) {
                matcher = new Ip6AddressMatcher();
                parseIpv6(matcher, address);
            } else {
                // IPv4 mapped IPv6
                if (indexDot >= lastIndexDot) {
                    throw new InvalidAddressException(address);
                }
                final int lastIndexColon2 = address.lastIndexOf(':');
                final String host2 = address.substring(lastIndexColon2 + 1);
                matcher = new Ip4AddressMatcher();
                parseIpv4(matcher, host2);
            }
        } else if (indexDot > -1 && lastIndexDot > indexDot && indexColon == -1) {
            // IPv4
            matcher = new Ip4AddressMatcher();
            parseIpv4(matcher, address);
        } else {
            throw new InvalidAddressException(address);
        }
        return matcher;
    }

    public static Collection<Integer> getOutboundPorts(Collection<Integer> ports,
                                                       Collection<String> portDefinitions) {
        if (ports == null) {
            ports = Collections.emptySet();
        }
        if (portDefinitions == null) {
            portDefinitions = Collections.emptySet();
        }
        if (portDefinitions.isEmpty() && ports.isEmpty()) {
            // means any port
            return Collections.emptySet();
        }
        if (portDefinitions.contains("*") || portDefinitions.contains("0")) {
            // means any port
            return Collections.emptySet();
        }
        Set<Integer> selectedPorts = new HashSet<>(ports);
        transformPortDefinitionsToPorts(portDefinitions, selectedPorts);
        if (selectedPorts.contains(0)) {
            // means any port
            return Collections.emptySet();
        }
        return selectedPorts;
    }

    public static Set<Integer> transformPortDefinitionsToPorts(Collection<String> portDefinitions, Set<Integer> ports) {
        // not checking port ranges...
        for (String portDef : portDefinitions) {
            String[] portDefs = portDef.split("[,; ]");
            for (String def : portDefs) {
                def = def.trim();
                if (def.isEmpty()) {
                    continue;
                }
                final int dashPos = def.indexOf('-');
                if (dashPos > 0) {
                    final int start = Integer.parseInt(def.substring(0, dashPos));
                    final int end = Integer.parseInt(def.substring(dashPos + 1));
                    for (int port = start; port <= end; port++) {
                        ports.add(port);
                    }
                } else {
                    ports.add(Integer.parseInt(def));
                }
            }
        }
        return ports;
    }

    private static void parseIpv4(AddressMatcher matcher, String address) {
        final String[] parts = address.split("\\.");
        if (parts.length != IPV4_LENGTH) {
            throw new InvalidAddressException(address);
        }
        for (String part : parts) {
            if (!isValidIpAddressPart(part, false)) {
                throw new InvalidAddressException(address);
            }
        }
        matcher.setAddress(parts);
    }

    private static boolean isValidIpAddressPart(String part, boolean ipv6) {
        boolean isValid = true;
        if (part.length() == 1 && "*".equals(part)) {
            return true;
        }
        final int rangeIndex = part.indexOf('-');
        if (rangeIndex > -1
                && (rangeIndex != part.lastIndexOf('-') || rangeIndex == part.length() - 1)) {
            return false;
        }
        final String[] subParts;
        if (rangeIndex > -1) {
            subParts = part.split("\\-");
        } else {
            subParts = new String[]{part};
        }
        try {
            for (String subPart : subParts) {
                final int num;
                if (ipv6) {
                    num = Integer.parseInt(subPart, HEXADECIMAL_RADIX);
                    if (num > IPV6_MAX_THRESHOLD) {
                        isValid = false;
                        break;
                    }
                } else {
                    num = Integer.parseInt(subPart);
                    if (num > IPV4_MAX_THRESHOLD) {
                        isValid = false;
                        break;
                    }
                }
            }
        } catch (NumberFormatException e) {
            isValid = false;
        }
        return isValid;
    }

    private static void parseIpv6(AddressMatcher matcher, String addrs) {
        String address = addrs;
        if (address.indexOf('%') > -1) {
            String[] parts = address.split("\\%");
            address = parts[0];
        }
        final String[] parts = address.split("((?<=:)|(?=:))");
        final Collection<String> ipString = parseIPV6parts(parts, address);
        if (ipString.size() != IPV6_LENGTH) {
            throw new InvalidAddressException(address);
        }
        final String[] addressParts = ipString.toArray(new String[0]);
        checkIfAddressPartsAreValid(addressParts, address);
        matcher.setAddress(addressParts);
    }

    private static Collection<String> parseIPV6parts(String[] parts, String address) {
        final LinkedList<String> ipString = new LinkedList<>();
        int count = 0;
        int mark = -1;
        for (int i = 0; i < parts.length; i++) {
            String part = parts[i];
            String nextPart = i < parts.length - 1 ? parts[i + 1] : null;
            if ("".equals(part)) {
                continue;
            } else if (":".equals(part) && ":".equals(nextPart)) {
                if (mark != -1) {
                    throw new InvalidAddressException(address);
                }
                mark = count;
            } else if (!":".equals(part)) {
                count++;
                ipString.add(part);
            }
        }
        if (mark > -1) {
            final int remaining = (IPV6_LENGTH - count);
            for (int i = 0; i < remaining; i++) {
                ipString.add((i + mark), "0");
            }
        }
        return ipString;
    }

    private static void checkIfAddressPartsAreValid(String[] addressParts, String address) {
        for (String part : addressParts) {
            if (!isValidIpAddressPart(part, true)) {
                throw new InvalidAddressException(address);
            }
        }
    }

    // ----------------- UTILITY CLASSES ------------------

    /**
     * Holds address
     */
    public static class AddressHolder {

        private final String address;
        private final String scopeId;
        private final int port;

        public AddressHolder(final String address, final int port,
                             final String scopeId) {
            this.address = address;
            this.scopeId = scopeId;
            this.port = port;
        }

        @Override
        public String toString() {
            return "AddressHolder [" + address + "]:" + port;
        }

        public String getAddress() {
            return address;
        }

        public String getScopeId() {
            return scopeId;
        }

        public int getPort() {
            return port;
        }
    }

    /**
     * http://docs.oracle.com/javase/1.5.0/docs/guide/net/ipv6_guide/index.html
     */
    public abstract static class AddressMatcher {

        protected final String[] address;

        protected AddressMatcher(final String[] address) {
            this.address = address;
        }

        public abstract boolean isIPv4();

        public abstract boolean isIPv6();

        public abstract void setAddress(String[] ip);

        protected final boolean match(final String[] mask, String[] input, int radix) {
            if (input != null && mask != null) {
                for (int i = 0; i < mask.length; i++) {
                    if (!doMatch(mask[i], input[i], radix)) {
                        return false;
                    }
                }
                return true;
            }
            return false;
        }

        protected final boolean doMatch(final String mask, String input, int radix) {
            final int dashIndex = mask.indexOf('-');
            final int ipa = Integer.parseInt(input, radix);
            if ("*".equals(mask)) {
                return true;
            } else if (dashIndex != -1) {
                final int start = Integer.parseInt(mask.substring(0, dashIndex).trim(), radix);
                final int end = Integer.parseInt(mask.substring(dashIndex + 1).trim(), radix);
                if (ipa >= start && ipa <= end) {
                    return true;
                }
            } else {
                final int x = Integer.parseInt(mask, radix);
                if (x == ipa) {
                    return true;
                }
            }
            return false;
        }

        public abstract String getAddress();

        public abstract boolean match(AddressMatcher matcher);

        public boolean match(final String address) {
            try {
                return match(getAddressMatcher(address));
            } catch (Exception e) {
                return false;
            }
        }

        @Override
        public String toString() {
            return getClass().getSimpleName() + '{' + getAddress() + '}';
        }
    }

    static class Ip4AddressMatcher extends AddressMatcher {

        Ip4AddressMatcher() {
            // d.d.d.d
            super(new String[IPV4_LENGTH]);
        }

        @Override
        public boolean isIPv4() {
            return true;
        }

        @Override
        public boolean isIPv6() {
            return false;
        }

        @Override
        public void setAddress(String[] ip) {
            System.arraycopy(ip, 0, this.address, 0, ip.length);
        }

        @Override
        public boolean match(final AddressMatcher matcher) {
            if (matcher.isIPv6()) {
                return false;
            }
            final String[] mask = this.address;
            final String[] input = ((Ip4AddressMatcher) matcher).address;
            return match(mask, input, DECIMAL_RADIX);
        }

        @Override
        public String getAddress() {
            final StringBuilder sb = new StringBuilder();
            for (int i = 0; i < address.length; i++) {
                sb.append(address[i]);
                if (i != address.length - 1) {
                    sb.append('.');
                }
            }
            return sb.toString();
        }
    }

    static class Ip6AddressMatcher extends AddressMatcher {

        Ip6AddressMatcher() {
            // x:x:x:x:x:x:x:x%s
            super(new String[IPV6_LENGTH]);
        }

        @Override
        public boolean isIPv4() {
            return false;
        }

        @Override
        public boolean isIPv6() {
            return true;
        }

        @Override
        public void setAddress(String[] ip) {
            System.arraycopy(ip, 0, this.address, 0, ip.length);
        }

        @Override
        public boolean match(final AddressMatcher matcher) {
            if (matcher.isIPv4()) {
                return false;
            }
            final Ip6AddressMatcher a = (Ip6AddressMatcher) matcher;
            final String[] mask = this.address;
            final String[] input = a.address;
            return match(mask, input, HEXADECIMAL_RADIX);
        }

        @Override
        public String getAddress() {
            final StringBuilder sb = new StringBuilder();
            for (int i = 0; i < address.length; i++) {
                sb.append(address[i]);
                if (i != address.length - 1) {
                    sb.append(':');
                }
            }
            return sb.toString();
        }
    }

    /**
     * Thrown when given address is not valid.
     */
    public static class InvalidAddressException extends IllegalArgumentException {

        public InvalidAddressException(final String message) {
            this(message, true);
        }

        public InvalidAddressException(final String message, boolean prependText) {
            super((prependText ? "Illegal IP address format: " : "") + message);
        }
    }
}
