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