/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.wicket.core.request.mapper;
import java.util.List;
import org.apache.wicket.Application;
import org.apache.wicket.core.request.handler.RequestSettingRequestHandler;
import org.apache.wicket.request.IRequestHandler;
import org.apache.wicket.request.IRequestMapper;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.Url;
import org.apache.wicket.util.IProvider;
import org.apache.wicket.util.crypt.ICrypt;
import org.apache.wicket.util.crypt.ICryptFactory;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.string.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Request mapper that encrypts urls generated by another mapper. The original URL (both segments
* and parameters) is encrypted and is represented as URL segment. To be able to handle relative
* URLs for images in .css file the same amount of URL segments that the original URL had are
* appended to the encrypted URL. Each segment has a precise 5 character value, calculated using a
* checksum. This helps in calculating the relative distance from the original URL. When a URL is
* returned by the browser, we iterate through these checksummed placeholder URL segments. If the
* segment matches the expected checksum, then the segment it deemed to be the corresponding segment
* in the encrypted URL. If the segment does not match the expected checksum, then the segment is
* deemed a plain text sibling of the corresponding segment in the encrypted URL, and all subsequent
* segments are considered plain text children of the current segment.
*
* @author igor.vaynberg
* @author Jesse Long
* @author svenmeier
*/
public class CryptoMapper implements IRequestMapper
{
private static final Logger log = LoggerFactory.getLogger(CryptoMapper.class);
private final IRequestMapper wrappedMapper;
private final IProvider<ICrypt> cryptProvider;
/**
* Encrypt with {@link org.apache.wicket.settings.SecuritySettings#getCryptFactory()}.
* <p>
* Note: Encryption is done with {@link org.apache.wicket.settings.SecuritySettings#DEFAULT_ENCRYPTION_KEY}
* if you haven't configured an alternative {@link ICryptFactory}. Alternatively use
* {@link CryptoMapper#CryptoMapper(IRequestMapper, IProvider)} with a specific {@link ICrypt}.
*
* @param wrappedMapper
* the non-crypted request mapper
* @param application
* the current application
*/
public CryptoMapper(final IRequestMapper wrappedMapper, final Application application)
{
this(wrappedMapper, new ApplicationCryptProvider(application));
}
/**
* Construct.
*
* @param wrappedMapper
* the non-crypted request mapper
* @param cryptProvider
* the custom crypt provider
*/
public CryptoMapper(final IRequestMapper wrappedMapper, final IProvider<ICrypt> cryptProvider)
{
this.wrappedMapper = Args.notNull(wrappedMapper, "wrappedMapper");
this.cryptProvider = Args.notNull(cryptProvider, "cryptProvider");
}
@Override
public int getCompatibilityScore(final Request request)
{
return wrappedMapper.getCompatibilityScore(request);
}
@Override
public Url mapHandler(final IRequestHandler requestHandler)
{
final Url url = wrappedMapper.mapHandler(requestHandler);
if (url == null)
{
return null;
}
if (url.isFull())
{
// do not encrypt full urls
return url;
}
return encryptUrl(url);
}
@Override
public IRequestHandler mapRequest(final Request request)
{
Url url = decryptUrl(request, request.getUrl());
if (url == null)
{
return wrappedMapper.mapRequest(request);
}
Request decryptedRequest = request.cloneWithUrl(url);
IRequestHandler handler = wrappedMapper.mapRequest(decryptedRequest);
if (handler != null)
{
handler = new RequestSettingRequestHandler(decryptedRequest, handler);
}
return handler;
}
/**
* @return the {@link ICrypt} implementation that may be used to encrypt/decrypt {@link Url}'s
* segments and/or query string
*/
protected final ICrypt getCrypt()
{
return cryptProvider.get();
}
/**
* @return the wrapped root request mapper
*/
protected final IRequestMapper getWrappedMapper()
{
return wrappedMapper;
}
protected Url encryptUrl(final Url url)
{
if (url.getSegments().isEmpty())
{
return url;
}
String encryptedUrlString = getCrypt().encryptUrlSafe(url.toString());
Url encryptedUrl = new Url(url.getCharset());
encryptedUrl.getSegments().add(encryptedUrlString);
int numberOfSegments = url.getSegments().size();
HashedSegmentGenerator generator = new HashedSegmentGenerator(encryptedUrlString);
for (int segNo = 0; segNo < numberOfSegments; segNo++)
{
encryptedUrl.getSegments().add(generator.next());
}
return encryptedUrl;
}
protected Url decryptUrl(final Request request, final Url encryptedUrl)
{
/*
* If the encrypted URL has no segments it is the home page URL, and does not need
* decrypting.
*/
if (encryptedUrl.getSegments().isEmpty())
{
return encryptedUrl;
}
List<String> encryptedSegments = encryptedUrl.getSegments();
Url url = new Url(request.getCharset());
try
{
/*
* The first encrypted segment contains an encrypted version of the entire plain text
* url.
*/
String encryptedUrlString = encryptedSegments.get(0);
if (Strings.isEmpty(encryptedUrlString))
{
return null;
}
String decryptedUrl = getCrypt().decryptUrlSafe(encryptedUrlString);
if (decryptedUrl == null)
{
return null;
}
Url originalUrl = Url.parse(decryptedUrl, request.getCharset());
int originalNumberOfSegments = originalUrl.getSegments().size();
int encryptedNumberOfSegments = encryptedUrl.getSegments().size();
HashedSegmentGenerator generator = new HashedSegmentGenerator(encryptedUrlString);
int segNo = 1;
for (; segNo < encryptedNumberOfSegments; segNo++)
{
if (segNo > originalNumberOfSegments)
{
break;
}
String next = generator.next();
String encryptedSegment = encryptedSegments.get(segNo);
if (!next.equals(encryptedSegment))
{
/*
* This segment received from the browser is not the same as the expected
* segment generated by the HashSegmentGenerator. Hence it, and all subsequent
* segments are considered plain text siblings of the original encrypted url.
*/
break;
}
/*
* This segments matches the expected checksum, so we add the corresponding segment
* from the original URL.
*/
url.getSegments().add(originalUrl.getSegments().get(segNo - 1));
}
/*
* Add all remaining segments from the encrypted url as plain text segments.
*/
for (; segNo < encryptedNumberOfSegments; segNo++)
{
// modified or additional segment
url.getSegments().add(encryptedUrl.getSegments().get(segNo));
}
url.getQueryParameters().addAll(originalUrl.getQueryParameters());
// WICKET-4923 additional parameters
url.getQueryParameters().addAll(encryptedUrl.getQueryParameters());
}
catch (Exception e)
{
log.error("Error decrypting URL", e);
url = null;
}
return url;
}
private static class ApplicationCryptProvider implements IProvider<ICrypt>
{
private final Application application;
public ApplicationCryptProvider(final Application application)
{
this.application = application;
}
@Override
public ICrypt get()
{
return application.getSecuritySettings().getCryptFactory().newCrypt();
}
}
/**
* A generator of hashed segments.
*/
public static class HashedSegmentGenerator
{
private char[] characters;
private int hash = 0;
public HashedSegmentGenerator(String string)
{
characters = string.toCharArray();
}
/**
* Generate the next segment
*
* @return segment
*/
public String next()
{
char a = characters[Math.abs(hash % characters.length)];
hash++;
char b = characters[Math.abs(hash % characters.length)];
hash++;
char c = characters[Math.abs(hash % characters.length)];
String segment = "" + a + b + c;
hash = hashString(segment);
segment += String.format("%02x", Math.abs(hash % 256));
hash = hashString(segment);
return segment;
}
public int hashString(final String str)
{
int hash = 97;
for (char c : str.toCharArray())
{
int i = c;
hash = 47 * hash + i;
}
return hash;
}
}
}