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}