001/* 002 * Copyright 2016-2025 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2016-2025 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2016-2025 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.util.args; 037 038 039 040import java.io.Serializable; 041 042import com.unboundid.ldap.sdk.LDAPConnectionOptions; 043import com.unboundid.util.Debug; 044import com.unboundid.util.NotMutable; 045import com.unboundid.util.NotNull; 046import com.unboundid.util.Nullable; 047import com.unboundid.util.ThreadSafety; 048import com.unboundid.util.ThreadSafetyLevel; 049import com.unboundid.util.Validator; 050 051import static com.unboundid.util.args.ArgsMessages.*; 052 053 054 055/** 056 * This class provides an implementation of an argument value validator that 057 * ensures that values can be parsed as valid IPv4 or IPV6 addresses. 058 */ 059@NotMutable() 060@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 061public final class IPAddressArgumentValueValidator 062 extends ArgumentValueValidator 063 implements Serializable 064{ 065 /** 066 * The serial version UID for this serializable class. 067 */ 068 private static final long serialVersionUID = -3923873375428600467L; 069 070 071 072 // Indicates whether to accept IPv4 addresses. 073 private final boolean acceptIPv4Addresses; 074 075 // Indicates whether to accept IPv6 addresses. 076 private final boolean acceptIPv6Addresses; 077 078 079 080 /** 081 * Creates a new IP address argument value validator that will accept both 082 * IPv4 and IPv6 addresses. 083 */ 084 public IPAddressArgumentValueValidator() 085 { 086 this(true, true); 087 } 088 089 090 091 /** 092 * Creates a new IP address argument value validator that will accept both 093 * IPv4 and IPv6 addresses. At least one of the {@code acceptIPv4Addresses} 094 * and {@code acceptIPv6Addresses} arguments must have a value of 095 * {@code true}. 096 * 097 * @param acceptIPv4Addresses Indicates whether IPv4 addresses will be 098 * accepted. If this is {@code false}, then the 099 * {@code acceptIPv6Addresses} argument must be 100 * {@code true}. 101 * @param acceptIPv6Addresses Indicates whether IPv6 addresses will be 102 * accepted. If this is {@code false}, then the 103 * {@code acceptIPv4Addresses} argument must be 104 * {@code true}. 105 */ 106 public IPAddressArgumentValueValidator(final boolean acceptIPv4Addresses, 107 final boolean acceptIPv6Addresses) 108 { 109 Validator.ensureTrue(acceptIPv4Addresses || acceptIPv6Addresses, 110 "One or both of the acceptIPv4Addresses and acceptIPv6Addresses " + 111 "arguments must have a value of 'true'."); 112 113 this.acceptIPv4Addresses = acceptIPv4Addresses; 114 this.acceptIPv6Addresses = acceptIPv6Addresses; 115 } 116 117 118 119 /** 120 * Indicates whether to accept IPv4 addresses. 121 * 122 * @return {@code true} if IPv4 addresses should be accepted, or 123 * {@code false} if not. 124 */ 125 public boolean acceptIPv4Addresses() 126 { 127 return acceptIPv4Addresses; 128 } 129 130 131 132 /** 133 * Indicates whether to accept IPv6 addresses. 134 * 135 * @return {@code true} if IPv6 addresses should be accepted, or 136 * {@code false} if not. 137 */ 138 public boolean acceptIPv6Addresses() 139 { 140 return acceptIPv6Addresses; 141 } 142 143 144 145 /** 146 * {@inheritDoc} 147 */ 148 @Override() 149 public void validateArgumentValue(@NotNull final Argument argument, 150 @NotNull final String valueString) 151 throws ArgumentException 152 { 153 // Look at the provided value to determine whether it has any colons. If 154 // so, then we'll assume that it's an IPv6 address and we can ensure that 155 // it is only comprised of colons, periods (in case it ends with an IPv4 156 // address), and hexadecimal digits. If it doesn't have any colons but it 157 // does have one or more periods, then assume that it's an IPv4 address and 158 // ensure that it is only comprised of base-10 digits and periods. This 159 // initial examination will only perform a very coarse validation. 160 final boolean isIPv6 = (valueString.indexOf(':') >= 0); 161 if (isIPv6) 162 { 163 for (final char c : valueString.toCharArray()) 164 { 165 if ((c == ':') || (c == '.') || ((c >= '0') && (c <= '9')) || 166 ((c >= 'a') && (c <= 'f')) || ((c >= 'A') && (c <= 'F'))) 167 { 168 // This character is allowed in an IPv6 address. 169 } 170 else 171 { 172 throw new ArgumentException(ERR_IP_VALIDATOR_ILLEGAL_IPV6_CHAR.get( 173 valueString, argument.getIdentifierString(), c)); 174 } 175 } 176 177 178 // If we've gotten here, then we know that the value string contains only 179 // characters that are allowed in an IPv6 address literal. Let 180 // InetAddress.getByName do the heavy lifting for the rest of the 181 // validation. 182 try 183 { 184 LDAPConnectionOptions.DEFAULT_NAME_RESOLVER.getByName(valueString); 185 } 186 catch (final Exception e) 187 { 188 Debug.debugException(e); 189 throw new ArgumentException( 190 ERR_IP_VALIDATOR_MALFORMED.get(valueString, 191 argument.getIdentifierString()), 192 e); 193 } 194 } 195 else if (valueString.indexOf('.') >= 0) 196 { 197 if (! isValidNumericIPv4Address(valueString)) 198 { 199 throw new ArgumentException(ERR_IP_VALIDATOR_INVALID_IPV4_ADDRESS.get( 200 valueString, argument.getIdentifierString())); 201 } 202 } 203 else 204 { 205 throw new ArgumentException(ERR_IP_VALIDATOR_MALFORMED.get(valueString, 206 argument.getIdentifierString())); 207 } 208 209 210 if (isIPv6) 211 { 212 if (! acceptIPv6Addresses) 213 { 214 throw new ArgumentException(ERR_IP_VALIDATOR_IPV6_NOT_ACCEPTED.get( 215 valueString, argument.getIdentifierString())); 216 } 217 } 218 else if (! acceptIPv4Addresses) 219 { 220 throw new ArgumentException(ERR_IP_VALIDATOR_IPV4_NOT_ACCEPTED.get( 221 valueString, argument.getIdentifierString())); 222 } 223 } 224 225 226 227 /** 228 * Indicates whether the provided string represents a valid IPv4 or IPv6 229 * address. 230 * 231 * @param s The string for which to make the determination. 232 * 233 * @return {@code true} if the provided string represents a valid IPv4 or 234 * IPv6 address, or {@code false} if not. 235 */ 236 public static boolean isValidNumericIPAddress(@Nullable final String s) 237 { 238 return isValidNumericIPv4Address(s) || 239 isValidNumericIPv6Address(s); 240 } 241 242 243 244 /** 245 * Indicates whether the provided string is a valid IPv4 address. 246 * 247 * @param s The string for which to make the determination. 248 * 249 * @return {@code true} if the provided string represents a valid IPv4 250 * address, or {@code false} if not. 251 */ 252 public static boolean isValidNumericIPv4Address(@Nullable final String s) 253 { 254 if ((s == null) || (s.length() == 0)) 255 { 256 return false; 257 } 258 259 final StringBuilder octetBuffer = new StringBuilder(); 260 261 int numPeriodsFound = 0; 262 for (final char c : s.toCharArray()) 263 { 264 if (c == '.') 265 { 266 if (! isValidNumericIPv4AddressOctet(octetBuffer.toString())) 267 { 268 return false; 269 } 270 271 numPeriodsFound++; 272 octetBuffer.setLength(0); 273 } 274 else 275 { 276 octetBuffer.append(c); 277 } 278 } 279 280 if (numPeriodsFound != 3) 281 { 282 return false; 283 } 284 285 if (! isValidNumericIPv4AddressOctet(octetBuffer.toString())) 286 { 287 return false; 288 } 289 290 return true; 291 } 292 293 294 295 /** 296 * Indicates whether the provided string represents a valid octet within an 297 * IPv4 address. 298 * 299 * @param s The string for which to make the determination. 300 * 301 * @return {@code true} if the provided string represents a valid IPv4 302 * address octet, or {@code false} if not. 303 */ 304 private static boolean isValidNumericIPv4AddressOctet(@NotNull final String s) 305 { 306 if (s.isEmpty()) 307 { 308 // The octet cannot be empty. 309 return false; 310 } 311 312 try 313 { 314 final int octetValue = Integer.parseInt(s); 315 if ((octetValue < 0) || (octetValue > 255)) 316 { 317 // The octet value is out of range. 318 return false; 319 } 320 } 321 catch (final NumberFormatException e) 322 { 323 Debug.debugException(e); 324 325 // The address has a non-numeric octet. 326 return false; 327 } 328 329 return true; 330 } 331 332 333 334 /** 335 * Indicates whether the provided string is a valid IPv6 address. 336 * 337 * @param s The string for which to make the determination. 338 * 339 * @return {@code true} if the provided string represents a valid IPv6 340 * address, or {@code false} if not. 341 */ 342 public static boolean isValidNumericIPv6Address(@Nullable final String s) 343 { 344 if ((s == null) || (s.length() == 0)) 345 { 346 return false; 347 } 348 349 boolean colonFound = false; 350 for (final char c : s.toCharArray()) 351 { 352 if (c == ':') 353 { 354 // This character is allowed in an IPv6 address, and you can't have a 355 // valid IPv6 address without colons. 356 colonFound = true; 357 } 358 else if ((c == '.') || ((c >= '0') && (c <= '9')) || 359 ((c >= 'a') && (c <= 'f')) || ((c >= 'A') && (c <= 'F'))) 360 { 361 // This character is allowed in an IPv6 address. 362 } 363 else 364 { 365 return false; 366 } 367 } 368 369 if (colonFound) 370 { 371 try 372 { 373 LDAPConnectionOptions.DEFAULT_NAME_RESOLVER.getByName(s); 374 return true; 375 } 376 catch (final Exception e) 377 { 378 Debug.debugException(e); 379 } 380 } 381 382 return false; 383 } 384 385 386 387 /** 388 * Retrieves a string representation of this argument value validator. 389 * 390 * @return A string representation of this argument value validator. 391 */ 392 @Override() 393 @NotNull() 394 public String toString() 395 { 396 final StringBuilder buffer = new StringBuilder(); 397 toString(buffer); 398 return buffer.toString(); 399 } 400 401 402 403 /** 404 * Appends a string representation of this argument value validator to the 405 * provided buffer. 406 * 407 * @param buffer The buffer to which the string representation should be 408 * appended. 409 */ 410 public void toString(@NotNull final StringBuilder buffer) 411 { 412 buffer.append("IPAddressArgumentValueValidator(acceptIPv4Addresses="); 413 buffer.append(acceptIPv4Addresses); 414 buffer.append(", acceptIPv6Addresses="); 415 buffer.append(acceptIPv6Addresses); 416 buffer.append(')'); 417 } 418}