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 public static Map<String, Object> parseQuery(String uri) throws URISyntaxException {
092 // must check for trailing & as the uri.split("&") will ignore those
093 if (uri != null && uri.endsWith("&")) {
094 throw new URISyntaxException(uri, "Invalid uri syntax: Trailing & marker found. "
095 + "Check the uri and remove the trailing & marker.");
096 }
097
098 try {
099 // use a linked map so the parameters is in the same order
100 Map<String, Object> rc = new LinkedHashMap<String, Object>();
101 if (uri != null) {
102 String[] parameters = uri.split("&");
103 for (String parameter : parameters) {
104 int p = parameter.indexOf("=");
105 if (p >= 0) {
106 // The replaceAll is an ugly workaround for CAMEL-4954, awaiting a cleaner fix once CAMEL-4425
107 // is fully resolved in all components
108 String name = URLDecoder.decode(parameter.substring(0, p), CHARSET);
109 String value = URLDecoder.decode(parameter.substring(p + 1).replaceAll("%", "%25"), CHARSET);
110
111 // does the key already exist?
112 if (rc.containsKey(name)) {
113 // yes it does, so make sure we can support multiple values, but using a list
114 // to hold the multiple values
115 Object existing = rc.get(name);
116 List<String> list;
117 if (existing instanceof List) {
118 list = CastUtils.cast((List<?>) existing);
119 } else {
120 // create a new list to hold the multiple values
121 list = new ArrayList<String>();
122 String s = existing != null ? existing.toString() : null;
123 if (s != null) {
124 list.add(s);
125 }
126 }
127 list.add(value);
128 rc.put(name, list);
129 } else {
130 rc.put(name, value);
131 }
132 } else {
133 rc.put(parameter, null);
134 }
135 }
136 }
137 return rc;
138 } catch (UnsupportedEncodingException e) {
139 URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding");
140 se.initCause(e);
141 throw se;
142 }
143 }
144
145 public static Map<String, Object> parseParameters(URI uri) throws URISyntaxException {
146 String query = uri.getQuery();
147 if (query == null) {
148 String schemeSpecificPart = uri.getSchemeSpecificPart();
149 int idx = schemeSpecificPart.indexOf('?');
150 if (idx < 0) {
151 // return an empty map
152 return new LinkedHashMap<String, Object>(0);
153 } else {
154 query = schemeSpecificPart.substring(idx + 1);
155 }
156 } else {
157 query = stripPrefix(query, "?");
158 }
159 return parseQuery(query);
160 }
161
162 /**
163 * Creates a URI with the given query
164 */
165 public static URI createURIWithQuery(URI uri, String query) throws URISyntaxException {
166 ObjectHelper.notNull(uri, "uri");
167
168 // assemble string as new uri and replace parameters with the query instead
169 String s = uri.toString();
170 String before = ObjectHelper.before(s, "?");
171 if (before != null) {
172 s = before;
173 }
174 if (query != null) {
175 s = s + "?" + query;
176 }
177 if ((!s.contains("#")) && (uri.getFragment() != null)) {
178 s = s + "#" + uri.getFragment();
179 }
180
181 return new URI(s);
182 }
183
184 public static String stripPrefix(String value, String prefix) {
185 if (value.startsWith(prefix)) {
186 return value.substring(prefix.length());
187 }
188 return value;
189 }
190
191 @SuppressWarnings("unchecked")
192 public static String createQueryString(Map<String, Object> options) throws URISyntaxException {
193 try {
194 if (options.size() > 0) {
195 StringBuilder rc = new StringBuilder();
196 boolean first = true;
197 for (Object o : options.keySet()) {
198 if (first) {
199 first = false;
200 } else {
201 rc.append("&");
202 }
203
204 String key = (String) o;
205 Object value = options.get(key);
206
207 // the value may be a list since the same key has multiple values
208 if (value instanceof List) {
209 List<String> list = (List<String>) value;
210 for (Iterator<String> it = list.iterator(); it.hasNext();) {
211 String s = it.next();
212 appendQueryStringParameter(key, s, rc);
213 // append & separator if there is more in the list to append
214 if (it.hasNext()) {
215 rc.append("&");
216 }
217 }
218 } else {
219 // use the value as a String
220 String s = value != null ? value.toString() : null;
221 appendQueryStringParameter(key, s, rc);
222 }
223 }
224 return rc.toString();
225 } else {
226 return "";
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 appendQueryStringParameter(String key, String value, StringBuilder rc) throws UnsupportedEncodingException {
236 rc.append(URLEncoder.encode(key, CHARSET));
237 // only append if value is not null
238 if (value != null) {
239 rc.append("=");
240 rc.append(URLEncoder.encode(value, CHARSET));
241 }
242 }
243
244
245 /**
246 * Creates a URI from the original URI and the remaining parameters
247 * <p/>
248 * Used by various Camel components
249 */
250 public static URI createRemainingURI(URI originalURI, Map<String, Object> params) throws URISyntaxException {
251 String s = createQueryString(params);
252 if (s.length() == 0) {
253 s = null;
254 }
255 return createURIWithQuery(originalURI, s);
256 }
257
258 /**
259 * Normalizes the uri by reordering the parameters so they are sorted and thus
260 * we can use the uris for endpoint matching.
261 *
262 * @param uri the uri
263 * @return the normalized uri
264 * @throws URISyntaxException in thrown if the uri syntax is invalid
265 * @throws UnsupportedEncodingException
266 */
267 public static String normalizeUri(String uri) throws URISyntaxException, UnsupportedEncodingException {
268
269 URI u = new URI(UnsafeUriCharactersEncoder.encode(uri));
270 String path = u.getSchemeSpecificPart();
271 String scheme = u.getScheme();
272
273 // not possible to normalize
274 if (scheme == null || path == null) {
275 return uri;
276 }
277
278 // lets trim off any query arguments
279 if (path.startsWith("//")) {
280 path = path.substring(2);
281 }
282 int idx = path.indexOf('?');
283 // when the path has ?
284 if (idx != -1) {
285 path = path.substring(0, idx);
286 }
287
288 path = UnsafeUriCharactersEncoder.encode(path);
289
290 // in case there are parameters we should reorder them
291 Map<String, Object> parameters = URISupport.parseParameters(u);
292 if (parameters.isEmpty()) {
293 // no parameters then just return
294 return buildUri(scheme, path, null);
295 } else {
296 // reorder parameters a..z
297 List<String> keys = new ArrayList<String>(parameters.keySet());
298 Collections.sort(keys);
299
300 Map<String, Object> sorted = new LinkedHashMap<String, Object>(parameters.size());
301 for (String key : keys) {
302 sorted.put(key, parameters.get(key));
303 }
304
305 // build uri object with sorted parameters
306 String query = URISupport.createQueryString(sorted);
307 return buildUri(scheme, path, query);
308 }
309 }
310
311 private static String buildUri(String scheme, String path, String query) {
312 // must include :// to do a correct URI all components can work with
313 return scheme + "://" + path + (query != null ? "?" + query : "");
314 }
315 }