/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2006-2008 Sun Microsystems, Inc.
* Portions Copyright 2013-2015 ForgeRock AS
*/
package org.opends.server.types;
import java.net.*;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import static org.opends.messages.ReplicationMessages.*;
/**
* This class defines a data structure that combines an address and port number,
* as may be used to accept a connection from or initiate a connection to a
* remote system.
* <p>
* Due to the possibility of live network configuration changes, instances of
* this class are not intended for caching and should be rebuilt on demand.
*/
@org.opends.server.types.PublicAPI(
stability=org.opends.server.types.StabilityLevel.UNCOMMITTED,
mayInstantiate=false,
mayExtend=false,
mayInvoke=true)
public final class HostPort
{
/** The tracer object for the debug logger. */
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
/** Constant that represents the local host. */
private static final String LOCALHOST = "localhost";
/**
* The wildcard address allows to instruct a server to
* "listen to all addresses".
*
* @see InetSocketAddress#InetSocketAddress(int) InetSocketAddress javadoc
*/
public static final String WILDCARD_ADDRESS = "0.0.0.0";
/**
* The supplied host for this object.
* <p>
* Keeping the supplied host name allows to rebuild the HostPort object in
* case the network configuration changed on the current machine.
*/
private final String host;
/**
* The normalized host for this object.
* <p>
* Normalization consists of:
* <ul>
* <li>convert all local addresses to "localhost"</li>
* <li>convert remote host name / addresses to the equivalent IP address</li>
* </ul>
*/
private final String normalizedHost;
/** The port for this object. */
private final int port;
/** Time-stamp acts as memory barrier for networkInterfaces. */
private static final long CACHED_LOCAL_ADDRESSES_TIMEOUT_MS = 30 * 1000;
private static volatile long localAddressesTimeStamp;
private static Set<InetAddress> localAddresses = new HashSet<>();
/**
* Returns {@code true} if the provided {@code InetAddress} represents the
* address of one of the interfaces on the current host machine.
*
* @param address
* The network address.
* @return {@code true} if the provided {@code InetAddress} represents the
* address of one of the interfaces on the current host machine.
*/
public static boolean isLocalAddress(InetAddress address)
{
return address.isLoopbackAddress() || getLocalAddresses().contains(address);
}
/**
* Returns a Set of all the local addresses as detected by the Java
* environment from the operating system configuration.
* <p>
* The local addresses are temporarily cached to balance the cost of this
* expensive computation vs. refreshing the data that can be changed while the
* system is running.
*
* @return a Set containing all the local addresses
*/
private static Set<InetAddress> getLocalAddresses()
{
final long currentTimeStamp = System.currentTimeMillis();
if (localAddressesTimeStamp
< (currentTimeStamp - CACHED_LOCAL_ADDRESSES_TIMEOUT_MS))
{
// Refresh the cache.
try
{
final Enumeration<NetworkInterface> i =
NetworkInterface.getNetworkInterfaces();
if (i != null) {
final Set<InetAddress> newLocalAddresses = new HashSet<>();
while (i.hasMoreElements())
{
NetworkInterface n = i.nextElement();
Enumeration<InetAddress> j = n.getInetAddresses();
while (j.hasMoreElements())
{
newLocalAddresses.add(j.nextElement());
}
}
localAddresses = newLocalAddresses;
}
}
catch (SocketException e)
{
// Ignore and keep the old set.
logger.traceException(e);
}
localAddressesTimeStamp = currentTimeStamp; // Publishes.
}
return localAddresses;
}
/**
* Returns a a new HostPort for all addresses, also known as a wildcard
* address.
*
* @param port
* The port number for the new {@code HostPort} object.
* @return a newly constructed HostPort object
*/
public static HostPort allAddresses(int port)
{
return new HostPort(port);
}
/**
* Builds a new instance of {@link HostPort} representing the local machine
* with the supplied port.
*
* @param port
* the port to use when building the new {@link HostPort} object
* @return a new {@link HostPort} instance representing the local machine with
* the supplied port.
*/
public static HostPort localAddress(int port)
{
return new HostPort(LOCALHOST, port);
}
/**
* Creates a new {@code HostPort} object with the specified port number but no
* host.
*
* @param port
* The port number for this {@code HostPort} object.
*/
private HostPort(int port)
{
this.host = null;
this.normalizedHost = null;
this.port = normalizePort(port);
}
/**
* Creates a new {@code HostPort} object with the specified port
* number but no explicit host.
*
* @param host The host address or name for this {@code HostPort}
* object, or {@code null} if there is none.
* @param port The port number for this {@code HostPort} object.
*/
public HostPort(String host, int port)
{
this.host = removeExtraChars(host);
this.normalizedHost = normalizeHost(this.host);
this.port = normalizePort(port);
}
/**
* Creates a new {@code HostPort} object by parsing the supplied
* "hostName:port" String URL. This method also accepts IPV6 style
* "[hostAddress]:port" String URLs.
*
* @param hostPort
* a String representing the URL made of a host and a port.
* @return a new {@link HostPort} built from the supplied string.
* @throws NumberFormatException
* If the "port" in the supplied string cannot be converted to an
* int
* @throws IllegalArgumentException
* if no port could be found in the supplied string, or if the port
* is not a valid port number
*/
public static HostPort valueOf(String hostPort) throws NumberFormatException,
IllegalArgumentException
{
final int sepIndex = hostPort.lastIndexOf(':');
if ((hostPort.charAt(0) == '['
&& hostPort.charAt(hostPort.length() - 1) == ']')
|| sepIndex == -1)
{
throw new IllegalArgumentException(
"Invalid host/port string: no network port was provided in '"
+ hostPort + "'");
}
else if (sepIndex == 0)
{
throw new IllegalArgumentException(
"Invalid host/port string: no host name was provided in '" + hostPort
+ "'");
}
else if (hostPort.lastIndexOf(':', sepIndex - 1) != -1
&& (hostPort.charAt(0) != '[' || hostPort.charAt(sepIndex - 1) != ']'))
{
throw new IllegalArgumentException(
"Invalid host/port string: Suspected IPv6 address provided in '"
+ hostPort + "'. The only allowed format for providing IPv6 "
+ "addresses is '[IPv6 address]:port'");
}
String host = hostPort.substring(0, sepIndex);
int port = Integer.parseInt(hostPort.substring(sepIndex + 1));
return new HostPort(host, port);
}
/**
* Removes extra characters from the host name: surrounding square brackets
* for IPv6 addresses.
*
* @param host
* the host name to clean
* @return the cleaned up host name
*/
private String removeExtraChars(String host)
{
final int startsWith = host.indexOf("[");
if (startsWith == -1)
{
return host;
}
return host.substring(1, host.length() - 1);
}
/**
* Returns a normalized String representation of the supplied host.
*
* @param host
* the host address to normalize
* @return a normalized String representation of the supplied host.
* @see #normalizedHost what host normalization covers
*/
private String normalizeHost(String host)
{
if (LOCALHOST.equals(host))
{ // it is already normalized
return LOCALHOST;
}
try
{
final InetAddress inetAddress = InetAddress.getByName(host);
if (isLocalAddress(inetAddress))
{
// normalize to localhost for easier identification.
return LOCALHOST;
}
// else normalize to IP address for easier identification.
// FIXME, this does not fix the multi homing issue where a single machine
// has several IP addresses
return inetAddress.getHostAddress();
}
catch (UnknownHostException e)
{
// We could not resolve this host name, default to the provided host name
logger.error(ERR_COULD_NOT_SOLVE_HOSTNAME, host);
return host;
}
}
/**
* Ensures the supplied port number is valid.
*
* @param port
* the port number to validate
* @return the port number if valid
*/
private int normalizePort(int port)
{
if (1 <= port && port <= 65535)
{
return port;
}
throw new IllegalArgumentException("Invalid network port provided: " + port
+ " is not included in the [1, 65535] range.");
}
/**
* Retrieves the host for this {@code HostPort} object.
*
* @return The host for this {@code HostPort} object, or
* {@code null} if none was provided.
*/
public String getHost()
{
return host;
}
/**
* Retrieves the port number for this {@code HostPort} object.
*
* @return The valid port number in the [1, 65535] range for this
* {@code HostPort} object.
*/
public int getPort()
{
return port;
}
/**
* Whether the current object represents a local address.
*
* @return true if this represents a local address, false otherwise.
*/
public boolean isLocalAddress()
{
return LOCALHOST.equals(this.normalizedHost);
}
/**
* Converts the current object to an equivalent {@link InetSocketAddress}
* object.
*
* @return a {@link InetSocketAddress} equivalent of the current object.
* @throws UnknownHostException
* If the current host name cannot be resolved to an
* {@link InetAddress}
*/
public InetSocketAddress toInetSocketAddress() throws UnknownHostException
{
return new InetSocketAddress(InetAddress.getByName(getHost()), getPort());
}
/**
* Returns a string representation of this {@code HostPort} object. It will be
* the host element (or nothing if no host was given) followed by a colon and
* the port number.
*
* @return A string representation of this {@code HostPort} object.
*/
@Override
public String toString()
{
return toString(host);
}
/**
* Returns a normalized string representation of this {@code HostPort} object.
*
* @return A string representation of this {@code HostPort} object.
* @see #normalizedHost what host normalization covers
*/
private String toNormalizedString()
{
return toString(normalizedHost);
}
/**
* Inner computation for #toString() and {@link #toNormalizedString()}.
*
* @param hostName
* the hostName to use for this computation
* @return the String representation fo4r this object
*/
private String toString(String hostName)
{
if (hostName != null)
{
if (hostName.contains(":"))
{
return "[" + hostName + "]:" + port;
}
return hostName + ":" + port;
}
return WILDCARD_ADDRESS + ":" + port;
}
/**
* Checks whether the supplied HostPort is an equivalent to the current
* HostPort.
*
* @param other
* the HostPort to compare to "this"
* @return true if the HostPorts are equivalent, false otherwise. False is
* also return if calling {@link InetAddress#getAllByName(String)}
* throws an UnknownHostException.
*/
public boolean isEquivalentTo(final HostPort other)
{
try
{
// Get and compare ports of RS1 and RS2
if (getPort() != other.getPort())
{
return false;
}
// Get and compare addresses of RS1 and RS2
// Normalize local addresses to null for fast comparison.
final InetAddress[] thisAddresses =
isLocalAddress() ? null : InetAddress.getAllByName(getHost());
final InetAddress[] otherAddresses =
other.isLocalAddress() ? null : InetAddress.getAllByName(other
.getHost());
// Now compare addresses, if at least one match, this is the same server.
if (thisAddresses == null && otherAddresses == null)
{
// Both local addresses.
return true;
}
else if (thisAddresses == null || otherAddresses == null)
{
// One local address and one non-local.
return false;
}
// Both non-local addresses: check for overlap.
for (InetAddress thisAddress : thisAddresses)
{
for (InetAddress otherAddress : otherAddresses)
{
if (thisAddress.equals(otherAddress))
{
return true;
}
}
}
return false;
}
catch (UnknownHostException ex)
{
// Unknown RS: should not happen
return false;
}
}
/**
* Returns {@code true} if the provided Object is a HostPort object with the
* same host name and port than this HostPort object.
*
* @param obj
* the reference object with which to compare.
* @return {@code true} if this object is the same as the obj argument;
* {@code false} otherwise.
*/
@Override
public boolean equals(Object obj)
{
if (obj == null)
{
return false;
}
if (obj == this)
{
return true;
}
if (getClass() != obj.getClass())
{
return false;
}
HostPort other = (HostPort) obj;
if (normalizedHost == null)
{
if (other.normalizedHost != null)
{
return false;
}
}
else if (!normalizedHost.equals(other.normalizedHost))
{
return false;
}
return port == other.port;
}
/**
* Retrieves a hash code for this HostPort object.
*
* @return A hash code for this HostPort object.
*/
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result
+ ((normalizedHost == null) ? 0 : normalizedHost.hashCode());
result = prime * result + port;
return result;
}
}