001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2006-2008 Sun Microsystems, Inc.
015 * Portions Copyright 2013-2016 ForgeRock AS.
016 */
017package org.opends.server.types;
018
019import java.net.InetAddress;
020import java.net.InetSocketAddress;
021import java.net.NetworkInterface;
022import java.net.SocketException;
023import java.net.UnknownHostException;
024import java.util.Enumeration;
025import java.util.HashSet;
026import java.util.Objects;
027import java.util.Set;
028
029import org.forgerock.i18n.slf4j.LocalizedLogger;
030
031import static org.opends.messages.ReplicationMessages.*;
032
033/**
034 * This class defines a data structure that combines an address and port number,
035 * as may be used to accept a connection from or initiate a connection to a
036 * remote system.
037 * <p>
038 * Due to the possibility of live network configuration changes, instances of
039 * this class are not intended for caching and should be rebuilt on demand.
040 */
041@org.opends.server.types.PublicAPI(
042     stability=org.opends.server.types.StabilityLevel.UNCOMMITTED,
043     mayInstantiate=false,
044     mayExtend=false,
045     mayInvoke=true)
046public final class HostPort
047{
048
049  /** The tracer object for the debug logger. */
050  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
051
052  /** Constant that represents the local host. */
053  private static final String LOCALHOST = "localhost";
054
055  /**
056   * The wildcard address allows to instruct a server to
057   * "listen to all addresses".
058   *
059   * @see InetSocketAddress#InetSocketAddress(int) InetSocketAddress javadoc
060   */
061  public static final String WILDCARD_ADDRESS = "0.0.0.0";
062
063
064
065  /**
066   * The supplied host for this object.
067   * <p>
068   * Keeping the supplied host name allows to rebuild the HostPort object in
069   * case the network configuration changed on the current machine.
070   */
071  private final String host;
072
073  /**
074   * The normalized host for this object.
075   * <p>
076   * Normalization consists of:
077   * <ul>
078   * <li>convert all local addresses to "localhost"</li>
079   * <li>convert remote host name / addresses to the equivalent IP address</li>
080   * </ul>
081   */
082  private final String normalizedHost;
083
084  /** The port for this object. */
085  private final int port;
086
087
088
089
090  /** Time-stamp acts as memory barrier for networkInterfaces. */
091  private static final long CACHED_LOCAL_ADDRESSES_TIMEOUT_MS = 30 * 1000;
092  private static volatile long localAddressesTimeStamp;
093  private static Set<InetAddress> localAddresses = new HashSet<>();
094
095  /**
096   * Returns {@code true} if the provided {@code InetAddress} represents the
097   * address of one of the interfaces on the current host machine.
098   *
099   * @param address
100   *          The network address.
101   * @return {@code true} if the provided {@code InetAddress} represents the
102   *         address of one of the interfaces on the current host machine.
103   */
104  public static boolean isLocalAddress(InetAddress address)
105  {
106    return address.isLoopbackAddress() || getLocalAddresses().contains(address);
107  }
108
109  /**
110   * Returns a Set of all the local addresses as detected by the Java
111   * environment from the operating system configuration.
112   * <p>
113   * The local addresses are temporarily cached to balance the cost of this
114   * expensive computation vs. refreshing the data that can be changed while the
115   * system is running.
116   *
117   * @return a Set containing all the local addresses
118   */
119  private static Set<InetAddress> getLocalAddresses()
120  {
121    final long currentTimeStamp = System.currentTimeMillis();
122    if (localAddressesTimeStamp
123        < (currentTimeStamp - CACHED_LOCAL_ADDRESSES_TIMEOUT_MS))
124    {
125      // Refresh the cache.
126      try
127      {
128        final Enumeration<NetworkInterface> i =
129            NetworkInterface.getNetworkInterfaces();
130        if (i != null) {
131          final Set<InetAddress> newLocalAddresses = new HashSet<>();
132          while (i.hasMoreElements())
133          {
134            NetworkInterface n = i.nextElement();
135            Enumeration<InetAddress> j = n.getInetAddresses();
136            while (j.hasMoreElements())
137            {
138              newLocalAddresses.add(j.nextElement());
139            }
140          }
141          localAddresses = newLocalAddresses;
142        }
143      }
144      catch (SocketException e)
145      {
146        // Ignore and keep the old set.
147        logger.traceException(e);
148      }
149      localAddressesTimeStamp = currentTimeStamp; // Publishes.
150    }
151    return localAddresses;
152  }
153
154  /**
155   * Returns a a new HostPort for all addresses, also known as a wildcard
156   * address.
157   *
158   * @param port
159   *          The port number for the new {@code HostPort} object.
160   * @return a newly constructed HostPort object
161   */
162  public static HostPort allAddresses(int port)
163  {
164    return new HostPort(WILDCARD_ADDRESS, port);
165  }
166
167  /**
168   * Builds a new instance of {@link HostPort} representing the local machine
169   * with the supplied port.
170   *
171   * @param port
172   *          the port to use when building the new {@link HostPort} object
173   * @return a new {@link HostPort} instance representing the local machine with
174   *         the supplied port.
175   */
176  public static HostPort localAddress(int port)
177  {
178    return new HostPort(LOCALHOST, port);
179  }
180
181  /**
182   * Creates a new {@code HostPort} object with the specified port
183   * number but no explicit host.
184   *
185   * @param  host  The host address or name for this {@code HostPort}
186   *               object, or {@code null} if there is none.
187   * @param  port  The port number for this {@code HostPort} object.
188   */
189  public HostPort(String host, int port)
190  {
191    if (host != null) {
192      this.host = removeExtraChars(host);
193      this.normalizedHost = normalizeHost(this.host);
194    } else {
195      this.host = null;
196      this.normalizedHost = null;
197    }
198    this.port = normalizePort(port, host);
199  }
200
201  /**
202   * Creates a new {@code HostPort} object by parsing the supplied "hostName:port" String URL.
203   * This method also accepts IPV6 style "[hostAddress]:port" String URLs.
204   *
205   * @param hostPort
206   *          a String representing the URL made of a host and a port.
207   * @return a new {@link HostPort} built from the supplied string.
208   * @throws NumberFormatException
209   *           If the "port" in the supplied string cannot be converted to an int
210   * @throws IllegalArgumentException
211   *           if no port could be found in the supplied string, or if the port
212   *           is not a valid port number
213   */
214  public static HostPort valueOf(String hostPort) throws NumberFormatException,
215          IllegalArgumentException
216  {
217    return HostPort.valueOf(hostPort, null);
218  }
219
220  /**
221   * Creates a new {@code HostPort} object by parsing the supplied "hostName:port" String URL.
222   * This method also accepts IPV6 style "[hostAddress]:port" String URLs. Values without ports
223   * are allowed if a default port is provided.
224   *
225   * @param hostPort
226   *          a String representing the URL made of a host and a port.
227   * @param defaultPort
228   *          if not {@code null} then a default port to use if none is present in the string.
229   * @return a new {@link HostPort} built from the supplied string.
230   * @throws NumberFormatException
231   *           If the "port" in the supplied string cannot be converted to an int
232   * @throws IllegalArgumentException
233   *           if no port could be found in the supplied string, or if the port
234   *           is not a valid port number
235   */
236  public static HostPort valueOf(String hostPort, Integer defaultPort) throws NumberFormatException,
237      IllegalArgumentException
238  {
239    final int sepIndex = hostPort.lastIndexOf(':');
240    if ((hostPort.charAt(0) == '['
241        && hostPort.charAt(hostPort.length() - 1) == ']')
242        || sepIndex == -1)
243    {
244      if (defaultPort != null)
245      {
246        return new HostPort(hostPort, defaultPort.intValue());
247      }
248      throw new IllegalArgumentException(
249          "Invalid host/port string: no network port was provided in '"
250              + hostPort + "'");
251    }
252    else if (sepIndex == 0)
253    {
254      throw new IllegalArgumentException(
255          "Invalid host/port string: no host name was provided in '" + hostPort
256              + "'");
257    }
258    else if (hostPort.lastIndexOf(':', sepIndex - 1) != -1
259        && (hostPort.charAt(0) != '[' || hostPort.charAt(sepIndex - 1) != ']'))
260    {
261      if (defaultPort != null)
262      {
263        return new HostPort(hostPort, defaultPort.intValue());
264      }
265      throw new IllegalArgumentException(
266          "Invalid host/port string: Suspected IPv6 address provided in '"
267              + hostPort + "'. The only allowed format for providing IPv6 "
268              + "addresses is '[IPv6 address]:port'");
269    }
270    String host = hostPort.substring(0, sepIndex);
271    int port = Integer.parseInt(hostPort.substring(sepIndex + 1));
272    return new HostPort(host, port);
273  }
274
275  /**
276   * Removes extra characters from the host name: surrounding square brackets
277   * for IPv6 addresses.
278   *
279   * @param host
280   *          the host name to clean
281   * @return the cleaned up host name
282   */
283  private String removeExtraChars(String host)
284  {
285    final int startsWith = host.indexOf("[");
286    if (startsWith == -1)
287    {
288      return host;
289    }
290    return host.substring(1, host.length() - 1);
291  }
292
293  /**
294   * Returns a normalized String representation of the supplied host.
295   *
296   * @param host
297   *          the host address to normalize
298   * @return a normalized String representation of the supplied host.
299   * @see #normalizedHost what host normalization covers
300   */
301  private String normalizeHost(String host)
302  {
303    if (LOCALHOST.equals(host))
304    { // it is already normalized
305      return LOCALHOST;
306    }
307
308    try
309    {
310      final InetAddress inetAddress = InetAddress.getByName(host);
311      if (isLocalAddress(inetAddress))
312      {
313        // normalize to localhost for easier identification.
314        return LOCALHOST;
315      }
316      // else normalize to IP address for easier identification.
317      // FIXME, this does not fix the multi homing issue where a single machine
318      // has several IP addresses
319      return inetAddress.getHostAddress();
320    }
321    catch (UnknownHostException e)
322    {
323      // We could not resolve this host name, default to the provided host name
324      logger.error(ERR_COULD_NOT_SOLVE_HOSTNAME, host);
325      return host;
326    }
327  }
328
329  /**
330   * Ensures the supplied port number is valid.
331   *
332   * @param port
333   *          the port number to validate
334   * @return the port number if valid
335   */
336  private int normalizePort(int port, String host)
337  {
338    if ((1 <= port && port <= 65535) || (port == 0 && host == null))
339    {
340      return port;
341    }
342    throw new IllegalArgumentException("Invalid network port provided: " + port
343        + " is not included in the [1, 65535] range.");
344  }
345
346  /**
347   * Retrieves the host for this {@code HostPort} object.
348   *
349   * @return  The host for this {@code HostPort} object, or
350   *          {@code null} if none was provided.
351   */
352  public String getHost()
353  {
354    return host;
355  }
356
357
358
359  /**
360   * Retrieves the port number for this {@code HostPort} object.
361   *
362   * @return The valid port number in the [1, 65535] range for this
363   *         {@code HostPort} object.
364   */
365  public int getPort()
366  {
367    return port;
368  }
369
370  /**
371   * Whether the current object represents a local address.
372   *
373   * @return true if this represents a local address, false otherwise.
374   */
375  public boolean isLocalAddress()
376  {
377    return LOCALHOST.equals(this.normalizedHost);
378  }
379
380  /**
381   * Converts the current object to an equivalent {@link InetSocketAddress}
382   * object.
383   *
384   * @return a {@link InetSocketAddress} equivalent of the current object.
385   * @throws UnknownHostException
386   *           If the current host name cannot be resolved to an
387   *           {@link InetAddress}
388   */
389  public InetSocketAddress toInetSocketAddress() throws UnknownHostException
390  {
391    return new InetSocketAddress(InetAddress.getByName(getHost()), getPort());
392  }
393
394  /**
395   * Returns a string representation of this {@code HostPort} object. It will be
396   * the host element (or nothing if no host was given) followed by a colon and
397   * the port number.
398   *
399   * @return A string representation of this {@code HostPort} object.
400   */
401  @Override
402  public String toString()
403  {
404    return toString(host, port);
405  }
406
407  /**
408   * Returns a string representation of the provided host and port. No validation is performed.
409   *
410   * @param host
411   *          the host name
412   * @param port
413   *          the port number
414   * @return A string representation of the provided host and port.
415   */
416  public static String toString(String host, int port)
417  {
418    if (host != null && host.contains(":"))
419    {
420      return "[" + host + "]:" + port;
421    }
422    return host + ":" + port;
423  }
424
425  /**
426   * Checks whether the supplied HostPort is an equivalent to the current
427   * HostPort.
428   *
429   * @param other
430   *          the HostPort to compare to "this"
431   * @return true if the HostPorts are equivalent, false otherwise. False is
432   *         also return if calling {@link InetAddress#getAllByName(String)}
433   *         throws an UnknownHostException.
434   */
435  public boolean isEquivalentTo(final HostPort other)
436  {
437    try
438    {
439      // Get and compare ports of RS1 and RS2
440      if (getPort() != other.getPort())
441      {
442        return false;
443      }
444
445      // Get and compare addresses of RS1 and RS2
446      // Normalize local addresses to null for fast comparison.
447      final InetAddress[] thisAddresses =
448          isLocalAddress() ? null : InetAddress.getAllByName(getHost());
449      final InetAddress[] otherAddresses =
450          other.isLocalAddress() ? null : InetAddress.getAllByName(other
451              .getHost());
452
453      // Now compare addresses, if at least one match, this is the same server.
454      if (thisAddresses == null && otherAddresses == null)
455      {
456        // Both local addresses.
457        return true;
458      }
459      else if (thisAddresses == null || otherAddresses == null)
460      {
461        // One local address and one non-local.
462        return false;
463      }
464
465      // Both non-local addresses: check for overlap.
466      for (InetAddress thisAddress : thisAddresses)
467      {
468        for (InetAddress otherAddress : otherAddresses)
469        {
470          if (thisAddress.equals(otherAddress))
471          {
472            return true;
473          }
474        }
475      }
476      return false;
477    }
478    catch (UnknownHostException ex)
479    {
480      // Unknown RS: should not happen
481      return false;
482    }
483  }
484
485  /**
486   * Returns {@code true} if the provided Object is a HostPort object with the
487   * same host name and port than this HostPort object.
488   *
489   * @param obj
490   *          the reference object with which to compare.
491   * @return {@code true} if this object is the same as the obj argument;
492   *         {@code false} otherwise.
493   */
494  @Override
495  public boolean equals(Object obj)
496  {
497    if (obj == null)
498    {
499      return false;
500    }
501    if (obj == this)
502    {
503      return true;
504    }
505    if (getClass() != obj.getClass())
506    {
507      return false;
508    }
509
510    HostPort other = (HostPort) obj;
511    return port == other.port && Objects.equals(normalizedHost, other.normalizedHost);
512  }
513
514  /**
515   * Retrieves a hash code for this HostPort object.
516   *
517   * @return A hash code for this HostPort object.
518   */
519  @Override
520  public int hashCode()
521  {
522    final int prime = 31;
523    int result = 1;
524    result = prime * result
525            + ((normalizedHost == null) ? 0 : normalizedHost.hashCode());
526    result = prime * result + port;
527    return result;
528  }
529}