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 2010-2011 ApexIdentity Inc.
015 * Portions Copyright 2011-2015 ForgeRock AS.
016 */
017
018package org.forgerock.openig.filter;
019
020import static java.lang.String.format;
021import static org.forgerock.openig.el.Bindings.bindings;
022import static org.forgerock.openig.util.JsonValues.asExpression;
023
024import java.net.URISyntaxException;
025import java.util.List;
026import java.util.Map;
027
028import org.forgerock.http.Filter;
029import org.forgerock.http.Handler;
030import org.forgerock.http.protocol.Form;
031import org.forgerock.http.protocol.Request;
032import org.forgerock.http.protocol.Response;
033import org.forgerock.http.util.CaseInsensitiveMap;
034import org.forgerock.http.util.MultiValueMap;
035import org.forgerock.json.JsonValue;
036import org.forgerock.openig.el.Bindings;
037import org.forgerock.openig.el.Expression;
038import org.forgerock.openig.heap.GenericHeapObject;
039import org.forgerock.openig.heap.GenericHeaplet;
040import org.forgerock.openig.heap.HeapException;
041import org.forgerock.openig.http.Responses;
042import org.forgerock.services.context.Context;
043import org.forgerock.util.promise.NeverThrowsException;
044import org.forgerock.util.promise.Promise;
045import org.forgerock.util.promise.Promises;
046
047/**
048 * Creates a new request and send it down the next handler (effectively replacing the previous request).
049 * The request can include a form, specified in the {@code form} field, which is included in an entity
050 * encoded in {@code application/x-www-form-urlencoded} format if request method
051 * is {@code POST}, or otherwise as (additional) query parameters in the URI.
052 *
053 * <pre>
054 * {@code
055 * {
056 *      "method"                     : string            [REQUIRED]
057 *      "uri"                        : expression        [REQUIRED]
058 *      "entity"                     : expression        [OPTIONAL - cannot be used simultaneously
059 *                                                                   with a form in POST mode* ]
060 *      "form"                       : object            [OPTIONAL]
061 *      "headers"                    : object            [OPTIONAL]
062 *      "version"                    : string            [OPTIONAL]
063 * }
064 * }
065 * </pre>
066 * <p>
067 * *Nota: When method is set to POST, the entity and the form CANNOT be used
068 * together in the heaplet because they both determine the request entity. They
069 * still can used programmatically together but the form will override any
070 * entity content.
071 * <p>
072 *
073 * Example of use:
074 *
075 * <pre>
076 * {@code
077 * {
078 *      "name": "customRequestFilter",
079 *      "type": "StaticRequestFilter",
080 *      "config": {
081 *          "method": "POST",
082 *          "uri": "http://10.10.0.2:8080/wp-login.php",
083 *          "entity": "{\"auth\":{\"passwordCredentials\":
084 *                     {\"username\":\"${attributes.username}\",
085 *                      \"password\":\"${attributes.password}\"}}}"
086 *          "headers": {
087 *              "Warning": [ "199 Miscellaneous warning" ]
088 *          }
089 *      }
090 * }
091 * }
092 * </pre>
093 */
094public class StaticRequestFilter extends GenericHeapObject implements Filter {
095
096    /** The message entity expression. */
097    private Expression<String> entity;
098
099    /** The HTTP method to be performed on the resource. */
100    private final String method;
101
102    /** URI as an expression to allow dynamic URI construction. */
103    private Expression<String> uri;
104
105    /** Protocol version (e.g. {@code "HTTP/1.1"}). */
106    private String version;
107
108    /** Message header fields whose values are expressions that are evaluated. */
109    private final MultiValueMap<String, Expression<String>> headers =
110            new MultiValueMap<>(new CaseInsensitiveMap<List<Expression<String>>>());
111
112    /** A form to include in the request, whose values are expressions that are evaluated. */
113    private final MultiValueMap<String, Expression<String>> form =
114            new MultiValueMap<>(new CaseInsensitiveMap<List<Expression<String>>>());
115
116    /**
117     * Builds a new {@link StaticRequestFilter} that will uses the given HTTP method on the resource.
118     *
119     * @param method
120     *         The HTTP method to be performed on the resource
121     */
122    public StaticRequestFilter(final String method) {
123        this.method = method;
124    }
125
126    /**
127     * Sets the message entity expression.
128     *
129     * @param entity
130     *            The message entity expression.
131     */
132    public void setEntity(final Expression<String> entity) {
133        this.entity = entity;
134    }
135
136    /**
137     * Sets the target URI as an expression to allow dynamic URI construction.
138     *
139     * @param uri
140     *         target URI expression
141     */
142    public void setUri(final Expression<String> uri) {
143        this.uri = uri;
144    }
145
146    /**
147     * Sets the new request message's version.
148     *
149     * @param version
150     *         Protocol version (e.g. {@code "HTTP/1.1"}).
151     */
152    public void setVersion(final String version) {
153        this.version = version;
154    }
155
156    /**
157     * Adds a new header value using the given {@code key} with the given {@link Expression}. As headers are
158     * multi-valued objects, it's perfectly legal to call this method multiple times with the same key.
159     *
160     * @param key
161     *         Header name
162     * @param value
163     *         {@link Expression} that represents the value of the new header
164     * @return this object for fluent usage
165     */
166    public StaticRequestFilter addHeaderValue(final String key, final Expression<String> value) {
167        headers.add(key, value);
168        return this;
169    }
170
171    /**
172     * Adds a new form parameter using the given {@code key} with the given {@link Expression}. As form parameters are
173     * multi-valued objects, it's perfectly legal to call this method multiple times with the same key.
174     *
175     * @param name
176     *         Form parameter name
177     * @param value
178     *         {@link Expression} that represents the value of the parameter
179     * @return this object for fluent usage
180     */
181    public StaticRequestFilter addFormParameter(final String name, final Expression<String> value) {
182        form.add(name, value);
183        return this;
184    }
185
186    @Override
187    public Promise<Response, NeverThrowsException> filter(final Context context,
188                                                          final Request request,
189                                                          final Handler next) {
190        Bindings bindings = bindings(context, request);
191
192        Request newRequest = new Request();
193        newRequest.setMethod(this.method);
194        String value = this.uri.eval(bindings);
195        if (value != null) {
196            try {
197                newRequest.setUri(value);
198            } catch (URISyntaxException e) {
199                logger.debug(e);
200                String message = format("The URI %s was not valid", value);
201                return Promises.newResultPromise(Responses.newInternalServerError(message, e));
202            }
203        } else {
204            String message = format("The URI expression '%s' could not be resolved", uri.toString());
205            logger.debug(message);
206            return Promises.newResultPromise(Responses.newInternalServerError(message));
207        }
208
209        if (entity != null) {
210            newRequest.setEntity(entity.eval(bindings));
211        }
212
213        if (this.version != null) {
214            // default in Message class
215            newRequest.setVersion(version);
216        }
217        for (String key : this.headers.keySet()) {
218            for (Expression<String> expression : this.headers.get(key)) {
219                String eval = expression.eval(bindings);
220                if (eval != null) {
221                    newRequest.getHeaders().add(key, eval);
222                }
223            }
224        }
225        if (this.form != null && !this.form.isEmpty()) {
226            Form f = new Form();
227            for (String key : this.form.keySet()) {
228                for (Expression<String> expression : this.form.get(key)) {
229                    String eval = expression.eval(bindings);
230                    if (eval != null) {
231                        f.add(key, eval);
232                    }
233                }
234            }
235            if ("POST".equals(newRequest.getMethod())) {
236                f.toRequestEntity(newRequest);
237            } else {
238                f.appendRequestQuery(newRequest);
239            }
240        }
241        return next.handle(context, newRequest);
242        // Note Can't restore in promise-land because I can't change the reference to the given request parameter
243    }
244
245    /** Creates and initializes a request filter in a heap environment. */
246    public static class Heaplet extends GenericHeaplet {
247        @Override
248        public Object create() throws HeapException {
249            final String method = config.get("method").required().asString();
250            StaticRequestFilter filter = new StaticRequestFilter(method);
251            filter.setUri(asExpression(config.get("uri").required(), String.class));
252            filter.setVersion(config.get("version").asString());
253            if (config.isDefined("entity")
254                    && config.isDefined("form")
255                    && "POST".equals(method)) {
256                throw new HeapException("Invalid configuration. When \"method\": \"POST\", \"form\" and \"entity\" "
257                        + "settings are mutually exclusive because they both determine the request entity.");
258            }
259            filter.setEntity(asExpression(config.get("entity"), String.class));
260
261            JsonValue headers = config.get("headers").expect(Map.class);
262            if (headers != null) {
263                for (String key : headers.keys()) {
264                    for (JsonValue value : headers.get(key).required().expect(List.class)) {
265                        filter.addHeaderValue(key, asExpression(value.required(), String.class));
266                    }
267                }
268            }
269            JsonValue form = config.get("form").expect(Map.class);
270            if (form != null) {
271                for (String key : form.keys()) {
272                    for (JsonValue value : form.get(key).required().expect(List.class)) {
273                        filter.addFormParameter(key, asExpression(value.required(), String.class));
274                    }
275                }
276            }
277            return filter;
278        }
279    }
280}