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}