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