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 2009 Sun Microsystems Inc. 015 * Portions Copyright 2010-2011 ApexIdentity Inc. 016 * Portions Copyright 2011-2015 ForgeRock AS. 017 */ 018 019package org.forgerock.openig.filter; 020 021import static org.forgerock.util.Utils.joinAsString; 022 023import java.io.IOException; 024import java.net.CookieManager; 025import java.net.CookiePolicy; 026import java.net.HttpCookie; 027import java.net.URI; 028import java.net.URISyntaxException; 029import java.util.ArrayList; 030import java.util.Arrays; 031import java.util.Collections; 032import java.util.List; 033import java.util.ListIterator; 034import java.util.regex.Pattern; 035 036import org.forgerock.http.protocol.Header; 037import org.forgerock.http.protocol.Message; 038import org.forgerock.services.context.Context; 039import org.forgerock.http.Filter; 040import org.forgerock.http.Handler; 041import org.forgerock.http.MutableUri; 042import org.forgerock.http.session.Session; 043import org.forgerock.http.session.SessionContext; 044import org.forgerock.http.protocol.Request; 045import org.forgerock.http.protocol.Response; 046import org.forgerock.http.util.CaseInsensitiveSet; 047import org.forgerock.openig.heap.GenericHeapObject; 048import org.forgerock.openig.heap.GenericHeaplet; 049import org.forgerock.openig.heap.HeapException; 050import org.forgerock.openig.http.Responses; 051import org.forgerock.util.Function; 052import org.forgerock.util.promise.NeverThrowsException; 053import org.forgerock.util.promise.Promise; 054import org.forgerock.util.promise.Promises; 055 056/** 057 * Suppresses, relays and manages cookies. The names of filtered cookies are stored in one of 058 * three action set variables: {@code suppressed}, {@code relayed} and {@code managed}. If a 059 * cookie is not found in any of the action sets, then a default action is selected. 060 * <p> 061 * The default action is controlled by setting the {@code defaultAction} field. The default 062 * action at initialization is to manage all cookies. In the event a cookie appears in more 063 * than one action set, then it will be selected in order of precedence: managed, suppressed, 064 * relayed. 065 * <p> 066 * Managed cookies are intercepted by the cookie filter itself and stored in the request 067 * {@link Session} object. The default {@code policy} is to accept all incoming cookies, but 068 * can be changed to others as appropriate. 069 */ 070public class CookieFilter extends GenericHeapObject implements Filter { 071 072 /** Action to be performed for a cookie. */ 073 public enum Action { 074 /** Intercept and manage the cookie within the proxy. */ 075 MANAGE, 076 /** Remove the cookie from request and response. */ 077 SUPPRESS, 078 /** Relay the cookie between remote client and remote host. */ 079 RELAY 080 } 081 082// TODO: Use the org.forgerock.openig.header framework now for parsing, not regexes anymore. 083 084 /** Splits string using comma delimiter, outside of quotes. */ 085 private static final Pattern DELIM_COMMA = Pattern.compile(",(?=([^\"]*\"[^\"]*\")*(?![^\"]*\"))"); 086 087 /** Splits string using equals sign delimiter, outside of quotes. */ 088 private static final Pattern DELIM_EQUALS = Pattern.compile("=(?=([^\"]*\"[^\"]*\")*(?![^\"]*\"))"); 089 090 /** Splits string using semicolon delimiter, outside of quotes. */ 091 private static final Pattern DELIM_SEMICOLON = Pattern.compile(";(?=([^\"]*\"[^\"]*\")*(?![^\"]*\"))"); 092 093 /** Splits string using colon delimiter. */ 094 private static final Pattern DELIM_COLON = Pattern.compile(":"); 095 096 /** Response headers to parse. */ 097 private static final String[] RESPONSE_HEADERS = {"Set-Cookie", "Set-Cookie2"}; 098 099 /** Action to perform for cookies that do not match an action set. Default: manage. */ 100 private Action defaultAction = Action.MANAGE; 101 102 /** The policy for managed cookies. Default: accept all cookies. */ 103 private CookiePolicy policy = CookiePolicy.ACCEPT_ALL; 104 105 /** Action set for cookies to be suppressed. */ 106 private final CaseInsensitiveSet suppressed = new CaseInsensitiveSet(); 107 108 /** Action set for cookies to be relayed. */ 109 private final CaseInsensitiveSet relayed = new CaseInsensitiveSet(); 110 111 /** Action set for cookies that filter should intercept and manage. */ 112 private final CaseInsensitiveSet managed = new CaseInsensitiveSet(); 113 114 /** 115 * Set the action to perform for cookies that do not match an action set. Default: {@link Action#MANAGE}. 116 * @param defaultAction the action to perform for cookies that do not match an action set. 117 */ 118 public void setDefaultAction(final Action defaultAction) { 119 this.defaultAction = defaultAction; 120 } 121 122 /** 123 * Set the policy for managed cookies. Default: accept all cookies ({@link CookiePolicy#ACCEPT_ALL}). 124 * @param policy the policy for managed cookies. 125 */ 126 public void setPolicy(final CookiePolicy policy) { 127 this.policy = policy; 128 } 129 130 /** 131 * Returns the set of cookie names that will be suppressed from the request and from the response. 132 * @return the set of suppressed cookie identifiers. 133 */ 134 public CaseInsensitiveSet getSuppressed() { 135 return suppressed; 136 } 137 138 /** 139 * Returns the set of cookie names that will be relayed ({@literal Cookie} transmitted from the 140 * client to the next handler in the context of a request, and {@literal Set-Cookie2} transmitted 141 * from the next handler to the client in the context of a response). 142 * @return the set of relayed cookie identifiers. 143 */ 144 public CaseInsensitiveSet getRelayed() { 145 return relayed; 146 } 147 148 /** 149 * Returns the set of cookie names that will be managed. 150 * @return the set of managed cookie identifiers. 151 */ 152 public CaseInsensitiveSet getManaged() { 153 return managed; 154 } 155 156 /** 157 * Resolves the request URI based on the request URI variable and optional 158 * Host header. This allows the request URI to contain a raw IP address, 159 * while the Host header resolves the hostname and port that the remote 160 * client used to access it. 161 * <p> 162 * Note: This method returns a normalized URI, as though returned by the 163 * {@link URI#normalize} method. 164 * 165 * @return the resolved URI value. 166 */ 167// TODO: Rewrite and put in URIutil. 168 private MutableUri resolveHostURI(Request request) { 169 MutableUri uri = request.getUri(); 170 String header = (request.getHeaders() != null ? request.getHeaders().getFirst("Host") : null); 171 if (uri != null && header != null) { 172 String[] hostport = DELIM_COLON.split(header, 2); 173 int port; 174 try { 175 port = (hostport.length == 2 ? Integer.parseInt(hostport[1]) : -1); 176 } catch (NumberFormatException nfe) { 177 port = -1; 178 } 179 try { 180 uri = new MutableUri(uri.getScheme(), 181 null, 182 hostport[0], 183 port, 184 "/", 185 null, 186 null).resolve(new MutableUri(uri.getScheme(), 187 null, 188 uri.getHost(), 189 uri.getPort(), 190 null, 191 null, 192 null).relativize(uri)); 193 } catch (URISyntaxException use) { 194 // suppress exception 195 } 196 } 197 return uri; 198 } 199 200 /** 201 * Sets all request cookies (existing in request plus those to add from cookie jar) in 202 * a single "Cookie" header in the request. 203 */ 204 private void addRequestCookies(CookieManager manager, MutableUri resolved, Request request) throws IOException { 205 Header cookieHeader = request.getHeaders().get("Cookie"); 206 List<String> cookies = new ArrayList<>(); 207 if (cookieHeader != null) { 208 cookies.addAll(cookieHeader.getValues()); 209 } 210 List<String> managed = manager.get(resolved.asURI(), request.getHeaders().copyAsMultiMapOfStrings()) 211 .get("Cookie"); 212 if (managed != null) { 213 cookies.addAll(managed); 214 } 215 StringBuilder sb = new StringBuilder(); 216 for (String cookie : cookies) { 217 if (sb.length() > 0) { 218 sb.append("; "); 219 } 220 sb.append(cookie); 221 } 222 if (sb.length() > 0) { 223 // replace any existing header(s) 224 request.getHeaders().put("Cookie", sb.toString()); 225 } 226 } 227 228 @Override 229 public Promise<Response, NeverThrowsException> filter(final Context context, 230 final Request request, 231 final Handler next) { 232 SessionContext sessionContext = context.asContext(SessionContext.class); 233 234 // resolve to client-supplied host header 235 final MutableUri resolved = resolveHostURI(request); 236 // session cookie jar 237 final CookieManager manager = getManager(sessionContext.getSession()); 238 // remove cookies that are suppressed or managed 239 suppress(request); 240 // add any request cookies to header 241 try { 242 addRequestCookies(manager, resolved, request); 243 } catch (IOException e) { 244 return Promises.newResultPromise(Responses.newInternalServerError("Can't add request cookies", e)); 245 } 246 247 // pass request to next handler in chain 248 return next.handle(context, request) 249 .then(new Function<Response, Response, NeverThrowsException>() { 250 @Override 251 public Response apply(final Response value) { 252 // manage cookie headers in response 253 try { 254 manager.put(resolved.asURI(), value.getHeaders().copyAsMultiMapOfStrings()); 255 } catch (IOException e) { 256 return Responses.newInternalServerError("Can't process managed cookies in response", e); 257 } 258 // remove cookies that are suppressed or managed 259 suppress(value); 260 return value; 261 } 262 }); 263 } 264 265 /** 266 * Computes what action to perform for the specified cookie name. 267 * 268 * @param name the name of the cookie to compute action for. 269 * @return the computed action to perform for the given cookie. 270 */ 271 private Action action(String name) { 272 if (managed.contains(name)) { 273 return Action.MANAGE; 274 } else if (suppressed.contains(name)) { 275 return Action.SUPPRESS; 276 } else if (relayed.contains(name)) { 277 return Action.RELAY; 278 } else { 279 return defaultAction; 280 } 281 } 282 283 /** 284 * Returns the cookie manager for the session, creating one if it does not already exist. 285 * 286 * @param session the session that contains the cookie manager. 287 * @return the retrieved (or created) cookie manager. 288 */ 289 private CookieManager getManager(Session session) { 290 CookieManager manager = null; 291 // prevent a race for the cookie manager 292 synchronized (session) { 293 manager = (CookieManager) session.get(CookieManager.class.getName()); 294 if (manager == null) { 295 manager = new CookieManager(null, new CookiePolicy() { 296 public boolean shouldAccept(URI uri, HttpCookie cookie) { 297 return (action(cookie.getName()) == Action.MANAGE && policy.shouldAccept(uri, cookie)); 298 } 299 }); 300 session.put(CookieManager.class.getName(), manager); 301 } 302 } 303 return manager; 304 } 305 306 /** 307 * Removes the cookies from the request that are suppressed or managed. 308 * 309 * @param request the request to suppress the cookies in. 310 */ 311 private void suppress(Request request) { 312 Header cookieHeader = request.getHeaders().get("Cookie"); 313 if (cookieHeader == null) { 314 return; 315 } 316 List<String> headers = new ArrayList<>(cookieHeader.getValues()); 317 for (ListIterator<String> hi = headers.listIterator(); hi.hasNext();) { 318 String header = hi.next(); 319 ArrayList<String> parts = new ArrayList<>(Arrays.asList(DELIM_SEMICOLON.split(header, 0))); 320 int originalSize = parts.size(); 321 boolean remove = false; 322 int intact = 0; 323 for (ListIterator<String> pi = parts.listIterator(); pi.hasNext();) { 324 String part = pi.next().trim(); 325 if (part.length() != 0 && part.charAt(0) == '$') { 326 if (remove) { 327 pi.remove(); 328 } 329 } else { 330 Action action = action((DELIM_EQUALS.split(part, 2))[0].trim()); 331 if (action == Action.SUPPRESS || action == Action.MANAGE) { 332 pi.remove(); 333 remove = true; 334 } else { 335 intact++; 336 remove = false; 337 } 338 } 339 } 340 if (intact == 0) { 341 hi.remove(); 342 } else if (parts.size() != originalSize) { 343 hi.set(joinAsString(";", parts)); 344 } 345 } 346 //if (headers.isEmpty()) { 347 // request.getHeaders().remove("Cookie"); 348 //} 349 commitNewValues(request, "Cookie", headers); 350 } 351 352 /** 353 * Removes the cookies from the response that are suppressed or managed. 354 * 355 * @param response the response to suppress the cookies in. 356 */ 357 private void suppress(Response response) { 358 for (String name : RESPONSE_HEADERS) { 359 Header setCookieHeader = response.getHeaders().get(name); 360 if (setCookieHeader != null) { 361 List<String> headers = new ArrayList<>(setCookieHeader.getValues()); 362 for (ListIterator<String> hi = headers.listIterator(); hi.hasNext();) { 363 String header = hi.next(); 364 ArrayList<String> parts; 365 if ("Set-Cookie2".equals(name)) { 366 // RFC 2965 cookie 367 parts = new ArrayList<>(Arrays.asList(DELIM_COMMA.split(header, 0))); 368 } else { 369 // Netscape cookie 370 parts = new ArrayList<>(); 371 parts.add(header); 372 } 373 int originalSize = parts.size(); 374 for (ListIterator<String> pi = parts.listIterator(); pi.hasNext();) { 375 String part = pi.next(); 376 Action action = action((DELIM_EQUALS.split(part, 2))[0].trim()); 377 if (action == Action.SUPPRESS || action == Action.MANAGE) { 378 pi.remove(); 379 } 380 } 381 if (parts.size() == 0) { 382 hi.remove(); 383 } else if (parts.size() != originalSize) { 384 hi.set(joinAsString(",", parts)); 385 } 386 } 387 commitNewValues(response, name, headers); 388 } 389 } 390 } 391 392 private void commitNewValues(final Message message, final String name, final List<String> values) { 393 if (values.isEmpty()) { 394 message.getHeaders().remove(name); 395 } else { 396 message.getHeaders().put(name, values); 397 } 398 } 399 400 /** 401 * Creates and initializes a cookie filter in a heap environment. 402 */ 403 public static class Heaplet extends GenericHeaplet { 404 @Override 405 public Object create() throws HeapException { 406 CookieFilter filter = new CookieFilter(); 407 filter.suppressed.addAll(config.get("suppressed") 408 .defaultTo(Collections.emptyList()) 409 .asList(String.class)); 410 filter.relayed.addAll(config.get("relayed") 411 .defaultTo(Collections.emptyList()) 412 .asList(String.class)); 413 filter.managed.addAll(config.get("managed") 414 .defaultTo(Collections.emptyList()) 415 .asList(String.class)); 416 filter.defaultAction = config.get("defaultAction") 417 .defaultTo(filter.defaultAction.toString()) 418 .asEnum(Action.class); 419 return filter; 420 } 421 } 422}