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 * http://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.camel.util; 018 019import java.io.UnsupportedEncodingException; 020import java.net.URI; 021import java.net.URISyntaxException; 022import java.net.URLEncoder; 023import java.util.ArrayList; 024import java.util.Iterator; 025import java.util.LinkedHashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.regex.Pattern; 029 030/** 031 * URI utilities. 032 * 033 * @version 034 */ 035public final class URISupport { 036 037 public static final String RAW_TOKEN_PREFIX = "RAW"; 038 public static final char[] RAW_TOKEN_START = {'(', '{' }; 039 public static final char[] RAW_TOKEN_END = {')', '}' }; 040 041 // Match any key-value pair in the URI query string whose key contains 042 // "passphrase" or "password" or secret key (case-insensitive). 043 // First capture group is the key, second is the value. 044 private static final Pattern SECRETS = Pattern.compile( 045 "([?&][^=]*(?:passphrase|password|secretKey|accessToken|clientSecret|saslJaasConfig)[^=]*)=(RAW[({].*[)}]|[^&]*)", 046 Pattern.CASE_INSENSITIVE); 047 048 // Match the user password in the URI as second capture group 049 // (applies to URI with authority component and userinfo token in the form "user:password"). 050 private static final Pattern USERINFO_PASSWORD = Pattern.compile("(.*://.*?:)(.*)(@)"); 051 052 // Match the user password in the URI path as second capture group 053 // (applies to URI path with authority component and userinfo token in the form "user:password"). 054 private static final Pattern PATH_USERINFO_PASSWORD = Pattern.compile("(.*?:)(.*)(@)"); 055 056 private static final String CHARSET = "UTF-8"; 057 058 private URISupport() { 059 // Helper class 060 } 061 062 /** 063 * Removes detected sensitive information (such as passwords) from the URI and returns the result. 064 * 065 * @param uri The uri to sanitize. 066 * @see #SECRETS and #USERINFO_PASSWORD for the matched pattern 067 * 068 * @return Returns null if the uri is null, otherwise the URI with the passphrase, password or secretKey sanitized. 069 */ 070 public static String sanitizeUri(String uri) { 071 // use xxxxx as replacement as that works well with JMX also 072 String sanitized = uri; 073 if (uri != null) { 074 sanitized = SECRETS.matcher(sanitized).replaceAll("$1=xxxxxx"); 075 sanitized = USERINFO_PASSWORD.matcher(sanitized).replaceFirst("$1xxxxxx$3"); 076 } 077 return sanitized; 078 } 079 080 /** 081 * Removes detected sensitive information (such as passwords) from the 082 * <em>path part</em> of an URI (that is, the part without the query 083 * parameters or component prefix) and returns the result. 084 * 085 * @param path the URI path to sanitize 086 * @return null if the path is null, otherwise the sanitized path 087 */ 088 public static String sanitizePath(String path) { 089 String sanitized = path; 090 if (path != null) { 091 sanitized = PATH_USERINFO_PASSWORD.matcher(sanitized).replaceFirst("$1xxxxxx$3"); 092 } 093 return sanitized; 094 } 095 096 /** 097 * Extracts the scheme specific path from the URI that is used as the remainder option when creating endpoints. 098 * 099 * @param u the URI 100 * @param useRaw whether to force using raw values 101 * @return the remainder path 102 */ 103 public static String extractRemainderPath(URI u, boolean useRaw) { 104 String path = useRaw ? u.getRawSchemeSpecificPart() : u.getSchemeSpecificPart(); 105 106 // lets trim off any query arguments 107 if (path.startsWith("//")) { 108 path = path.substring(2); 109 } 110 int idx = path.indexOf('?'); 111 if (idx > -1) { 112 path = path.substring(0, idx); 113 } 114 115 return path; 116 } 117 118 /** 119 * Parses the query part of the uri (eg the parameters). 120 * <p/> 121 * The URI parameters will by default be URI encoded. However you can define a parameter 122 * values with the syntax: <tt>key=RAW(value)</tt> which tells Camel to not encode the value, 123 * and use the value as is (eg key=value) and the value has <b>not</b> been encoded. 124 * 125 * @param uri the uri 126 * @return the parameters, or an empty map if no parameters (eg never null) 127 * @throws URISyntaxException is thrown if uri has invalid syntax. 128 * @see #RAW_TOKEN_PREFIX 129 * @see #RAW_TOKEN_START 130 * @see #RAW_TOKEN_END 131 */ 132 public static Map<String, Object> parseQuery(String uri) throws URISyntaxException { 133 return parseQuery(uri, false); 134 } 135 136 /** 137 * Parses the query part of the uri (eg the parameters). 138 * <p/> 139 * The URI parameters will by default be URI encoded. However you can define a parameter 140 * values with the syntax: <tt>key=RAW(value)</tt> which tells Camel to not encode the value, 141 * and use the value as is (eg key=value) and the value has <b>not</b> been encoded. 142 * 143 * @param uri the uri 144 * @param useRaw whether to force using raw values 145 * @return the parameters, or an empty map if no parameters (eg never null) 146 * @throws URISyntaxException is thrown if uri has invalid syntax. 147 * @see #RAW_TOKEN_PREFIX 148 * @see #RAW_TOKEN_START 149 * @see #RAW_TOKEN_END 150 */ 151 public static Map<String, Object> parseQuery(String uri, boolean useRaw) throws URISyntaxException { 152 return parseQuery(uri, useRaw, false); 153 } 154 155 /** 156 * Parses the query part of the uri (eg the parameters). 157 * <p/> 158 * The URI parameters will by default be URI encoded. However you can define a parameter 159 * values with the syntax: <tt>key=RAW(value)</tt> which tells Camel to not encode the value, 160 * and use the value as is (eg key=value) and the value has <b>not</b> been encoded. 161 * 162 * @param uri the uri 163 * @param useRaw whether to force using raw values 164 * @param lenient whether to parse lenient and ignore trailing & markers which has no key or value which can happen when using HTTP components 165 * @return the parameters, or an empty map if no parameters (eg never null) 166 * @throws URISyntaxException is thrown if uri has invalid syntax. 167 * @see #RAW_TOKEN_PREFIX 168 * @see #RAW_TOKEN_START 169 * @see #RAW_TOKEN_END 170 */ 171 public static Map<String, Object> parseQuery(String uri, boolean useRaw, boolean lenient) throws URISyntaxException { 172 if (uri == null || ObjectHelper.isEmpty(uri)) { 173 // return an empty map 174 return new LinkedHashMap<>(0); 175 } 176 177 // must check for trailing & as the uri.split("&") will ignore those 178 if (!lenient && uri.endsWith("&")) { 179 throw new URISyntaxException(uri, "Invalid uri syntax: Trailing & marker found. " 180 + "Check the uri and remove the trailing & marker."); 181 } 182 183 URIScanner scanner = new URIScanner(CHARSET); 184 return scanner.parseQuery(uri, useRaw); 185 } 186 187 /** 188 * Scans RAW tokens in the string and returns the list of pair indexes which tell where 189 * a RAW token starts and ends in the string. 190 * <p/> 191 * This is a companion method with {@link #isRaw(int, List)} and the returned value is 192 * supposed to be used as the parameter of that method. 193 * 194 * @param str the string to scan RAW tokens 195 * @return the list of pair indexes which represent the start and end positions of a RAW token 196 * @see #isRaw(int, List) 197 * @see #RAW_TOKEN_PREFIX 198 * @see #RAW_TOKEN_START 199 * @see #RAW_TOKEN_END 200 */ 201 public static List<Pair<Integer>> scanRaw(String str) { 202 return URIScanner.scanRaw(str); 203 } 204 205 /** 206 * Tests if the index is within any pair of the start and end indexes which represent 207 * the start and end positions of a RAW token. 208 * <p/> 209 * This is a companion method with {@link #scanRaw(String)} and is supposed to consume 210 * the returned value of that method as the second parameter <tt>pairs</tt>. 211 * 212 * @param index the index to be tested 213 * @param pairs the list of pair indexes which represent the start and end positions of a RAW token 214 * @return <tt>true</tt> if the index is within any pair of the indexes, <tt>false</tt> otherwise 215 * @see #scanRaw(String) 216 * @see #RAW_TOKEN_PREFIX 217 * @see #RAW_TOKEN_START 218 * @see #RAW_TOKEN_END 219 */ 220 public static boolean isRaw(int index, List<Pair<Integer>> pairs) { 221 return URIScanner.isRaw(index, pairs); 222 } 223 224 /** 225 * Parses the query parameters of the uri (eg the query part). 226 * 227 * @param uri the uri 228 * @return the parameters, or an empty map if no parameters (eg never null) 229 * @throws URISyntaxException is thrown if uri has invalid syntax. 230 */ 231 public static Map<String, Object> parseParameters(URI uri) throws URISyntaxException { 232 String query = uri.getQuery(); 233 if (query == null) { 234 String schemeSpecificPart = uri.getSchemeSpecificPart(); 235 int idx = schemeSpecificPart.indexOf('?'); 236 if (idx < 0) { 237 // return an empty map 238 return new LinkedHashMap<>(0); 239 } else { 240 query = schemeSpecificPart.substring(idx + 1); 241 } 242 } else { 243 query = stripPrefix(query, "?"); 244 } 245 return parseQuery(query); 246 } 247 248 /** 249 * Traverses the given parameters, and resolve any parameter values which uses the RAW token 250 * syntax: <tt>key=RAW(value)</tt>. This method will then remove the RAW tokens, and replace 251 * the content of the value, with just the value. 252 * 253 * @param parameters the uri parameters 254 * @see #parseQuery(String) 255 * @see #RAW_TOKEN_PREFIX 256 * @see #RAW_TOKEN_START 257 * @see #RAW_TOKEN_END 258 */ 259 @SuppressWarnings("unchecked") 260 public static void resolveRawParameterValues(Map<String, Object> parameters) { 261 for (Map.Entry<String, Object> entry : parameters.entrySet()) { 262 if (entry.getValue() == null) { 263 continue; 264 } 265 // if the value is a list then we need to iterate 266 Object value = entry.getValue(); 267 if (value instanceof List) { 268 List list = (List) value; 269 for (int i = 0; i < list.size(); i++) { 270 Object obj = list.get(i); 271 if (obj == null) { 272 continue; 273 } 274 String str = obj.toString(); 275 final int index = i; 276 URIScanner.resolveRaw(str, (s, raw) -> { 277 // update the string in the list 278 list.set(index, raw); 279 }); 280 } 281 } else { 282 String str = entry.getValue().toString(); 283 URIScanner.resolveRaw(str, (s, raw) -> entry.setValue(raw)); 284 } 285 } 286 } 287 288 /** 289 * Creates a URI with the given query 290 * 291 * @param uri the uri 292 * @param query the query to append to the uri 293 * @return uri with the query appended 294 * @throws URISyntaxException is thrown if uri has invalid syntax. 295 */ 296 public static URI createURIWithQuery(URI uri, String query) throws URISyntaxException { 297 ObjectHelper.notNull(uri, "uri"); 298 299 // assemble string as new uri and replace parameters with the query instead 300 String s = uri.toString(); 301 String before = StringHelper.before(s, "?"); 302 if (before == null) { 303 before = StringHelper.before(s, "#"); 304 } 305 if (before != null) { 306 s = before; 307 } 308 if (query != null) { 309 s = s + "?" + query; 310 } 311 if ((!s.contains("#")) && (uri.getFragment() != null)) { 312 s = s + "#" + uri.getFragment(); 313 } 314 315 return new URI(s); 316 } 317 318 /** 319 * Strips the prefix from the value. 320 * <p/> 321 * Returns the value as-is if not starting with the prefix. 322 * 323 * @param value the value 324 * @param prefix the prefix to remove from value 325 * @return the value without the prefix 326 */ 327 public static String stripPrefix(String value, String prefix) { 328 if (value == null || prefix == null) { 329 return value; 330 } 331 332 if (value.startsWith(prefix)) { 333 return value.substring(prefix.length()); 334 } 335 336 return value; 337 } 338 339 /** 340 * Strips the suffix from the value. 341 * <p/> 342 * Returns the value as-is if not ending with the prefix. 343 * 344 * @param value the value 345 * @param suffix the suffix to remove from value 346 * @return the value without the suffix 347 */ 348 public static String stripSuffix(final String value, final String suffix) { 349 if (value == null || suffix == null) { 350 return value; 351 } 352 353 if (value.endsWith(suffix)) { 354 return value.substring(0, value.length() - suffix.length()); 355 } 356 357 return value; 358 } 359 360 /** 361 * Assembles a query from the given map. 362 * 363 * @param options the map with the options (eg key/value pairs) 364 * @return a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there is no options. 365 * @throws URISyntaxException is thrown if uri has invalid syntax. 366 */ 367 @SuppressWarnings("unchecked") 368 public static String createQueryString(Map<String, Object> options) throws URISyntaxException { 369 try { 370 if (options.size() > 0) { 371 StringBuilder rc = new StringBuilder(); 372 boolean first = true; 373 for (Object o : options.keySet()) { 374 if (first) { 375 first = false; 376 } else { 377 rc.append("&"); 378 } 379 380 String key = (String) o; 381 Object value = options.get(key); 382 383 // the value may be a list since the same key has multiple values 384 if (value instanceof List) { 385 List<String> list = (List<String>) value; 386 for (Iterator<String> it = list.iterator(); it.hasNext();) { 387 String s = it.next(); 388 appendQueryStringParameter(key, s, rc); 389 // append & separator if there is more in the list to append 390 if (it.hasNext()) { 391 rc.append("&"); 392 } 393 } 394 } else { 395 // use the value as a String 396 String s = value != null ? value.toString() : null; 397 appendQueryStringParameter(key, s, rc); 398 } 399 } 400 return rc.toString(); 401 } else { 402 return ""; 403 } 404 } catch (UnsupportedEncodingException e) { 405 URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding"); 406 se.initCause(e); 407 throw se; 408 } 409 } 410 411 private static void appendQueryStringParameter(String key, String value, StringBuilder rc) throws UnsupportedEncodingException { 412 rc.append(URLEncoder.encode(key, CHARSET)); 413 if (value == null) { 414 return; 415 } 416 // only append if value is not null 417 rc.append("="); 418 boolean isRaw = URIScanner.resolveRaw(value, (str, raw) -> { 419 // do not encode RAW parameters unless it has % 420 // need to replace % with %25 to avoid losing "%" when decoding 421 String s = StringHelper.replaceAll(str, "%", "%25"); 422 rc.append(s); 423 }); 424 if (!isRaw) { 425 rc.append(URLEncoder.encode(value, CHARSET)); 426 } 427 } 428 429 /** 430 * Creates a URI from the original URI and the remaining parameters 431 * <p/> 432 * Used by various Camel components 433 */ 434 public static URI createRemainingURI(URI originalURI, Map<String, Object> params) throws URISyntaxException { 435 String s = createQueryString(params); 436 if (s.length() == 0) { 437 s = null; 438 } 439 return createURIWithQuery(originalURI, s); 440 } 441 442 /** 443 * Appends the given parameters to the given URI. 444 * <p/> 445 * It keeps the original parameters and if a new parameter is already defined in 446 * {@code originalURI}, it will be replaced by its value in {@code newParameters}. 447 * 448 * @param originalURI the original URI 449 * @param newParameters the parameters to add 450 * @return the URI with all the parameters 451 * @throws URISyntaxException is thrown if the uri syntax is invalid 452 * @throws UnsupportedEncodingException is thrown if encoding error 453 */ 454 public static String appendParametersToURI(String originalURI, Map<String, Object> newParameters) throws URISyntaxException, UnsupportedEncodingException { 455 URI uri = new URI(normalizeUri(originalURI)); 456 Map<String, Object> parameters = parseParameters(uri); 457 parameters.putAll(newParameters); 458 return createRemainingURI(uri, parameters).toString(); 459 } 460 461 /** 462 * Normalizes the uri by reordering the parameters so they are sorted and thus 463 * we can use the uris for endpoint matching. 464 * <p/> 465 * The URI parameters will by default be URI encoded. However you can define a parameter 466 * values with the syntax: <tt>key=RAW(value)</tt> which tells Camel to not encode the value, 467 * and use the value as is (eg key=value) and the value has <b>not</b> been encoded. 468 * 469 * @param uri the uri 470 * @return the normalized uri 471 * @throws URISyntaxException in thrown if the uri syntax is invalid 472 * @throws UnsupportedEncodingException is thrown if encoding error 473 * @see #RAW_TOKEN_PREFIX 474 * @see #RAW_TOKEN_START 475 * @see #RAW_TOKEN_END 476 */ 477 public static String normalizeUri(String uri) throws URISyntaxException, UnsupportedEncodingException { 478 479 URI u = new URI(UnsafeUriCharactersEncoder.encode(uri, true)); 480 String path = u.getSchemeSpecificPart(); 481 String scheme = u.getScheme(); 482 483 // not possible to normalize 484 if (scheme == null || path == null) { 485 return uri; 486 } 487 488 // lets trim off any query arguments 489 if (path.startsWith("//")) { 490 path = path.substring(2); 491 } 492 int idx = path.indexOf('?'); 493 // when the path has ? 494 if (idx != -1) { 495 path = path.substring(0, idx); 496 } 497 498 if (u.getScheme().startsWith("http")) { 499 path = UnsafeUriCharactersEncoder.encodeHttpURI(path); 500 } else { 501 path = UnsafeUriCharactersEncoder.encode(path); 502 } 503 504 // okay if we have user info in the path and they use @ in username or password, 505 // then we need to encode them (but leave the last @ sign before the hostname) 506 // this is needed as Camel end users may not encode their user info properly, but expect 507 // this to work out of the box with Camel, and hence we need to fix it for them 508 String userInfoPath = path; 509 if (userInfoPath.contains("/")) { 510 userInfoPath = userInfoPath.substring(0, userInfoPath.indexOf("/")); 511 } 512 if (StringHelper.countChar(userInfoPath, '@') > 1) { 513 int max = userInfoPath.lastIndexOf('@'); 514 String before = userInfoPath.substring(0, max); 515 // after must be from original path 516 String after = path.substring(max); 517 518 // replace the @ with %40 519 before = StringHelper.replaceAll(before, "@", "%40"); 520 path = before + after; 521 } 522 523 // in case there are parameters we should reorder them 524 Map<String, Object> parameters = URISupport.parseParameters(u); 525 if (parameters.isEmpty()) { 526 // no parameters then just return 527 return buildUri(scheme, path, null); 528 } else { 529 // reorder parameters a..z 530 List<String> keys = new ArrayList<>(parameters.keySet()); 531 keys.sort(null); 532 533 Map<String, Object> sorted = new LinkedHashMap<>(parameters.size()); 534 for (String key : keys) { 535 sorted.put(key, parameters.get(key)); 536 } 537 538 // build uri object with sorted parameters 539 String query = URISupport.createQueryString(sorted); 540 return buildUri(scheme, path, query); 541 } 542 } 543 544 private static String buildUri(String scheme, String path, String query) { 545 // must include :// to do a correct URI all components can work with 546 return scheme + "://" + path + (query != null ? "?" + query : ""); 547 } 548 549 public static Map<String, Object> extractProperties(Map<String, Object> properties, String optionPrefix) { 550 Map<String, Object> rc = new LinkedHashMap<>(properties.size()); 551 552 for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) { 553 Map.Entry<String, Object> entry = it.next(); 554 String name = entry.getKey(); 555 if (name.startsWith(optionPrefix)) { 556 Object value = properties.get(name); 557 name = name.substring(optionPrefix.length()); 558 rc.put(name, value); 559 it.remove(); 560 } 561 } 562 563 return rc; 564 } 565 566 public static String pathAndQueryOf(final URI uri) { 567 final String path = uri.getPath(); 568 569 String pathAndQuery = path; 570 if (ObjectHelper.isEmpty(path)) { 571 pathAndQuery = "/"; 572 } 573 574 final String query = uri.getQuery(); 575 if (ObjectHelper.isNotEmpty(query)) { 576 pathAndQuery += "?" + query; 577 } 578 579 return pathAndQuery; 580 } 581 582 public static String joinPaths(final String... paths) { 583 if (paths == null || paths.length == 0) { 584 return ""; 585 } 586 587 final StringBuilder joined = new StringBuilder(); 588 589 boolean addedLast = false; 590 for (int i = paths.length - 1; i >= 0; i--) { 591 String path = paths[i]; 592 if (ObjectHelper.isNotEmpty(path)) { 593 if (addedLast) { 594 path = stripSuffix(path, "/"); 595 } 596 597 addedLast = true; 598 599 if (path.charAt(0) == '/') { 600 joined.insert(0, path); 601 } else { 602 if (i > 0) { 603 joined.insert(0, '/').insert(1, path); 604 } else { 605 joined.insert(0, path); 606 } 607 } 608 } 609 } 610 611 return joined.toString(); 612 } 613}