You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2016/11/16 18:29:14 UTC
[4/6] brooklyn-server git commit: switch CSRF to use cookies for
tokens
switch CSRF to use cookies for tokens
now supports AngularJS semantics.
also now it doesn't needlessly create sessions.
Project: http://git-wip-us.apache.org/repos/asf/brooklyn-server/repo
Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-server/commit/c8186f8d
Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-server/tree/c8186f8d
Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-server/diff/c8186f8d
Branch: refs/heads/master
Commit: c8186f8df25a72caf885020b2536a94ae976df23
Parents: ce0db93
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Sun Nov 13 02:45:58 2016 +0000
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Sun Nov 13 09:39:52 2016 -0700
----------------------------------------------------------------------
.../brooklyn/rest/filter/CsrfTokenFilter.java | 272 +++++++++++++++----
.../rest/CsrfTokenFilterLauncherTest.java | 54 ++--
.../brooklyn/util/http/HttpToolResponse.java | 26 ++
3 files changed, 275 insertions(+), 77 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/c8186f8d/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/CsrfTokenFilter.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/CsrfTokenFilter.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/CsrfTokenFilter.java
index aa7e62a..67889bd 100644
--- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/CsrfTokenFilter.java
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/CsrfTokenFilter.java
@@ -19,69 +19,191 @@
package org.apache.brooklyn.rest.filter;
import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
import javax.annotation.Priority;
import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.HttpMethod;
+import javax.servlet.http.HttpSession;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.Context;
+import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
+import org.apache.brooklyn.core.mgmt.entitlement.Entitlements;
+import org.apache.brooklyn.rest.api.ServerApi;
import org.apache.brooklyn.rest.domain.ApiError;
import org.apache.brooklyn.util.text.Identifiers;
+import org.apache.brooklyn.util.text.Strings;
+import org.apache.commons.collections.EnumerationUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Predicates;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
/**
- * If client specifies {@link #REQUEST_CSRF_TOKEN_HEADER}
- * (as any of the values {@link #REQUEST_CSRF_TOKEN_HEADER_LOGIN}, {@link #REQUEST_CSRF_TOKEN_HEADER_NEW}, {@link #REQUEST_CSRF_TOKEN_HEADER_TRUE})
- * then the server will return a {@link #REQUIRED_CSRF_TOKEN_HEADER} header containing a token.
+ * Causes the server to return a "Csrf-Token" cookie in certain circumstances,
+ * and thereafter to require clients to send that token as an "X-Csrf-Token" header on certain requests.
+ * <p>
+ * To enable REST requests to work from the <code>br</code> CLI and regular <code>curl</code> without CSRF,
+ * this CSRF protection only applies when a session is in effect.
+ * Sessions are only established by some REST requests:
+ * {@link ServerApi#getUpExtended()} is a good choice (/v1/server/up/extended).
+ * It can also be forced by using the {@link #CSRF_TOKEN_REQUIRED_HEADER} header on any REST request.
+ * Browser-based UI clients should use one of the above methods early in operation.
+ * <p>
+ * The standard model is that the token is required for all <i>write</i> (ie non-GET) requests being made
+ * with a valid session cookie (ie the request is part of an ongoing session).
+ * In such cases, the client must send the X-Csrf-Token as a header.
+ * This prevents a third-party site from effecting a mutating cross-site request via the browser.
+ * <p>
+ * For transitional reasons, the default behaviour in the current implementation is to warn (not fail)
+ * if no token is supplied in the above case. This will likely be changed to enforce the standard model
+ * in a subsequent version, but it avoids breaking backwards compatibility for any existing session-based clients.
+ * <p>
+ * Clients can force different required behaviour (e.g. "fail") by including the
+ * {@link #CSRF_TOKEN_REQUIRED_HEADER} with one of the values in {@link CsrfTokenRequiredForRequests},
+ * viz. to require the header on ALL requests, or on NONE, instead of just on WRITE requests (the default).
+ * If such a mode is explicitly specified it is enforced (instead of just displaying the transitional warning),
+ * so while transitioning to enforce CSRF this header should be supplied.
+ * The Brooklyn UI does this.
+ * <p>
+ * This uses *two* names, <code>Csrf-Token</code> and <code>Xsrf-Token</code>.
+ * The former seems the usual convention, but Angular apps use the latter.
+ * This strategy should mean that Angular apps should get CSRF protection with no special configuration.
+ * <p>
+ * The cookies are sent by the client on every request, by virtue of being cookies,
+ * although this is not necessary (and rather wasteful). A client may optimise by deleting the cookies
+ * and caching the information in another form.
+ * The cookie names do not have any salt/uid, so in dev on localhost you may get conflicts, e.g. between
+ * multiple Brooklyn instances or other apps that use the same token names.
+ * (In contrast the session ID token, usually JSESSIONID, has a BROOKLYN<uid> field at runtime to prevent
+ * such collisions.)
* <p>
- * The server will require that header with the given token on subsequent POST requests
- * (and other actions -- excluding GET which is always permitted;
- * future enhancement may allow client to control whether GET is allowed or not).
+ * Additional CSRF strategies might be worth considering, in addition to the one described above:
+ * <ul>
+ * <li> Compare "Referer / Origin" against "Host" / "X-Forwarded-Host" (or app knowing its location)
+ * (disallowing if different is probably a good idea)
+ * <li> Look for "X-Requested-With: XMLHttpRequest" on POST, esp if there is cookie and/or user agent is a browser
+ * (but angular drops this one)
+ * </ul>
+ * More info at: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet .
+ * (These have not been implemented because the cookie pattern is sufficient, and one of the strongest.)
+ *
*/
@Provider
@Priority(400)
public class CsrfTokenFilter implements ContainerRequestFilter, ContainerResponseFilter {
- public static final String REQUIRED_CSRF_TOKEN_ATTR = CsrfTokenFilter.class.getName()+"."+"REQUIRED_CSRF_TOKEN";
+ private static final Logger log = LoggerFactory.getLogger(CsrfTokenFilter.class);
+
+ private static final String CSRF_TOKEN_VALUE_BASE = "Csrf-Token";
+ public static final String CSRF_TOKEN_VALUE_COOKIE = CSRF_TOKEN_VALUE_BASE.toUpperCase();
+ public static final String CSRF_TOKEN_VALUE_HEADER = HEADER_OF_COOKIE(CSRF_TOKEN_VALUE_BASE);
+ // also send this so angular works as expected
+ public static final String CSRF_TOKEN_VALUE_BASE_ANGULAR_NAME = "Xsrf-Token";
+ public static final String CSRF_TOKEN_VALUE_COOKIE_ANGULAR_NAME = CSRF_TOKEN_VALUE_BASE_ANGULAR_NAME.toUpperCase();
+ public static final String CSRF_TOKEN_VALUE_HEADER_ANGULAR_NAME = HEADER_OF_COOKIE(CSRF_TOKEN_VALUE_BASE_ANGULAR_NAME);
+
+ public static final String CSRF_TOKEN_VALUE_ATTR = CsrfTokenFilter.class.getName()+"."+CSRF_TOKEN_VALUE_COOKIE;
- public static final String REQUIRED_CSRF_TOKEN_HEADER = CsrfTokenFilter.class.getName()+"."+"X-CsrfToken";
- public static final String REQUEST_CSRF_TOKEN_HEADER = CsrfTokenFilter.class.getName()+"."+"X-CsrfToken-Request";
- /** means to return the token */
- public static final String REQUEST_CSRF_TOKEN_HEADER_TRUE = "true";
- /** means to create a new token */
- public static final String REQUEST_CSRF_TOKEN_HEADER_NEW = "new";
- /** means to create and return a token on login only (ie if one doesn't exist for the session) */
- public static final String REQUEST_CSRF_TOKEN_HEADER_LOGIN = "login";
+ public static String HEADER_OF_COOKIE(String cookieName) {
+ return "X-"+cookieName;
+ }
+ public static final String CSRF_TOKEN_REQUIRED_HEADER = "X-Csrf-Token-Required-For-Requests";
+ public static final String CSRF_TOKEN_REQUIRED_ATTR = CsrfTokenFilter.class.getName()+"."+CSRF_TOKEN_REQUIRED_HEADER;
+ public static enum CsrfTokenRequiredForRequests { ALL, WRITE, NONE, }
+ public static CsrfTokenRequiredForRequests DEFAULT_REQUIRED_FOR_REQUESTS = CsrfTokenRequiredForRequests.WRITE;
+
@Context
private HttpServletRequest request;
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
- Object requiredToken = request.getSession().getAttribute(REQUIRED_CSRF_TOKEN_ATTR);
- if (requiredToken!=null) {
- String suppliedToken = request.getHeader(REQUIRED_CSRF_TOKEN_HEADER);
- if (suppliedToken==null) {
- if (HttpMethod.GET.equals(requestContext.getMethod())) {
- // GETs are permitted with no token (but an invalid token will be caught)
- return;
- }
-
- fail(requestContext, ApiError.builder().errorCode(Response.Status.UNAUTHORIZED)
- .message(REQUIRED_CSRF_TOKEN_HEADER+" header required containing token previously supplied")
- .build());
- } else if (suppliedToken.equals(requiredToken)) {
- // approved
+ // if header supplied, check it is valid
+ String requiredWhenS = request.getHeader(CSRF_TOKEN_REQUIRED_HEADER);
+ if (Strings.isNonBlank(requiredWhenS) && getRequiredForRequests(requiredWhenS, null)==null) {
+ fail(requestContext, ApiError.builder().errorCode(Response.Status.BAD_REQUEST)
+ .message(HEADER_OF_COOKIE(CSRF_TOKEN_REQUIRED_HEADER)+" header if supplied must be one of "
+ + Arrays.asList(CsrfTokenRequiredForRequests.values()))
+ .build());
+ return;
+ }
+
+ if (!request.isRequestedSessionIdValid()) {
+ // no session; just return
+ return;
+ }
+
+ @SuppressWarnings("unchecked")
+ List<String> suppliedTokensDefault = (List<String>) EnumerationUtils.toList(request.getHeaders(CSRF_TOKEN_VALUE_HEADER));
+ @SuppressWarnings("unchecked")
+ List<String> suppliedTokensAngular = (List<String>) EnumerationUtils.toList(request.getHeaders(CSRF_TOKEN_VALUE_HEADER_ANGULAR_NAME));
+ List<String> suppliedTokens = Lists.newArrayList(suppliedTokensDefault);
+ suppliedTokens.addAll(suppliedTokensAngular);
+
+ Object requiredToken = request.getSession().getAttribute(CSRF_TOKEN_VALUE_ATTR);
+ CsrfTokenRequiredForRequests whenRequired = (CsrfTokenRequiredForRequests) request.getSession().getAttribute(CSRF_TOKEN_REQUIRED_ATTR);
+
+ boolean isRequired;
+
+ if (whenRequired==null) {
+ if (suppliedTokens.isEmpty()) {
+ log.warn("No CSRF token expected or supplied but a cookie-session is active: client should be updated"
+ + " (in future CSRF tokens or instructions may be required for session-based clients)"
+ + " - " + Entitlements.getEntitlementContext());
+ whenRequired = CsrfTokenRequiredForRequests.NONE;
} else {
- fail(requestContext, ApiError.builder().errorCode(Response.Status.UNAUTHORIZED)
- .message(REQUIRED_CSRF_TOKEN_HEADER+" header did not match current token")
- .build());
+ // default
+ whenRequired = DEFAULT_REQUIRED_FOR_REQUESTS;
}
+ // remember it to suppress warnings subsequently
+ request.getSession().setAttribute(CSRF_TOKEN_REQUIRED_ATTR, whenRequired);
+ }
+
+ switch (whenRequired) {
+ case NONE:
+ isRequired = false;
+ break;
+ case WRITE:
+ isRequired = !org.eclipse.jetty.http.HttpMethod.GET.toString().equals(requestContext.getMethod());
+ break;
+ case ALL:
+ isRequired = true;
+ break;
+ default:
+ log.warn("Unexpected "+CSRF_TOKEN_REQUIRED_ATTR+" value "+whenRequired);
+ isRequired = true;
+ }
+
+ if (Iterables.any(suppliedTokens, Predicates.equalTo(requiredToken))) {
+ // matching token supplied, or not being used
+ return;
+ }
+
+ if (!isRequired) {
+ // csrf not required, but it doesn't match
+ if (requiredToken!=null) {
+ log.trace("CSRF optional token mismatch: client did not send valid token, but it isn't required so proceeding");
+ }
+ return;
+ }
+
+ if (suppliedTokens.isEmpty()) {
+ fail(requestContext, ApiError.builder().errorCode(Response.Status.UNAUTHORIZED)
+ .message(HEADER_OF_COOKIE(CSRF_TOKEN_VALUE_COOKIE)+" header is required, containing token previously returned from server in cookie")
+ .build());
+ } else {
+ fail(requestContext, ApiError.builder().errorCode(Response.Status.UNAUTHORIZED)
+ .message(HEADER_OF_COOKIE(CSRF_TOKEN_VALUE_COOKIE)+" header did not match expected CSRF token")
+ .build());
}
}
@@ -91,37 +213,69 @@ public class CsrfTokenFilter implements ContainerRequestFilter, ContainerRespons
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
- String requestHeader = request.getHeader(REQUEST_CSRF_TOKEN_HEADER);
- if (requestHeader==null) return;
- String oldToken = (String) request.getSession().getAttribute(REQUIRED_CSRF_TOKEN_ATTR);
- String tokenToReturn = null;
- boolean createToken = false;
+ HttpSession session = request.getSession(false);
+ String token = (String) (session==null ? null : session.getAttribute(CSRF_TOKEN_VALUE_ATTR));
+ String requiredWhenS = request.getHeader(CSRF_TOKEN_REQUIRED_HEADER);
- if (REQUEST_CSRF_TOKEN_HEADER_LOGIN.equals(requestHeader)) {
- if (oldToken==null) createToken = true;
- else return;
- } else if (REQUEST_CSRF_TOKEN_HEADER_TRUE.equals(requestHeader)) {
- tokenToReturn = oldToken;
- if (tokenToReturn==null) createToken = true;
- } else if (REQUEST_CSRF_TOKEN_HEADER_NEW.equals(requestHeader)) {
- createToken = true;
- }
-
- if (createToken) {
- tokenToReturn = Identifiers.makeRandomId(16);
- request.getSession().setAttribute(REQUIRED_CSRF_TOKEN_ATTR, tokenToReturn);
+ if (session==null) {
+ if (Strings.isBlank(requiredWhenS)) {
+ // no session and no requirement specified, bail out
+ return;
+ }
+ // explicit requirement request forces a session
+ session = request.getSession();
}
- if (tokenToReturn==null) {
- fail(requestContext, ApiError.builder().errorCode(Response.Status.UNAUTHORIZED)
- .message(REQUEST_CSRF_TOKEN_HEADER+" contained invalid value; expected: "+
- REQUEST_CSRF_TOKEN_HEADER_TRUE+" | "+
- REQUEST_CSRF_TOKEN_HEADER_LOGIN+" | "+
- REQUEST_CSRF_TOKEN_HEADER_NEW)
- .build());
+ CsrfTokenRequiredForRequests requiredWhen;
+ if (Strings.isNonBlank(requiredWhenS)) {
+ requiredWhen = getRequiredForRequests(requiredWhenS, DEFAULT_REQUIRED_FOR_REQUESTS);
+ request.getSession().setAttribute(CSRF_TOKEN_REQUIRED_ATTR, requiredWhen);
+ if (Strings.isNonBlank(token)) {
+ // already set csrf token, and the client got it
+ // (with the session token if they are in a session;
+ // or if client didn't get it it isn't in a session)
+ return;
+ }
} else {
- responseContext.getHeaders().add(REQUIRED_CSRF_TOKEN_HEADER, tokenToReturn);
+ if (Strings.isNonBlank(token)) {
+ // already set csrf token, and the client got it
+ // (with the session token if they are in a session;
+ // or if client didn't get it it isn't in a session)
+ return;
+ }
+ requiredWhen = (CsrfTokenRequiredForRequests) request.getSession().getAttribute(CSRF_TOKEN_REQUIRED_ATTR);
+ if (requiredWhen==null) {
+ requiredWhen = DEFAULT_REQUIRED_FOR_REQUESTS;
+ request.getSession().setAttribute(CSRF_TOKEN_REQUIRED_ATTR, requiredWhen);
+ }
+ }
+
+ if (requiredWhen==CsrfTokenRequiredForRequests.NONE) {
+ // not required; so don't set
+ return;
+ }
+
+ // create the token
+ token = Identifiers.makeRandomId(16);
+ request.getSession().setAttribute(CSRF_TOKEN_VALUE_ATTR, token);
+
+ addCookie(responseContext, CSRF_TOKEN_VALUE_COOKIE, token, "Clients should send this value in header "+CSRF_TOKEN_VALUE_HEADER+" for validation");
+ addCookie(responseContext, CSRF_TOKEN_VALUE_COOKIE_ANGULAR_NAME, token, "Compatibility cookie for "+CSRF_TOKEN_VALUE_COOKIE+" following AngularJS conventions");
+ }
+
+ protected NewCookie addCookie(ContainerResponseContext responseContext, String cookieName, String token, String comment) {
+ NewCookie cookie = new NewCookie(cookieName, token, "/", null, comment, -1, false);
+ responseContext.getHeaders().add("Set-Cookie", cookie);
+ return cookie;
+ }
+
+ protected CsrfTokenRequiredForRequests getRequiredForRequests(String requiredWhenS, CsrfTokenRequiredForRequests defaultMode) {
+ CsrfTokenRequiredForRequests result = null;
+ if (requiredWhenS!=null) {
+ result = CsrfTokenRequiredForRequests.valueOf(requiredWhenS.toUpperCase());
}
+ if (result!=null) return result;
+ return defaultMode;
}
}
http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/c8186f8d/rest/rest-server/src/test/java/org/apache/brooklyn/rest/CsrfTokenFilterLauncherTest.java
----------------------------------------------------------------------
diff --git a/rest/rest-server/src/test/java/org/apache/brooklyn/rest/CsrfTokenFilterLauncherTest.java b/rest/rest-server/src/test/java/org/apache/brooklyn/rest/CsrfTokenFilterLauncherTest.java
index f9f2ec2..a9af873 100644
--- a/rest/rest-server/src/test/java/org/apache/brooklyn/rest/CsrfTokenFilterLauncherTest.java
+++ b/rest/rest-server/src/test/java/org/apache/brooklyn/rest/CsrfTokenFilterLauncherTest.java
@@ -23,6 +23,7 @@ import static org.testng.Assert.assertEquals;
import java.net.URI;
import java.util.List;
+import java.util.Map;
import javax.ws.rs.core.HttpHeaders;
@@ -48,49 +49,66 @@ public class CsrfTokenFilterLauncherTest extends BrooklynRestApiLauncherTestFixt
.withoutJsgui()
.start());
+ HttpClient client = client();
+
HttpToolResponse response = HttpTool.httpGet(
- client(), URI.create(getBaseUriRest() + "server/status"),
+ client, URI.create(getBaseUriRest() + "server/status"),
ImmutableMap.<String,String>of(
- CsrfTokenFilter.REQUEST_CSRF_TOKEN_HEADER, CsrfTokenFilter.REQUEST_CSRF_TOKEN_HEADER_TRUE ));
+ CsrfTokenFilter.CSRF_TOKEN_REQUIRED_HEADER, CsrfTokenFilter.CsrfTokenRequiredForRequests.WRITE.toString()));
// comes back okay
assertOkayResponse(response, "MASTER");
- System.out.println(response.getHeaderLists());
- List<String> tokens = response.getHeaderLists().get(CsrfTokenFilter.REQUIRED_CSRF_TOKEN_HEADER);
- String token = Iterables.getOnlyElement(tokens);
+ Map<String, List<String>> cookies = response.getCookieKeyValues();
+ String token = Iterables.getOnlyElement(cookies.get(CsrfTokenFilter.CSRF_TOKEN_VALUE_COOKIE));
Assert.assertNotNull(token);
+ String tokenAngular = Iterables.getOnlyElement(cookies.get(CsrfTokenFilter.CSRF_TOKEN_VALUE_COOKIE_ANGULAR_NAME));
+ Assert.assertEquals(token, tokenAngular);
- List<String> cookies = response.getHeaderLists().get(HttpHeaders.SET_COOKIE);
- String cookie = Iterables.getOnlyElement(cookies);
- Assert.assertNotNull(cookie);
-
// can post subsequently with token
response = HttpTool.httpPost(
- client(), URI.create(getBaseUriRest() + "script/groovy"),
+ client, URI.create(getBaseUriRest() + "script/groovy"),
ImmutableMap.<String,String>of(
HttpHeaders.CONTENT_TYPE, "application/text",
- HttpHeaders.COOKIE, cookie,
- CsrfTokenFilter.REQUIRED_CSRF_TOKEN_HEADER, token ),
+ CsrfTokenFilter.CSRF_TOKEN_VALUE_HEADER, token ),
"return 0;".getBytes());
assertOkayResponse(response, "{\"result\":\"0\"}");
// but fails without token
response = HttpTool.httpPost(
- client(), URI.create(getBaseUriRest() + "script/groovy"),
+ client, URI.create(getBaseUriRest() + "script/groovy"),
ImmutableMap.<String,String>of(
- HttpHeaders.COOKIE, cookie,
HttpHeaders.CONTENT_TYPE, "application/text" ),
"return 0;".getBytes());
assertEquals(response.getResponseCode(), HttpStatus.SC_UNAUTHORIZED);
- // but you can get subsequently without token
+ // can get without token
response = HttpTool.httpGet(
- client(), URI.create(getBaseUriRest() + "server/status"),
+ client, URI.create(getBaseUriRest() + "server/status"),
+ ImmutableMap.<String,String>of());
+ assertOkayResponse(response, "MASTER");
+
+ // but if we set required ALL then need a token to get
+ response = HttpTool.httpGet(
+ client, URI.create(getBaseUriRest() + "server/status"),
ImmutableMap.<String,String>of(
- HttpHeaders.COOKIE, cookie ));
-
+ CsrfTokenFilter.CSRF_TOKEN_REQUIRED_HEADER, CsrfTokenFilter.CsrfTokenRequiredForRequests.ALL.toString().toLowerCase()
+ ));
assertOkayResponse(response, "MASTER");
+ response = HttpTool.httpGet(
+ client, URI.create(getBaseUriRest() + "server/status"),
+ ImmutableMap.<String,String>of());
+ assertEquals(response.getResponseCode(), HttpStatus.SC_UNAUTHORIZED);
+
+ // however note if we use a new client, with no session, then we can post with no token
+ // (ie we don't guard against CSRF if your brooklyn is unsecured)
+ client = client();
+ response = HttpTool.httpPost(
+ client, URI.create(getBaseUriRest() + "script/groovy"),
+ ImmutableMap.<String,String>of(
+ HttpHeaders.CONTENT_TYPE, "application/text" ),
+ "return 0;".getBytes());
+ assertOkayResponse(response, "{\"result\":\"0\"}");
}
protected HttpClient client() {
http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/c8186f8d/utils/common/src/main/java/org/apache/brooklyn/util/http/HttpToolResponse.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/http/HttpToolResponse.java b/utils/common/src/main/java/org/apache/brooklyn/util/http/HttpToolResponse.java
index 811b3e1..27dc237 100644
--- a/utils/common/src/main/java/org/apache/brooklyn/util/http/HttpToolResponse.java
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/http/HttpToolResponse.java
@@ -39,8 +39,10 @@ import org.slf4j.LoggerFactory;
import com.google.common.base.Objects;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.io.ByteStreams;
+import com.google.common.net.HttpHeaders;
/**
* A response class for use with {@link HttpTool}.
@@ -149,6 +151,30 @@ public class HttpToolResponse {
return headerLists;
}
+ public Map<String, List<String>> getCookieKeyValues() {
+ List<String> cookiesRaw = getHeaderLists().get(HttpHeaders.SET_COOKIE);
+ Map<String, List<String>> result = Maps.newLinkedHashMap();
+ for (String c: cookiesRaw) {
+ // poor man's parse; would need to copy routines in jetty's CookieCutter (not in scope) or similar
+ // to treat quotes/escapes correctly, and ideally return typed cookies
+
+ int keyI = c.indexOf('=');
+ if (keyI<0) continue; //not a valid cookie
+ String key = c.substring(0, keyI);
+ String value = c.substring(keyI+1);
+ int flagsI = value.indexOf(';');
+ if (flagsI>=0) value = value.substring(0, flagsI);
+
+ List<String> values = result.get(key);
+ if (values==null) {
+ values = Lists.newArrayList();
+ result.put(key, values);
+ }
+ values.add(value);
+ }
+ return result;
+ }
+
public byte[] getContent() {
synchronized (mutex) {
if (content == null) {