001/* 002 * Copyright (C) 2011 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 static com.google.common.base.Preconditions.checkArgument; 018import static com.google.common.base.Preconditions.checkNotNull; 019import static com.google.common.base.Preconditions.checkState; 020import static com.google.common.base.Strings.isNullOrEmpty; 021 022import com.google.common.annotations.GwtCompatible; 023import com.google.common.annotations.GwtIncompatible; 024import com.google.common.annotations.J2ktIncompatible; 025import com.google.common.base.Objects; 026import com.google.common.primitives.Ints; 027import com.google.errorprone.annotations.CanIgnoreReturnValue; 028import com.google.errorprone.annotations.Immutable; 029import java.io.Serializable; 030import org.jspecify.annotations.Nullable; 031 032/** 033 * An immutable representation of a host and port. 034 * 035 * <p>Example usage: 036 * 037 * <pre> 038 * HostAndPort hp = HostAndPort.fromString("[2001:db8::1]") 039 * .withDefaultPort(80) 040 * .requireBracketsForIPv6(); 041 * hp.getHost(); // returns "2001:db8::1" 042 * hp.getPort(); // returns 80 043 * hp.toString(); // returns "[2001:db8::1]:80" 044 * </pre> 045 * 046 * <p>Here are some examples of recognized formats: 047 * 048 * <ul> 049 * <li>example.com 050 * <li>example.com:80 051 * <li>192.0.2.1 052 * <li>192.0.2.1:80 053 * <li>[2001:db8::1] - {@link #getHost()} omits brackets 054 * <li>[2001:db8::1]:80 - {@link #getHost()} omits brackets 055 * <li>2001:db8::1 - Use {@link #requireBracketsForIPv6()} to prohibit this 056 * </ul> 057 * 058 * <p>Note that this is not an exhaustive list, because these methods are only concerned with 059 * brackets, colons, and port numbers. Full validation of the host field (if desired) is the 060 * caller's responsibility. 061 * 062 * @author Paul Marks 063 * @since 10.0 064 */ 065@Immutable 066@GwtCompatible 067public final class HostAndPort implements Serializable { 068 /** Magic value indicating the absence of a port number. */ 069 private static final int NO_PORT = -1; 070 071 /** Hostname, IPv4/IPv6 literal, or unvalidated nonsense. */ 072 private final String host; 073 074 /** Validated port number in the range [0..65535], or NO_PORT */ 075 private final int port; 076 077 /** True if the parsed host has colons, but no surrounding brackets. */ 078 private final boolean hasBracketlessColons; 079 080 private HostAndPort(String host, int port, boolean hasBracketlessColons) { 081 this.host = host; 082 this.port = port; 083 this.hasBracketlessColons = hasBracketlessColons; 084 } 085 086 /** 087 * Returns the portion of this {@code HostAndPort} instance that should represent the hostname or 088 * IPv4/IPv6 literal. 089 * 090 * <p>A successful parse does not imply any degree of sanity in this field. For additional 091 * validation, see the {@link HostSpecifier} class. 092 * 093 * @since 20.0 (since 10.0 as {@code getHostText}) 094 */ 095 public String getHost() { 096 return host; 097 } 098 099 /** Return true if this instance has a defined port. */ 100 public boolean hasPort() { 101 return port >= 0; 102 } 103 104 /** 105 * Get the current port number, failing if no port is defined. 106 * 107 * @return a validated port number, in the range [0..65535] 108 * @throws IllegalStateException if no port is defined. You can use {@link #withDefaultPort(int)} 109 * to prevent this from occurring. 110 */ 111 public int getPort() { 112 checkState(hasPort()); 113 return port; 114 } 115 116 /** Returns the current port number, with a default if no port is defined. */ 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. Use {@link #requireBracketsForIPv6()} to 125 * 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, or {@code port} is out 131 * of range. 132 */ 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. Use {@link #requireBracketsForIPv6()} to 144 * 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 */ 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 * <p>Note that the host-only formats will leave the port field undefined. You can use {@link 161 * #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 */ 167 @CanIgnoreReturnValue // TODO(b/219820829): consider removing 168 public static HostAndPort fromString(String hostPortString) { 169 checkNotNull(hostPortString); 170 String host; 171 String portString = null; 172 boolean hasBracketlessColons = false; 173 174 if (hostPortString.startsWith("[")) { 175 String[] hostAndPort = getHostAndPortFromBracketedHost(hostPortString); 176 host = hostAndPort[0]; 177 portString = hostAndPort[1]; 178 } else { 179 int colonPos = hostPortString.indexOf(':'); 180 if (colonPos >= 0 && hostPortString.indexOf(':', colonPos + 1) == -1) { 181 // Exactly 1 colon. Split into host:port. 182 host = hostPortString.substring(0, colonPos); 183 portString = hostPortString.substring(colonPos + 1); 184 } else { 185 // 0 or 2+ colons. Bare hostname or IPv6 literal. 186 host = hostPortString; 187 hasBracketlessColons = (colonPos >= 0); 188 } 189 } 190 191 Integer port; 192 if (isNullOrEmpty(portString)) { 193 port = NO_PORT; 194 } else { 195 port = Ints.tryParse(portString); 196 checkArgument(port != null, "Unparseable port number: %s", hostPortString); 197 checkArgument(isValidPort(port), "Port number out of range: %s", hostPortString); 198 } 199 200 return new HostAndPort(host, port, hasBracketlessColons); 201 } 202 203 /** 204 * Parses a bracketed host-port string, throwing IllegalArgumentException if parsing fails. 205 * 206 * @param hostPortString the full bracketed host-port specification. Port might not be specified. 207 * @return an array with 2 strings: host and port, in that order. 208 * @throws IllegalArgumentException if parsing the bracketed host-port string fails. 209 */ 210 private static String[] getHostAndPortFromBracketedHost(String hostPortString) { 211 checkArgument( 212 hostPortString.charAt(0) == '[', 213 "Bracketed host-port string must start with a bracket: %s", 214 hostPortString); 215 int colonIndex = hostPortString.indexOf(':'); 216 int closeBracketIndex = hostPortString.lastIndexOf(']'); 217 checkArgument( 218 colonIndex > -1 && closeBracketIndex > colonIndex, 219 "Invalid bracketed host/port: %s", 220 hostPortString); 221 222 String host = hostPortString.substring(1, closeBracketIndex); 223 if (closeBracketIndex + 1 == hostPortString.length()) { 224 return new String[] {host, ""}; 225 } else { 226 checkArgument( 227 hostPortString.charAt(closeBracketIndex + 1) == ':', 228 "Only a colon may follow a close bracket: %s", 229 hostPortString); 230 for (int i = closeBracketIndex + 2; i < hostPortString.length(); ++i) { 231 checkArgument( 232 Character.isDigit(hostPortString.charAt(i)), 233 "Port must be numeric: %s", 234 hostPortString); 235 } 236 return new String[] {host, hostPortString.substring(closeBracketIndex + 2)}; 237 } 238 } 239 240 /** 241 * Provide a default port if the parsed string contained only a host. 242 * 243 * <p>You can chain this after {@link #fromString(String)} to include a port in case the port was 244 * omitted from the input string. If a port was already provided, then this method is a no-op. 245 * 246 * @param defaultPort a port number, from [0..65535] 247 * @return a HostAndPort instance, guaranteed to have a defined port. 248 */ 249 public HostAndPort withDefaultPort(int defaultPort) { 250 checkArgument(isValidPort(defaultPort)); 251 if (hasPort()) { 252 return this; 253 } 254 return new HostAndPort(host, defaultPort, hasBracketlessColons); 255 } 256 257 /** 258 * Generate an error if the host might be a non-bracketed IPv6 literal. 259 * 260 * <p>URI formatting requires that IPv6 literals be surrounded by brackets, like "[2001:db8::1]". 261 * Chain this call after {@link #fromString(String)} to increase the strictness of the parser, and 262 * disallow IPv6 literals that don't contain these brackets. 263 * 264 * <p>Note that this parser identifies IPv6 literals solely based on the presence of a colon. To 265 * perform actual validation of IP addresses, see the {@link InetAddresses#forString(String)} 266 * method. 267 * 268 * @return {@code this}, to enable chaining of calls. 269 * @throws IllegalArgumentException if bracketless IPv6 is detected. 270 */ 271 @CanIgnoreReturnValue 272 public HostAndPort requireBracketsForIPv6() { 273 checkArgument(!hasBracketlessColons, "Possible bracketless IPv6 literal: %s", host); 274 return this; 275 } 276 277 @Override 278 public boolean equals(@Nullable Object other) { 279 if (this == other) { 280 return true; 281 } 282 if (other instanceof HostAndPort) { 283 HostAndPort that = (HostAndPort) other; 284 return Objects.equal(this.host, that.host) && this.port == that.port; 285 } 286 return false; 287 } 288 289 @Override 290 public int hashCode() { 291 return Objects.hashCode(host, port); 292 } 293 294 /** Rebuild the host:port string, including brackets if necessary. */ 295 @Override 296 public String toString() { 297 // "[]:12345" requires 8 extra bytes. 298 StringBuilder builder = new StringBuilder(host.length() + 8); 299 if (host.indexOf(':') >= 0) { 300 builder.append('[').append(host).append(']'); 301 } else { 302 builder.append(host); 303 } 304 if (hasPort()) { 305 builder.append(':').append(port); 306 } 307 return builder.toString(); 308 } 309 310 /** Return true for valid port numbers. */ 311 private static boolean isValidPort(int port) { 312 return port >= 0 && port <= 65535; 313 } 314 315 @GwtIncompatible @J2ktIncompatible private static final long serialVersionUID = 0; 316}