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