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