001/*
002 * ====================================================================
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *   http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing,
014 * software distributed under the License is distributed on an
015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016 * KIND, either express or implied.  See the License for the
017 * specific language governing permissions and limitations
018 * under the License.
019 * ====================================================================
020 *
021 * This software consists of voluntary contributions made by many
022 * individuals on behalf of the Apache Software Foundation.  For more
023 * information on the Apache Software Foundation, please see
024 * <http://www.apache.org/>.
025 *
026 */
027
028package org.apache.http.impl.client;
029
030import java.net.URI;
031import java.net.URISyntaxException;
032import java.util.Locale;
033
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.apache.http.Header;
037import org.apache.http.HttpHost;
038import org.apache.http.HttpRequest;
039import org.apache.http.HttpResponse;
040import org.apache.http.HttpStatus;
041import org.apache.http.ProtocolException;
042import org.apache.http.annotation.Contract;
043import org.apache.http.annotation.ThreadingBehavior;
044import org.apache.http.client.CircularRedirectException;
045import org.apache.http.client.RedirectStrategy;
046import org.apache.http.client.config.RequestConfig;
047import org.apache.http.client.methods.HttpGet;
048import org.apache.http.client.methods.HttpHead;
049import org.apache.http.client.methods.HttpUriRequest;
050import org.apache.http.client.methods.RequestBuilder;
051import org.apache.http.client.protocol.HttpClientContext;
052import org.apache.http.client.utils.URIBuilder;
053import org.apache.http.client.utils.URIUtils;
054import org.apache.http.protocol.HttpContext;
055import org.apache.http.util.Args;
056import org.apache.http.util.Asserts;
057import org.apache.http.util.TextUtils;
058
059/**
060 * Default implementation of {@link RedirectStrategy}. This strategy honors the restrictions
061 * on automatic redirection of entity enclosing methods such as POST and PUT imposed by the
062 * HTTP specification. {@code 302 Moved Temporarily}, {@code 301 Moved Permanently} and
063 * {@code 307 Temporary Redirect} status codes will result in an automatic redirect of
064 * HEAD and GET methods only. POST and PUT methods will not be automatically redirected
065 * as requiring user confirmation.
066 * <p>
067 * The restriction on automatic redirection of POST methods can be relaxed by using
068 * {@link LaxRedirectStrategy} instead of {@link DefaultRedirectStrategy}.
069 * </p>
070 *
071 * @see LaxRedirectStrategy
072 * @since 4.1
073 */
074@Contract(threading = ThreadingBehavior.IMMUTABLE)
075public class DefaultRedirectStrategy implements RedirectStrategy {
076
077    private final Log log = LogFactory.getLog(getClass());
078
079    /**
080     * @deprecated (4.3) use {@link org.apache.http.client.protocol.HttpClientContext#REDIRECT_LOCATIONS}.
081     */
082    @Deprecated
083    public static final String REDIRECT_LOCATIONS = "http.protocol.redirect-locations";
084
085    public static final DefaultRedirectStrategy INSTANCE = new DefaultRedirectStrategy();
086
087    /**
088     * Redirectable methods.
089     */
090    private static final String[] REDIRECT_METHODS = new String[] {
091        HttpGet.METHOD_NAME,
092        HttpHead.METHOD_NAME
093    };
094
095    public DefaultRedirectStrategy() {
096        super();
097    }
098
099    @Override
100    public boolean isRedirected(
101            final HttpRequest request,
102            final HttpResponse response,
103            final HttpContext context) throws ProtocolException {
104        Args.notNull(request, "HTTP request");
105        Args.notNull(response, "HTTP response");
106
107        final int statusCode = response.getStatusLine().getStatusCode();
108        final String method = request.getRequestLine().getMethod();
109        final Header locationHeader = response.getFirstHeader("location");
110        switch (statusCode) {
111        case HttpStatus.SC_MOVED_TEMPORARILY:
112            return isRedirectable(method) && locationHeader != null;
113        case HttpStatus.SC_MOVED_PERMANENTLY:
114        case HttpStatus.SC_TEMPORARY_REDIRECT:
115            return isRedirectable(method);
116        case HttpStatus.SC_SEE_OTHER:
117            return true;
118        default:
119            return false;
120        } //end of switch
121    }
122
123    public URI getLocationURI(
124            final HttpRequest request,
125            final HttpResponse response,
126            final HttpContext context) throws ProtocolException {
127        Args.notNull(request, "HTTP request");
128        Args.notNull(response, "HTTP response");
129        Args.notNull(context, "HTTP context");
130
131        final HttpClientContext clientContext = HttpClientContext.adapt(context);
132
133        //get the location header to find out where to redirect to
134        final Header locationHeader = response.getFirstHeader("location");
135        if (locationHeader == null) {
136            // got a redirect response, but no location header
137            throw new ProtocolException(
138                    "Received redirect response " + response.getStatusLine()
139                    + " but no location header");
140        }
141        final String location = locationHeader.getValue();
142        if (this.log.isDebugEnabled()) {
143            this.log.debug("Redirect requested to location '" + location + "'");
144        }
145
146        final RequestConfig config = clientContext.getRequestConfig();
147
148        URI uri = createLocationURI(location);
149
150        // rfc2616 demands the location value be a complete URI
151        // Location       = "Location" ":" absoluteURI
152        try {
153            if (!uri.isAbsolute()) {
154                if (!config.isRelativeRedirectsAllowed()) {
155                    throw new ProtocolException("Relative redirect location '"
156                            + uri + "' not allowed");
157                }
158                // Adjust location URI
159                final HttpHost target = clientContext.getTargetHost();
160                Asserts.notNull(target, "Target host");
161                final URI requestURI = new URI(request.getRequestLine().getUri());
162                final URI absoluteRequestURI = URIUtils.rewriteURI(requestURI, target, false);
163                uri = URIUtils.resolve(absoluteRequestURI, uri);
164            }
165        } catch (final URISyntaxException ex) {
166            throw new ProtocolException(ex.getMessage(), ex);
167        }
168
169        RedirectLocations redirectLocations = (RedirectLocations) clientContext.getAttribute(
170                HttpClientContext.REDIRECT_LOCATIONS);
171        if (redirectLocations == null) {
172            redirectLocations = new RedirectLocations();
173            context.setAttribute(HttpClientContext.REDIRECT_LOCATIONS, redirectLocations);
174        }
175        if (!config.isCircularRedirectsAllowed()) {
176            if (redirectLocations.contains(uri)) {
177                throw new CircularRedirectException("Circular redirect to '" + uri + "'");
178            }
179        }
180        redirectLocations.add(uri);
181        return uri;
182    }
183
184    /**
185     * @since 4.1
186     */
187    protected URI createLocationURI(final String location) throws ProtocolException {
188        try {
189            final URIBuilder b = new URIBuilder(new URI(location).normalize());
190            final String host = b.getHost();
191            if (host != null) {
192                b.setHost(host.toLowerCase(Locale.ROOT));
193            }
194            final String path = b.getPath();
195            if (TextUtils.isEmpty(path)) {
196                b.setPath("/");
197            }
198            return b.build();
199        } catch (final URISyntaxException ex) {
200            throw new ProtocolException("Invalid redirect URI: " + location, ex);
201        }
202    }
203
204    /**
205     * @since 4.2
206     */
207    protected boolean isRedirectable(final String method) {
208        for (final String m: REDIRECT_METHODS) {
209            if (m.equalsIgnoreCase(method)) {
210                return true;
211            }
212        }
213        return false;
214    }
215
216    @Override
217    public HttpUriRequest getRedirect(
218            final HttpRequest request,
219            final HttpResponse response,
220            final HttpContext context) throws ProtocolException {
221        final URI uri = getLocationURI(request, response, context);
222        final String method = request.getRequestLine().getMethod();
223        if (method.equalsIgnoreCase(HttpHead.METHOD_NAME)) {
224            return new HttpHead(uri);
225        } else if (method.equalsIgnoreCase(HttpGet.METHOD_NAME)) {
226            return new HttpGet(uri);
227        } else {
228            final int status = response.getStatusLine().getStatusCode();
229            if (status == HttpStatus.SC_TEMPORARY_REDIRECT) {
230                return RequestBuilder.copy(request).setUri(uri).build();
231            } else {
232                return new HttpGet(uri);
233            }
234        }
235    }
236
237}