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    static Logger log = Logger.getLogger( HttpUtil.class );
042    
043    /** Private constructor to prevent direct instantiation. */
044    private HttpUtil() {
045    }
046    
047    /**
048     * returns the remote address by looking into {@code x-forwarded-for} header or, if unavailable, 
049     * into {@link HttpServletRequest#getRemoteAddr()}.
050     * 
051     * @param req http request
052     * @return remote address associated to the request.
053     */
054    public static String getRemoteAddress( HttpServletRequest req ) {
055        return StringUtils.isNotEmpty ( req.getHeader( "X-Forwarded-For" ) ) ? req.getHeader( "X-Forwarded-For" ) : 
056                                                                               req.getRemoteAddr();
057    }
058
059    /**
060     *  Attempts to retrieve the given cookie value from the request.
061     *  Returns the string value (which may or may not be decoded
062     *  correctly, depending on browser!), or null if the cookie is
063     *  not found. The algorithm will automatically trim leading
064     *  and trailing double quotes, if found.
065     *
066     *  @param request The current request
067     *  @param cookieName The name of the cookie to fetch.
068     *  @return Value of the cookie, or null, if there is no such cookie.
069     */
070    public static String retrieveCookieValue( HttpServletRequest request, String cookieName ) {
071        Cookie[] cookies = request.getCookies();
072
073        if( cookies != null ) {
074            for( int i = 0; i < cookies.length; i++ ) {
075                if( cookies[i].getName().equals( cookieName ) ) {
076                    String value = cookies[i].getValue();
077                    if( 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
093     *  and version, so it can be used to check if the page has changed.  Do not
094     *  assume that the ETag is in any particular format.
095     *  
096     *  @param pageName  The page name for which the ETag should be created.
097     *  @param lastModified  The page last modified date for which the ETag should be created.
098     *  @return A String depiction of an ETag.
099     */
100    public static String createETag( String pageName, Date lastModified ) {
101        return Long.toString( pageName.hashCode() ^ lastModified.getTime() );
102    }
103    
104    /**
105     *  If returns true, then should return a 304 (HTTP_NOT_MODIFIED)
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( HttpServletRequest req, String pageName, Date lastModified ) {
112        //
113        //  We'll do some handling for CONDITIONAL GET (and return a 304)
114        //  If the client has set the following headers, do not try for a 304.
115        //
116        //    pragma: no-cache
117        //    cache-control: no-cache
118        //
119
120        if( "no-cache".equalsIgnoreCase( req.getHeader( "Pragma" ) )
121            || "no-cache".equalsIgnoreCase( req.getHeader( "cache-control" ) ) ) {
122            // Wants specifically a fresh copy
123        } else {
124            //
125            //  HTTP 1.1 ETags go first
126            //
127            String thisTag = createETag( pageName, lastModified );
128                        
129            String eTag = req.getHeader( "If-None-Match" );
130            
131            if( eTag != null && eTag.equals(thisTag) ) {
132                return true;
133            }
134            
135            //
136            //  Next, try if-modified-since
137            //
138            DateFormat rfcDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
139
140            try {
141                long ifModifiedSince = req.getDateHeader( "If-Modified-Since" );
142
143                //log.info("ifModifiedSince:"+ifModifiedSince);
144                if( ifModifiedSince != -1 ) {
145                    long lastModifiedTime = lastModified.getTime();
146
147                    //log.info("lastModifiedTime:" + lastModifiedTime);
148                    if( lastModifiedTime <= ifModifiedSince ) {
149                        return true;
150                    }
151                } else {
152                    try {
153                        String s = req.getHeader("If-Modified-Since");
154
155                        if( s != null ) {
156                            Date ifModifiedSinceDate = rfcDateFormat.parse(s);
157                            //log.info("ifModifiedSinceDate:" + ifModifiedSinceDate);
158                            if( lastModified.before(ifModifiedSinceDate) ) {
159                                return true;
160                            }
161                        }
162                    } catch (ParseException e) {
163                        log.warn(e.getLocalizedMessage(), e);
164                    }
165                }
166            } catch( IllegalArgumentException e ) {
167                // Illegal date/time header format.
168                // We fail quietly, and return false.
169                // FIXME: Should really move to ETags.
170            }
171        }
172         
173        return false;
174    }
175
176    /**
177     *  Attempts to form a valid URI based on the string given.  Currently
178     *  it can guess email addresses (mailto:).  If nothing else is given,
179     *  it assumes it to be an http:// url.
180     * 
181     *  @param uri  URI to take a poke at
182     *  @return Possibly a valid URI
183     *  @since 2.2.8
184     */
185    public static String guessValidURI( String uri ) {
186        if( uri.indexOf( '@' ) != -1 ) {
187            if( !uri.startsWith( "mailto:" ) ) {
188                // Assume this is an email address
189                uri = "mailto:" + uri;
190            }
191        } else if( notBeginningWithHttpOrHttps( uri ) ) {
192            uri = "http://" + uri;
193        }
194        
195        return uri;
196    }
197
198    static boolean notBeginningWithHttpOrHttps( final String uri ) {
199        return uri.length() > 0 && !( ( uri.startsWith("http://" ) || uri.startsWith( "https://" ) ) );
200    }
201
202    /**
203     *  Returns the query string (the portion after the question mark).
204     *
205     *  @param request The HTTP request to parse.
206     *  @return The query string. If the query string is null, returns an empty string.
207     *
208     *  @since 2.1.3 (method moved from WikiEngine on 2.11.0.M6)
209     */
210    public static String safeGetQueryString( final HttpServletRequest request, final Charset contentEncoding ) {
211        if( request == null ) {
212            return "";
213        }
214
215        String res = request.getQueryString();
216        if( res != null ) {
217            res = new String( res.getBytes( StandardCharsets.ISO_8859_1 ), contentEncoding );
218
219            //
220            // Ensure that the 'page=xyz' attribute is removed
221            // FIXME: Is it really the mandate of this routine to do that?
222            //
223            final int pos1 = res.indexOf("page=");
224            if( pos1 >= 0 ) {
225                String tmpRes = res.substring( 0, pos1 );
226                final int pos2 = res.indexOf( "&",pos1 ) + 1;
227                if ( ( pos2 > 0 ) && ( pos2 < res.length() ) ) {
228                    tmpRes = tmpRes + res.substring(pos2);
229                }
230                res = tmpRes;
231            }
232        }
233
234        return res;
235    }
236
237}