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 // 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)[^=]*)=([^&]*)", 043 Pattern.CASE_INSENSITIVE); 044 045 // Match the user password in the URI as second capture group 046 // (applies to URI with authority component and userinfo token in the form "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 form "user:password"). 051 private static final Pattern PATH_USERINFO_PASSWORD = Pattern.compile("(.*:)(.*)(@)"); 052 053 private static final String CHARSET = "UTF-8"; 054 055 private URISupport() { 056 // Helper class 057 } 058 059 /** 060 * Removes detected sensitive information (such as passwords) from the URI and returns the result. 061 * @param uri The uri to sanitize. 062 * @see #SECRETS for the matched pattern 063 * 064 * @return Returns null if the uri is null, otherwise the URI with the passphrase, password or secretKey sanitized. 065 */ 066 public static String sanitizeUri(String uri) { 067 String sanitized = uri; 068 if (uri != null) { 069 sanitized = SECRETS.matcher(sanitized).replaceAll("$1=******"); 070 sanitized = USERINFO_PASSWORD.matcher(sanitized).replaceFirst("$1******$3"); 071 } 072 return sanitized; 073 } 074 075 /** 076 * Removes detected sensitive information (such as passwords) from the 077 * <em>path part</em> of an URI (that is, the part without the query 078 * parameters or component prefix) and returns the result. 079 * 080 * @param path the URI path to sanitize 081 * @return null if the path is null, otherwise the sanitized path 082 */ 083 public static String sanitizePath(String path) { 084 String sanitized = path; 085 if (path != null) { 086 sanitized = PATH_USERINFO_PASSWORD.matcher(sanitized).replaceFirst("$1******$3"); 087 } 088 return sanitized; 089 } 090 091 /** 092 * Parses the query part of the uri (eg the parameters). 093 * 094 * @param uri the uri 095 * @return the parameters, or an empty map if no parameters (eg never null) 096 * @throws URISyntaxException is thrown if uri has invalid syntax. 097 */ 098 @SuppressWarnings("unchecked") 099 public static Map<String, Object> parseQuery(String uri) throws URISyntaxException { 100 // must check for trailing & as the uri.split("&") will ignore those 101 if (uri != null && uri.endsWith("&")) { 102 throw new URISyntaxException(uri, "Invalid uri syntax: Trailing & marker found. " 103 + "Check the uri and remove the trailing & marker."); 104 } 105 106 if (ObjectHelper.isEmpty(uri)) { 107 // return an empty map 108 return new LinkedHashMap<String, Object>(0); 109 } 110 111 try { 112 // use a linked map so the parameters is in the same order 113 Map<String, Object> rc = new LinkedHashMap<String, Object>(); 114 if (uri != null) { 115 String[] parameters = uri.split("&"); 116 for (String parameter : parameters) { 117 int p = parameter.indexOf("="); 118 if (p >= 0) { 119 // The replaceAll is an ugly workaround for CAMEL-4954, awaiting a cleaner fix once CAMEL-4425 120 // is fully resolved in all components 121 String name = URLDecoder.decode(parameter.substring(0, p), CHARSET); 122 String value = URLDecoder.decode(parameter.substring(p + 1).replaceAll("%", "%25"), CHARSET); 123 124 // does the key already exist? 125 if (rc.containsKey(name)) { 126 // yes it does, so make sure we can support multiple values, but using a list 127 // to hold the multiple values 128 Object existing = rc.get(name); 129 List<String> list; 130 if (existing instanceof List) { 131 list = CastUtils.cast((List<?>) existing); 132 } else { 133 // create a new list to hold the multiple values 134 list = new ArrayList<String>(); 135 String s = existing != null ? existing.toString() : null; 136 if (s != null) { 137 list.add(s); 138 } 139 } 140 list.add(value); 141 rc.put(name, list); 142 } else { 143 rc.put(name, value); 144 } 145 } else { 146 rc.put(parameter, null); 147 } 148 } 149 } 150 return rc; 151 } catch (UnsupportedEncodingException e) { 152 URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding"); 153 se.initCause(e); 154 throw se; 155 } 156 } 157 158 /** 159 * Parses the query parameters of the uri (eg the query part). 160 * 161 * @param uri the uri 162 * @return the parameters, or an empty map if no parameters (eg never null) 163 * @throws URISyntaxException is thrown if uri has invalid syntax. 164 */ 165 public static Map<String, Object> parseParameters(URI uri) throws URISyntaxException { 166 String query = uri.getQuery(); 167 if (query == null) { 168 String schemeSpecificPart = uri.getSchemeSpecificPart(); 169 int idx = schemeSpecificPart.indexOf('?'); 170 if (idx < 0) { 171 // return an empty map 172 return new LinkedHashMap<String, Object>(0); 173 } else { 174 query = schemeSpecificPart.substring(idx + 1); 175 } 176 } else { 177 query = stripPrefix(query, "?"); 178 } 179 return parseQuery(query); 180 } 181 182 /** 183 * Creates a URI with the given query 184 * 185 * @param uri the uri 186 * @param query the query to append to the uri 187 * @return uri with the query appended 188 * @throws URISyntaxException is thrown if uri has invalid syntax. 189 */ 190 public static URI createURIWithQuery(URI uri, String query) throws URISyntaxException { 191 ObjectHelper.notNull(uri, "uri"); 192 193 // assemble string as new uri and replace parameters with the query instead 194 String s = uri.toString(); 195 String before = ObjectHelper.before(s, "?"); 196 if (before != null) { 197 s = before; 198 } 199 if (query != null) { 200 s = s + "?" + query; 201 } 202 if ((!s.contains("#")) && (uri.getFragment() != null)) { 203 s = s + "#" + uri.getFragment(); 204 } 205 206 return new URI(s); 207 } 208 209 /** 210 * Strips the prefix from the value. 211 * <p/> 212 * Returns the value as-is if not starting with the prefix. 213 * 214 * @param value the value 215 * @param prefix the prefix to remove from value 216 * @return the value without the prefix 217 */ 218 public static String stripPrefix(String value, String prefix) { 219 if (value.startsWith(prefix)) { 220 return value.substring(prefix.length()); 221 } 222 return value; 223 } 224 225 /** 226 * Assembles a query from the given map. 227 * 228 * @param options the map with the options (eg key/value pairs) 229 * @return a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there is no options. 230 * @throws URISyntaxException is thrown if uri has invalid syntax. 231 */ 232 @SuppressWarnings("unchecked") 233 public static String createQueryString(Map<String, Object> options) throws URISyntaxException { 234 try { 235 if (options.size() > 0) { 236 StringBuilder rc = new StringBuilder(); 237 boolean first = true; 238 for (Object o : options.keySet()) { 239 if (first) { 240 first = false; 241 } else { 242 rc.append("&"); 243 } 244 245 String key = (String) o; 246 Object value = options.get(key); 247 248 // the value may be a list since the same key has multiple values 249 if (value instanceof List) { 250 List<String> list = (List<String>) value; 251 for (Iterator<String> it = list.iterator(); it.hasNext();) { 252 String s = it.next(); 253 appendQueryStringParameter(key, s, rc); 254 // append & separator if there is more in the list to append 255 if (it.hasNext()) { 256 rc.append("&"); 257 } 258 } 259 } else { 260 // use the value as a String 261 String s = value != null ? value.toString() : null; 262 appendQueryStringParameter(key, s, rc); 263 } 264 } 265 return rc.toString(); 266 } else { 267 return ""; 268 } 269 } catch (UnsupportedEncodingException e) { 270 URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding"); 271 se.initCause(e); 272 throw se; 273 } 274 } 275 276 private static void appendQueryStringParameter(String key, String value, StringBuilder rc) throws UnsupportedEncodingException { 277 rc.append(URLEncoder.encode(key, CHARSET)); 278 // only append if value is not null 279 if (value != null) { 280 rc.append("="); 281 rc.append(URLEncoder.encode(value, CHARSET)); 282 } 283 } 284 285 /** 286 * Creates a URI from the original URI and the remaining parameters 287 * <p/> 288 * Used by various Camel components 289 */ 290 public static URI createRemainingURI(URI originalURI, Map<String, Object> params) throws URISyntaxException { 291 String s = createQueryString(params); 292 if (s.length() == 0) { 293 s = null; 294 } 295 return createURIWithQuery(originalURI, s); 296 } 297 298 /** 299 * Normalizes the uri by reordering the parameters so they are sorted and thus 300 * we can use the uris for endpoint matching. 301 * 302 * @param uri the uri 303 * @return the normalized uri 304 * @throws URISyntaxException in thrown if the uri syntax is invalid 305 * @throws UnsupportedEncodingException is thrown if encoding error 306 */ 307 public static String normalizeUri(String uri) throws URISyntaxException, UnsupportedEncodingException { 308 309 URI u = new URI(UnsafeUriCharactersEncoder.encode(uri)); 310 String path = u.getSchemeSpecificPart(); 311 String scheme = u.getScheme(); 312 313 // not possible to normalize 314 if (scheme == null || path == null) { 315 return uri; 316 } 317 318 // lets trim off any query arguments 319 if (path.startsWith("//")) { 320 path = path.substring(2); 321 } 322 int idx = path.indexOf('?'); 323 // when the path has ? 324 if (idx != -1) { 325 path = path.substring(0, idx); 326 } 327 328 path = UnsafeUriCharactersEncoder.encode(path); 329 330 // in case there are parameters we should reorder them 331 Map<String, Object> parameters = URISupport.parseParameters(u); 332 if (parameters.isEmpty()) { 333 // no parameters then just return 334 return buildUri(scheme, path, null); 335 } else { 336 // reorder parameters a..z 337 List<String> keys = new ArrayList<String>(parameters.keySet()); 338 Collections.sort(keys); 339 340 Map<String, Object> sorted = new LinkedHashMap<String, Object>(parameters.size()); 341 for (String key : keys) { 342 sorted.put(key, parameters.get(key)); 343 } 344 345 // build uri object with sorted parameters 346 String query = URISupport.createQueryString(sorted); 347 return buildUri(scheme, path, query); 348 } 349 } 350 351 private static String buildUri(String scheme, String path, String query) { 352 // must include :// to do a correct URI all components can work with 353 return scheme + "://" + path + (query != null ? "?" + query : ""); 354 } 355 }