001/*
002 * Copyright (C) 2009 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005 * in compliance with the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the License
010 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
011 * or implied. See the License for the specific language governing permissions and limitations under
012 * the License.
013 */
014
015package com.google.common.net;
016
017import com.google.common.annotations.GwtIncompatible;
018import com.google.common.annotations.J2ktIncompatible;
019import com.google.common.base.Preconditions;
020import com.google.errorprone.annotations.CanIgnoreReturnValue;
021import java.net.InetAddress;
022import java.text.ParseException;
023import org.jspecify.annotations.Nullable;
024
025/**
026 * A syntactically valid host specifier, suitable for use in a URI. This may be either a numeric IP
027 * address in IPv4 or IPv6 notation, or a domain name.
028 *
029 * <p>Because this class is intended to represent host specifiers which can reasonably be used in a
030 * URI, the domain name case is further restricted to include only those domain names which end in a
031 * recognized public suffix; see {@link InternetDomainName#isPublicSuffix()} for details.
032 *
033 * <p>In this context, "URI" is used in the general sense, and {@code HostSpecifier} does not
034 * guarantee that every URI or URL parser will accept the original input without additional
035 * normalization. {@code HostSpecifier} follows the syntactic rules of {@link InetAddresses} and
036 * {@link InternetDomainName}, including their documented acceptance of some inputs that particular
037 * URI implementations may reject, such as non-ASCII digits, non-ASCII domain names, or domain
038 * labels containing underscores.
039 *
040 * <p>Note that no network lookups are performed by any {@code HostSpecifier} methods. No attempt is
041 * made to verify that a provided specifier corresponds to a real or accessible host. Only syntactic
042 * and pattern-based checks are performed.
043 *
044 * <p>If you know that a given string represents a numeric IP address, use {@link InetAddresses} to
045 * obtain and manipulate a {@link java.net.InetAddress} instance from it rather than using this
046 * class. Similarly, if you know that a given string represents a domain name, use {@link
047 * InternetDomainName} rather than this class.
048 *
049 * @author Craig Berry
050 * @since 5.0
051 */
052@J2ktIncompatible
053@GwtIncompatible
054public final class HostSpecifier {
055
056  private final String canonicalForm;
057
058  private HostSpecifier(String canonicalForm) {
059    this.canonicalForm = canonicalForm;
060  }
061
062  /**
063   * Returns a {@code HostSpecifier} built from the provided {@code specifier}, which is already
064   * known to be valid. If the {@code specifier} might be invalid, use {@link #from(String)}
065   * instead.
066   *
067   * <p>The specifier must be in one of these formats:
068   *
069   * <ul>
070   *   <li>A domain name, like {@code google.com}
071   *   <li>A IPv4 address string, like {@code 127.0.0.1}
072   *   <li>An IPv6 address string with or without brackets, like {@code [2001:db8::1]} or {@code
073   *       2001:db8::1}
074   * </ul>
075   *
076   * @throws IllegalArgumentException if the specifier is not valid.
077   */
078  public static HostSpecifier fromValid(String specifier) {
079    // Verify that no port was specified, and strip optional brackets from
080    // IPv6 literals.
081    HostAndPort parsedHost = HostAndPort.fromString(specifier);
082    Preconditions.checkArgument(!parsedHost.hasPort());
083    String host = parsedHost.getHost();
084
085    // Try to interpret the specifier as an IP address. Note we build
086    // the address rather than using the .is* methods because we want to
087    // use InetAddresses.toUriString to convert the result to a string in
088    // canonical form.
089    InetAddress addr = null;
090    try {
091      addr = InetAddresses.forString(host);
092    } catch (IllegalArgumentException e) {
093      // It is not an IPv4 or IPv6 literal
094    }
095
096    if (addr != null) {
097      return new HostSpecifier(InetAddresses.toUriString(addr));
098    }
099
100    // It is not any kind of IP address; must be a domain name or invalid.
101
102    // TODO(user): different versions of this for different factories?
103    InternetDomainName domain = InternetDomainName.from(host);
104
105    if (domain.hasPublicSuffix()) {
106      return new HostSpecifier(domain.toString());
107    }
108
109    throw new IllegalArgumentException(
110        "Domain name does not have a recognized public suffix: " + host);
111  }
112
113  /**
114   * Attempts to return a {@code HostSpecifier} for the given string, throwing an exception if
115   * parsing fails. Always use this method in preference to {@link #fromValid(String)} for a
116   * specifier that is not already known to be valid.
117   *
118   * @throws ParseException if the specifier is not valid.
119   */
120  @CanIgnoreReturnValue // TODO(b/219820829): consider removing
121  public static HostSpecifier from(String specifier) throws ParseException {
122    try {
123      return fromValid(specifier);
124    } catch (IllegalArgumentException e) {
125      // Since the IAE can originate at several different points inside
126      // fromValid(), we implement this method in terms of that one rather
127      // than the reverse.
128
129      ParseException parseException = new ParseException("Invalid host specifier: " + specifier, 0);
130      parseException.initCause(e);
131      throw parseException;
132    }
133  }
134
135  /**
136   * Determines whether {@code specifier} represents a valid {@link HostSpecifier} as described in
137   * the documentation for {@link #fromValid(String)}.
138   */
139  public static boolean isValid(String specifier) {
140    try {
141      HostSpecifier unused = fromValid(specifier);
142      return true;
143    } catch (IllegalArgumentException e) {
144      return false;
145    }
146  }
147
148  @Override
149  public boolean equals(@Nullable Object other) {
150    if (this == other) {
151      return true;
152    }
153
154    if (other instanceof HostSpecifier) {
155      HostSpecifier that = (HostSpecifier) other;
156      return this.canonicalForm.equals(that.canonicalForm);
157    }
158
159    return false;
160  }
161
162  @Override
163  public int hashCode() {
164    return canonicalForm.hashCode();
165  }
166
167  /**
168   * Returns a string representation of the host specifier suitable for inclusion in a URI. If the
169   * host specifier is a domain name, the string will be normalized to all lower case. If the
170   * specifier was an IPv6 address without brackets, brackets are added so that the result will be
171   * usable in the host part of a URI.
172   */
173  @Override
174  public String toString() {
175    return canonicalForm;
176  }
177}