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}