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}