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