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.log4j.Logger;
023
024import javax.servlet.http.Cookie;
025import javax.servlet.http.HttpServletRequest;
026import java.nio.charset.Charset;
027import java.nio.charset.StandardCharsets;
028import java.text.DateFormat;
029import java.text.ParseException;
030import java.text.SimpleDateFormat;
031import java.util.Date;
032
033
034/**
035 *  Contains useful utilities for some common HTTP tasks.
036 *
037 *  @since 2.1.61.
038 */
039public final class HttpUtil {
040
041    private static final Logger log = Logger.getLogger( HttpUtil.class );
042    private static final int    ONE                   = 48;
043    private static final int    NINE                  = 57;
044    private static final int    DOT                   = 46;
045    
046    /** Private constructor to prevent direct instantiation. */
047    private HttpUtil() {
048    }
049    
050    /**
051     * returns the remote address by looking into {@code x-forwarded-for} header or, if unavailable, 
052     * into {@link HttpServletRequest#getRemoteAddr()}.
053     * 
054     * @param req http request
055     * @return remote address associated to the request.
056     */
057    public static String getRemoteAddress( final HttpServletRequest req ) {
058        return StringUtils.isNotEmpty ( req.getHeader( "X-Forwarded-For" ) ) ? req.getHeader( "X-Forwarded-For" ) : 
059                                                                                      req.getRemoteAddr();
060    }
061
062    /**
063     *  Attempts to retrieve the given cookie value from the request. Returns the string value (which may or may not be decoded
064     *  correctly, depending on browser!), or null if the cookie is not found. The algorithm will automatically trim leading
065     *  and trailing double quotes, if found.
066     *
067     *  @param request The current request
068     *  @param cookieName The name of the cookie to fetch.
069     *  @return Value of the cookie, or null, if there is no such cookie.
070     */
071    public static String retrieveCookieValue( final HttpServletRequest request, final String cookieName ) {
072        final Cookie[] cookies = request.getCookies();
073        if( cookies != null ) {
074            for( final Cookie cookie : cookies ) {
075                if( cookie.getName().equals( cookieName ) ) {
076                    String value = cookie.getValue();
077                    if( value == null || value.length() == 0 ) {
078                        return null;
079                    }
080                    if( value.charAt( 0 ) == '"' && value.charAt( value.length() - 1 ) == '"' ) {
081                        value = value.substring( 1, value.length() - 1 );
082                    }
083                    return value;
084                }
085            }
086        }
087
088        return null;
089    }
090
091    /**
092     *  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
093     *  changed. Do not assume that the ETag is in any particular format.
094     *  
095     *  @param pageName  The page name for which the ETag should be created.
096     *  @param lastModified  The page last modified date for which the ETag should be created.
097     *  @return A String depiction of an ETag.
098     */
099    public static String createETag( final String pageName, final Date lastModified ) {
100        return Long.toString( pageName.hashCode() ^ lastModified.getTime() );
101    }
102    
103    /**
104     *  If returns true, then should return a 304 (HTTP_NOT_MODIFIED)
105     *
106     *  @param req the HTTP request
107     *  @param pageName the wiki page name to check for
108     *  @param lastModified the last modified date of the wiki page to check for
109     *  @return the result of the check
110     */
111    public static boolean checkFor304( final HttpServletRequest req, final String pageName, final Date lastModified ) {
112        // 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.
113        //    pragma: no-cache
114        //    cache-control: no-cache
115        if( "no-cache".equalsIgnoreCase( req.getHeader( "Pragma" ) )
116            || "no-cache".equalsIgnoreCase( req.getHeader( "cache-control" ) ) ) {
117            // Wants specifically a fresh copy
118        } else {
119            //  HTTP 1.1 ETags go first
120            final String thisTag = createETag( pageName, lastModified );
121            final String eTag = req.getHeader( "If-None-Match" );
122            
123            if( eTag != null && eTag.equals(thisTag) ) {
124                return true;
125            }
126            
127            //  Next, try if-modified-since
128            final DateFormat rfcDateFormat = new SimpleDateFormat( "EEE, dd MMM yyyy HH:mm:ss z" );
129
130            try {
131                final long ifModifiedSince = req.getDateHeader( "If-Modified-Since" );
132
133                if( ifModifiedSince != -1 ) {
134                    final long lastModifiedTime = lastModified.getTime();
135                    if( lastModifiedTime <= ifModifiedSince ) {
136                        return true;
137                    }
138                } else {
139                    try {
140                        final String s = req.getHeader("If-Modified-Since");
141                        if( s != null ) {
142                            final Date ifModifiedSinceDate = rfcDateFormat.parse( s );
143                            if( lastModified.before( ifModifiedSinceDate ) ) {
144                                return true;
145                            }
146                        }
147                    } catch( final ParseException e ) {
148                        log.warn( e.getLocalizedMessage(), e );
149                    }
150                }
151            } catch( final IllegalArgumentException e ) {
152                // Illegal date/time header format.  We fail quietly, and return false.
153                // FIXME: Should really move to ETags.
154            }
155        }
156         
157        return false;
158    }
159
160    /**
161     * Attempts to form a valid URI based on the string given.  Currently it can guess email addresses (mailto:).  If nothing else is given,
162     * it assumes it to be an http:// url.
163     * 
164     * @param uri  URI to take a poke at
165     * @return Possibly a valid URI
166     * @since 2.2.8
167     */
168    public static String guessValidURI( String uri ) {
169        if( uri.indexOf( '@' ) != -1 ) {
170            if( !uri.startsWith( "mailto:" ) ) {
171                // Assume this is an email address
172                uri = "mailto:" + uri;
173            }
174        } else if( notBeginningWithHttpOrHttps( uri ) ) {
175            uri = "http://" + uri;
176        }
177        
178        return uri;
179    }
180
181    static boolean notBeginningWithHttpOrHttps( final String uri ) {
182        return uri.length() > 0 && !( uri.startsWith("http://" ) || uri.startsWith( "https://" ) );
183    }
184
185    /**
186     *  Returns the query string (the portion after the question mark).
187     *
188     *  @param request The HTTP request to parse.
189     *  @return The query string. If the query string is null, returns an empty string.
190     *  @since 2.1.3 (method moved from WikiEngine on 2.11.0.M6)
191     */
192    public static String safeGetQueryString( final HttpServletRequest request, final Charset contentEncoding ) {
193        if( request == null ) {
194            return "";
195        }
196
197        String res = request.getQueryString();
198        if( res != null ) {
199            res = new String( res.getBytes( StandardCharsets.ISO_8859_1 ), contentEncoding );
200
201            //
202            // Ensure that the 'page=xyz' attribute is removed
203            // FIXME: Is it really the mandate of this routine to do that?
204            //
205            final int pos1 = res.indexOf( "page=" );
206            if( pos1 >= 0 ) {
207                String tmpRes = res.substring( 0, pos1 );
208                final int pos2 = res.indexOf( '&', pos1 ) + 1;
209                if( ( pos2 > 0 ) && ( pos2 < res.length() ) ) {
210                    tmpRes = tmpRes + res.substring( pos2 );
211                }
212                res = tmpRes;
213            }
214        }
215
216        return res;
217    }
218
219    /**
220     * Verifies whether a String represents an IPv4 address. The algorithm is extremely efficient and does not allocate any objects.
221     *
222     * @param name the address to test
223     * @return the result
224     */
225    public static boolean isIPV4Address( final String name ) {
226        if( StringUtils.isEmpty( name ) || name.charAt( 0 ) == DOT || name.charAt( name.length() - 1 ) == DOT ) {
227            return false;
228        }
229
230        final int[] addr = new int[] { 0, 0, 0, 0 };
231        int currentOctet = 0;
232        for( int i = 0; i < name.length(); i++ ) {
233            if( currentOctet > 3 ) {
234                return false;
235            }
236            final int ch = name.charAt( i );
237            final boolean isDigit = ch >= ONE && ch <= NINE;
238            final boolean isDot = ch == DOT;
239            if( !isDigit && !isDot ) {
240                return false;
241            }
242            if( isDigit ) {
243                addr[ currentOctet ] = 10 * addr[ currentOctet ] + ( ch - ONE );
244                if( addr[ currentOctet ] > 255 ) {
245                    return false;
246                }
247            } else if( name.charAt( i - 1 ) == DOT ) {
248                return false;
249            } else {
250                currentOctet++;
251            }
252        }
253        return currentOctet == 3;
254    }
255
256}