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.wicket.protocol.http.request;
018:
019: import java.io.UnsupportedEncodingException;
020: import java.net.URLDecoder;
021: import java.net.URLEncoder;
022: import java.util.Locale;
023: import java.util.Map;
024: import java.util.regex.Matcher;
025: import java.util.regex.Pattern;
026:
027: import org.apache.wicket.Application;
028: import org.apache.wicket.IRequestTarget;
029: import org.apache.wicket.Request;
030: import org.apache.wicket.RequestCycle;
031: import org.apache.wicket.WicketRuntimeException;
032: import org.apache.wicket.protocol.http.RequestUtils;
033: import org.apache.wicket.request.IRequestCodingStrategy;
034: import org.apache.wicket.request.RequestParameters;
035: import org.apache.wicket.request.target.coding.IRequestTargetUrlCodingStrategy;
036: import org.apache.wicket.util.crypt.ICrypt;
037: import org.apache.wicket.util.string.AppendingStringBuffer;
038: import org.apache.wicket.util.string.Strings;
039: import org.apache.wicket.util.value.ValueMap;
040: import org.slf4j.Logger;
041: import org.slf4j.LoggerFactory;
042:
043: /**
044: * This is a request coding strategy which encrypts the URL and hence makes it
045: * impossible for users to guess what is in the url and rebuild it manually. It
046: * uses the CryptFactory registered with the application to encode and decode
047: * the URL. Hence, the coding algorithm must be a two-way one (reversable).
048: * Because the algrithm is reversible, URLs which were bookmarkable before will
049: * remain bookmarkable.
050: * <p>
051: * To register the request coding strategy to need to do the following:
052: *
053: * <pre>
054: * protected IRequestCycleProcessor newRequestCycleProcessor()
055: * {
056: * return new WebRequestCycleProcessor()
057: * {
058: * protected IRequestCodingStrategy newRequestCodingStrategy()
059: * {
060: * return new CryptedUrlWebRequestCodingStrategy(new WebRequestCodingStrategy());
061: * }
062: * };
063: * }
064: * </pre>
065: *
066: * <b>Note:</b> When trying to hack urls in the browser an exception might be
067: * caught while decoding the URL. By default, for safety reasons a very simple
068: * WicketRuntimeException is thrown. The original stack trace is only logged.
069: *
070: * @author Juergen Donnerstag
071: */
072: public class CryptedUrlWebRequestCodingStrategy implements
073: IRequestCodingStrategy {
074: /** log. */
075: private static final Logger log = LoggerFactory
076: .getLogger(CryptedUrlWebRequestCodingStrategy.class);
077:
078: /** The default request coding strategy most of the methods are delegated to */
079: private final IRequestCodingStrategy defaultStrategy;
080:
081: /**
082: * Construct.
083: *
084: * @param defaultStrategy
085: * The default strategy most requests are forwarded to
086: */
087: public CryptedUrlWebRequestCodingStrategy(
088: final IRequestCodingStrategy defaultStrategy) {
089: this .defaultStrategy = defaultStrategy;
090: }
091:
092: /**
093: * Decode the querystring of the URL
094: *
095: * @see org.apache.wicket.request.IRequestCodingStrategy#decode(org.apache.wicket.Request)
096: */
097: public RequestParameters decode(final Request request) {
098: String url = request.decodeURL(request.getURL());
099: String decodedQueryParams = decodeURL(url);
100: if (decodedQueryParams != null) {
101: // The difficulty now is that this.defaultStrategy.decode(request)
102: // doesn't know the just decoded url which is why must create
103: // a fake Request for.
104: Request fakeRequest = new DecodedUrlRequest(request, url,
105: decodedQueryParams);
106: return this .defaultStrategy.decode(fakeRequest);
107: }
108:
109: return this .defaultStrategy.decode(request);
110: }
111:
112: /**
113: * Encode the querystring of the URL
114: *
115: * @see org.apache.wicket.request.IRequestCodingStrategy#encode(org.apache.wicket.RequestCycle,
116: * org.apache.wicket.IRequestTarget)
117: */
118: public CharSequence encode(final RequestCycle requestCycle,
119: final IRequestTarget requestTarget) {
120: CharSequence url = this .defaultStrategy.encode(requestCycle,
121: requestTarget);
122: url = encodeURL(url);
123: return url;
124: }
125:
126: /**
127: * @see wicket.request.IRequestTargetMounter#mount(
128: * wicket.request.target.coding.IRequestTargetUrlCodingStrategy)
129: */
130: public void mount(IRequestTargetUrlCodingStrategy urlCodingStrategy) {
131: this .defaultStrategy.mount(urlCodingStrategy);
132: }
133:
134: /**
135: * @see org.apache.wicket.request.IRequestTargetMounter#unmount(java.lang.String)
136: */
137: public void unmount(String path) {
138: this .defaultStrategy.unmount(path);
139: }
140:
141: /**
142: * @see org.apache.wicket.request.IRequestTargetMounter#urlCodingStrategyForPath(java.lang.String)
143: */
144: public IRequestTargetUrlCodingStrategy urlCodingStrategyForPath(
145: String path) {
146: return this .defaultStrategy.urlCodingStrategyForPath(path);
147: }
148:
149: /**
150: * @see org.apache.wicket.request.IRequestTargetMounter#pathForTarget(org.apache.wicket.IRequestTarget)
151: */
152: public CharSequence pathForTarget(IRequestTarget requestTarget) {
153: return this .defaultStrategy.pathForTarget(requestTarget);
154: }
155:
156: /**
157: * @see org.apache.wicket.request.IRequestTargetMounter#targetForRequest(org.apache.wicket.request.RequestParameters)
158: */
159: public IRequestTarget targetForRequest(
160: RequestParameters requestParameters) {
161: return this .defaultStrategy.targetForRequest(requestParameters);
162: }
163:
164: /**
165: * Returns the given url encoded.
166: *
167: * @param url
168: * The URL to encode
169: * @return The encoded url
170: */
171: protected CharSequence encodeURL(final CharSequence url) {
172: // Get the crypt implementation from the application
173: ICrypt urlCrypt = Application.get().getSecuritySettings()
174: .getCryptFactory().newCrypt();
175: if (urlCrypt != null) {
176: // The url must have a query string, otherwise keep the url
177: // unchanged
178: final int pos = url.toString().indexOf('?');
179: if (pos > -1) {
180: // The url's path
181: CharSequence urlPrefix = url.subSequence(0, pos);
182:
183: // Extract the querystring
184: String queryString = url.subSequence(pos + 1,
185: url.length()).toString();
186:
187: // if the querystring starts with a parameter like
188: // "x=", than don't change the querystring as it
189: // has been encoded already
190: if (!queryString.startsWith("x=")) {
191: // The length of the encrypted string depends on the
192: // length of the original querystring. Let's try to
193: // make the querystring shorter first without loosing
194: // information.
195: queryString = shortenUrl(queryString).toString();
196:
197: // encrypt the query string
198: String encryptedQueryString = urlCrypt
199: .encryptUrlSafe(queryString);
200:
201: try {
202: encryptedQueryString = URLEncoder.encode(
203: encryptedQueryString, Application.get()
204: .getRequestCycleSettings()
205: .getResponseRequestEncoding());
206: } catch (UnsupportedEncodingException ex) {
207: throw new WicketRuntimeException(ex);
208: }
209:
210: // build the new complete url
211: return new AppendingStringBuffer(urlPrefix).append(
212: "?x=").append(encryptedQueryString);
213: }
214: }
215: }
216:
217: // we didn't change anything
218: return url;
219: }
220:
221: /**
222: * Decode the "x" parameter of the querystring
223: *
224: * @param url
225: * The encoded URL
226: * @return The decoded 'x' parameter of the querystring
227: */
228: protected String decodeURL(final String url) {
229: int startIndex = url.indexOf("?x=");
230: if (startIndex != -1) {
231: try {
232: startIndex = startIndex + 3;
233: final int endIndex = url.indexOf("&", startIndex);
234: String secureParam;
235: if (endIndex == -1) {
236: secureParam = url.substring(startIndex);
237: } else {
238: secureParam = url.substring(startIndex, endIndex);
239: }
240:
241: secureParam = URLDecoder.decode(secureParam,
242: Application.get().getRequestCycleSettings()
243: .getResponseRequestEncoding());
244:
245: // Get the crypt implementation from the application
246: final ICrypt urlCrypt = Application.get()
247: .getSecuritySettings().getCryptFactory()
248: .newCrypt();
249:
250: // Decrypt the query string
251: String queryString = urlCrypt
252: .decryptUrlSafe(secureParam);
253:
254: // The querystring might have been shortened (length reduced).
255: // In that case, lengthen the query string again.
256: queryString = rebuildUrl(queryString);
257: return queryString;
258: } catch (Exception ex) {
259: return onError(ex);
260: }
261: }
262: return null;
263: }
264:
265: /**
266: * @param ex
267: *
268: * @return decoded URL
269: */
270: protected String onError(final Exception ex) {
271: log.error("Invalid URL", ex);
272:
273: throw new HackAttackException("Invalid URL");
274: }
275:
276: /**
277: * Try to shorten the querystring without loosing information. Note:
278: * WebRequestWithCryptedUrl must implement exactly the opposite logic.
279: *
280: * @param queryString
281: * The original query string
282: * @return The shortened querystring
283: */
284: protected CharSequence shortenUrl(CharSequence queryString) {
285: queryString = Strings.replaceAll(queryString,
286: WebRequestCodingStrategy.BEHAVIOR_ID_PARAMETER_NAME
287: + "=", "1-");
288: queryString = Strings.replaceAll(queryString,
289: WebRequestCodingStrategy.INTERFACE_PARAMETER_NAME
290: + "=IRedirectListener", "2-");
291: queryString = Strings.replaceAll(queryString,
292: WebRequestCodingStrategy.INTERFACE_PARAMETER_NAME
293: + "=IFormSubmitListener", "3-");
294: queryString = Strings.replaceAll(queryString,
295: WebRequestCodingStrategy.INTERFACE_PARAMETER_NAME
296: + "=IOnChangeListener", "4-");
297: queryString = Strings.replaceAll(queryString,
298: WebRequestCodingStrategy.INTERFACE_PARAMETER_NAME
299: + "=ILinkListener", "5-");
300: queryString = Strings
301: .replaceAll(
302: queryString,
303: WebRequestCodingStrategy.INTERFACE_PARAMETER_NAME
304: + "=", "6-");
305: queryString = Strings
306: .replaceAll(
307: queryString,
308: WebRequestCodingStrategy.BOOKMARKABLE_PAGE_PARAMETER_NAME
309: + "=", "7-");
310:
311: // For debugging only: determine possibilities to further shorten
312: // the query string
313: if (log.isDebugEnabled()) {
314: // Every word with at least 3 letters
315: Pattern words = Pattern.compile("\\w\\w\\w+");
316: Matcher matcher = words.matcher(queryString);
317: while (matcher.find()) {
318: CharSequence word = queryString.subSequence(matcher
319: .start(), matcher.end());
320: log.debug("URL pattern NOT shortened: '" + word
321: + "' - '" + queryString + "'");
322: }
323: }
324:
325: return queryString;
326: }
327:
328: /**
329: * In case the query string has been shortened prior to encryption, than
330: * rebuild (lengthen) the query string now. Note: This implementation must
331: * exactly match the reverse one implemented in WebResponseWithCryptedUrl.
332: *
333: * @param queryString
334: * The URL's query string
335: * @return The lengthened query string
336: */
337: protected String rebuildUrl(CharSequence queryString) {
338: queryString = Strings.replaceAll(queryString, "1-",
339: WebRequestCodingStrategy.BEHAVIOR_ID_PARAMETER_NAME
340: + "=");
341: queryString = Strings.replaceAll(queryString, "2-",
342: WebRequestCodingStrategy.INTERFACE_PARAMETER_NAME
343: + "=IRedirectListener");
344: queryString = Strings.replaceAll(queryString, "3-",
345: WebRequestCodingStrategy.INTERFACE_PARAMETER_NAME
346: + "=IFormSubmitListener");
347: queryString = Strings.replaceAll(queryString, "4-",
348: WebRequestCodingStrategy.INTERFACE_PARAMETER_NAME
349: + "=IOnChangeListener");
350: queryString = Strings.replaceAll(queryString, "5-",
351: WebRequestCodingStrategy.INTERFACE_PARAMETER_NAME
352: + "=ILinkListener");
353: queryString = Strings
354: .replaceAll(
355: queryString,
356: "6-",
357: WebRequestCodingStrategy.INTERFACE_PARAMETER_NAME
358: + "=");
359: queryString = Strings
360: .replaceAll(
361: queryString,
362: "7-",
363: WebRequestCodingStrategy.BOOKMARKABLE_PAGE_PARAMETER_NAME
364: + "=");
365:
366: return queryString.toString();
367: }
368:
369: /**
370: * IRequestCodingStrategy.decode(Request) requires a Request parameter and
371: * not a URL. Hence, based on the original URL and the decoded 'x' parameter
372: * a new Request object must be created to serve the default coding strategy
373: * as input for analyzing the URL.
374: */
375: private static class DecodedUrlRequest extends Request {
376: /** The original request */
377: private final Request request;
378:
379: /** The new URL with the 'x' param decoded */
380: private final String url;
381:
382: /**
383: * The new parameter map with the 'x' param removed and the 'new' one
384: * included
385: */
386: private final Map parameterMap;
387:
388: /**
389: * Construct.
390: *
391: * @param request
392: * @param url
393: * @param encodedParamReplacement
394: */
395: public DecodedUrlRequest(final Request request,
396: final String url, final String encodedParamReplacement) {
397: this .request = request;
398:
399: // Create a copy of the original parameter map
400: this .parameterMap = this .request.getParameterMap();
401:
402: // Remove the 'x' parameter which contains ALL the encoded params
403: this .parameterMap.remove("x");
404: String decodedParamReplacement = encodedParamReplacement;
405: try {
406: decodedParamReplacement = URLDecoder.decode(
407: encodedParamReplacement, Application.get()
408: .getRequestCycleSettings()
409: .getResponseRequestEncoding());
410: } catch (UnsupportedEncodingException ex) {
411: log.error("error decoding url: "
412: + encodedParamReplacement, ex);
413: }
414:
415: // Add ALL of the params from the decoded 'x' param
416: ValueMap params = new ValueMap();
417: RequestUtils.decodeParameters(decodedParamReplacement,
418: params);
419: this .parameterMap.putAll(params);
420:
421: // Rebuild the URL with the 'x' param removed
422: int pos1 = url.indexOf("?x=");
423: if (pos1 == -1) {
424: throw new WicketRuntimeException(
425: "Programming error: we should come here");
426: }
427: int pos2 = url.indexOf("&");
428:
429: AppendingStringBuffer urlBuf = new AppendingStringBuffer(
430: url.length() + encodedParamReplacement.length());
431: urlBuf.append(url.subSequence(0, pos1 + 1));
432: urlBuf.append(encodedParamReplacement);
433: if (pos2 != -1) {
434: urlBuf.append(url.substring(pos2));
435: }
436: this .url = urlBuf.toString();
437: }
438:
439: /**
440: * Delegate to the original request
441: *
442: * @see org.apache.wicket.Request#getLocale()
443: */
444: public Locale getLocale() {
445: return this .request.getLocale();
446: }
447:
448: /**
449: * @see org.apache.wicket.Request#getParameter(java.lang.String)
450: */
451: public String getParameter(final String key) {
452: if (key == null) {
453: return null;
454: }
455:
456: Object val = this .parameterMap.get(key);
457: if (val == null) {
458: return null;
459: } else if (val instanceof String[]) {
460: String[] arrayVal = (String[]) val;
461: return arrayVal.length > 0 ? arrayVal[0] : null;
462: } else if (val instanceof String) {
463: return (String) val;
464: } else {
465: // never happens, just being defensive
466: return val.toString();
467: }
468: }
469:
470: /**
471: * @see org.apache.wicket.Request#getParameterMap()
472: */
473: public Map getParameterMap() {
474: return this .parameterMap;
475: }
476:
477: /**
478: * @see org.apache.wicket.Request#getParameters(java.lang.String)
479: */
480: public String[] getParameters(final String key) {
481: if (key == null) {
482: return null;
483: }
484:
485: Object val = this .parameterMap.get(key);
486: if (val == null) {
487: return null;
488: } else if (val instanceof String[]) {
489: return (String[]) val;
490: } else if (val instanceof String) {
491: return new String[] { (String) val };
492: } else {
493: // never happens, just being defensive
494: return new String[] { val.toString() };
495: }
496: }
497:
498: /**
499: * @see org.apache.wicket.Request#getPath()
500: */
501: public String getPath() {
502: // Hasn't changed. We only encoded the querystring
503: return this .request.getPath();
504: }
505:
506: public String getRelativePathPrefixToContextRoot() {
507: return request.getRelativePathPrefixToContextRoot();
508: }
509:
510: public String getRelativePathPrefixToWicketHandler() {
511: return request.getRelativePathPrefixToWicketHandler();
512: }
513:
514: /**
515: * @see org.apache.wicket.Request#getURL()
516: */
517: public String getURL() {
518: return this .url;
519: }
520: }
521:
522: /**
523: *
524: */
525: public class HackAttackException extends WicketRuntimeException {
526: private static final long serialVersionUID = 1L;
527:
528: /**
529: * Construct.
530: *
531: * @param msg
532: */
533: public HackAttackException(final String msg) {
534: super (msg);
535: }
536:
537: /**
538: * No stack trace. We won't tell the hackers about the internals of
539: * wicket
540: *
541: * @see java.lang.Throwable#getStackTrace()
542: */
543: public StackTraceElement[] getStackTrace() {
544: return new StackTraceElement[0];
545: }
546:
547: /**
548: * No additional information. We won't tell the hackers about the
549: * internals of wicket
550: *
551: * @see java.lang.Throwable#toString()
552: */
553: public String toString() {
554: return getMessage();
555: }
556: }
557: }
|