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 jakarta.servlet.ServletResponse; 022import jakarta.servlet.http.Cookie; 023import jakarta.servlet.http.HttpServletRequest; 024import jakarta.servlet.http.HttpServletResponse; 025import org.apache.commons.lang3.StringUtils; 026import org.apache.commons.net.util.SubnetUtils; 027import org.apache.logging.log4j.LogManager; 028import org.apache.logging.log4j.Logger; 029 030import java.nio.charset.Charset; 031import java.nio.charset.StandardCharsets; 032import java.text.DateFormat; 033import java.text.ParseException; 034import java.text.SimpleDateFormat; 035import java.util.Date; 036 037 038/** 039 * Contains useful utilities for some common HTTP tasks. 040 * 041 * @since 2.1.61. 042 */ 043public final class HttpUtil { 044 045 private static final Logger LOG = LogManager.getLogger( HttpUtil.class ); 046 private static final int ONE = 48; 047 private static final int NINE = 57; 048 private static final int DOT = 46; 049 050 /** Private constructor to prevent direct instantiation. */ 051 private HttpUtil() { 052 } 053 054 /** 055 * returns the remote address by looking into {@code x-forwarded-for} header or, if unavailable, 056 * into {@link HttpServletRequest#getRemoteAddr()}. 057 * 058 * @param req http request 059 * @return remote address associated to the request. 060 */ 061 public static String getRemoteAddress( final HttpServletRequest req ) { 062 String realIP = StringUtils.isNotEmpty ( req.getHeader( "X-Forwarded-For" ) ) ? req.getHeader( "X-Forwarded-For" ) : 063 req.getRemoteAddr(); 064 // can be a comma-separated list of IPs 065 if (realIP.contains(",")) 066 realIP = realIP.substring(realIP.indexOf(",")); 067 068 return realIP; 069 070 } 071 072 /** 073 * Returns whether or not the IP address of the request equals a given IP, 074 * or is in a given IP range 075 * 076 * @param req http request 077 * @param ipOrRange IP address or IP range to test against 078 * @since 3.0.0 079 * @return 080 */ 081 public static boolean ipIsInRange(final HttpServletRequest req, final String ipOrRange) { 082 String requestIP = getRemoteAddress(req); 083 if (ipOrRange.contains("/")) { 084 SubnetUtils subnet = new SubnetUtils(ipOrRange); 085 SubnetUtils.SubnetInfo subnetInfo = subnet.getInfo(); 086 return subnetInfo.isInRange(requestIP); 087 } else { 088 return requestIP.equals(ipOrRange); 089 } 090 } 091 092 /** 093 * Attempts to retrieve the given cookie value from the request. Returns the string value (which may or may not be decoded 094 * correctly, depending on browser!), or null if the cookie is not found. The algorithm will automatically trim leading 095 * and trailing double quotes, if found. 096 * 097 * @param request The current request 098 * @param cookieName The name of the cookie to fetch. 099 * @return Value of the cookie, or null, if there is no such cookie. 100 */ 101 public static String retrieveCookieValue( final HttpServletRequest request, final String cookieName ) { 102 final Cookie[] cookies = request.getCookies(); 103 if( cookies != null ) { 104 for( final Cookie cookie : cookies ) { 105 if( cookie.getName().equals( cookieName ) ) { 106 String value = cookie.getValue(); 107 if( value == null || value.isEmpty() ) { 108 return null; 109 } 110 if( value.charAt( 0 ) == '"' && value.charAt( value.length() - 1 ) == '"' ) { 111 value = value.substring( 1, value.length() - 1 ); 112 } 113 return value; 114 } 115 } 116 } 117 118 return null; 119 } 120 121 /** 122 * 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 123 * changed. Do not assume that the ETag is in any particular format. 124 * 125 * @param pageName The page name for which the ETag should be created. 126 * @param lastModified The page last modified date for which the ETag should be created. 127 * @return A String depiction of an ETag. 128 */ 129 public static String createETag( final String pageName, final Date lastModified ) { 130 return Long.toString( pageName.hashCode() ^ lastModified.getTime() ); 131 } 132 133 /** 134 * If returns true, then should return a 304 (HTTP_NOT_MODIFIED) 135 * 136 * @param req the HTTP request 137 * @param pageName the wiki page name to check for 138 * @param lastModified the last modified date of the wiki page to check for 139 * @return the result of the check 140 */ 141 public static boolean checkFor304( final HttpServletRequest req, final String pageName, final Date lastModified ) { 142 // 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. 143 // pragma: no-cache 144 // cache-control: no-cache 145 if( "no-cache".equalsIgnoreCase( req.getHeader( "Pragma" ) ) 146 || "no-cache".equalsIgnoreCase( req.getHeader( "cache-control" ) ) ) { 147 // Wants specifically a fresh copy 148 } else { 149 // HTTP 1.1 ETags go first 150 final String thisTag = createETag( pageName, lastModified ); 151 final String eTag = req.getHeader( "If-None-Match" ); 152 153 if( eTag != null && eTag.equals(thisTag) ) { 154 return true; 155 } 156 157 // Next, try if-modified-since 158 final DateFormat rfcDateFormat = new SimpleDateFormat( "EEE, dd MMM yyyy HH:mm:ss z" ); 159 160 try { 161 final long ifModifiedSince = req.getDateHeader( "If-Modified-Since" ); 162 163 if( ifModifiedSince != -1 ) { 164 final long lastModifiedTime = lastModified.getTime(); 165 if( lastModifiedTime <= ifModifiedSince ) { 166 return true; 167 } 168 } else { 169 try { 170 final String s = req.getHeader("If-Modified-Since"); 171 if( s != null ) { 172 final Date ifModifiedSinceDate = rfcDateFormat.parse( s ); 173 if( lastModified.before( ifModifiedSinceDate ) ) { 174 return true; 175 } 176 } 177 } catch( final ParseException e ) { 178 LOG.warn( e.getLocalizedMessage(), e ); 179 } 180 } 181 } catch( final IllegalArgumentException e ) { 182 LOG.debug(e.getMessage(), e); 183 // Illegal date/time header format. We fail quietly, and return false. 184 // FIXME: Should really move to ETags. 185 } 186 } 187 188 return false; 189 } 190 191 /** 192 * Attempts to form a valid URI based on the string given. Currently it can guess email addresses (mailto:). If nothing else is given, 193 * it assumes it to be an http:// url. 194 * 195 * @param uri URI to take a poke at 196 * @return Possibly a valid URI 197 * @since 2.2.8 198 */ 199 public static String guessValidURI( String uri ) { 200 if( uri.indexOf( '@' ) != -1 ) { 201 if( !uri.startsWith( "mailto:" ) ) { 202 // Assume this is an email address 203 uri = "mailto:" + uri; 204 } 205 } else if( notBeginningWithHttpOrHttps( uri ) ) { 206 uri = "http://" + uri; 207 } 208 209 return uri; 210 } 211 212 static boolean notBeginningWithHttpOrHttps( final String uri ) { 213 return !uri.isEmpty() && !( uri.startsWith("http://" ) || uri.startsWith( "https://" ) ); 214 } 215 216 /** 217 * Returns the query string (the portion after the question mark). 218 * 219 * @param request The HTTP request to parse. 220 * @return The query string. If the query string is null, returns an empty string. 221 * @since 2.1.3 (method moved from WikiEngine on 2.11.0.M6) 222 */ 223 public static String safeGetQueryString( final HttpServletRequest request, final Charset contentEncoding ) { 224 if( request == null ) { 225 return ""; 226 } 227 228 String res = request.getQueryString(); 229 if( res != null ) { 230 res = new String( res.getBytes( StandardCharsets.ISO_8859_1 ), contentEncoding ); 231 232 // 233 // Ensure that the 'page=xyz' attribute is removed 234 // FIXME: Is it really the mandate of this routine to do that? 235 // 236 final int pos1 = res.indexOf( "page=" ); 237 if( pos1 >= 0 ) { 238 String tmpRes = res.substring( 0, pos1 ); 239 final int pos2 = res.indexOf( '&', pos1 ) + 1; 240 if( ( pos2 > 0 ) && ( pos2 < res.length() ) ) { 241 tmpRes = tmpRes + res.substring( pos2 ); 242 } 243 res = tmpRes; 244 } 245 } 246 247 return res; 248 } 249 250 /** 251 * Verifies whether a String represents an IPv4 address. The algorithm is extremely efficient and does not allocate any objects. 252 * 253 * @param name the address to test 254 * @return the result 255 */ 256 public static boolean isIPV4Address( final String name ) { 257 if( StringUtils.isEmpty( name ) || name.charAt( 0 ) == DOT || name.charAt( name.length() - 1 ) == DOT ) { 258 return false; 259 } 260 261 final int[] addr = new int[] { 0, 0, 0, 0 }; 262 int currentOctet = 0; 263 for( int i = 0; i < name.length(); i++ ) { 264 if( currentOctet > 3 ) { 265 return false; 266 } 267 final int ch = name.charAt( i ); 268 final boolean isDigit = ch >= ONE && ch <= NINE; 269 final boolean isDot = ch == DOT; 270 if( !isDigit && !isDot ) { 271 return false; 272 } 273 if( isDigit ) { 274 addr[ currentOctet ] = 10 * addr[ currentOctet ] + ( ch - ONE ); 275 if( addr[ currentOctet ] > 255 ) { 276 return false; 277 } 278 } else if( name.charAt( i - 1 ) == DOT ) { 279 return false; 280 } else { 281 currentOctet++; 282 } 283 } 284 return currentOctet == 3; 285 } 286 287 public static void clearCookie( final HttpServletResponse response, final String cookieName ) { 288 final Cookie cookie = new Cookie( cookieName, "" ); 289 cookie.setMaxAge( 0 ); 290 response.addCookie( cookie ); 291 } 292 293 /** 294 * Generates an absolute URL based on the given HttpServletRequest and a relative URL. 295 * This method takes into account various headers like X-Forwarded-Host, X-Forwarded-Proto, 296 * and X-Forwarded-Server to construct the absolute URL. 297 * 298 * @param request The HttpServletRequest object, used to obtain scheme, server name, and port. 299 * @param relativeUrl The relative URL to be appended to the base URL. Can be null. 300 * @return The absolute URL as a String. 301 * @since 2.12.2 302 */ 303 public static String getAbsoluteUrl(final HttpServletRequest request, final String relativeUrl) { 304 StringBuilder baseUrl = new StringBuilder(); 305 306 // Check for proxy headers 307 final String forwardedHost = request.getHeader("X-Forwarded-Host"); 308 final String forwardedProto = request.getHeader("X-Forwarded-Proto"); 309 final String forwardedServer = request.getHeader("X-Forwarded-Server"); 310 311 if (forwardedHost != null && forwardedProto != null) { 312 baseUrl.append(forwardedProto).append("://").append(forwardedHost); 313 } else if (forwardedServer != null && forwardedProto != null) { 314 baseUrl.append(forwardedProto).append("://").append(forwardedServer); 315 } else { 316 // Fallback to HttpServletRequest 317 final String scheme = request.getScheme(); 318 final String serverName = request.getServerName(); 319 final int port = request.getServerPort(); 320 321 baseUrl.append(scheme).append("://").append(serverName); 322 323 // Include port only if it's not the default port for the scheme 324 if ((URIScheme.HTTP.same(scheme) && port != 80) 325 || (URIScheme.HTTPS.same(scheme) && port != 443)) { 326 baseUrl.append(':'); 327 baseUrl.append(port); 328 } 329 } 330 331 if (relativeUrl != null) { 332 baseUrl.append(relativeUrl); 333 } 334 335 return baseUrl.toString(); 336 } 337 338 339 /** 340 * Generate an absolute URL based solely on the given HttpServletRequest. 341 * This is a convenience method that calls {@link #getAbsoluteUrl(HttpServletRequest, String)} 342 * with a null relative URL. 343 * 344 * @param request The HttpServletRequest object, used to obtain scheme, server name, and port. 345 * @return The absolute URL as a String. 346 * @see #getAbsoluteUrl(HttpServletRequest, String) 347 * @since 2.12.2 348 */ 349 public static String getAbsoluteUrl(final HttpServletRequest request) { 350 return getAbsoluteUrl(request, null); 351 } 352 353 /** 354 * Add's a header to the response 355 * 356 * @param response servlet response in which the header is added 357 * @param headerName header's name 358 * @param headerValue header's value 359 */ 360 public static void addHeader( final ServletResponse response, final String headerName, final String headerValue ) { 361 final HttpServletResponse res = ( HttpServletResponse )response; 362 res.addHeader( headerName, headerValue ); 363 } 364 365}