org.apache.sshd.common.util.net.SshdSocketAddress Maven / Gradle / Ivy
The newest version!
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.sshd.common.util.net;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.SocketAddress;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.MapEntryUtils;
import org.apache.sshd.common.util.NumberUtils;
import org.apache.sshd.common.util.ValidateUtils;
/**
*
* A simple socket address holding the host name and port number. The reason it does not extend
* {@link InetSocketAddress} is twofold:
*
*
* -
*
* The {@link InetSocketAddress} performs a DNS resolution on the provided host name - which we don't want do use until
* we want to create a connection using this address (thus the {@link #toInetSocketAddress()} call which executes this
* query
*
*
*
* -
*
* If empty host name is provided we replace it with the any address of 0.0.0.0
*
*
*
*
* @author Apache MINA SSHD Project
*/
public class SshdSocketAddress extends SocketAddress {
public static final String LOCALHOST_NAME = "localhost";
public static final String LOCALHOST_IPV4 = "127.0.0.1";
public static final String IPV4_ANYADDR = "0.0.0.0";
public static final Set WELL_KNOWN_IPV4_ADDRESSES = Collections.unmodifiableSet(
new LinkedHashSet<>(
Arrays.asList(LOCALHOST_IPV4, IPV4_ANYADDR)));
// 10.0.0.0 - 10.255.255.255
public static final String PRIVATE_CLASS_A_PREFIX = "10.";
// 172.16.0.0 - 172.31.255.255
public static final String PRIVATE_CLASS_B_PREFIX = "172.";
// 192.168.0.0 - 192.168.255.255
public static final String PRIVATE_CLASS_C_PREFIX = "192.168.";
// 100.64.0.0 - 100.127.255.255
public static final String CARRIER_GRADE_NAT_PREFIX = "100.";
// The IPv4 broadcast address
public static final String BROADCAST_ADDRESS = "255.255.255.255";
/** Max. number of hex groups (separated by ":") in an IPV6 address */
public static final int IPV6_MAX_HEX_GROUPS = 8;
/** Max. hex digits in each IPv6 group */
public static final int IPV6_MAX_HEX_DIGITS_PER_GROUP = 4;
public static final String IPV6_LONG_ANY_ADDRESS = "0:0:0:0:0:0:0:0";
public static final String IPV6_SHORT_ANY_ADDRESS = "::";
public static final String IPV6_LONG_LOCALHOST = "0:0:0:0:0:0:0:1";
public static final String IPV6_SHORT_LOCALHOST = "::1";
public static final Set WELL_KNOWN_IPV6_ADDRESSES = Collections.unmodifiableSet(
new LinkedHashSet<>(
Arrays.asList(
IPV6_LONG_LOCALHOST, IPV6_SHORT_LOCALHOST,
IPV6_LONG_ANY_ADDRESS, IPV6_SHORT_ANY_ADDRESS)));
/**
* A dummy placeholder that can be used instead of {@code null}s
*/
public static final SshdSocketAddress LOCALHOST_ADDRESS = new SshdSocketAddress(LOCALHOST_IPV4, 0);
/**
* Compares {@link InetAddress}-es according to their {@link InetAddress#getHostAddress()} value case
* insensitive
*
* @see #toAddressString(InetAddress)
*/
public static final Comparator BY_HOST_ADDRESS = (a1, a2) -> {
String n1 = GenericUtils.trimToEmpty(toAddressString(a1));
String n2 = GenericUtils.trimToEmpty(toAddressString(a2));
return String.CASE_INSENSITIVE_ORDER.compare(n1, n2);
};
/**
* Compares {@link SocketAddress}-es according to their host case insensitive and if equals, then according
* to their port value (if any)
*
* @see #toAddressString(SocketAddress)
* @see #toAddressPort(SocketAddress)
*/
public static final Comparator BY_HOST_AND_PORT = (a1, a2) -> {
String n1 = GenericUtils.trimToEmpty(toAddressString(a1));
String n2 = GenericUtils.trimToEmpty(toAddressString(a2));
int nRes = String.CASE_INSENSITIVE_ORDER.compare(n1, n2);
if (nRes != 0) {
return nRes;
}
int p1 = toAddressPort(a1);
int p2 = toAddressPort(a2);
nRes = Integer.compare(p1, p2);
if (nRes != 0) {
return nRes;
}
return 0;
};
private static final long serialVersionUID = 6461645947151952729L;
private final String hostName;
private final int port;
public SshdSocketAddress(int port) {
this(IPV4_ANYADDR, port);
}
public SshdSocketAddress(InetSocketAddress addr) {
Objects.requireNonNull(addr, "No address provided");
String host = addr.getHostString();
hostName = GenericUtils.isEmpty(host) ? IPV4_ANYADDR : host;
port = addr.getPort();
ValidateUtils.checkTrue(port >= 0, "Port must be >= 0: %d", port);
}
public SshdSocketAddress(String hostName, int port) {
Objects.requireNonNull(hostName, "Host name may not be null");
this.hostName = GenericUtils.isEmpty(hostName) ? IPV4_ANYADDR : hostName;
ValidateUtils.checkTrue(port >= 0, "Port must be >= 0: %d", port);
this.port = port;
}
public String getHostName() {
return hostName;
}
public int getPort() {
return port;
}
public InetSocketAddress toInetSocketAddress() {
return new InetSocketAddress(getHostName(), getPort());
}
@Override
public String toString() {
return getHostName() + ":" + getPort();
}
protected boolean isEquivalent(SshdSocketAddress that) {
if (that == null) {
return false;
} else if (that == this) {
return true;
} else {
return (this.getPort() == that.getPort())
&& isEquivalentHostName(this.getHostName(), that.getHostName(), false);
}
}
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (getClass() != o.getClass()) {
return false;
}
return isEquivalent((SshdSocketAddress) o);
}
@Override
public int hashCode() {
return GenericUtils.hashCode(getHostName(), Boolean.FALSE) + 31 * Integer.hashCode(getPort());
}
/**
* Returns the first external network address assigned to this machine or null if one is not found.
*
* @return Inet4Address associated with an external interface DevNote: We actually return InetAddress here, as
* Inet4Addresses are final and cannot be mocked.
*/
public static InetAddress getFirstExternalNetwork4Address() {
List extends InetAddress> addresses = getExternalNetwork4Addresses();
return (GenericUtils.size(addresses) > 0) ? addresses.get(0) : null;
}
/**
* @return a {@link List} of local network addresses which are not multicast or localhost sorted according to
* {@link #BY_HOST_ADDRESS}
*/
public static List getExternalNetwork4Addresses() {
List addresses = new ArrayList<>();
try {
for (Enumeration nets = NetworkInterface.getNetworkInterfaces();
(nets != null) && nets.hasMoreElements();) {
NetworkInterface netint = nets.nextElement();
/*
* TODO - uncomment when 1.5 compatibility no longer required if (!netint.isUp()) { continue; // ignore
* non-running interfaces }
*/
for (Enumeration inetAddresses = netint.getInetAddresses();
(inetAddresses != null) && inetAddresses.hasMoreElements();) {
InetAddress inetAddress = inetAddresses.nextElement();
if (isValidHostAddress(inetAddress)) {
addresses.add(inetAddress);
}
}
}
} catch (SocketException e) {
// swallow
}
if (GenericUtils.size(addresses) > 1) {
Collections.sort(addresses, BY_HOST_ADDRESS);
}
return addresses;
}
/**
* @param addr The {@link InetAddress} to be verified
* @return
*
* true
if the address is:
*
*
*
* - Not {@code null}
* - An {@link Inet4Address}
* - Not link local
* - Not a multicast
* - Not a loopback
*
* @see InetAddress#isLinkLocalAddress()
* @see InetAddress#isMulticastAddress()
* @see InetAddress#isMulticastAddress()
*/
public static boolean isValidHostAddress(InetAddress addr) {
if (addr == null) {
return false;
}
if (addr.isLinkLocalAddress()) {
return false;
}
if (addr.isMulticastAddress()) {
return false;
}
if (!(addr instanceof Inet4Address)) {
return false; // TODO add support for IPv6 - see SSHD-746
}
return !isLoopback(addr);
}
/**
* @param addr The {@link InetAddress} to be considered
* @return true
if the address is a loopback one. Note: if
* {@link InetAddress#isLoopbackAddress()} returns false
the address string is
* checked
* @see #toAddressString(InetAddress)
* @see #isLoopback(String)
*/
public static boolean isLoopback(InetAddress addr) {
if (addr == null) {
return false;
}
if (addr.isLoopbackAddress()) {
return true;
}
String ip = toAddressString(addr);
return isLoopback(ip);
}
/**
* @param ip IP value to be tested
* @return true
if the IP is "localhost" or "127.x.x.x".
*/
public static boolean isLoopback(String ip) {
if (GenericUtils.isEmpty(ip)) {
return false;
}
if (LOCALHOST_NAME.equals(ip)) {
return true;
}
return isIPv4LoopbackAddress(ip) || isIPv6LoopbackAddress(ip);
}
public static boolean isIPv4LoopbackAddress(String ip) {
if (GenericUtils.isEmpty(ip)) {
return false;
}
if (LOCALHOST_IPV4.equals(ip)) {
return true; // most used
}
String[] values = GenericUtils.split(ip, '.');
if (GenericUtils.length(values) != 4) {
return false;
}
for (int index = 0; index < values.length; index++) {
String val = values[index];
if (!isValidIPv4AddressComponent(val)) {
return false;
}
if (index == 0) {
int number = Integer.parseInt(val);
if (number != 127) {
return false;
}
}
}
return true;
}
public static boolean isIPv6LoopbackAddress(String ip) {
// TODO add more patterns - e.g., https://tools.ietf.org/id/draft-smith-v6ops-larger-ipv6-loopback-prefix-04.html
return IPV6_LONG_LOCALHOST.equals(ip) || IPV6_SHORT_LOCALHOST.equals(ip);
}
public static boolean isEquivalentHostName(String h1, String h2, boolean allowWildcard) {
if (GenericUtils.safeCompare(h1, h2, false) == 0) {
return true;
}
if (allowWildcard) {
return isWildcardAddress(h1) || isWildcardAddress(h2);
}
return false;
}
public static boolean isLoopbackAlias(String h1, String h2) {
return (LOCALHOST_NAME.equals(h1) && isLoopback(h2))
|| (LOCALHOST_NAME.equals(h2) && isLoopback(h1));
}
public static boolean isWildcardAddress(String addr) {
return IPV4_ANYADDR.equalsIgnoreCase(addr)
|| IPV6_LONG_ANY_ADDRESS.equalsIgnoreCase(addr)
|| IPV6_SHORT_ANY_ADDRESS.equalsIgnoreCase(addr);
}
public static SshdSocketAddress toSshdSocketAddress(SocketAddress addr) {
if (addr == null) {
return null;
} else if (addr instanceof SshdSocketAddress) {
return (SshdSocketAddress) addr;
} else if (addr instanceof InetSocketAddress) {
InetSocketAddress isockAddress = (InetSocketAddress) addr;
return new SshdSocketAddress(isockAddress.getHostString(), isockAddress.getPort());
} else {
throw new UnsupportedOperationException("Cannot convert " + addr.getClass().getSimpleName()
+ "=" + addr + " to " + SshdSocketAddress.class.getSimpleName());
}
}
public static String toAddressString(SocketAddress addr) {
if (addr == null) {
return null;
} else if (addr instanceof InetSocketAddress) {
return ((InetSocketAddress) addr).getHostString();
} else if (addr instanceof SshdSocketAddress) {
return ((SshdSocketAddress) addr).getHostName();
} else {
return addr.toString();
}
}
/**
* Attempts to resolve the port value
*
* @param addr The {@link SocketAddress} to examine
* @return The associated port value - negative if failed to resolve
*/
public static int toAddressPort(SocketAddress addr) {
if (addr instanceof InetSocketAddress) {
return ((InetSocketAddress) addr).getPort();
} else if (addr instanceof SshdSocketAddress) {
return ((SshdSocketAddress) addr).getPort();
} else {
return -1;
}
}
/**
*
* Converts a {@code SocketAddress} into an {@link InetSocketAddress} if possible:
*
*
*
* - If already an {@link InetSocketAddress} then cast it as such
* - If an {@code SshdSocketAddress} then invoke {@link #toInetSocketAddress()}
* - Otherwise, throw an exception
*
*
* @param remoteAddress The {@link SocketAddress} - ignored if {@code null}
* @return The {@link InetSocketAddress} instance
* @throws ClassCastException if argument is not already an {@code InetSocketAddress} or a {@code SshdSocketAddress}
*/
public static InetSocketAddress toInetSocketAddress(SocketAddress remoteAddress) {
if (remoteAddress == null) {
return null;
} else if (remoteAddress instanceof InetSocketAddress) {
return (InetSocketAddress) remoteAddress;
} else if (remoteAddress instanceof SshdSocketAddress) {
return ((SshdSocketAddress) remoteAddress).toInetSocketAddress();
} else {
throw new ClassCastException("Unknown remote address type: " + remoteAddress);
}
}
public static String toAddressString(InetAddress addr) {
String ip = (addr == null) ? null : addr.toString();
if (GenericUtils.isEmpty(ip)) {
return null;
} else {
return ip.replaceAll(".*/", "");
}
}
public static boolean isIPv4Address(String addr) {
addr = GenericUtils.trimToEmpty(addr);
if (GenericUtils.isEmpty(addr)) {
return false;
}
if (WELL_KNOWN_IPV4_ADDRESSES.contains(addr)) {
return true;
}
String[] comps = GenericUtils.split(addr, '.');
if (GenericUtils.length(comps) != 4) {
return false;
}
for (String c : comps) {
if (!isValidIPv4AddressComponent(c)) {
return false;
}
}
return true;
}
/**
* Checks if the address is one of the allocated private blocks
*
* @param addr The address string
* @return {@code true} if this is one of the allocated private blocks. Note: it assumes that the
* address string is indeed an IPv4 address
* @see #isIPv4Address(String)
* @see #PRIVATE_CLASS_A_PREFIX
* @see #PRIVATE_CLASS_B_PREFIX
* @see #PRIVATE_CLASS_C_PREFIX
* @see Wiki page
*/
public static boolean isPrivateIPv4Address(String addr) {
if (GenericUtils.isEmpty(addr)) {
return false;
}
if (addr.startsWith(PRIVATE_CLASS_A_PREFIX) || addr.startsWith(PRIVATE_CLASS_C_PREFIX)) {
return true;
}
// for 172.x.x.x we need further checks
if (!addr.startsWith(PRIVATE_CLASS_B_PREFIX)) {
return false;
}
int nextCompPos = addr.indexOf('.', PRIVATE_CLASS_B_PREFIX.length());
if (nextCompPos <= PRIVATE_CLASS_B_PREFIX.length()) {
return false;
}
String value = addr.substring(PRIVATE_CLASS_B_PREFIX.length(), nextCompPos);
if (!isValidIPv4AddressComponent(value)) {
return false;
}
int v = Integer.parseInt(value);
return (v >= 16) && (v <= 31);
}
/**
* @param addr The address to be checked
* @return {@code true} if the address is in the 100.64.0.0/10 range
* @see RFC6598
*/
public static boolean isCarrierGradeNatIPv4Address(String addr) {
if (GenericUtils.isEmpty(addr)) {
return false;
}
if (!addr.startsWith(CARRIER_GRADE_NAT_PREFIX)) {
return false;
}
int nextCompPos = addr.indexOf('.', CARRIER_GRADE_NAT_PREFIX.length());
if (nextCompPos <= CARRIER_GRADE_NAT_PREFIX.length()) {
return false;
}
String value = addr.substring(CARRIER_GRADE_NAT_PREFIX.length(), nextCompPos);
if (!isValidIPv4AddressComponent(value)) {
return false;
}
int v = Integer.parseInt(value);
return (v >= 64) && (v <= 127);
}
/**
*
* Checks if the provided argument is a valid IPv4 address component:
*
*
*
* - Not {@code null}/empty
* - Has at most 3 digits
* - Its value is ≤ 255
*
*
* @param c The {@link CharSequence} to be validate
* @return {@code true} if valid IPv4 address component
*/
public static boolean isValidIPv4AddressComponent(CharSequence c) {
if (GenericUtils.isEmpty(c) || (c.length() > 3)) {
return false;
}
char ch = c.charAt(0);
if ((ch < '0') || (ch > '9')) {
return false;
}
if (!NumberUtils.isIntegerNumber(c)) {
return false;
}
int v = Integer.parseInt(c.toString());
return (v >= 0) && (v <= 255);
}
// Based on org.apache.commons.validator.routines.InetAddressValidator#isValidInet6Address
public static boolean isIPv6Address(String address) {
address = GenericUtils.trimToEmpty(address);
if (GenericUtils.isEmpty(address)) {
return false;
}
if (WELL_KNOWN_IPV6_ADDRESSES.contains(address)) {
return true;
}
boolean containsCompressedZeroes = address.contains("::");
if (containsCompressedZeroes && (address.indexOf("::") != address.lastIndexOf("::"))) {
return false;
}
if (((address.indexOf(':') == 0) && (!address.startsWith("::")))
|| (address.endsWith(":") && (!address.endsWith("::")))) {
return false;
}
String[] splitOctets = GenericUtils.split(address, ':');
List octetList = new ArrayList<>(Arrays.asList(splitOctets));
if (containsCompressedZeroes) {
if (address.endsWith("::")) {
// String.split() drops ending empty segments
octetList.add("");
} else if (address.startsWith("::") && (!octetList.isEmpty())) {
octetList.remove(0);
}
}
int numOctests = octetList.size();
if (numOctests > IPV6_MAX_HEX_GROUPS) {
return false;
}
int validOctets = 0;
int emptyOctets = 0; // consecutive empty chunks
for (int index = 0; index < numOctests; index++) {
String octet = octetList.get(index);
int pos = octet.indexOf('%'); // is it a zone index
if (pos >= 0) {
// zone index must come last
if (index != (numOctests - 1)) {
return false;
}
octet = (pos > 0) ? octet.substring(0, pos) : "";
}
int octetLength = octet.length();
if (octetLength == 0) {
emptyOctets++;
if (emptyOctets > 1) {
return false;
}
validOctets++;
continue;
}
emptyOctets = 0;
// Is last chunk an IPv4 address?
if ((index == (numOctests - 1)) && (octet.indexOf('.') > 0)) {
if (!isIPv4Address(octet)) {
return false;
}
validOctets += 2;
continue;
}
if (octetLength > IPV6_MAX_HEX_DIGITS_PER_GROUP) {
return false;
}
int octetInt = 0;
try {
octetInt = Integer.parseInt(octet, 16);
} catch (NumberFormatException e) {
return false;
}
if ((octetInt < 0) || (octetInt > 0x000ffff)) {
return false;
}
validOctets++;
}
if ((validOctets > IPV6_MAX_HEX_GROUPS)
|| ((validOctets < IPV6_MAX_HEX_GROUPS) && (!containsCompressedZeroes))) {
return false;
}
return true;
}
public static V findByOptionalWildcardAddress(Map map, SshdSocketAddress address) {
Map.Entry entry = findMatchingOptionalWildcardEntry(map, address);
return (entry == null) ? null : entry.getValue();
}
public static V removeByOptionalWildcardAddress(Map map, SshdSocketAddress address) {
Map.Entry entry = findMatchingOptionalWildcardEntry(map, address);
return (entry == null) ? null : map.remove(entry.getKey());
}
public static Map.Entry findMatchingOptionalWildcardEntry(
Map map, SshdSocketAddress address) {
if (MapEntryUtils.isEmpty(map) || (address == null)) {
return null;
}
String hostName = address.getHostName();
Map.Entry candidate = null;
for (Map.Entry e : map.entrySet()) {
SshdSocketAddress a = e.getKey();
if (a.getPort() != address.getPort()) {
continue;
}
String candidateName = a.getHostName();
if (hostName.equalsIgnoreCase(candidateName)) {
return e; // If found exact match then use it
}
if (isEquivalentHostName(hostName, candidateName, true)) {
if (candidate != null) {
throw new IllegalStateException("Multiple candidate matches for " + address + ": " + candidate + ", " + e);
}
candidate = e;
}
}
return candidate;
}
}