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