1 /*
2  * Copyright (C) 2011 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.net;
18 
19 import static com.google.common.base.Preconditions.checkArgument;
20 import static com.google.common.base.Preconditions.checkNotNull;
21 import static com.google.common.base.Preconditions.checkState;
22 
23 import com.google.common.annotations.Beta;
24 import com.google.common.annotations.GwtCompatible;
25 import com.google.common.base.Objects;
26 import com.google.common.base.Strings;
27 
28 import java.io.Serializable;
29 
30 import javax.annotation.Nullable;
31 import javax.annotation.concurrent.Immutable;
32 
33 /**
34  * An immutable representation of a host and port.
35  *
36  * <p>Example usage:
37  * <pre>
38  * HostAndPort hp = HostAndPort.fromString("[2001:db8::1]")
39  *     .withDefaultPort(80)
40  *     .requireBracketsForIPv6();
41  * hp.getHostText();  // returns "2001:db8::1"
42  * hp.getPort();      // returns 80
43  * hp.toString();     // returns "[2001:db8::1]:80"
44  * </pre>
45  *
46  * <p>Here are some examples of recognized formats:
47  * <ul>
48  *   <li>example.com
49  *   <li>example.com:80
50  *   <li>192.0.2.1
51  *   <li>192.0.2.1:80
52  *   <li>[2001:db8::1]     - {@link #getHostText()} omits brackets
53  *   <li>[2001:db8::1]:80  - {@link #getHostText()} omits brackets
54  *   <li>2001:db8::1       - Use {@link #requireBracketsForIPv6()} to prohibit this
55  * </ul>
56  *
57  * <p>Note that this is not an exhaustive list, because these methods are only
58  * concerned with brackets, colons, and port numbers.  Full validation of the
59  * host field (if desired) is the caller's responsibility.
60  *
61  * @author Paul Marks
62  * @since 10.0
63  */
64 @Beta
65 @Immutable
66 @GwtCompatible
67 public final class HostAndPort implements Serializable {
68   /** Magic value indicating the absence of a port number. */
69   private static final int NO_PORT = -1;
70 
71   /** Hostname, IPv4/IPv6 literal, or unvalidated nonsense. */
72   private final String host;
73 
74   /** Validated port number in the range [0..65535], or NO_PORT */
75   private final int port;
76 
77   /** True if the parsed host has colons, but no surrounding brackets. */
78   private final boolean hasBracketlessColons;
79 
HostAndPort(String host, int port, boolean hasBracketlessColons)80   private HostAndPort(String host, int port, boolean hasBracketlessColons) {
81     this.host = host;
82     this.port = port;
83     this.hasBracketlessColons = hasBracketlessColons;
84   }
85 
86   /**
87    * Returns the portion of this {@code HostAndPort} instance that should
88    * represent the hostname or IPv4/IPv6 literal.
89    *
90    * <p>A successful parse does not imply any degree of sanity in this field.
91    * For additional validation, see the {@link HostSpecifier} class.
92    */
getHostText()93   public String getHostText() {
94     return host;
95   }
96 
97   /** Return true if this instance has a defined port. */
hasPort()98   public boolean hasPort() {
99     return port >= 0;
100   }
101 
102   /**
103    * Get the current port number, failing if no port is defined.
104    *
105    * @return a validated port number, in the range [0..65535]
106    * @throws IllegalStateException if no port is defined.  You can use
107    *         {@link #withDefaultPort(int)} to prevent this from occurring.
108    */
getPort()109   public int getPort() {
110     checkState(hasPort());
111     return port;
112   }
113 
114   /**
115    * Returns the current port number, with a default if no port is defined.
116    */
getPortOrDefault(int defaultPort)117   public int getPortOrDefault(int defaultPort) {
118     return hasPort() ? port : defaultPort;
119   }
120 
121   /**
122    * Build a HostAndPort instance from separate host and port values.
123    *
124    * <p>Note: Non-bracketed IPv6 literals are allowed.
125    * Use {@link #requireBracketsForIPv6()} to prohibit these.
126    *
127    * @param host the host string to parse.  Must not contain a port number.
128    * @param port a port number from [0..65535]
129    * @return if parsing was successful, a populated HostAndPort object.
130    * @throws IllegalArgumentException if {@code host} contains a port number,
131    *     or {@code port} is out of range.
132    */
fromParts(String host, int port)133   public static HostAndPort fromParts(String host, int port) {
134     checkArgument(isValidPort(port), "Port out of range: %s", port);
135     HostAndPort parsedHost = fromString(host);
136     checkArgument(!parsedHost.hasPort(), "Host has a port: %s", host);
137     return new HostAndPort(parsedHost.host, port, parsedHost.hasBracketlessColons);
138   }
139 
140   /**
141    * Build a HostAndPort instance from a host only.
142    *
143    * <p>Note: Non-bracketed IPv6 literals are allowed.
144    * Use {@link #requireBracketsForIPv6()} to prohibit these.
145    *
146    * @param host the host-only string to parse.  Must not contain a port number.
147    * @return if parsing was successful, a populated HostAndPort object.
148    * @throws IllegalArgumentException if {@code host} contains a port number.
149    * @since 17.0
150    */
fromHost(String host)151   public static HostAndPort fromHost(String host) {
152     HostAndPort parsedHost = fromString(host);
153     checkArgument(!parsedHost.hasPort(), "Host has a port: %s", host);
154     return parsedHost;
155   }
156 
157   /**
158    * Split a freeform string into a host and port, without strict validation.
159    *
160    * Note that the host-only formats will leave the port field undefined.  You
161    * can use {@link #withDefaultPort(int)} to patch in a default value.
162    *
163    * @param hostPortString the input string to parse.
164    * @return if parsing was successful, a populated HostAndPort object.
165    * @throws IllegalArgumentException if nothing meaningful could be parsed.
166    */
fromString(String hostPortString)167   public static HostAndPort fromString(String hostPortString) {
168     checkNotNull(hostPortString);
169     String host;
170     String portString = null;
171     boolean hasBracketlessColons = false;
172 
173     if (hostPortString.startsWith("[")) {
174       String[] hostAndPort = getHostAndPortFromBracketedHost(hostPortString);
175       host = hostAndPort[0];
176       portString = hostAndPort[1];
177     } else {
178       int colonPos = hostPortString.indexOf(':');
179       if (colonPos >= 0 && hostPortString.indexOf(':', colonPos + 1) == -1) {
180         // Exactly 1 colon.  Split into host:port.
181         host = hostPortString.substring(0, colonPos);
182         portString = hostPortString.substring(colonPos + 1);
183       } else {
184         // 0 or 2+ colons.  Bare hostname or IPv6 literal.
185         host = hostPortString;
186         hasBracketlessColons = (colonPos >= 0);
187       }
188     }
189 
190     int port = NO_PORT;
191     if (!Strings.isNullOrEmpty(portString)) {
192       // Try to parse the whole port string as a number.
193       // JDK7 accepts leading plus signs. We don't want to.
194       checkArgument(!portString.startsWith("+"), "Unparseable port number: %s", hostPortString);
195       try {
196         port = Integer.parseInt(portString);
197       } catch (NumberFormatException e) {
198         throw new IllegalArgumentException("Unparseable port number: " + hostPortString);
199       }
200       checkArgument(isValidPort(port), "Port number out of range: %s", hostPortString);
201     }
202 
203     return new HostAndPort(host, port, hasBracketlessColons);
204   }
205 
206   /**
207    * Parses a bracketed host-port string, throwing IllegalArgumentException if parsing fails.
208    *
209    * @param hostPortString the full bracketed host-port specification. Post might not be specified.
210    * @return an array with 2 strings: host and port, in that order.
211    * @throws IllegalArgumentException if parsing the bracketed host-port string fails.
212    */
getHostAndPortFromBracketedHost(String hostPortString)213   private static String[] getHostAndPortFromBracketedHost(String hostPortString) {
214     int colonIndex = 0;
215     int closeBracketIndex = 0;
216     boolean hasPort = false;
217     checkArgument(hostPortString.charAt(0) == '[',
218         "Bracketed host-port string must start with a bracket: %s", hostPortString);
219     colonIndex = hostPortString.indexOf(':');
220     closeBracketIndex = hostPortString.lastIndexOf(']');
221     checkArgument(colonIndex > -1 && closeBracketIndex > colonIndex,
222         "Invalid bracketed host/port: %s", hostPortString);
223 
224     String host = hostPortString.substring(1, closeBracketIndex);
225     if (closeBracketIndex + 1 == hostPortString.length()) {
226       return new String[] { host, "" };
227     } else {
228       checkArgument(hostPortString.charAt(closeBracketIndex + 1) == ':',
229           "Only a colon may follow a close bracket: %s", hostPortString);
230       for (int i = closeBracketIndex + 2; i < hostPortString.length(); ++i) {
231         checkArgument(Character.isDigit(hostPortString.charAt(i)),
232             "Port must be numeric: %s", hostPortString);
233       }
234       return new String[] { host, hostPortString.substring(closeBracketIndex + 2) };
235     }
236   }
237 
238   /**
239    * Provide a default port if the parsed string contained only a host.
240    *
241    * You can chain this after {@link #fromString(String)} to include a port in
242    * case the port was omitted from the input string.  If a port was already
243    * provided, then this method is a no-op.
244    *
245    * @param defaultPort a port number, from [0..65535]
246    * @return a HostAndPort instance, guaranteed to have a defined port.
247    */
withDefaultPort(int defaultPort)248   public HostAndPort withDefaultPort(int defaultPort) {
249     checkArgument(isValidPort(defaultPort));
250     if (hasPort() || port == defaultPort) {
251       return this;
252     }
253     return new HostAndPort(host, defaultPort, hasBracketlessColons);
254   }
255 
256   /**
257    * Generate an error if the host might be a non-bracketed IPv6 literal.
258    *
259    * <p>URI formatting requires that IPv6 literals be surrounded by brackets,
260    * like "[2001:db8::1]".  Chain this call after {@link #fromString(String)}
261    * to increase the strictness of the parser, and disallow IPv6 literals
262    * that don't contain these brackets.
263    *
264    * <p>Note that this parser identifies IPv6 literals solely based on the
265    * presence of a colon.  To perform actual validation of IP addresses, see
266    * the {@link InetAddresses#forString(String)} method.
267    *
268    * @return {@code this}, to enable chaining of calls.
269    * @throws IllegalArgumentException if bracketless IPv6 is detected.
270    */
requireBracketsForIPv6()271   public HostAndPort requireBracketsForIPv6() {
272     checkArgument(!hasBracketlessColons, "Possible bracketless IPv6 literal: %s", host);
273     return this;
274   }
275 
276   @Override
equals(@ullable Object other)277   public boolean equals(@Nullable Object other) {
278     if (this == other) {
279       return true;
280     }
281     if (other instanceof HostAndPort) {
282       HostAndPort that = (HostAndPort) other;
283       return Objects.equal(this.host, that.host)
284           && this.port == that.port
285           && this.hasBracketlessColons == that.hasBracketlessColons;
286     }
287     return false;
288   }
289 
290   @Override
hashCode()291   public int hashCode() {
292     return Objects.hashCode(host, port, hasBracketlessColons);
293   }
294 
295   /** Rebuild the host:port string, including brackets if necessary. */
296   @Override
toString()297   public String toString() {
298     // "[]:12345" requires 8 extra bytes.
299     StringBuilder builder = new StringBuilder(host.length() + 8);
300     if (host.indexOf(':') >= 0) {
301       builder.append('[').append(host).append(']');
302     } else {
303       builder.append(host);
304     }
305     if (hasPort()) {
306       builder.append(':').append(port);
307     }
308     return builder.toString();
309   }
310 
311   /** Return true for valid port numbers. */
isValidPort(int port)312   private static boolean isValidPort(int port) {
313     return port >= 0 && port <= 65535;
314   }
315 
316   private static final long serialVersionUID = 0;
317 }
318