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 static Logger log = Logger.getLogger( HttpUtil.class ); 042 043 /** Private constructor to prevent direct instantiation. */ 044 private HttpUtil() { 045 } 046 047 /** 048 * returns the remote address by looking into {@code x-forwarded-for} header or, if unavailable, 049 * into {@link HttpServletRequest#getRemoteAddr()}. 050 * 051 * @param req http request 052 * @return remote address associated to the request. 053 */ 054 public static String getRemoteAddress( HttpServletRequest req ) { 055 return StringUtils.isNotEmpty ( req.getHeader( "X-Forwarded-For" ) ) ? req.getHeader( "X-Forwarded-For" ) : 056 req.getRemoteAddr(); 057 } 058 059 /** 060 * Attempts to retrieve the given cookie value from the request. 061 * Returns the string value (which may or may not be decoded 062 * correctly, depending on browser!), or null if the cookie is 063 * not found. The algorithm will automatically trim leading 064 * and trailing double quotes, if found. 065 * 066 * @param request The current request 067 * @param cookieName The name of the cookie to fetch. 068 * @return Value of the cookie, or null, if there is no such cookie. 069 */ 070 public static String retrieveCookieValue( HttpServletRequest request, String cookieName ) { 071 Cookie[] cookies = request.getCookies(); 072 073 if( cookies != null ) { 074 for( int i = 0; i < cookies.length; i++ ) { 075 if( cookies[i].getName().equals( cookieName ) ) { 076 String value = cookies[i].getValue(); 077 if( 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 093 * and version, so it can be used to check if the page has changed. Do not 094 * assume that the ETag is in any particular format. 095 * 096 * @param pageName The page name for which the ETag should be created. 097 * @param lastModified The page last modified date for which the ETag should be created. 098 * @return A String depiction of an ETag. 099 */ 100 public static String createETag( String pageName, Date lastModified ) { 101 return Long.toString( pageName.hashCode() ^ lastModified.getTime() ); 102 } 103 104 /** 105 * If returns true, then should return a 304 (HTTP_NOT_MODIFIED) 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( HttpServletRequest req, String pageName, Date lastModified ) { 112 // 113 // We'll do some handling for CONDITIONAL GET (and return a 304) 114 // If the client has set the following headers, do not try for a 304. 115 // 116 // pragma: no-cache 117 // cache-control: no-cache 118 // 119 120 if( "no-cache".equalsIgnoreCase( req.getHeader( "Pragma" ) ) 121 || "no-cache".equalsIgnoreCase( req.getHeader( "cache-control" ) ) ) { 122 // Wants specifically a fresh copy 123 } else { 124 // 125 // HTTP 1.1 ETags go first 126 // 127 String thisTag = createETag( pageName, lastModified ); 128 129 String eTag = req.getHeader( "If-None-Match" ); 130 131 if( eTag != null && eTag.equals(thisTag) ) { 132 return true; 133 } 134 135 // 136 // Next, try if-modified-since 137 // 138 DateFormat rfcDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z"); 139 140 try { 141 long ifModifiedSince = req.getDateHeader( "If-Modified-Since" ); 142 143 //log.info("ifModifiedSince:"+ifModifiedSince); 144 if( ifModifiedSince != -1 ) { 145 long lastModifiedTime = lastModified.getTime(); 146 147 //log.info("lastModifiedTime:" + lastModifiedTime); 148 if( lastModifiedTime <= ifModifiedSince ) { 149 return true; 150 } 151 } else { 152 try { 153 String s = req.getHeader("If-Modified-Since"); 154 155 if( s != null ) { 156 Date ifModifiedSinceDate = rfcDateFormat.parse(s); 157 //log.info("ifModifiedSinceDate:" + ifModifiedSinceDate); 158 if( lastModified.before(ifModifiedSinceDate) ) { 159 return true; 160 } 161 } 162 } catch (ParseException e) { 163 log.warn(e.getLocalizedMessage(), e); 164 } 165 } 166 } catch( IllegalArgumentException e ) { 167 // Illegal date/time header format. 168 // We fail quietly, and return false. 169 // FIXME: Should really move to ETags. 170 } 171 } 172 173 return false; 174 } 175 176 /** 177 * Attempts to form a valid URI based on the string given. Currently 178 * it can guess email addresses (mailto:). If nothing else is given, 179 * it assumes it to be an http:// url. 180 * 181 * @param uri URI to take a poke at 182 * @return Possibly a valid URI 183 * @since 2.2.8 184 */ 185 public static String guessValidURI( String uri ) { 186 if( uri.indexOf( '@' ) != -1 ) { 187 if( !uri.startsWith( "mailto:" ) ) { 188 // Assume this is an email address 189 uri = "mailto:" + uri; 190 } 191 } else if( notBeginningWithHttpOrHttps( uri ) ) { 192 uri = "http://" + uri; 193 } 194 195 return uri; 196 } 197 198 static boolean notBeginningWithHttpOrHttps( final String uri ) { 199 return uri.length() > 0 && !( ( uri.startsWith("http://" ) || uri.startsWith( "https://" ) ) ); 200 } 201 202 /** 203 * Returns the query string (the portion after the question mark). 204 * 205 * @param request The HTTP request to parse. 206 * @return The query string. If the query string is null, returns an empty string. 207 * 208 * @since 2.1.3 (method moved from WikiEngine on 2.11.0.M6) 209 */ 210 public static String safeGetQueryString( final HttpServletRequest request, final Charset contentEncoding ) { 211 if( request == null ) { 212 return ""; 213 } 214 215 String res = request.getQueryString(); 216 if( res != null ) { 217 res = new String( res.getBytes( StandardCharsets.ISO_8859_1 ), contentEncoding ); 218 219 // 220 // Ensure that the 'page=xyz' attribute is removed 221 // FIXME: Is it really the mandate of this routine to do that? 222 // 223 final int pos1 = res.indexOf("page="); 224 if( pos1 >= 0 ) { 225 String tmpRes = res.substring( 0, pos1 ); 226 final int pos2 = res.indexOf( "&",pos1 ) + 1; 227 if ( ( pos2 > 0 ) && ( pos2 < res.length() ) ) { 228 tmpRes = tmpRes + res.substring(pos2); 229 } 230 res = tmpRes; 231 } 232 } 233 234 return res; 235 } 236 237}