    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    /**
    265     * Generates an absolute URL based on the given HttpServletRequest and a relative URL.
    266     * This method takes into account various headers like X-Forwarded-Host, X-Forwarded-Proto,
    267     * and X-Forwarded-Server to construct the absolute URL.
    268     *
    269     * @param request The HttpServletRequest object, used to obtain scheme, server name, and port.
    270     * @param relativeUrl The relative URL to be appended to the base URL. Can be null.
    271     * @return The absolute URL as a String.
    272     * @since 2.12.2
    273     */
    274    public static String getAbsoluteUrl(final HttpServletRequest request, final String relativeUrl) {
    275        StringBuilder baseUrl = new StringBuilder();
    276
    277        // Check for proxy headers
    278        final String forwardedHost = request.getHeader("X-Forwarded-Host");
    279        final String forwardedProto = request.getHeader("X-Forwarded-Proto");
    280        final String forwardedServer = request.getHeader("X-Forwarded-Server");
    281
    282        if (forwardedHost != null && forwardedProto != null) {
    283            baseUrl.append(forwardedProto).append("://").append(forwardedHost);
    284        } else if (forwardedServer != null && forwardedProto != null) {
    285            baseUrl.append(forwardedProto).append("://").append(forwardedServer);
    286        } else {
    287            // Fallback to HttpServletRequest
    288            final String scheme = request.getScheme();
    289            final String serverName = request.getServerName();
    290            final int port = request.getServerPort();
    291
    292            baseUrl.append(scheme).append("://").append(serverName);
    293
    294            // Include port only if it's not the default port for the scheme
    295            if ((URIScheme.HTTP.same(scheme) && port != 80)
    296                    || (URIScheme.HTTPS.same(scheme) && port != 443)) {
    297                baseUrl.append(':');
    298                baseUrl.append(port);
    299            }
    300        }
    301
    302        if (relativeUrl != null) {
    303            baseUrl.append(relativeUrl);
    304        }
    305
    306        return baseUrl.toString();
    307    }
    308
    309
    310    /**
    311     * Generate an absolute URL based solely on the given HttpServletRequest.
    312     * This is a convenience method that calls {@link #getAbsoluteUrl(HttpServletRequest, String)}
    313     * with a null relative URL.
    314     *
    315     * @param request The HttpServletRequest object, used to obtain scheme, server name, and port.
    316     * @return The absolute URL as a String.
    317     * @see #getAbsoluteUrl(HttpServletRequest, String)
    318     * @since 2.12.2
    319     */
    320    public static String getAbsoluteUrl(final HttpServletRequest request) {
    321        return getAbsoluteUrl(request, null);
    322    }
    323
    324}