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}