
net.sf.webdav.util.RequestUtil Maven / Gradle / Ivy
/**
* Copyright (C) 2006 Apache Software Foundation ([email protected])
*
* Licensed 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 net.sf.webdav.util;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Locale.Category;
import java.util.Map;
import java.util.TimeZone;
/**
* General purpose request parsing and encoding utility methods.
*
* @author Craig R. McClanahan
* @author Tim Tye
* @version $Revision: 1.2 $ $Date: 2008-08-05 07:38:45 $
*/
public final class RequestUtil
{
/**
* The DateFormat to use for generating readable dates in cookies.
*/
private static SimpleDateFormat FORMAT = new SimpleDateFormat( " EEEE, dd-MMM-yy kk:mm:ss zz" );
static
{
FORMAT.setTimeZone( TimeZone.getTimeZone( "GMT" ) );
}
/**
* Shamelessly copied from: http://stackoverflow.com/questions/6824157/parse-accept-language-header-in-java
*/
public static Locale parseLocale( final String header )
{
// String header = "en-ca,en;q=0.8,en-us;q=0.6,de-de;q=0.4,de;q=0.2";
if ( header == null || header.trim()
.length() < 1 )
{
return Locale.getDefault( Category.FORMAT );
}
final Map prefs = parseQualityHeader( header );
final List sortedKeys = new ArrayList( prefs.keySet() );
Collections.sort( sortedKeys );
Collections.reverse( sortedKeys );
final Locale[] available = Locale.getAvailableLocales();
Locale result = null;
for ( final Double key : sortedKeys )
{
final String[] parts = prefs.get( key )
.split( "_" );
for ( final Locale l : available )
{
if ( !parts[0].equals( l.getISO3Language() ) )
{
continue;
}
if ( parts.length > 1 && !parts[1].equals( l.getISO3Country() ) )
{
continue;
}
if ( parts.length > 2 && !parts[2].equals( l.getVariant() ) )
{
continue;
}
result = l;
}
}
return result == null ? Locale.getDefault( Category.FORMAT ) : result;
}
/**
* Encode a cookie as per RFC 2109. The resulting string can be used as the
* value for a Set-Cookie
header.
*
* @param cookie
* The cookie to encode.
* @return A string following RFC 2109.
*/
// public static String encodeCookie( final Cookie cookie )
// {
//
// final StringBuilder buf = new StringBuilder( cookie.getName() );
// buf.append( "=" );
// buf.append( cookie.getValue() );
//
// final String comment = cookie.getComment();
// if ( comment != null )
// {
// buf.append( "; Comment=\"" );
// buf.append( comment );
// buf.append( "\"" );
// }
//
// final String domain = cookie.getDomain();
// if ( domain != null )
// {
// buf.append( "; Domain=\"" );
// buf.append( domain );
// buf.append( "\"" );
// }
//
// final int age = cookie.getMaxAge();
// if ( age >= 0 )
// {
// buf.append( "; Max-Age=\"" );
// buf.append( age );
// buf.append( "\"" );
// }
//
// final String path = cookie.getPath();
// if ( path != null )
// {
// buf.append( "; Path=\"" );
// buf.append( path );
// buf.append( "\"" );
// }
//
// if ( cookie.getSecure() )
// {
// buf.append( "; Secure" );
// }
//
// final int version = cookie.getVersion();
// if ( version > 0 )
// {
// buf.append( "; Version=\"" );
// buf.append( version );
// buf.append( "\"" );
// }
//
// return ( buf.toString() );
// }
public static Map parseQualityHeader( final String header )
{
final Map parsed = new HashMap();
for ( final String str : header.split( "," ) )
{
final String[] arr = str.trim()
.replace( "-", "_" )
.split( ";" );
//Parse the q-value
Double q = 1.0D;
for ( String s : arr )
{
s = s.trim();
if ( s.startsWith( "q=" ) )
{
q = Double.parseDouble( s.substring( 2 )
.trim() );
break;
}
}
parsed.put( q, arr[0] );
}
return parsed;
}
/**
* Filter the specified message string for characters that are sensitive in
* HTML. This avoids potential attacks caused by including JavaScript codes
* in the request URL that is often reported in error messages.
*
* @param message
* The message string to be filtered
*/
public static String filter( final String message )
{
if ( message == null )
{
return ( null );
}
final char content[] = new char[message.length()];
message.getChars( 0, message.length(), content, 0 );
final StringBuilder result = new StringBuilder( content.length + 50 );
for ( final char element : content )
{
switch ( element )
{
case '<':
result.append( "<" );
break;
case '>':
result.append( ">" );
break;
case '&':
result.append( "&" );
break;
case '"':
result.append( """ );
break;
default:
result.append( element );
}
}
return ( result.toString() );
}
/**
* Normalize a relative URI path that may have relative values ("/./",
* "/../", and so on ) it it. WARNING - This method is
* useful only for normalizing application-generated paths. It does not try
* to perform security checks for malicious input.
* @param absolutize
*
* @param path
* Relative path to be normalized
*/
public static String deRelativize( final boolean absolutize, final String path )
{
if ( path == null )
{
return null;
}
// Create a place for the normalized path
String result = path;
if ( result.equals( "/." ) )
{
return "/";
}
// Add a leading "/" if necessary
if ( absolutize && !result.startsWith( "/" ) )
{
result = "/" + result;
}
// Resolve occurrences of "//" in the normalized path
while ( true )
{
final int index = result.indexOf( "//" );
if ( index < 0 )
{
break;
}
result = result.substring( 0, index ) + result.substring( index + 1 );
}
// Resolve occurrences of "/./" in the normalized path
while ( true )
{
final int index = result.indexOf( "/./" );
if ( index < 0 )
{
break;
}
result = result.substring( 0, index ) + result.substring( index + 2 );
}
// Resolve occurrences of "/../" in the normalized path
while ( true )
{
final int index = result.indexOf( "/../" );
if ( index < 0 )
{
break;
}
if ( index == 0 )
{
return ( null ); // Trying to go outside our context
}
final int index2 = result.lastIndexOf( '/', index - 1 );
result = result.substring( 0, index2 ) + result.substring( index + 3 );
}
// Return the normalized path that we have completed
return result;
}
public static String normalize( final boolean absolutize, final String... path )
{
if ( path == null || path.length < 1 )
{
return null;
}
final StringBuilder sb = new StringBuilder();
int idx = 0;
for ( String part : path )
{
if ( part.length() < 1 || "/".equals( part ) )
{
continue;
}
if ( idx == 0 && part.startsWith( "file:" ) )
{
if ( part.length() > 5 )
{
sb.append( part.substring( 5 ) );
}
continue;
}
if ( idx > 0 )
{
while ( part.charAt( 0 ) == '/' )
{
if ( part.length() < 2 )
{
continue;
}
part = part.substring( 1 );
}
}
while ( part.charAt( part.length() - 1 ) == '/' )
{
if ( part.length() < 2 )
{
continue;
}
part = part.substring( 0, part.length() - 1 );
}
if ( sb.length() > 0 )
{
sb.append( '/' );
}
sb.append( part );
idx++;
}
return deRelativize( absolutize, sb.toString() );
}
/**
* Parse the character encoding from the specified content type header. If
* the content type is null, or there is no explicit character encoding,
* null
is returned.
*
* @param contentType
* a content type header
*/
public static String parseCharacterEncoding( final String contentType )
{
if ( contentType == null )
{
return ( null );
}
final int start = contentType.indexOf( "charset=" );
if ( start < 0 )
{
return ( null );
}
String encoding = contentType.substring( start + 8 );
final int end = encoding.indexOf( ';' );
if ( end >= 0 )
{
encoding = encoding.substring( 0, end );
}
encoding = encoding.trim();
if ( ( encoding.length() > 2 ) && ( encoding.startsWith( "\"" ) ) && ( encoding.endsWith( "\"" ) ) )
{
encoding = encoding.substring( 1, encoding.length() - 1 );
}
return ( encoding.trim() );
}
// /**
// * Parse a cookie header into an array of cookies according to RFC 2109.
// *
// * @param header
// * Value of an HTTP "Cookie" header
// */
// public static Cookie[] parseCookieHeader( String header )
// {
//
// if ( ( header == null ) || ( header.length() < 1 ) )
// {
// return ( new Cookie[0] );
// }
//
// final ArrayList cookies = new ArrayList();
// while ( header.length() > 0 )
// {
// int semicolon = header.indexOf( ';' );
// if ( semicolon < 0 )
// {
// semicolon = header.length();
// }
// if ( semicolon == 0 )
// {
// break;
// }
// final String token = header.substring( 0, semicolon );
// if ( semicolon < header.length() )
// {
// header = header.substring( semicolon + 1 );
// }
// else
// {
// header = "";
// }
// try
// {
// final int equals = token.indexOf( '=' );
// if ( equals > 0 )
// {
// final String name = token.substring( 0, equals )
// .trim();
// final String value = token.substring( equals + 1 )
// .trim();
// cookies.add( new CookieImpl( name, value ) );
// }
// }
// catch ( final Throwable e )
// {
// ;
// }
// }
//
// return ( cookies.toArray( new Cookie[cookies.size()] ) );
//
// }
/**
* Append request parameters from the specified String to the specified Map.
* It is presumed that the specified Map is not accessed from any other
* thread, so no synchronization is performed.
*
* IMPLEMENTATION NOTE: URL decoding is performed
* individually on the parsed name and value elements, rather than on the
* entire query string ahead of time, to properly deal with the case where
* the name or value includes an encoded "=" or "&" character that would
* otherwise be interpreted as a delimiter.
*
* @param map
* Map that accumulates the resulting parameters
* @param data
* Input string containing request parameters
*
* @exception IllegalArgumentException
* if the data is malformed
*/
public static void parseParameters( final Map map, final String data, final String encoding )
throws UnsupportedEncodingException
{
if ( ( data != null ) && ( data.length() > 0 ) )
{
// use the specified encoding to extract bytes out of the
// given string so that the encoding is not lost. If an
// encoding is not specified, let it use platform default
byte[] bytes = null;
try
{
if ( encoding == null )
{
bytes = data.getBytes();
}
else
{
bytes = data.getBytes( encoding );
}
}
catch ( final UnsupportedEncodingException uee )
{
}
parseParameters( map, bytes, encoding );
}
}
/**
* Decode and return the specified URL-encoded String. When the byte array
* is converted to a string, the system default character encoding is
* used... This may be different than some other servers.
*
* @param str
* The url-encoded string
*
* @exception IllegalArgumentException
* if a '%' character is not followed by a valid 2-digit hexadecimal
* number
*/
public static String URLDecode( final String str )
{
return URLDecode( str, null );
}
/**
* Decode and return the specified URL-encoded String.
*
* @param str
* The url-encoded string
* @param enc
* The encoding to use; if null, the default encoding is used
* @exception IllegalArgumentException
* if a '%' character is not followed by a valid 2-digit hexadecimal
* number
*/
public static String URLDecode( final String str, final String enc )
{
if ( str == null )
{
return ( null );
}
// use the specified encoding to extract bytes out of the
// given string so that the encoding is not lost. If an
// encoding is not specified, let it use platform default
byte[] bytes = null;
try
{
if ( enc == null )
{
bytes = str.getBytes();
}
else
{
bytes = str.getBytes( enc );
}
}
catch ( final UnsupportedEncodingException uee )
{
}
return URLDecode( bytes, enc );
}
/**
* Decode and return the specified URL-encoded byte array.
*
* @param bytes
* The url-encoded byte array
* @exception IllegalArgumentException
* if a '%' character is not followed by a valid 2-digit hexadecimal
* number
*/
public static String URLDecode( final byte[] bytes )
{
return URLDecode( bytes, null );
}
/**
* Decode and return the specified URL-encoded byte array.
*
* @param bytes
* The url-encoded byte array
* @param enc
* The encoding to use; if null, the default encoding is used
* @exception IllegalArgumentException
* if a '%' character is not followed by a valid 2-digit hexadecimal
* number
*/
public static String URLDecode( final byte[] bytes, final String enc )
{
if ( bytes == null )
{
return ( null );
}
final int len = bytes.length;
int ix = 0;
int ox = 0;
while ( ix < len )
{
byte b = bytes[ix++]; // Get byte to test
if ( b == '+' )
{
b = (byte) ' ';
}
else if ( b == '%' )
{
b = (byte) ( ( convertHexDigit( bytes[ix++] ) << 4 ) + convertHexDigit( bytes[ix++] ) );
}
bytes[ox++] = b;
}
if ( enc != null )
{
try
{
return new String( bytes, 0, ox, enc );
}
catch ( final Exception e )
{
// FIXME
e.printStackTrace();
}
}
return new String( bytes, 0, ox );
}
/**
* Convert a byte character value to hexidecimal digit value.
*
* @param b
* the character value byte
*/
private static byte convertHexDigit( final byte b )
{
if ( ( b >= '0' ) && ( b <= '9' ) )
{
return (byte) ( b - '0' );
}
if ( ( b >= 'a' ) && ( b <= 'f' ) )
{
return (byte) ( b - 'a' + 10 );
}
if ( ( b >= 'A' ) && ( b <= 'F' ) )
{
return (byte) ( b - 'A' + 10 );
}
return 0;
}
/**
* Put name and value pair in map. When name already exist, add value to
* array of values.
*
* @param map
* The map to populate
* @param name
* The parameter name
* @param value
* The parameter value
*/
private static void putMapEntry( final Map map, final String name, final String value )
{
String[] newValues = null;
final String[] oldValues = map.get( name );
if ( oldValues == null )
{
newValues = new String[1];
newValues[0] = value;
}
else
{
newValues = new String[oldValues.length + 1];
System.arraycopy( oldValues, 0, newValues, 0, oldValues.length );
newValues[oldValues.length] = value;
}
map.put( name, newValues );
}
/**
* Append request parameters from the specified String to the specified Map.
* It is presumed that the specified Map is not accessed from any other
* thread, so no synchronization is performed.
*
* IMPLEMENTATION NOTE: URL decoding is performed
* individually on the parsed name and value elements, rather than on the
* entire query string ahead of time, to properly deal with the case where
* the name or value includes an encoded "=" or "&" character that would
* otherwise be interpreted as a delimiter. NOTE: byte array data is
* modified by this method. Caller beware.
*
* @param map
* Map that accumulates the resulting parameters
* @param data
* Input string containing request parameters
* @param encoding
* Encoding to use for converting hex
*
* @exception UnsupportedEncodingException
* if the data is malformed
*/
public static void parseParameters( final Map map, final byte[] data, final String encoding )
throws UnsupportedEncodingException
{
if ( data != null && data.length > 0 )
{
int ix = 0;
int ox = 0;
String key = null;
String value = null;
while ( ix < data.length )
{
final byte c = data[ix++];
switch ( (char) c )
{
case '&':
value = new String( data, 0, ox, encoding );
if ( key != null )
{
putMapEntry( map, key, value );
key = null;
}
ox = 0;
break;
case '=':
if ( key == null )
{
key = new String( data, 0, ox, encoding );
ox = 0;
}
else
{
data[ox++] = c;
}
break;
case '+':
data[ox++] = (byte) ' ';
break;
case '%':
data[ox++] = (byte) ( ( convertHexDigit( data[ix++] ) << 4 ) + convertHexDigit( data[ix++] ) );
break;
default:
data[ox++] = c;
}
}
// The last value does not end in '&'. So save it now.
if ( key != null )
{
value = new String( data, 0, ox, encoding );
putMapEntry( map, key, value );
}
}
}
}