001/* 002 * Copyright 2020-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2020-2024 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) 2020-2024 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.ldap.sdk.NameResolver; 044import com.unboundid.util.Debug; 045import com.unboundid.util.NotMutable; 046import com.unboundid.util.NotNull; 047import com.unboundid.util.Nullable; 048import com.unboundid.util.ThreadSafety; 049import com.unboundid.util.ThreadSafetyLevel; 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 DNS host names. As per 058 * <A HREF="https://www.ietf.org/rfc/rfc952.txt">RFC 952</A> and 059 * <A HREF="https://www.ietf.org/rfc/rfc1123.txt">RFC 1123</A>, valid DNS host 060 * names must satisfy the following constraints: 061 * <UL> 062 * <LI>Host names are split into one or more components, which are separated 063 * by periods.</LI> 064 * <LI>Each component may contain only ASCII letters, digits, and hyphens. 065 * While host names may contain non-ASCII characters in some contexts, 066 * they are not valid in all contexts, and host names with non-ASCII 067 * characters should be represented in an ASCII-only encoding called 068 * punycode (as described in 069 * <A HREF="https://www.ietf.org/rfc/rfc3492.txt">RFC 3492</A>). This 070 * implementation expects any hostnames with non-ASCII characters to use 071 * the punycode representation, but it does not currently attempt to 072 * validate the punycode representation.</LI> 073 * <LI>Components must not start with a hyphen.</LI> 074 * <LI>Each component of a hostname must be between 1 and 63 characters.</LI> 075 * <LI>The entire hostname (including the periods between components) must 076 * not exceed 255 characters.</LI> 077 * <LI>Host names must not contain consecutive periods, as that would 078 * indicate an empty internal component.</LI> 079 * <LI>Host names must not start with a period, as that would indicate an 080 * empty initial component.</LI> 081 * <LI>Host names may end with a period as a way of explicitly indicating that 082 * it is fully qualified. This is primarily used for host names that 083 * only contain a single component (for example, "localhost."), but it is 084 * allowed for any fully qualified host name.</LI> 085 * <LI>This implementation may optionally require fully qualified host 086 * names.</LI> 087 * <LI>This implementation may optionally reject host names that cannot be 088 * resolved to IP addresses.</LI> 089 * <LI>This implementation may optionally reject values that are numeric IP 090 * addresses rather than host names.</LI> 091 * </UL> 092 */ 093@NotMutable() 094@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 095public final class DNSHostNameArgumentValueValidator 096 extends ArgumentValueValidator 097 implements Serializable 098{ 099 /** 100 * The serial version UID for this serializable class. 101 */ 102 private static final long serialVersionUID = 1525611526290885612L; 103 104 105 106 // Indicates whether to allow IP addresses in addition to DNS host names. 107 private final boolean allowIPAddresses; 108 109 // Indicates whether to allow unqualified names. 110 private final boolean allowUnqualifiedNames; 111 112 // Indicates whether to allow unresolvable names. 113 private final boolean allowUnresolvableNames; 114 115 // The name resolver that will be used to attempt to resolve host names to IP 116 // addresses. 117 @NotNull private final NameResolver nameResolver; 118 119 120 121 /** 122 * Creates a new DNS host name argument value validator with the default 123 * settings. It will allow IP addresses in addition to host names, it will 124 * allow unqualified names, and it will allow unresolvable names. 125 */ 126 public DNSHostNameArgumentValueValidator() 127 { 128 this(true, true, true, null); 129 } 130 131 132 133 /** 134 * Creates a new DNS host name argument value validator with the provided 135 * settings. 136 * 137 * @param allowIPAddresses Indicates whether this validator will allow 138 * values that represent numeric IP addresses 139 * rather than DNS host names. If this is 140 * {@code true}, then valid IP addresses will 141 * be accepted as well as valid DNS host 142 * names. If this is {@code false}, then only 143 * valid DNS host names will be accepted. 144 * @param allowUnqualifiedNames Indicates whether this validator will allow 145 * values that represent unqualified host 146 * names. If this is {@code true}, then 147 * unqualified names will be accepted as long 148 * as they are otherwise acceptable. If this 149 * is {@code false}, then only fully qualified 150 * host names will be accepted. 151 * @param allowUnresolvableNames Indicates whether this validator will allow 152 * host name values that do not resolve to 153 * IP addresses. If this is {@code true}, 154 * then this validator will not attempt to 155 * resolve host names. If this is 156 * {@code false}, then this validator will 157 * reject any host name that cannot be 158 * resolved to an IP address. 159 * @param nameResolver The name resolver that will be used when 160 * attempting to resolve host names to IP 161 * addresses. If this is {@code null}, then 162 * the LDAP SDK's default name resolver will 163 * be used. 164 */ 165 public DNSHostNameArgumentValueValidator( 166 final boolean allowIPAddresses, 167 final boolean allowUnqualifiedNames, 168 final boolean allowUnresolvableNames, 169 @Nullable final NameResolver nameResolver) 170 { 171 this.allowIPAddresses = allowIPAddresses; 172 this.allowUnqualifiedNames = allowUnqualifiedNames; 173 this.allowUnresolvableNames = allowUnresolvableNames; 174 175 if (nameResolver == null) 176 { 177 this.nameResolver = LDAPConnectionOptions.DEFAULT_NAME_RESOLVER; 178 } 179 else 180 { 181 this.nameResolver = nameResolver; 182 } 183 } 184 185 186 187 /** 188 * Indicates whether this validator will allow values that represent valid 189 * numeric IP addresses rather than DNS host names. 190 * 191 * @return {@code true} if this validator will accept values that represent 192 * either valid numeric IP addresses or numeric DNS host names, or 193 * {@code false} if it will reject values that represent numeric 194 * IP addresses. 195 */ 196 public boolean allowIPAddresses() 197 { 198 return allowIPAddresses; 199 } 200 201 202 203 /** 204 * Indicates whether this validator will allow unqualified DNS host names 205 * (that is, host names that do not include a domain component). 206 * 207 * @return {@code true} if this validator will allow both unqualified and 208 * fully qualified host names, or {@code false} if it will only 209 * accept fully qualified host names. 210 */ 211 public boolean allowUnqualifiedNames() 212 { 213 return allowUnqualifiedNames; 214 } 215 216 217 218 /** 219 * Indicates whether this validator will allow DNS host names that cannot be 220 * resolved to IP addresses. 221 * 222 * @return {@code true} if this validator will only validate the syntax for 223 * DNS host names and will not make any attempt to resolve them to 224 * IP addresses, or {@code false} if it will attempt to resolve host 225 * names to IP addresses and will reject any names that cannot be 226 * resolved. 227 */ 228 public boolean allowUnresolvableNames() 229 { 230 return allowUnresolvableNames; 231 } 232 233 234 235 /** 236 * Retrieves the name resolver that will be used when attempting to resolve 237 * host names to IP addresses. 238 * 239 * @return The name resolver that will be used when attempting to resolve 240 * host names to IP addresses. 241 */ 242 @NotNull() 243 public NameResolver getNameResolver() 244 { 245 return nameResolver; 246 } 247 248 249 250 /** 251 * {@inheritDoc} 252 */ 253 @Override() 254 public void validateArgumentValue(@NotNull final Argument argument, 255 @NotNull final String valueString) 256 throws ArgumentException 257 { 258 try 259 { 260 validateDNSHostName(valueString, allowIPAddresses, allowUnqualifiedNames, 261 allowUnresolvableNames, nameResolver); 262 } 263 catch (final ArgumentException e) 264 { 265 Debug.debugException(e); 266 throw new ArgumentException( 267 ERR_DNS_NAME_VALIDATOR_INVALID_ARG_VALUE.get( 268 String.valueOf(valueString), argument.getIdentifierString(), 269 e.getMessage()), 270 e); 271 } 272 } 273 274 275 276 /** 277 * Ensures that the provided name represents a valid DNS host name using the 278 * default settings. IP addresses, unqualified names, and unresolvable names 279 * will all be allowed as long as the provided name is otherwise syntactically 280 * valid. 281 * 282 * @param name The name to validate as a DNS host name. It must not be 283 * {@code null} or empty. 284 * 285 * @throws ArgumentException If the provided name is not considered valid. 286 */ 287 public static void validateDNSHostName(@NotNull final String name) 288 throws ArgumentException 289 { 290 validateDNSHostName(name, true, true, true, null); 291 } 292 293 294 295 /** 296 * Ensures that the provided name represents a valid DNS host name using the 297 * provided settings. 298 * 299 * @param name The name to validate as a DNS host name. 300 * @param allowIPAddresses Indicates whether this validator will allow 301 * values that represent numeric IP addresses 302 * rather than DNS host names. If this is 303 * {@code true}, then valid IP addresses will 304 * be accepted as well as valid DNS host 305 * names. If this is {@code false}, then only 306 * valid DNS host names will be accepted. 307 * @param allowUnqualifiedNames Indicates whether this validator will allow 308 * values that represent unqualified host 309 * names. If this is {@code true}, then 310 * unqualified names will be accepted as long 311 * as they are otherwise acceptable. If this 312 * is {@code false}, then only fully qualified 313 * host names will be accepted. 314 * @param allowUnresolvableNames Indicates whether this validator will allow 315 * host name values that do not resolve to 316 * IP addresses. If this is {@code true}, 317 * then this validator will not attempt to 318 * resolve host names. If this is 319 * {@code false}, then this validator will 320 * reject any host name that cannot be 321 * resolved to an IP address. 322 * @param nameResolver The name resolver that will be used when 323 * attempting to resolve host names to IP 324 * addresses. If this is {@code null}, then 325 * the LDAP SDK's default name resolver will 326 * be used. 327 * 328 * @throws ArgumentException If the provided name is not considered valid. 329 */ 330 public static void validateDNSHostName( 331 @Nullable final String name, 332 final boolean allowIPAddresses, 333 final boolean allowUnqualifiedNames, 334 final boolean allowUnresolvableNames, 335 @Nullable final NameResolver nameResolver) 336 throws ArgumentException 337 { 338 // Make sure that the provided name is not null or empty. 339 if ((name == null) || name.isEmpty()) 340 { 341 throw new ArgumentException(ERR_DNS_NAME_VALIDATOR_NULL_OR_EMPTY.get()); 342 } 343 344 345 // Make sure that the provided name does not contain consecutive periods. 346 if (name.contains("..")) 347 { 348 throw new ArgumentException( 349 ERR_DNS_NAME_VALIDATOR_CONSECUTIVE_PERIODS.get()); 350 } 351 352 353 // See if the provided name represents an IP address. If so, then see if 354 // that's acceptable. 355 if (IPAddressArgumentValueValidator.isValidNumericIPAddress(name)) 356 { 357 if (allowIPAddresses) 358 { 359 // If an IP address was provided and allowed, then we don't require any 360 // more validation. 361 return; 362 } 363 else 364 { 365 throw new ArgumentException(ERR_DNS_NAME_VALIDATOR_IP_ADDRESS.get()); 366 } 367 } 368 369 370 // Make sure that the host name looks like it's syntactically valid. 371 validateDNSHostNameSyntax(name); 372 373 374 // If we should require fully qualified names, then make sure that the 375 // original name contains at least one period. 376 if ((! allowUnqualifiedNames) && (name.indexOf('.') < 0)) 377 { 378 throw new ArgumentException(ERR_DNS_NAME_VALIDATOR_NOT_QUALIFIED.get()); 379 } 380 381 382 // If we should attempt to resolve the address, then do so now. 383 if (! allowUnresolvableNames) 384 { 385 try 386 { 387 final NameResolver resolver; 388 if (nameResolver == null) 389 { 390 resolver = LDAPConnectionOptions.DEFAULT_NAME_RESOLVER; 391 } 392 else 393 { 394 resolver = nameResolver; 395 } 396 397 resolver.getByName(name); 398 } 399 catch (final Exception e) 400 { 401 Debug.debugException(e); 402 throw new ArgumentException(ERR_DNS_NAME_VALIDATOR_NOT_RESOLVABLE.get(), 403 e); 404 } 405 } 406 } 407 408 409 410 /** 411 * Validates the provided name to ensure that it conforms to the expected 412 * syntax. 413 * 414 * @param name The name to validate. 415 * 416 * @throws ArgumentException If the provided name is not considered valid. 417 */ 418 private static void validateDNSHostNameSyntax(@NotNull final String name) 419 throws ArgumentException 420 { 421 // If the name ends with a trailing period, then strip it off and used the 422 // stripped host name for the rest of the validation. Note that 423 // technically, a string containing just a period is a valid fully qualified 424 // host name that represents the root label, so if we end up with an empty 425 // string after removing a trailing period, then just return without doing 426 // any more validation. 427 final String nameWithoutTrailingPeriod; 428 if (name.endsWith(".")) 429 { 430 nameWithoutTrailingPeriod = name.substring(0, (name.length() - 1)); 431 if (nameWithoutTrailingPeriod.isEmpty()) 432 { 433 return; 434 } 435 } 436 else 437 { 438 nameWithoutTrailingPeriod = name; 439 } 440 441 442 // Make sure that the provided name is not more than 255 characters long. 443 if (nameWithoutTrailingPeriod.length() > 255) 444 { 445 throw new ArgumentException( 446 ERR_DNS_NAME_VALIDATOR_NAME_TOO_LONG.get( 447 nameWithoutTrailingPeriod.length())); 448 } 449 450 451 // Make sure that the provided name does not start with a period. 452 if (nameWithoutTrailingPeriod.startsWith(".")) 453 { 454 throw new ArgumentException( 455 ERR_DNS_NAME_VALIDATOR_STARTS_WITH_PERIOD.get()); 456 } 457 458 459 // Iterate through and validate each of the components. 460 int startPos = 0; 461 int periodPos = nameWithoutTrailingPeriod.indexOf('.'); 462 while (periodPos > 0) 463 { 464 final String component = 465 nameWithoutTrailingPeriod.substring(startPos, periodPos); 466 validateDNSHostNameComponentSyntax(component); 467 468 startPos = periodPos+1; 469 periodPos = nameWithoutTrailingPeriod.indexOf('.', startPos); 470 } 471 472 final String lastComponent = nameWithoutTrailingPeriod.substring(startPos); 473 validateDNSHostNameComponentSyntax(lastComponent); 474 } 475 476 477 478 /** 479 * Validates the provided name component to ensure that it conforms to the 480 * expected syntax. 481 * 482 * @param component The name component to validate. 483 * 484 * @throws ArgumentException If the provided name is not considered valid. 485 */ 486 private static void validateDNSHostNameComponentSyntax( 487 @NotNull final String component) 488 throws ArgumentException 489 { 490 if (component.length() > 63) 491 { 492 throw new ArgumentException( 493 ERR_DNS_NAME_VALIDATOR_COMPONENT_TOO_LONG.get( 494 component, component.length())); 495 } 496 497 if (component.charAt(0) == '-') 498 { 499 throw new ArgumentException( 500 ERR_DNS_NAME_VALIDATOR_COMPONENT_STARTS_WITH_HYPHEN.get( 501 component)); 502 } 503 504 for (int i=0; i < component.length(); i++) 505 { 506 final char c = component.charAt(i); 507 if (! isLetterDigitOrDash(c)) 508 { 509 if (c <= 127) 510 { 511 throw new ArgumentException( 512 ERR_DNS_NAME_VALIDATOR_COMPONENT_ILLEGAL_ASCII_CHARACTER. get( 513 component, (i+1))); 514 } 515 else 516 { 517 throw new ArgumentException( 518 ERR_DNS_NAME_VALIDATOR_COMPONENT_NON_ASCII_CHARACTER.get( 519 component, (i+1))); 520 } 521 } 522 } 523 } 524 525 526 527 /** 528 * Indicates whether the provided character is an ASCII letter, digit, or 529 * dash. 530 * 531 * @param c The character for which to make the determination. 532 * 533 * @return {@code true} if the provided character is an ASCII letter, digit, 534 * or dash, or {@code false} if not. 535 */ 536 private static boolean isLetterDigitOrDash(final char c) 537 { 538 if ((c >= 'a') && (c <= 'z')) 539 { 540 return true; 541 } 542 543 if ((c >= 'A') && (c <= 'Z')) 544 { 545 return true; 546 } 547 548 if ((c >= '0') && (c <= '9')) 549 { 550 return true; 551 } 552 553 return (c == '-'); 554 } 555 556 557 558 /** 559 * Retrieves a string representation of this argument value validator. 560 * 561 * @return A string representation of this argument value validator. 562 */ 563 @Override() 564 @NotNull() 565 public String toString() 566 { 567 final StringBuilder buffer = new StringBuilder(); 568 toString(buffer); 569 return buffer.toString(); 570 } 571 572 573 574 /** 575 * Appends a string representation of this argument value validator to the 576 * provided buffer. 577 * 578 * @param buffer The buffer to which the string representation should be 579 * appended. 580 */ 581 public void toString(@NotNull final StringBuilder buffer) 582 { 583 buffer.append("DNSHostNameArgumentValueValidator(allowIPAddresses="); 584 buffer.append(allowIPAddresses); 585 buffer.append(", allowUnqualifiedNames="); 586 buffer.append(allowUnqualifiedNames); 587 buffer.append(", allowUnresolvableNames="); 588 buffer.append(allowUnresolvableNames); 589 buffer.append(')'); 590 } 591}