/*******************************************************************************
* Copyright (c) 2012 GigaSpaces Technologies Ltd. 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 org.cloudifysource.dsl.utils;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang.StringUtils;
import com.googlecode.ipv6.IPv6Address;
/**
* A utility class for IP manipulation and validation.
*
* @author noak
* @since 2.6.1
*/
public final class IPUtils {
private static final int MAX_PORT_NUMBER = 65535;
// hidden constructor
private IPUtils() {
}
// timeout in seconds, waiting for a socket to connect
private static final int DEFAULT_CONNECTION_TIMEOUT = 10;
private static final int MILLISECONDS_IN_A_SECOND = 1000;
private static final int IP_BYTE_RANGE = 256;
private static final int IPV4_PARTS = 4;
private static final String NETWORK_INTERFACE_SEPARATOR = "%";
private static final String PORT_SEPARATOR = ":";
private static final String PORT_RANGE_SEPARATOR = "-";
//security constants
private static final String SPRING_ACTIVE_PROFILE_ENV_VAR = "SPRING_PROFILES_ACTIVE";
private static final String SPRING_PROFILE_NON_SECURE = "nonsecure";
private static final String SPRING_SECURITY_PROFILE = System.getenv(SPRING_ACTIVE_PROFILE_ENV_VAR);
private static final int PORT_VALIDATION_RETRY_COUNT = 3;
/**
* Converts a standard IP address to a long-format IP address.
*
* @param ipAddress
* A standard IP address
* @return IP address as a long value
* @throws IllegalArgumentException
* Indicates the given IP is invalid
*/
public static long ip2Long(final String ipAddress) throws IllegalArgumentException {
if (!validateIPAddress(ipAddress)) {
throw new IllegalArgumentException("Invalid IP address: " + ipAddress);
}
final byte[] ipBytes = getIPv4BytesArray(ipAddress);
return ((long) (byte2int(ipBytes[0]) * IP_BYTE_RANGE + byte2int(ipBytes[1]))
* IP_BYTE_RANGE + byte2int(ipBytes[2]))
* IP_BYTE_RANGE + byte2int(ipBytes[3]);
}
/**
* Converts a long value representing an IP address to a standard IP address
* (dotted decimal format).
*
* @param ip
* long value representing an IP address
* @return A standard IP address
*/
public static String long2String(final long ip) {
final long a = (ip & 0xff000000) >> 24;
final long b = (ip & 0x00ff0000) >> 16;
final long c = (ip & 0x0000ff00) >> 8;
final long d = ip & 0xff;
return a + "." + b + "." + c + "." + d;
}
/**
* Converts a standard IP address to a byte array.
*
* @param ipAddress
* IP address as a standard IP address (dotted decimal format)
* @return IP as a 4-element byte array
* @throws IllegalArgumentException
* Indicates the given IP is invalid
*/
public static byte[] getIPv4BytesArray(final String ipAddress) throws IllegalArgumentException {
// This implementation is commented out because it involves resolving
// the host, which we want to avoid.
// return InetAddress.getByName(ipAddress).getAddress();
final byte[] addrArr = new byte[IPV4_PARTS];
final String[] ipParts = ipAddress.split("\\.");
if (ipParts.length == IPV4_PARTS) {
for (int i = 0; i < IPV4_PARTS; i++) {
addrArr[i] = (byte) Integer.parseInt(ipParts[i]);
}
} else {
throw new IllegalArgumentException("Invalid IP address: " + ipAddress);
}
return addrArr;
}
/**
* Converts (unsigned) byte to int.
*
* @param b
* byte to convert
* @return int value representing the given byte
*/
public static int byte2int(final byte b) {
int i = b;
if (b < 0) {
i = b & 0x7f + 128;
}
return i;
}
/**
* Converts a CIDR IP format to an IP range format (e.g. 192.168.9.60/31
* becomes 192.168.9.60 - 192.168.9.61)
*
* @param ipCidr
* IP addresses formatted as CIDR
* @return IP addresses formatted as a simple range
* @throws UnknownHostException
* Indicates the given IP cannot be resolved
* @throws IllegalArgumentException
* Indicates the given IP is invalid
*/
public static String ipCIDR2Range(final String ipCidr) throws UnknownHostException, IllegalArgumentException {
final String[] parts = ipCidr.split("/");
final String ipAddress = parts[0];
int maskBits;
if (parts.length < 2) {
maskBits = 0;
} else {
maskBits = Integer.parseInt(parts[1]);
}
if (!validateIPAddress(ipAddress)) {
throw new IllegalArgumentException("Invalid IP address: " + ipAddress);
}
// Convert IPs into ints (32 bits).
// E.g. 157.166.224.26 becomes 10011101 10100110 11100000 00011010
// a simple split by dots (.), escaped.
final String[] ipParts = ipAddress.split("\\.");
final int addr = Integer.parseInt(ipParts[0]) << 24 & 0xFF000000 | Integer.parseInt(ipParts[1]) << 16
& 0xFF0000 | Integer.parseInt(ipParts[2]) << 8 & 0xFF00 | Integer.parseInt(ipParts[3]) & 0xFF;
// Get CIDR mask
final int mask = 0xffffffff << 32 - maskBits;
// Find lowest IP address
final int lowest = addr & mask;
final String lowestIP = buildIPV4String(toIntArray(lowest));
// Find highest IP address
final int highest = lowest + ~mask;
final String highestIP = buildIPV4String(toIntArray(highest));
return lowestIP + "-" + highestIP;
}
/**
* Convert a packed integer IPv4 address into a 4-element array.
*
* @param ip
* IPv4 address as an int
* @return IP as a 4-element array
*/
public static int[] toIntArray(final int ip) {
final int[] ret = new int[IPV4_PARTS];
for (int j = 3; j >= 0; --j) {
ret[j] |= ip >>> 8 * (3 - j) & 0xff;
}
return ret;
}
/**
* Converts a 4-element array into a standard IP address (dotted decimal
* format).
*
* @param ipBytes
* as array of IP bytes
* @return A standard IP address
*/
public static String buildIPV4String(final int[] ipBytes) {
final StringBuilder str = new StringBuilder();
for (int i = 0; i < ipBytes.length; ++i) {
str.append(ipBytes[i]);
if (i != ipBytes.length - 1) {
str.append(".");
}
}
return str.toString();
}
/**
* Gets the next IP address as a standard IP address (dotted decimal
* format).
*
* @param ipAddress
* IP address (dotted decimal format)
* @return The following IP address
* @throws IllegalArgumentException
* Indicates the given IP is invalid
*/
public static String getNextIP(final String ipAddress) throws IllegalArgumentException {
return long2String(ip2Long(ipAddress) + 1);
}
/**
* Validates a standard IP address (dotted decimal format).
*
* @param ipAddress
* IP address to validate (in a dotted decimal format)
* @return true if valid, false if invalid
*/
public static boolean validateIPAddress(final String ipAddress) {
boolean valid = false;
if (isIPv6Address(ipAddress)) {
// if we're here - this is valid IPv6 address
valid = true;
} else {
// a simple split by dots (.), escaped.
final String[] ipParts = ipAddress.split("\\.");
if (ipParts.length == IPV4_PARTS) {
for (final String part : ipParts) {
final int intValue = Integer.parseInt(part);
if (intValue < 0 || intValue > IP_BYTE_RANGE - 1) {
valid = false;
break;
}
valid = true;
}
}
}
return valid;
}
/**
* Validates a connection can be made to the given address and port, within
* the given time limit.
*
* @param ipAddress
* The IP address to connect to
* @param port
* The port number to use
* @throws IOException
* Reports a failure to connect or resolve the given address.
*/
public static void validateConnection(final String ipAddress, final int port) throws IOException {
validateConnection(ipAddress, port, DEFAULT_CONNECTION_TIMEOUT);
}
/**
* Validates a connection can be made to the given address and port, within
* the given time limit.
*
* @param ipAddress
* The IP address to connect to
* @param port
* The port number to use
* @param timeout
* The time to wait before timing out, in seconds
* @throws IOException
* Reports a failure to connect or resolve the given address.
*/
public static void validateConnection(final String ipAddress, final int port, final int timeout)
throws IOException {
final Socket socket = new Socket();
try {
final InetSocketAddress endPoint = new InetSocketAddress(ipAddress, port);
if (endPoint.isUnresolved()) {
throw new UnknownHostException(ipAddress);
}
socket.connect(endPoint, safeLongToInt(TimeUnit.SECONDS.toMillis(timeout), true));
} finally {
try {
socket.close();
} catch (final IOException ioe) {
// ignore
}
}
}
/**
* Validates the at least one port in the specified range is free on the given host.
*
* @param host
* the host to validate
* @param lowestPort
* The lowest port number in the port range to validate
* @param highestPort
* The highest port number in the port range to validate
* @throws UnknownHostException
* Reports the IP address of the host could not be determined.
* @throws IOException
* Reports an I/O error occurs when creating the socket.
* @throws SecurityException
* Reports a failure to connect or resolve the given address.
*/
public static void validatePortIsFreeInRange(final String host, final int lowestPort,
final int highestPort) throws UnknownHostException, IOException, SecurityException {
if (lowestPort > highestPort) {
throw new IllegalArgumentException("Invalid port range: " + lowestPort + "-" + highestPort + ". The "
+ " lowest port must be smaller than the highest port in the range.");
}
boolean connectionEstablished = false;
Exception lastException = null;
for (int port = lowestPort; port <= highestPort && !connectionEstablished; port++) {
try {
validatePortIsFree(host, port);
connectionEstablished = true;
break;
} catch (UnknownHostException uhe) {
// the hostname couldn't be resolved into an InetAddress
// no need to try other ports
throw uhe;
} catch (SecurityException se) {
// a security manager is present and permission is denied
// no need to try other ports
throw se;
} catch (Exception e) {
// validation failed, try the next port
lastException = e;
continue;
}
}
if (!connectionEstablished) {
throw new IOException("Failed to find any free ports in the range " + lowestPort + "-" + highestPort
+ " on host " + host + ", reported error: " + lastException.getMessage(), lastException);
}
}
/**
* Validates the specified port is free on the given host.
* @param host the host to validate
* @param port the port to validate
* @throws IOException indicates an I/O error occurs when creating the socket or connecting to it.
* @throws SecurityException .
*/
public static void validatePortIsFree(final String host, final int port) throws IOException,
SecurityException {
ServerSocket serverSocket = null;
IOException ex = null;
for (int i = 0; i < PORT_VALIDATION_RETRY_COUNT; i++) {
try {
InetSocketAddress socketAddress = new InetSocketAddress(host, port);
serverSocket = new ServerSocket();
serverSocket.bind(socketAddress);
return;
} catch (final IOException e) {
ex = e;
//wait for two seconds.
IOUtils.threadSleep(MILLISECONDS_IN_A_SECOND * 2);
} finally {
if (serverSocket != null) {
try {
serverSocket.close();
serverSocket = null;
} catch (Exception e) {
//ignore
}
}
}
}
throw ex;
}
/**
* Validates it is possible to open a server socket (using a random port) on the given host.
* @host the host to bind the server socket to
* @throws IOException indicates an I/O error occurs when creating the socket or connecting to it.
* @throws SecurityException .
*/
public static void testOpenServerSocket(final String host) throws IOException,
SecurityException {
ServerSocket serverSocket = null;
try {
InetSocketAddress socketAddress = new InetSocketAddress(host, 0);
serverSocket = new ServerSocket();
serverSocket.bind(socketAddress);
} finally {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (Exception e) {
//ignore
}
}
}
}
/**
* Safely casts long to int.
*
* @param longValue The long to cast
* @param roundIfNeeded Indicating whether to change the value of the number if it exceeds int's max/min values. If
* set to false and the long is too large/small, an {@link IllegalArgumentException} is thrown.
* @return int representing of the given long.
*/
public static int safeLongToInt(final long longValue, final boolean roundIfNeeded) {
int intValue;
if (longValue < Integer.MIN_VALUE || longValue > Integer.MAX_VALUE) {
if (roundIfNeeded) {
if (longValue < Integer.MIN_VALUE) {
intValue = Integer.MIN_VALUE;
} else {
intValue = Integer.MAX_VALUE;
}
} else {
throw new IllegalArgumentException(longValue + " cannot be cast to int without changing its value.");
}
} else {
intValue = (int) longValue;
}
return intValue;
}
/**
* Resolves IP address to host name.
* @param ip The IP to resolve
* @return host name
* @throws UnknownHostException Indicates the IP is not a known host
*/
public static String resolveIpToHostName(final String ip) throws UnknownHostException {
String hostName = "";
try {
InetAddress addr = InetAddress.getByName(ip);
hostName = addr.getHostName();
} catch (UnknownHostException e) {
throw new IllegalStateException("could not resolve host name of ip " + ip);
}
return hostName;
}
/**
* Resolves the host name and returns its IP address.
*
* @param hostName
* The name of the host
* @return The IP address of the host
* @throws UnknownHostException
* Indicates the host doesn't represent an available network
* object
*/
public static String resolveHostNameToIp(final String hostName) throws UnknownHostException {
final InetAddress byName = InetAddress.getByName(hostName);
return byName.getHostAddress();
}
/**
* Removes the interface part of the given IP address, if found. Examples:
* [fe80::9da2:25f7:86ce:cddb%45] will be returned as [fe80::9da2:25f7:86ce:cddb]
* fe80::9da2:25f7:86ce:cddb%45 will be returned as fe80::9da2:25f7:86ce:cddb
* [fe80::9da2:25f7:86ce:cddb] will be returned as [fe80::9da2:25f7:86ce:cddb]
* fe80::9da2:25f7:86ce:cddb will be returned as fe80::9da2:25f7:86ce:cddb
*
* @param ipAddress
* The ipAddress to parse
* @param surroundWithBrackets
* if True - The returned IP address will be surrounded by "[]".
* False - the returned IP address will not be surrounded by
* "[]".
* @return the given ipAddress, with the interface part, if found.
* @throws IllegalArgumentException
* Indicated the given ipAddress is invalid
*/
/*public static String removeInterfaceFromIpAddress(final String ipAddress, final boolean surroundWithBrackets)
throws IllegalArgumentException {
String cleanIp = "";
cleanIp = StringUtils.substringBefore(StringUtils.strip(ipAddress, "[]"), NETWORK_INTERFACE_SEPARATOR);
if (surroundWithBrackets) {
cleanIp = bracketIfNeeded(cleanIp);
}
return cleanIp;
}*/
/**
* Compares two IPv6 addresses. The addresses can be in the full format,
* short format or iPv4MappedIPv6Address.
* If an IPv6 address is mis-formatted, False is returned.
*
* @param address1
* The first IPv6 address.
* @param address2
* The second IPv6 address.
* @return True if the two addresses represent the same address, False
* otherwise.
*/
public static boolean isSameIpv6Address(final String address1, final String address2) {
boolean adressesEquals = false;
try {
final IPv6Address iPv6Address1 = IPv6Address.fromString(address1);
final IPv6Address iPv6Address2 = IPv6Address.fromString(address2);
adressesEquals = iPv6Address1.equals(iPv6Address2);
} catch (IllegalArgumentException e) {
//no need to throw the exception, just return false;
}
return adressesEquals;
}
/**
* Compares two IP addresses. The addresses can be in the full IPv6 format,
* short format or iPv4MappedIPv6Address.
* If the IP address is empty or mis-formatted, False is returned.
*
* @param address1
* The first IP address.
* @param address2
* The second IP address.
* @return True if the two addresses represent the same address, False otherwise.
*/
public static boolean isSameIpAddress(final String address1, final String address2) {
boolean isSameAddress = false;
if (StringUtils.isBlank(address1) || StringUtils.isBlank(address2)) {
//remain false;
} else {
if (isIPv6Address(address1) && isIPv6Address(address2)) {
isSameAddress = isSameIpv6Address(address1, address2);
} else {
isSameAddress = address1.equalsIgnoreCase(address2);
}
}
return isSameAddress;
}
/**
* Chechs if the given address is an IPv6 address.
* @param ipAddress IP address
* @return True is the address represents and IPv6 address, False otherwise
*/
public static boolean isIPv6Address(final String ipAddress) {
boolean isIPv6 = false;
String strippedIp = StringUtils.strip(ipAddress, "[]");
if (strippedIp.indexOf(NETWORK_INTERFACE_SEPARATOR) > -1) {
strippedIp = StringUtils.substringBefore(strippedIp, NETWORK_INTERFACE_SEPARATOR);
}
try {
IPv6Address.fromString(strippedIp);
isIPv6 = true;
} catch (IllegalArgumentException e) {
//this is not a valid IPv6 address
}
return isIPv6;
}
/**
* Returns a "safe" formatted IP address - IPv4 addresses are not changed,
* IPv6 addresses may change - if they include an "interface" section it is removed,
* and the address itself is surrounded by brackets ("[]") to allow for port concatenation.
* @param ipAddress The ipAddress to handle
* @return an IP address, "safe" for concatenation.
*/
public static String getSafeIpAddress(final String ipAddress) {
String safeIpAddress;
try {
String strippedIp = StringUtils.strip(ipAddress, "[]");
strippedIp = StringUtils.substringBefore(strippedIp, NETWORK_INTERFACE_SEPARATOR);
IPv6Address.fromString(strippedIp); //verifies this is an IPv6 address
safeIpAddress = "[" + strippedIp + "]";
} catch (IllegalArgumentException e) {
//this is not a valid IPv6 address, assume this is IPv4 or host name, leave as is
safeIpAddress = ipAddress;
}
return safeIpAddress;
}
/**
* Gets the host name/address from a full address.
* Examples:
* [fe80::9da2:25f7:86ce:cddb%45]:4174 will be returned as fe80::9da2:25f7:86ce:cddb
* [fe80::9da2:25f7:86ce:cddb]:4174 will be returned as fe80::9da2:25f7:86ce:cddb
* mylocalhost:4174 will be returned as mylocalhost
* 127.0.0.1:4174 will be returned as 127.0.0.1
* 127.0.0.1 will be returned as 127.0.0.1
* @param ipAddress the address to parse
* @return only the host name / address part from the full address.
*/
public static String getHostFromFullAddress(final String ipAddress) {
String host = ipAddress;
if (host.indexOf(PORT_SEPARATOR) > -1) {
host = StringUtils.substringBeforeLast(host, PORT_SEPARATOR);
}
if (host.indexOf("[") > -1) {
host = StringUtils.substringAfter(host, "[");
}
if (host.indexOf("]") > -1) {
host = StringUtils.substringBefore(host, "]");
}
if (host.indexOf(NETWORK_INTERFACE_SEPARATOR) > -1) {
host = StringUtils.substringBefore(host, NETWORK_INTERFACE_SEPARATOR);
}
return host;
}
/**
* Gets the port number from a full address.
* @param ipAddress the address to parse
* @return the port included in the full address, as int.
*/
public static int getPortFromFullAddress(final String ipAddress) {
int port;
String address = ipAddress;
if (address.indexOf(PORT_SEPARATOR) == -1) {
throw new IllegalArgumentException("Port not found in address: " + ipAddress);
}
address = StringUtils.substringAfterLast(address, PORT_SEPARATOR);
port = Integer.parseInt(address);
return port;
}
/**
* Gets the relevant rest protocol, considering the SPRING_PROFILES_ACTIVE environment variable.
* @return https if the rest server is secured, http otherwise.
*/
public static String getRestProtocol() {
String protocol = "https";
if (StringUtils.isBlank(SPRING_SECURITY_PROFILE)
|| SPRING_SECURITY_PROFILE.contains(SPRING_PROFILE_NON_SECURE)) {
//security is off
protocol = "http";
}
return protocol;
}
/**
* Validates the given String as a port range.
* @param portRange the port range to validate
* @return True - is the given string is a valid port range, False otherwise.
*/
public static boolean isValidPortRange(final String portRange) {
boolean valid = false;
try {
int minPort = getMinimumPort(portRange);
int maxPort = getMaximumPort(portRange);
if (isValidPortNumber(minPort) && isValidPortNumber(maxPort)) {
valid = true;
}
} catch (Exception e) {
// invalid range
}
return valid;
}
/**
* Validates the given number as a port number.
* A valid port value is between 0 and 65535.
* @param portNumber the port number to validate
* @return True - is the given number is a valid port number, False otherwise.
*/
public static boolean isValidPortNumber(final int portNumber) {
boolean valid = false;
if (portNumber >= 0 && portNumber <= MAX_PORT_NUMBER) {
valid = true;
}
return valid;
}
/**
* Returns the minimum port (i.e. the starting port) specified by the given range.
* @param portRange the range to parse
* @return the minimum port specified by the given range
*/
public static int getMinimumPort(final String portRange) {
if (portRange.indexOf(PORT_RANGE_SEPARATOR) == -1) {
throw new IllegalArgumentException("Invalid port range: " + portRange + ". The expected "
+ "format is <lowest port>-<highest port>, e.g. 7010-7110");
}
String lowestPortStr = StringUtils.substringBefore(portRange, PORT_RANGE_SEPARATOR);
return Integer.parseInt(lowestPortStr);
}
/**
* Returns the maximum port (i.e. the ending port) specified by the given range.
* @param portRange the range to parse
* @return the maximum port specified by the given range
*/
public static int getMaximumPort(final String portRange) {
if (portRange.indexOf(PORT_RANGE_SEPARATOR) == -1) {
throw new IllegalArgumentException("Invalid port range: " + portRange + ". The expected "
+ "format is <lowest port>-<highest port>, e.g. 7010-7110");
}
String highestPortStr = StringUtils.substringAfter(portRange, PORT_RANGE_SEPARATOR);
return Integer.parseInt(highestPortStr);
}
/************
* Creates a compatible LOOKUPLOCATORS string from one or more locators.
* @param locators the locators.
* @param port the LUS port.
* @return the LOOKUPLOCATORS string.
*/
public static String createLocatorsString(final String[] locators, final int port) {
if(locators == null) {
throw new IllegalArgumentException("locators can't be null");
}
final StringBuilder lookupSb = new StringBuilder();
for (final String locator: locators) {
final String ip = IPUtils.getSafeIpAddress(locator);
lookupSb.append(ip).append(":").append(port).append(',');
}
if (!(lookupSb.length() == 0)) {
lookupSb.setLength(lookupSb.length() - 1);
}
return lookupSb.toString();
}
}