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