    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}