001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.validator.routines; 018 019import java.io.Serializable; 020import java.io.UnsupportedEncodingException; 021import java.net.URI; 022import java.net.URISyntaxException; 023import java.net.URLDecoder; 024import java.nio.charset.StandardCharsets; 025import java.util.Collections; 026import java.util.HashSet; 027import java.util.Locale; 028import java.util.Set; 029import java.util.regex.Matcher; 030import java.util.regex.Pattern; 031 032import org.apache.commons.validator.GenericValidator; 033 034/** 035 * <p><strong>URL Validation</strong> routines.</p> 036 * Behavior of validation is modified by passing in options: 037 * <ul> 038 * <li>ALLOW_2_SLASHES - [FALSE] Allows double '/' characters in the path 039 * component.</li> 040 * <li>NO_FRAGMENT- [FALSE] By default fragments are allowed, if this option is 041 * included then fragments are flagged as illegal.</li> 042 * <li>ALLOW_ALL_SCHEMES - [FALSE] By default only http, https, and ftp are 043 * considered valid schemes. Enabling this option will let any scheme pass validation.</li> 044 * </ul> 045 * 046 * <p>Originally based in on php script by Debbie Dyer, validation.php v1.2b, Date: 03/07/02, 047 * https://javascript.internet.com. However, this validation now bears little resemblance 048 * to the php original.</p> 049 * <pre> 050 * Example of usage: 051 * Construct a UrlValidator with valid schemes of "http", and "https". 052 * 053 * String[] schemes = {"http","https"}. 054 * UrlValidator urlValidator = new UrlValidator(schemes); 055 * if (urlValidator.isValid("ftp://foo.bar.com/")) { 056 * System.out.println("URL is valid"); 057 * } else { 058 * System.out.println("URL is invalid"); 059 * } 060 * 061 * prints "URL is invalid" 062 * If instead the default constructor is used. 063 * 064 * UrlValidator urlValidator = new UrlValidator(); 065 * if (urlValidator.isValid("ftp://foo.bar.com/")) { 066 * System.out.println("URL is valid"); 067 * } else { 068 * System.out.println("URL is invalid"); 069 * } 070 * 071 * prints out "URL is valid" 072 * </pre> 073 * 074 * @see 075 * <a href="https://www.ietf.org/rfc/rfc2396.txt"> 076 * Uniform Resource Identifiers (URI): Generic Syntax 077 * </a> 078 * 079 * @since 1.4 080 */ 081public class UrlValidator implements Serializable { 082 083 private static final long serialVersionUID = 7557161713937335013L; 084 085 private static final int MAX_UNSIGNED_16_BIT_INT = 0xFFFF; // port max 086 087 /** 088 * Allows all validly formatted schemes to pass validation instead of 089 * supplying a set of valid schemes. 090 */ 091 public static final long ALLOW_ALL_SCHEMES = 1 << 0; 092 093 /** 094 * Allow two slashes in the path component of the URL. 095 */ 096 public static final long ALLOW_2_SLASHES = 1 << 1; 097 098 /** 099 * Enabling this options disallows any URL fragments. 100 */ 101 public static final long NO_FRAGMENTS = 1 << 2; 102 103 /** 104 * Allow local URLs, such as https://localhost/ or https://machine/ . 105 * This enables a broad-brush check, for complex local machine name 106 * validation requirements you should create your validator with 107 * a {@link RegexValidator} instead ({@link #UrlValidator(RegexValidator, long)}) 108 */ 109 public static final long ALLOW_LOCAL_URLS = 1 << 3; // CHECKSTYLE IGNORE MagicNumber 110 111 /** 112 * Protocol scheme (for example, http, ftp, https). 113 */ 114 private static final String SCHEME_REGEX = "^\\p{Alpha}[\\p{Alnum}\\+\\-\\.]*"; 115 private static final Pattern SCHEME_PATTERN = Pattern.compile(SCHEME_REGEX); 116 117 // Drop numeric, and "+-." for now 118 // TODO does not allow for optional userinfo. 119 // Validation of character set is done by isValidAuthority 120 private static final String AUTHORITY_CHARS_REGEX = "\\p{Alnum}\\-\\."; // allows for IPV4 but not IPV6 121 // Captured inside [ ] in AUTHORITY_REGEX and validated by InetAddressValidator.isValidInet6Address, so the 122 // dot is allowed for IPv4-mapped/embedded forms (for example ::ffff:1.2.3.4 or 2001:db8::1.2.3.4), not just ::FFFF: 123 private static final String IPV6_REGEX = "[0-9a-fA-F:.]+"; // the brackets remove the port-prefix ':' ambiguity 124 125 // userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) 126 // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 127 // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" 128 // We assume that password has the same valid chars as user info 129 private static final String USERINFO_CHARS_REGEX = "[a-zA-Z0-9%-._~!$&'()*+,;=]"; 130 131 // since neither ':' nor '@' are allowed chars, we don't need to use non-greedy matching 132 private static final String USERINFO_FIELD_REGEX = 133 USERINFO_CHARS_REGEX + "+" + // At least one character for the name 134 "(?::" + USERINFO_CHARS_REGEX + "*)?@"; // colon and password may be absent 135 136 // Optional userinfo ("user:pass@") precedes the host so it may come before either host form: a bracketed IPv6 137 // literal (group 1) or a hostname/IPv4 host (group 2). Group 3 is the port, group 4 any trailing remainder. 138 private static final String AUTHORITY_REGEX = 139 "(?:" + USERINFO_FIELD_REGEX + ")?(?:\\[(" + IPV6_REGEX + ")\\]|([" + AUTHORITY_CHARS_REGEX + "]*))(?::(\\d*))?(.*)?"; 140 private static final Pattern AUTHORITY_PATTERN = Pattern.compile(AUTHORITY_REGEX); 141 142 private static final int PARSE_AUTHORITY_IPV6 = 1; 143 144 private static final int PARSE_AUTHORITY_HOST_IP = 2; // excludes userinfo, if present 145 146 private static final int PARSE_AUTHORITY_PORT = 3; // excludes leading colon 147 148 /** 149 * Should always be empty. The code currently allows spaces. 150 */ 151 private static final int PARSE_AUTHORITY_EXTRA = 4; 152 153 private static final String PATH_REGEX = "^(/[-\\w:@&?=+,.!/~*'%$_;\\(\\)]*)?$"; 154 private static final Pattern PATH_PATTERN = Pattern.compile(PATH_REGEX); 155 156 private static final String QUERY_REGEX = "^(\\S*)$"; 157 private static final Pattern QUERY_PATTERN = Pattern.compile(QUERY_REGEX); 158 159 /** 160 * If no schemes are provided, default to this set. 161 */ 162 private static final String[] DEFAULT_SCHEMES = {"http", "https", "ftp"}; // Must be lower-case 163 164 /** 165 * Singleton instance of this class with default schemes and options. 166 */ 167 private static final UrlValidator DEFAULT_URL_VALIDATOR = new UrlValidator(); 168 169 /** 170 * Returns the singleton instance of this class with default schemes and options. 171 * 172 * @return singleton instance with default schemes and options 173 */ 174 public static UrlValidator getInstance() { 175 return DEFAULT_URL_VALIDATOR; 176 } 177 178 /** 179 * Tests whether the given flag is on. If the flag is not a power of 2 180 * (for example, 3) this tests whether the combination of flags is on. 181 * 182 * @param flag Flag value to check. 183 * @param options what to check 184 * @return whether the specified flag value is on. 185 */ 186 private static boolean isOn(final long flag, final long options) { 187 return (options & flag) > 0; 188 } 189 190 /** 191 * Holds the set of current validation options. 192 */ 193 private final long options; 194 195 /** 196 * The set of schemes that are allowed to be in a URL. 197 */ 198 private final Set<String> allowedSchemes; // Must be lower-case 199 200 /** 201 * Regular expressions used to manually validate authorities if IANA 202 * domain name validation isn't desired. 203 */ 204 private final RegexValidator authorityValidator; 205 206 /** 207 * The domain validator. 208 */ 209 private final DomainValidator domainValidator; 210 211 /** 212 * Constructs a new instance with default properties. 213 */ 214 public UrlValidator() { 215 this(null); 216 } 217 218 /** 219 * Constructs a new instance with the given validation options. 220 * 221 * @param options The options should be set using the public constants declared in 222 * this class. To set multiple options you simply add them together. For example, 223 * ALLOW_2_SLASHES + NO_FRAGMENTS enables both of those options. 224 */ 225 public UrlValidator(final long options) { 226 this(null, null, options); 227 } 228 229 /** 230 * Constructs a new instance with the given validation options. 231 * 232 * @param authorityValidator Regular expression validator used to validate the authority part 233 * This allows the user to override the standard set of domains. 234 * @param options Validation options. Set using the public constants of this class. 235 * To set multiple options, simply add them together: 236 * <p>{@code ALLOW_2_SLASHES + NO_FRAGMENTS}</p> 237 * enables both of those options. 238 */ 239 public UrlValidator(final RegexValidator authorityValidator, final long options) { 240 this(null, authorityValidator, options); 241 } 242 243 /** 244 * Behavior of validation is modified by passing in several strings options: 245 * 246 * @param schemes Pass in one or more URL schemes to consider valid, passing in 247 * a null will default to "http,https,ftp" being valid. 248 * If a non-null schemes is specified then all valid schemes must 249 * be specified. Setting the ALLOW_ALL_SCHEMES option will 250 * ignore the contents of schemes. 251 */ 252 public UrlValidator(final String[] schemes) { 253 this(schemes, 0L); 254 } 255 256 /** 257 * Behavior of validation is modified by passing in options: 258 * 259 * @param schemes The set of valid schemes. Ignored if the ALLOW_ALL_SCHEMES option is set. 260 * @param options The options should be set using the public constants declared in 261 * this class. To set multiple options you simply add them together. For example, 262 * ALLOW_2_SLASHES + NO_FRAGMENTS enables both of those options. 263 */ 264 public UrlValidator(final String[] schemes, final long options) { 265 this(schemes, null, options); 266 } 267 268 /** 269 * Customizable constructor. Validation behavior is modified by passing in options. 270 * 271 * @param schemes The set of valid schemes. Ignored if the ALLOW_ALL_SCHEMES option is set. 272 * @param authorityValidator Regular expression validator used to validate the authority part 273 * @param options Validation options. Set using the public constants of this class. 274 * To set multiple options, simply add them together: 275 * <p>{@code ALLOW_2_SLASHES + NO_FRAGMENTS}</p> 276 * enables both of those options. 277 */ 278 public UrlValidator(final String[] schemes, final RegexValidator authorityValidator, final long options) { 279 this(schemes, authorityValidator, options, DomainValidator.getInstance(isOn(ALLOW_LOCAL_URLS, options))); 280 } 281 282 /** 283 * Customizable constructor. Validation behavior is modified by passing in options. 284 * 285 * @param schemes The set of valid schemes. Ignored if the ALLOW_ALL_SCHEMES option is set. 286 * @param authorityValidator Regular expression validator used to validate the authority part 287 * @param options Validation options. Set using the public constants of this class. 288 * To set multiple options, simply add them together: 289 * <p>{@code ALLOW_2_SLASHES + NO_FRAGMENTS}</p> 290 * enables both of those options. 291 * @param domainValidator The DomainValidator to use; must agree with ALLOW_LOCAL_URLS setting 292 * @since 1.7 293 */ 294 public UrlValidator(String[] schemes, final RegexValidator authorityValidator, final long options, final DomainValidator domainValidator) { 295 this.options = options; 296 if (domainValidator == null) { 297 throw new IllegalArgumentException("DomainValidator must not be null"); 298 } 299 if (domainValidator.isAllowLocal() != (options & ALLOW_LOCAL_URLS) > 0) { 300 throw new IllegalArgumentException("DomainValidator disagrees with ALLOW_LOCAL_URLS setting"); 301 } 302 this.domainValidator = domainValidator; 303 304 if (isOn(ALLOW_ALL_SCHEMES)) { 305 allowedSchemes = Collections.emptySet(); 306 } else { 307 if (schemes == null) { 308 schemes = DEFAULT_SCHEMES; 309 } 310 allowedSchemes = new HashSet<>(schemes.length); 311 for (final String scheme : schemes) { 312 allowedSchemes.add(scheme.toLowerCase(Locale.ENGLISH)); 313 } 314 } 315 316 this.authorityValidator = authorityValidator; 317 } 318 319 /** 320 * Returns the number of times the token appears in the target. 321 * 322 * @param token Token value to be counted. 323 * @param target Target value to count tokens in. 324 * @return The number of tokens. 325 */ 326 protected int countToken(final String token, final String target) { 327 int tokenIndex = 0; 328 int count = 0; 329 while (tokenIndex != -1) { 330 tokenIndex = target.indexOf(token, tokenIndex); 331 if (tokenIndex > -1) { 332 tokenIndex++; 333 count++; 334 } 335 } 336 return count; 337 } 338 339 /** 340 * Tests whether the given flag is off. If the flag is not a power of 2 341 * (for example, 3) this tests whether the combination of flags is off. 342 * 343 * @param flag Flag value to check. 344 * @return whether the specified flag value is off. 345 */ 346 private boolean isOff(final long flag) { 347 return (options & flag) == 0; 348 } 349 350 /** 351 * Tests whether the given flag is on. If the flag is not a power of 2 352 * (for example, 3) this tests whether the combination of flags is on. 353 * 354 * @param flag Flag value to check. 355 * @return whether the specified flag value is on. 356 */ 357 private boolean isOn(final long flag) { 358 return (options & flag) > 0; 359 } 360 361 /** 362 * <p>Checks if a field has a valid URL address.</p> 363 * 364 * Note that the method calls #isValidAuthority() 365 * which checks that the domain is valid. 366 * 367 * @param value The value validation is being performed on. A {@code null} 368 * value is considered invalid. 369 * @return true if the URL is valid. 370 */ 371 public boolean isValid(final String value) { 372 if (value == null) { 373 return false; 374 } 375 final URI uri; // ensure value is a valid URI 376 try { 377 uri = new URI(value); 378 } catch (final URISyntaxException e) { 379 return false; 380 } 381 // OK, perform additional validation 382 final String scheme = uri.getScheme(); 383 if (!isValidScheme(scheme)) { 384 return false; 385 } 386 final String authority = uri.getRawAuthority(); 387 if ("file".equals(scheme) && GenericValidator.isBlankOrNull(authority)) { // Special case - file: allows an empty authority 388 return true; // this is a local file - nothing more to do here 389 } 390 // Validate the authority 391 if ("file".equals(scheme) && authority != null && authority.contains(":") || !isValidAuthority(authority)) { 392 return false; 393 } 394 if (!isValidPath(uri.getRawPath()) || !isValidQuery(uri.getRawQuery()) || !isValidFragment(uri.getRawFragment())) { 395 return false; 396 } 397 return true; 398 } 399 400 /** 401 * Returns true if the authority is properly formatted. An authority is the combination 402 * of hostname and port. A {@code null} authority value is considered invalid. 403 * Note: this implementation validates the domain unless a RegexValidator was provided. 404 * If a RegexValidator was supplied, and it matches, then the authority is regarded 405 * as valid with no further checks, otherwise the method checks against the 406 * AUTHORITY_PATTERN and the DomainValidator (ALLOW_LOCAL_URLS) 407 * 408 * @param authority Authority value to validate, allows IDN 409 * @return true if authority (hostname and port) is valid. 410 */ 411 protected boolean isValidAuthority(final String authority) { 412 if (authority == null) { 413 return false; 414 } 415 416 // check manual authority validation if specified 417 if (authorityValidator != null && authorityValidator.isValid(authority)) { 418 return true; 419 } 420 // convert to ASCII if possible 421 final String authorityASCII = DomainValidator.unicodeToASCII(authority); 422 423 final Matcher authorityMatcher = AUTHORITY_PATTERN.matcher(authorityASCII); 424 if (!authorityMatcher.matches()) { 425 return false; 426 } 427 428 // We have to process IPV6 separately because that is parsed in a different group 429 final String ipv6 = authorityMatcher.group(PARSE_AUTHORITY_IPV6); 430 if (ipv6 != null) { 431 final InetAddressValidator inetAddressValidator = InetAddressValidator.getInstance(); 432 if (!inetAddressValidator.isValidInet6Address(ipv6)) { 433 return false; 434 } 435 } else { 436 final String hostLocation = authorityMatcher.group(PARSE_AUTHORITY_HOST_IP); 437 // check if authority is hostname or IP address: 438 // try a hostname first since that's much more likely 439 if (!domainValidator.isValid(hostLocation)) { 440 // try an IPv4 address 441 final InetAddressValidator inetAddressValidator = InetAddressValidator.getInstance(); 442 if (!inetAddressValidator.isValidInet4Address(hostLocation)) { 443 // isn't IPv4, so the URL is invalid 444 return false; 445 } 446 } 447 } 448 449 // the port is captured in the same group regardless of host form, so it must be 450 // range checked for a bracketed IPv6 host too, not just the hostname/IPv4 branch 451 final String port = authorityMatcher.group(PARSE_AUTHORITY_PORT); 452 if (!GenericValidator.isBlankOrNull(port)) { 453 try { 454 final int iPort = Integer.parseInt(port); 455 if (iPort < 0 || iPort > MAX_UNSIGNED_16_BIT_INT) { 456 return false; 457 } 458 } catch (final NumberFormatException nfe) { 459 return false; // this can happen for big numbers 460 } 461 } 462 463 final String extra = authorityMatcher.group(PARSE_AUTHORITY_EXTRA); 464 if (extra != null && !extra.trim().isEmpty()) { 465 return false; 466 } 467 468 return true; 469 } 470 471 /** 472 * Returns true if the given fragment is null or fragments are allowed. 473 * 474 * @param fragment Fragment value to validate. 475 * @return true if fragment is valid. 476 */ 477 protected boolean isValidFragment(final String fragment) { 478 if (fragment == null) { 479 return true; 480 } 481 482 return isOff(NO_FRAGMENTS); 483 } 484 485 /** 486 * Returns true if the path is valid. A {@code null} value is considered invalid. 487 * 488 * @param path Path value to validate. 489 * @return true if path is valid. 490 */ 491 protected boolean isValidPath(final String path) { 492 if (path == null || !PATH_PATTERN.matcher(path).matches()) { 493 return false; 494 } 495 496 try { 497 // This will convert '+' to space, but that is irrelevant for the purpose of validation 498 final String decodedPath = URLDecoder.decode(path, StandardCharsets.UTF_8.name()); 499 // Don't omit host otherwise leading path may be taken as host if it starts with // 500 final URI uri = new URI(null, "localhost", decodedPath, null); 501 final String norm = uri.normalize().getPath(); 502 if (norm.startsWith("/../") // Trying to go via the parent dir 503 || norm.equals("/..")) { // Trying to go to the parent dir 504 return false; 505 } 506 final int slash2Count = countToken("//", decodedPath); 507 if (isOff(ALLOW_2_SLASHES) && slash2Count > 0) { 508 return false; 509 } 510 } catch (final UnsupportedEncodingException | IllegalArgumentException | URISyntaxException e) { 511 return false; 512 } 513 514 return true; 515 } 516 517 /** 518 * Returns true if the query is null, or it's a properly formatted query string. 519 * 520 * @param query Query value to validate. 521 * @return true if query is valid. 522 */ 523 protected boolean isValidQuery(final String query) { 524 if (query == null) { 525 return true; 526 } 527 return QUERY_PATTERN.matcher(query).matches(); 528 } 529 530 /** 531 * Validate scheme. If schemes[] was initialized to a non-null, 532 * then only those schemes are allowed. 533 * Otherwise, the default schemes are "http", "https", "ftp". 534 * Matching is case-blind. 535 * 536 * @param scheme The scheme to validate. A {@code null} value is considered 537 * invalid. 538 * @return true if valid. 539 */ 540 protected boolean isValidScheme(final String scheme) { 541 if (scheme == null || !SCHEME_PATTERN.matcher(scheme).matches() 542 || isOff(ALLOW_ALL_SCHEMES) && !allowedSchemes.contains(scheme.toLowerCase(Locale.ENGLISH))) { 543 return false; 544 } 545 546 return true; 547 } 548 549}