001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2012-2015 ForgeRock AS.
015 */
016package org.forgerock.openig.filter;
017
018import static java.lang.String.format;
019import static org.forgerock.openig.el.Bindings.bindings;
020import static org.forgerock.openig.util.JsonValues.asExpression;
021
022import java.net.URI;
023import java.net.URISyntaxException;
024
025import org.forgerock.http.Filter;
026import org.forgerock.http.Handler;
027import org.forgerock.http.header.LocationHeader;
028import org.forgerock.http.protocol.Request;
029import org.forgerock.http.protocol.Response;
030import org.forgerock.http.protocol.ResponseException;
031import org.forgerock.http.routing.UriRouterContext;
032import org.forgerock.http.util.Uris;
033import org.forgerock.openig.el.Bindings;
034import org.forgerock.openig.el.Expression;
035import org.forgerock.openig.heap.GenericHeapObject;
036import org.forgerock.openig.heap.GenericHeaplet;
037import org.forgerock.openig.heap.HeapException;
038import org.forgerock.openig.http.Responses;
039import org.forgerock.services.context.Context;
040import org.forgerock.util.Function;
041import org.forgerock.util.promise.NeverThrowsException;
042import org.forgerock.util.promise.Promise;
043
044/**
045 * Rewrites Location headers on responses that generate a redirect that would
046 * take the user directly to the application being proxied rather than taking
047 * the user through OpenIG. Options:
048 *
049 * <pre>
050 * {@code
051 * {
052 *   "baseURI"                     : expression        [OPTIONAL - default to the original URI
053 *                                                                 of the request ]
054 * }
055 * }
056 * </pre>
057 *
058 * Example:
059 *
060 * <pre>
061 * {@code
062 * {
063 *      "name": "LocationRewriter",
064 *      "type": "LocationHeaderFilter",
065 *      "config": {
066 *          "baseURI": "https://proxy.example.com:443/"
067 *       }
068 * }
069 * }
070 * </pre>
071 */
072public class LocationHeaderFilter extends GenericHeapObject implements Filter {
073
074    /** The base URI of the OpenIG instance, used to rewrite Location headers. */
075    private Expression<String> baseURI;
076
077    /**
078     * Sets the base URI used to rewrite Location headers.
079     * @param baseURI expression that, when evaluated, will represents the base URI of this OpenIG instance
080     */
081    public void setBaseURI(final Expression<String> baseURI) {
082        this.baseURI = baseURI;
083    }
084
085    /**
086     * Rewrite Location header if it would have the user go directly to the application.
087     *
088     * @param response http message to be updated
089     * @param bindings bindings to use when evaluating the new {@literal Location} value
090     * @param originalUri request's original Uri
091     */
092    private Response processResponse(Response response, Bindings bindings, final URI originalUri) {
093        LocationHeader header = LocationHeader.valueOf(response);
094        if (header.getLocationUri() != null) {
095            try {
096                URI currentURI = new URI(header.getLocationUri());
097                URI rebasedURI = Uris.rebase(currentURI, evaluateBaseUri(bindings, originalUri));
098                // Only rewrite header if it has changed
099                if (!currentURI.equals(rebasedURI)) {
100                    response.getHeaders().put(LocationHeader.NAME, rebasedURI.toString());
101                }
102            } catch (URISyntaxException | ResponseException ex) {
103                logger.debug(ex);
104                return Responses.newInternalServerError(ex);
105            }
106        }
107
108        return response;
109    }
110
111    private URI evaluateBaseUri(final Bindings bindings, final URI originalUri)
112            throws URISyntaxException, ResponseException {
113        if (baseURI != null) {
114            String uri = baseURI.eval(bindings);
115
116            if (uri == null) {
117                throw logger.debug(new ResponseException(format("The baseURI expression '%s' could not be resolved",
118                        baseURI.toString())));
119            }
120            return new URI(uri);
121        } else {
122            return originalUri;
123        }
124    }
125
126    @Override
127    public Promise<Response, NeverThrowsException> filter(final Context context,
128                                                          final Request request,
129                                                          final Handler next) {
130        // We only care about responses so just call the next handler in the chain.
131        return next.handle(context, request)
132                   .then(new Function<Response, Response, NeverThrowsException>() {
133                       @Override
134                       public Response apply(final Response value) {
135                           UriRouterContext routerContext = context.asContext(UriRouterContext.class);
136                           return processResponse(value, bindings(context, request, value),
137                                                  routerContext.getOriginalUri());
138                       }
139                   });
140    }
141
142    /** Creates and initializes a LocationHeaderFilter in a heap environment. */
143    public static class Heaplet extends GenericHeaplet {
144        @Override
145        public Object create() throws HeapException {
146
147            LocationHeaderFilter filter = new LocationHeaderFilter();
148            filter.baseURI = asExpression(config.get("baseURI"), String.class);
149
150            return filter;
151        }
152    }
153}