001/*
002 * Copyright 2016-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2016-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) 2016-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.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    else if (valueString.indexOf('.') >= 0)
178    {
179      for (final char c : valueString.toCharArray())
180      {
181        if ((c == '.') || ((c >= '0') && (c <= '9')))
182        {
183          // This character is allowed in an IPv4 address.
184        }
185        else
186        {
187          throw new ArgumentException(ERR_IP_VALIDATOR_ILLEGAL_IPV4_CHAR.get(
188               valueString, argument.getIdentifierString(), c));
189        }
190      }
191    }
192    else
193    {
194      throw new ArgumentException(ERR_IP_VALIDATOR_MALFORMED.get(valueString,
195           argument.getIdentifierString()));
196    }
197
198
199    // If we've gotten here, then we know that the value string contains only
200    // characters that are allowed in IP address literal.  Let
201    // InetAddress.getByName do the heavy lifting for the rest of the
202    // validation.
203    try
204    {
205      LDAPConnectionOptions.DEFAULT_NAME_RESOLVER.getByName(valueString);
206    }
207    catch (final Exception e)
208    {
209      Debug.debugException(e);
210      throw new ArgumentException(
211           ERR_IP_VALIDATOR_MALFORMED.get(valueString,
212                argument.getIdentifierString()),
213           e);
214    }
215
216
217    if (isIPv6)
218    {
219      if (! acceptIPv6Addresses)
220      {
221        throw new ArgumentException(ERR_IP_VALIDATOR_IPV6_NOT_ACCEPTED.get(
222             valueString, argument.getIdentifierString()));
223      }
224    }
225    else if (! acceptIPv4Addresses)
226    {
227      throw new ArgumentException(ERR_IP_VALIDATOR_IPV4_NOT_ACCEPTED.get(
228           valueString, argument.getIdentifierString()));
229    }
230  }
231
232
233
234  /**
235   * Indicates whether the provided string represents a valid IPv4 or IPv6
236   * address.
237   *
238   * @param  s  The string for which to make the determination.
239   *
240   * @return  {@code true} if the provided string represents a valid IPv4 or
241   *          IPv6 address, or {@code false} if not.
242   */
243  public static boolean isValidNumericIPAddress(@Nullable final String s)
244  {
245    return isValidNumericIPv4Address(s) ||
246         isValidNumericIPv6Address(s);
247  }
248
249
250
251  /**
252   * Indicates whether the provided string is a valid IPv4 address.
253   *
254   * @param  s  The string for which to make the determination.
255   *
256   * @return  {@code true} if the provided string represents a valid IPv4
257   *          address, or {@code false} if not.
258   */
259  public static boolean isValidNumericIPv4Address(@Nullable final String s)
260  {
261    if ((s == null) || (s.length() == 0))
262    {
263      return false;
264    }
265
266    for (final char c : s.toCharArray())
267    {
268      if ((c == '.') || ((c >= '0') && (c <= '9')))
269      {
270        // This character is allowed in an IPv4 address.
271      }
272      else
273      {
274        return false;
275      }
276    }
277
278    try
279    {
280      LDAPConnectionOptions.DEFAULT_NAME_RESOLVER.getByName(s);
281      return true;
282    }
283    catch (final Exception e)
284    {
285      Debug.debugException(e);
286      return false;
287    }
288  }
289
290
291
292  /**
293   * Indicates whether the provided string is a valid IPv6 address.
294   *
295   * @param  s  The string for which to make the determination.
296   *
297   * @return  {@code true} if the provided string represents a valid IPv6
298   *          address, or {@code false} if not.
299   */
300  public static boolean isValidNumericIPv6Address(@Nullable final String s)
301  {
302    if ((s == null) || (s.length() == 0))
303    {
304      return false;
305    }
306
307    boolean colonFound = false;
308    for (final char c : s.toCharArray())
309    {
310      if (c == ':')
311      {
312        // This character is allowed in an IPv6 address, and you can't have a
313        // valid IPv6 address without colons.
314        colonFound = true;
315      }
316      else if ((c == '.') || ((c >= '0') && (c <= '9')) ||
317           ((c >= 'a') && (c <= 'f')) || ((c >= 'A') && (c <= 'F')))
318      {
319        // This character is allowed in an IPv6 address.
320      }
321      else
322      {
323        return false;
324      }
325    }
326
327    if (colonFound)
328    {
329      try
330      {
331        LDAPConnectionOptions.DEFAULT_NAME_RESOLVER.getByName(s);
332        return true;
333      }
334      catch (final Exception e)
335      {
336        Debug.debugException(e);
337      }
338    }
339
340    return false;
341  }
342
343
344
345  /**
346   * Retrieves a string representation of this argument value validator.
347   *
348   * @return  A string representation of this argument value validator.
349   */
350  @Override()
351  @NotNull()
352  public String toString()
353  {
354    final StringBuilder buffer = new StringBuilder();
355    toString(buffer);
356    return buffer.toString();
357  }
358
359
360
361  /**
362   * Appends a string representation of this argument value validator to the
363   * provided buffer.
364   *
365   * @param  buffer  The buffer to which the string representation should be
366   *                 appended.
367   */
368  public void toString(@NotNull  final StringBuilder buffer)
369  {
370    buffer.append("IPAddressArgumentValueValidator(acceptIPv4Addresses=");
371    buffer.append(acceptIPv4Addresses);
372    buffer.append(", acceptIPv6Addresses=");
373    buffer.append(acceptIPv6Addresses);
374    buffer.append(')');
375  }
376}