001/* 002 Licensed to the Apache Software Foundation (ASF) under one 003 or more contributor license agreements. See the NOTICE file 004 distributed with this work for additional information 005 regarding copyright ownership. The ASF licenses this file 006 to you under the Apache License, Version 2.0 (the 007 "License"); you may not use this file except in compliance 008 with the License. You may obtain a copy of the License at 009 010 http://www.apache.org/licenses/LICENSE-2.0 011 012 Unless required by applicable law or agreed to in writing, 013 software distributed under the License is distributed on an 014 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 KIND, either express or implied. See the License for the 016 specific language governing permissions and limitations 017 under the License. 018 */ 019package org.apache.wiki.util; 020 021import org.apache.commons.lang3.StringUtils; 022import org.apache.log4j.Logger; 023 024import javax.servlet.http.Cookie; 025import javax.servlet.http.HttpServletRequest; 026import java.nio.charset.Charset; 027import java.nio.charset.StandardCharsets; 028import java.text.DateFormat; 029import java.text.ParseException; 030import java.text.SimpleDateFormat; 031import java.util.Date; 032 033 034/** 035 * Contains useful utilities for some common HTTP tasks. 036 * 037 * @since 2.1.61. 038 */ 039public final class HttpUtil { 040 041 private static final Logger log = Logger.getLogger( HttpUtil.class ); 042 private static final int ONE = 48; 043 private static final int NINE = 57; 044 private static final int DOT = 46; 045 046 /** Private constructor to prevent direct instantiation. */ 047 private HttpUtil() { 048 } 049 050 /** 051 * returns the remote address by looking into {@code x-forwarded-for} header or, if unavailable, 052 * into {@link HttpServletRequest#getRemoteAddr()}. 053 * 054 * @param req http request 055 * @return remote address associated to the request. 056 */ 057 public static String getRemoteAddress( final HttpServletRequest req ) { 058 return StringUtils.isNotEmpty ( req.getHeader( "X-Forwarded-For" ) ) ? req.getHeader( "X-Forwarded-For" ) : 059 req.getRemoteAddr(); 060 } 061 062 /** 063 * Attempts to retrieve the given cookie value from the request. Returns the string value (which may or may not be decoded 064 * correctly, depending on browser!), or null if the cookie is not found. The algorithm will automatically trim leading 065 * and trailing double quotes, if found. 066 * 067 * @param request The current request 068 * @param cookieName The name of the cookie to fetch. 069 * @return Value of the cookie, or null, if there is no such cookie. 070 */ 071 public static String retrieveCookieValue( final HttpServletRequest request, final String cookieName ) { 072 final Cookie[] cookies = request.getCookies(); 073 if( cookies != null ) { 074 for( final Cookie cookie : cookies ) { 075 if( cookie.getName().equals( cookieName ) ) { 076 String value = cookie.getValue(); 077 if( value == null || value.length() == 0 ) { 078 return null; 079 } 080 if( value.charAt( 0 ) == '"' && value.charAt( value.length() - 1 ) == '"' ) { 081 value = value.substring( 1, value.length() - 1 ); 082 } 083 return value; 084 } 085 } 086 } 087 088 return null; 089 } 090 091 /** 092 * Creates an ETag based on page information. An ETag is unique to each page and version, so it can be used to check if the page has 093 * changed. Do not assume that the ETag is in any particular format. 094 * 095 * @param pageName The page name for which the ETag should be created. 096 * @param lastModified The page last modified date for which the ETag should be created. 097 * @return A String depiction of an ETag. 098 */ 099 public static String createETag( final String pageName, final Date lastModified ) { 100 return Long.toString( pageName.hashCode() ^ lastModified.getTime() ); 101 } 102 103 /** 104 * If returns true, then should return a 304 (HTTP_NOT_MODIFIED) 105 * 106 * @param req the HTTP request 107 * @param pageName the wiki page name to check for 108 * @param lastModified the last modified date of the wiki page to check for 109 * @return the result of the check 110 */ 111 public static boolean checkFor304( final HttpServletRequest req, final String pageName, final Date lastModified ) { 112 // We'll do some handling for CONDITIONAL GET (and return a 304). If the client has set the following headers, do not try for a 304. 113 // pragma: no-cache 114 // cache-control: no-cache 115 if( "no-cache".equalsIgnoreCase( req.getHeader( "Pragma" ) ) 116 || "no-cache".equalsIgnoreCase( req.getHeader( "cache-control" ) ) ) { 117 // Wants specifically a fresh copy 118 } else { 119 // HTTP 1.1 ETags go first 120 final String thisTag = createETag( pageName, lastModified ); 121 final String eTag = req.getHeader( "If-None-Match" ); 122 123 if( eTag != null && eTag.equals(thisTag) ) { 124 return true; 125 } 126 127 // Next, try if-modified-since 128 final DateFormat rfcDateFormat = new SimpleDateFormat( "EEE, dd MMM yyyy HH:mm:ss z" ); 129 130 try { 131 final long ifModifiedSince = req.getDateHeader( "If-Modified-Since" ); 132 133 if( ifModifiedSince != -1 ) { 134 final long lastModifiedTime = lastModified.getTime(); 135 if( lastModifiedTime <= ifModifiedSince ) { 136 return true; 137 } 138 } else { 139 try { 140 final String s = req.getHeader("If-Modified-Since"); 141 if( s != null ) { 142 final Date ifModifiedSinceDate = rfcDateFormat.parse( s ); 143 if( lastModified.before( ifModifiedSinceDate ) ) { 144 return true; 145 } 146 } 147 } catch( final ParseException e ) { 148 log.warn( e.getLocalizedMessage(), e ); 149 } 150 } 151 } catch( final IllegalArgumentException e ) { 152 // Illegal date/time header format. We fail quietly, and return false. 153 // FIXME: Should really move to ETags. 154 } 155 } 156 157 return false; 158 } 159 160 /** 161 * Attempts to form a valid URI based on the string given. Currently it can guess email addresses (mailto:). If nothing else is given, 162 * it assumes it to be an http:// url. 163 * 164 * @param uri URI to take a poke at 165 * @return Possibly a valid URI 166 * @since 2.2.8 167 */ 168 public static String guessValidURI( String uri ) { 169 if( uri.indexOf( '@' ) != -1 ) { 170 if( !uri.startsWith( "mailto:" ) ) { 171 // Assume this is an email address 172 uri = "mailto:" + uri; 173 } 174 } else if( notBeginningWithHttpOrHttps( uri ) ) { 175 uri = "http://" + uri; 176 } 177 178 return uri; 179 } 180 181 static boolean notBeginningWithHttpOrHttps( final String uri ) { 182 return uri.length() > 0 && !( uri.startsWith("http://" ) || uri.startsWith( "https://" ) ); 183 } 184 185 /** 186 * Returns the query string (the portion after the question mark). 187 * 188 * @param request The HTTP request to parse. 189 * @return The query string. If the query string is null, returns an empty string. 190 * @since 2.1.3 (method moved from WikiEngine on 2.11.0.M6) 191 */ 192 public static String safeGetQueryString( final HttpServletRequest request, final Charset contentEncoding ) { 193 if( request == null ) { 194 return ""; 195 } 196 197 String res = request.getQueryString(); 198 if( res != null ) { 199 res = new String( res.getBytes( StandardCharsets.ISO_8859_1 ), contentEncoding ); 200 201 // 202 // Ensure that the 'page=xyz' attribute is removed 203 // FIXME: Is it really the mandate of this routine to do that? 204 // 205 final int pos1 = res.indexOf( "page=" ); 206 if( pos1 >= 0 ) { 207 String tmpRes = res.substring( 0, pos1 ); 208 final int pos2 = res.indexOf( '&', pos1 ) + 1; 209 if( ( pos2 > 0 ) && ( pos2 < res.length() ) ) { 210 tmpRes = tmpRes + res.substring( pos2 ); 211 } 212 res = tmpRes; 213 } 214 } 215 216 return res; 217 } 218 219 /** 220 * Verifies whether a String represents an IPv4 address. The algorithm is extremely efficient and does not allocate any objects. 221 * 222 * @param name the address to test 223 * @return the result 224 */ 225 public static boolean isIPV4Address( final String name ) { 226 if( StringUtils.isEmpty( name ) || name.charAt( 0 ) == DOT || name.charAt( name.length() - 1 ) == DOT ) { 227 return false; 228 } 229 230 final int[] addr = new int[] { 0, 0, 0, 0 }; 231 int currentOctet = 0; 232 for( int i = 0; i < name.length(); i++ ) { 233 if( currentOctet > 3 ) { 234 return false; 235 } 236 final int ch = name.charAt( i ); 237 final boolean isDigit = ch >= ONE && ch <= NINE; 238 final boolean isDot = ch == DOT; 239 if( !isDigit && !isDot ) { 240 return false; 241 } 242 if( isDigit ) { 243 addr[ currentOctet ] = 10 * addr[ currentOctet ] + ( ch - ONE ); 244 if( addr[ currentOctet ] > 255 ) { 245 return false; 246 } 247 } else if( name.charAt( i - 1 ) == DOT ) { 248 return false; 249 } else { 250 currentOctet++; 251 } 252 } 253 return currentOctet == 3; 254 } 255 256}