/*
* Copyright (c) 2014 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.coordinator.client.service.impl;
import com.emc.storageos.coordinator.exceptions.CoordinatorException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.regex.Pattern;
import com.emc.storageos.coordinator.exceptions.NotConnectableException;
public final class DualInetAddress {
private static int[] parseInet4Address(String ip4String) {
if (ip4String == null) {
return null;
}
// Split into parts.
final String[] parts = ip4String.split("\\.", 4 + 1);
// Can't have more then 4 parts.
if (parts.length > 4) {
return null;
}
// Convert parts into octets.
int[] octets = new int[4];
for (int i = 0; i < parts.length; i++) {
try {
final int octet = Integer.parseInt(parts[i]);
if (octet > 255 || octet < 0 || octet == 0 && parts[i].length() > 1) {
return null;
}
octets[i] = octet;
} catch (NumberFormatException e) {
return null;
}
}
// Fill remaining octets with zeros.
for (int i = parts.length; i < 4; i++) {
octets[i] = 0;
}
return octets;
}
/**
* Normalize a string representing an IPv4 address to
* the canonical representation with four decimal octets (d.d.d.d).
*
* @param ip4String
* @return Normalized IPv4 string or null if the input string was invalid
*/
public static String normalizeInet4Address(String ip4String) {
final int[] octets = parseInet4Address(ip4String);
if (octets == null) {
return null;
}
// Construct the canonical representation.
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 4; i++) {
if (i > 0) {
sb.append('.');
}
sb.append(octets[i]);
}
return sb.toString();
}
/**
* Normalize a string representing an IPv6 address to
* the canonical representation with the longest sequence of zero
* hextets substituted with ::
*
* @param ip6String
* @return Normalized IPv6 string or null if the input string was invalid
*/
public static String normalizeInet6Address(String ip6String) {
if (ip6String == null) {
return null;
}
// Split the input string into parts.
String parts[] = ip6String.split(":", 8 + 2);
// Special case for ffff:ff00:1.2.3.4
if (parts.length > 1 && parts[parts.length - 1].length() > 0 && parts[parts.length - 1].indexOf('.') > 0) {
final int[] octets = parseInet4Address(parts[parts.length - 1]);
if (octets != null) {
String[] parts2 = new String[parts.length + 1];
System.arraycopy(parts, 0, parts2, 0, parts.length - 1);
parts2[parts2.length - 2] = Integer.toHexString(octets[0] * 256 + octets[1]);
parts2[parts2.length - 1] = Integer.toHexString(octets[2] * 256 + octets[3]);
parts = parts2;
}
}
// The address can have 2 to 8 colons.
// Thus, we should have between 3 and 9 parts.
if (parts.length < 3 || parts.length > 8 + 1) {
return null;
}
// Check middle parts:
// - Find index of an empty part (between ::)
// - We must have only 0 or 1 empty parts
// - Also, check that parts are four characters or less.
int index = -1;
for (int i = 1; i < parts.length - 1; i++) {
if (parts[i].length() > 4) {
return null;
} else if (parts[i].length() == 0) {
if (index != -1) {
return null;
}
index = i;
}
}
// Check first part
if (parts[0].length() > 4 || parts[0].length() == 0 && index != 1) {
return null;
}
// Check last part
if (parts[parts.length - 1].length() > 4 || parts[parts.length - 1].length() == 0 && index != parts.length - 2) {
return null;
}
// If there is no empty part we must have exactly 8 parts
if (index == -1 && parts.length != 8) {
return null;
}
// Convert parts to hextets.
int hextets[] = new int[8];
for (int i = 0; i < parts.length; i++) {
try {
final int block = parts[i].length() == 0 ? 0 : Integer.parseInt(parts[i], 0x10);
if (block > 0xffff || block < 0) {
return null;
}
if (index == -1 || i < index) {
hextets[i] = block;
} else {
hextets[i + 8 - parts.length] = block;
}
} catch (NumberFormatException e) {
return null;
}
}
// Fill skipped hextets with zeros
if (index != -1) {
for (int i = 0; i < 8 - parts.length; i++) {
hextets[index + i] = 0;
}
}
// Find the longest sequence of zero hextets.
int longestIndex = -1;
int longestLength = 0;
for (int i = 0, k = -1, l = 0; i < 8; i++) {
if (hextets[i] == 0) {
if (k == -1) {
k = i;
l = 1;
} else {
l++;
}
if (l > longestLength) {
longestIndex = k;
longestLength = l;
}
} else {
k = -1;
}
}
// Construct the canonical representation.
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 8; i++) {
if (longestIndex == -1 || i < longestIndex) {
if (i > 0) {
sb.append(':');
}
sb.append(Integer.toHexString(hextets[i]).toLowerCase());
} else if (i == longestIndex) {
sb.append(":");
if (i + longestLength == 8) {
sb.append(':');
}
} else if (longestIndex != -1 && i > longestIndex && i < longestIndex + longestLength) {
continue;
} else {
sb.append(':');
sb.append(Integer.toHexString(hextets[i]).toLowerCase());
}
}
return sb.toString();
}
private static final String INVALID_INET4ADDRESS = normalizeInet4Address("0.0.0.0");
private static final String INVALID_INET6ADDRESS = normalizeInet6Address("::0");
private final String ip4;
private final String ip6;
private DualInetAddress() {
this.ip4 = null;
this.ip6 = null;
}
private DualInetAddress(String ip4, String ip6) {
this.ip4 = (ip4 == null || ip4.equals(INVALID_INET4ADDRESS)) ? null : ip4;
this.ip6 = (ip6 == null || ip6.equals(INVALID_INET6ADDRESS)) ? null : ip6;
}
public String getInet4() {
return ip4;
}
public String getInet6() {
return ip6;
}
public boolean hasInet4() {
return getInet4() != null;
}
public boolean hasInet6() {
return getInet6() != null;
}
public static DualInetAddress fromAddress(String ipString) throws UnknownHostException {
if (ipString == null || ipString.isEmpty()) {
return new DualInetAddress();
}
final String ip4 = normalizeInet4Address(ipString);
if (ip4 != null) {
return new DualInetAddress(ip4, null);
}
final String ip6 = normalizeInet6Address(ipString);
if (ip6 != null) {
return new DualInetAddress(null, ip6);
}
throw new UnknownHostException(ipString);
}
public static DualInetAddress fromAddresses(String ip4String, String ip6String) throws UnknownHostException {
final String ip4 = normalizeInet4Address(ip4String);
if (ip4String != null && !ip4String.isEmpty() && ip4 == null) {
throw new UnknownHostException(ip4String);
}
final String ip6 = normalizeInet6Address(ip6String);
if (ip6String != null && !ip6String.isEmpty() && ip6 == null) {
throw new UnknownHostException(ip6String);
}
return new DualInetAddress(ip4, ip6);
}
private static final Pattern REGEX = Pattern.compile("^[0-9.]+$");
public static DualInetAddress fromHostname(String hostname) throws UnknownHostException {
try {
return fromAddress(hostname);
} catch (UnknownHostException e) {
;
}
if (REGEX.matcher(hostname).matches()) {
throw new UnknownHostException(hostname);
}
String ip4 = null;
String ip6 = null;
for (InetAddress addr : InetAddress.getAllByName(hostname)) {
if (ip4 != null && ip6 != null) {
break;
} else if (ip4 == null && addr instanceof Inet4Address) {
ip4 = normalizeInet4Address(addr.getHostAddress());
} else if (ip6 == null && addr instanceof Inet6Address) {
ip6 = normalizeInet6Address(addr.getHostAddress());
}
}
return new DualInetAddress(ip4, ip6);
}
/**
* A class representing connection peer points (client and server)
*/
public static class ConnectableInetAddresses {
private final String client;
private final String server;
public ConnectableInetAddresses(String c, String s) {
client = c;
server = s;
}
public String getClient() {
return client;
}
public String getServer() {
return server;
}
}
/**
* @return A pair of connectable client and server addresses.
* - If both the client and server have IPv4 addresses, then IPv4 will be used.
* - Otherwise, IPv6 addresses will be used.
*
* @throws NotConnectableException if there is no common IP flavor between the client and the server.
*/
public static ConnectableInetAddresses getConnectableAddresses(DualInetAddress client, DualInetAddress server) {
if (client.hasInet4() && server.hasInet4()) {
return new ConnectableInetAddresses(client.getInet4(), server.getInet4());
}
if (client.hasInet6() && server.hasInet6()) {
return new ConnectableInetAddresses(client.getInet6(), server.getInet6());
}
throw CoordinatorException.fatals.notConnectableError("Unable to find connectable addresses for " + client + " and " + server);
}
/**
* @return A connectable server address, assuming caller is the client.
*
* @throws NotConnectableException if there is no common IP flavor between the client and the server.
*
* @Refer getConnectableAddresses(DualInetAddress client, DualInetAddress server)
*/
public String getConnectableAddress(DualInetAddress server) {
return getConnectableAddresses(this, server).getServer();
}
/**
* @return A connectable local address, assuming caller is the server.
*
* @throws NotConnectableException if there is no common IP flavor between the client and the server.
*
* @Refer getConnectableAddresses(DualInetAddress client, DualInetAddress server)
*/
public String getConnectableLocalAddress(DualInetAddress client) {
return getConnectableAddresses(client, this).getClient();
}
/**
* A common address resolution method:
* - First, we try to treat client and server strings as a textual representation of InetAddress;
* - If this fails, then we query DNS, searching for both IPv4 and IPv6 addresses.
* - Ultimately, all names are represented as DualInetAddresses. Then,
* we use DualInetAddress.getConnectableAddresses() to find a matching pair.
*
* @throws NotConnectableException
*/
public static ConnectableInetAddresses getConnectableAddresses(String client, String server) throws UnknownHostException {
return getConnectableAddresses(fromHostname(client), fromHostname(server));
}
/**
* Return a connectable server address for the hostname provided, assuming caller is the client.
*
* @throws UnknownHostException
* , NotConnectableException
*
* @Refer getConnectableAddresses(String client, String server)
*/
public String getConnectableAddress(String server) throws UnknownHostException {
return getConnectableAddress(fromHostname(server));
}
/**
* Return a connectable server address for the hostname provided, assuming caller is the client.
*
* @throws UnknownHostException
* , NotConnectableException
*
* @Refer getConnectableAddresses(String client, String server)
*/
public String getConnectableLocalAddress(String client) throws UnknownHostException {
return getConnectableLocalAddress(fromHostname(client));
}
@Override
public String toString() {
if (ip4 != null && ip6 != null) {
return ip4 + "," + ip6;
} else if (ip4 != null) {
return ip4;
} else if (ip6 != null) {
return ip6;
} else {
return "(null)";
}
}
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (o == this) {
return true;
}
if (!(o instanceof DualInetAddress)) {
return false;
}
DualInetAddress d = (DualInetAddress) o;
return toString().equals(d.toString());
}
@Override
public int hashCode() {
return 31 * ((ip4 != null) ? ip4.hashCode() : 0) + ((ip6 != null) ? ip6.hashCode() : 0);
}
}