All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.day.crx.security.token.TokenCookie Maven / Gradle / Ivy

/*************************************************************************
*
* ADOBE CONFIDENTIAL
* ___________________
*
*  Copyright 1997 Adobe Systems Incorporated
*  All Rights Reserved.
*
* NOTICE:  All information contained herein is, and remains
* the property of Adobe Systems Incorporated and its suppliers,
* if any.  The intellectual and technical concepts contained
* herein are proprietary to Adobe Systems Incorporated and its
* suppliers and are protected by trade secret or copyright law.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from Adobe Systems Incorporated.
**************************************************************************/
package com.day.crx.security.token;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.BitSet;
import java.util.Map;
import java.util.TreeMap;
import java.util.Vector;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * TokenCookie provides methods to read and manipulate the value of
 * a token cookie.
 * 

* The TokenCookie value is extracted from a request as follows: *

    *
  1. If a Cookie named {@link #NAME} is present, its value is used
  2. *
  3. If a request parameter named {@link #PARAM_NAME} is present, its first * value is used
  4. *
*

* The value has the following format: * *

 * value  := info ( ";" info )* .
 * info   := [ repoid ":" ] workspace ":" token .
 * repoid := CRXClusterId | RepositorySystemId | RequestPort .
 * 
*/ public class TokenCookie { /** * default logger */ private static final Logger log = LoggerFactory.getLogger(TokenCookie.class); /** * Name of the cookie that provides the login token. */ public static final String NAME = "login-token"; /** * Name of the request header optionally providing the token cookie value * instead of the HTTP Cookie. * * @since 1.0.2 (Bundle version 2.2.0.2) */ public static final String PARAM_NAME = "j_login_token"; /** * name of the request attribute */ public static final String ATTR_NAME = TokenCookie.class.getName(); private final Map infos = new TreeMap<>(); public Map getInfos() { return infos; } /** * Returns the cookie from the request. First checks if decoded cookie is * already present as request attribute and reads if from the request * cookies if needed. * * @param request servlet request * @return a token cookie. */ public static TokenCookie fromRequest(HttpServletRequest request) { TokenCookie t = (TokenCookie) request.getAttribute(ATTR_NAME); if (t == null) { String tokenString = getCookie(request, NAME); if (tokenString == null || tokenString.length() == 0) { tokenString = request.getParameter(PARAM_NAME); } t = TokenCookie.fromString(tokenString); request.setAttribute(ATTR_NAME, t); } return t; } /** * Returns the token info for the given request, respecting the port * specified in the host header. *

* This implementation calls the * {@link #getTokenInfo(HttpServletRequest, String)} method using the * request port as returned from {@link #getPort(HttpServletRequest)} as the * repository ID. * * @param request the request * @return the info or {@link Info#INVALID} * @deprecated use {@link #getTokenInfo(HttpServletRequest, String)} instead */ @Deprecated public static Info getTokenInfo(HttpServletRequest request) { return getTokenInfo(request, getPort(request)); } /** * Returns the {@link Info} from the request for the given repository ID. * * @param request The request to extract the {@link Info} from * @param repoId The repository ID identifying the actual {@link Info} * instance from the {@link TokenCookie}. This must not be * null. * @return the info or {@link Info#INVALID} if no {@link Info} is available * for the given repository ID */ public static Info getTokenInfo(HttpServletRequest request, String repoId) { Info info = (Info) request.getAttribute(Info.ATTR_NAME); if (info == null) { TokenCookie t = fromRequest(request); info = t.getInfos().get(repoId); if (info == null) { info = Info.INVALID; } request.setAttribute(Info.ATTR_NAME, info); } return info; } /** * Returns the port form the host header. * * @param request request * @return the port. */ public static String getPort(HttpServletRequest request) { String host = request.getHeader("Host"); String port = ""; if (host == null || host.length() == 0) { log.warn( "Request to {} does not include a host header. Using default port.", request.getRequestURI()); } else { int idx = host.indexOf(':'); if (idx > 0) { port = host.substring(idx + 1); } } if (port.length() == 0) { port = request.isSecure() ? "443" : "80"; } return port; } /** * Updates the token cookie and sets the response cookie accordingly. if * token is null, the token information is * removed. *

* This implementation calls the * {@link #update(HttpServletRequest, HttpServletResponse, String, String, String, boolean)} * with the repository ID set to the request's port as returned from * #getport and not setting the HttpOnly cookie flag. * * @param request servlet request * @param response servlet response * @param token token * @param wsp workspace * @deprecated use * {@link #update(HttpServletRequest, HttpServletResponse, String, String, String, boolean)} * instead */ @Deprecated public static void update(HttpServletRequest request, HttpServletResponse response, String token, String wsp) { update(request, response, getPort(request), token, wsp, false); } /** * Updates the token cookie and sets the response cookie accordingly. if * token is null, the token information is * removed. * * @param request The request object providing the original token Cookie to * be updated by this method. * @param response The response object used to set the cookie on * @param repoId The repository ID identifying the {@link Info} whose token * value should be updated or removed. * @param token The actual token or null to remove the * {@link Info} for the repository ID from the cookie. * @param wsp The workspace which the token is mainly used to access. Ignored * if token is null. * @param isHttpOnly Whether or not to set the HttpOnly * attribute on the cookie. For security reasons it is * recommended to always set this parameter to true * . The parameter mainly exists for backwards compatibility * reasons to allow old use cases to still make the cookie * visible to client side JavaScript. */ public static void update(HttpServletRequest request, HttpServletResponse response, String repoId, String token, String wsp, boolean isHttpOnly) { TokenCookie t = TokenCookie.fromRequest(request); Info newInfo = Info.INVALID; if (token == null) { t.getInfos().remove(repoId); } else { newInfo = new Info(token, wsp); t.getInfos().put(repoId, newInfo); } request.setAttribute(Info.ATTR_NAME, newInfo); String path = request.getContextPath(); if (path == null || path.length() == 0) { path = "/"; } String v = t.toString(); if (v.length() == 0) { setCookie(response, NAME, v, 0, path, null, isHttpOnly, request.isSecure()); } else { setCookie(response, NAME, v, -1, path, null, isHttpOnly, request.isSecure()); } } /** * Decodes a token cookie value. *

* This is the reverse operation to the {@link TokenCookie#toString()} * method. * * @param value cookie value * @return a token cookie */ public static TokenCookie fromString(String value) { TokenCookie t = new TokenCookie(); if (value == null) { return t; } value = unescape(value); String[] infos = explode(value.trim(), ';', false); for (String info : infos) { String[] parts = explode(info.trim(), ':', true); if (parts.length != 3) { log.warn("invalid value in cookie: {}", info); continue; } t.infos.put(parts[0].trim(), new Info(unescape(parts[1].trim()), // token unescape(parts[2].trim()) // workspace )); } return t; } /** * Removes the info with the specified repository ID * * @param repoId The repository ID whose {@link Info} has to be removed * @return true if an {@link Info} object for the repository ID * existed and is now removed. */ public boolean remove(String repoId) { return infos.remove(repoId) != null; } /** * Returns the string representation of this token cookie. The value * returned by this method can be decoded with the * {@link #fromString(String)} method. * * @return the string */ @Override public String toString() { StringBuilder b = new StringBuilder(); String delim = ""; for (Map.Entry e : infos.entrySet()) { b.append(delim); if (e.getKey().length() > 0) { b.append(e.getKey()).append(":"); } b.append(e.getValue()); delim = ";"; } return escape(b.toString()); } /** * Retrieves the cookie with the given name from the request * * @param request servlet request * @param name the name * @return the cookie value or null if no cookie with the given * name exists whose value is not empty. */ public static String getCookie(HttpServletRequest request, String name) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (cookie.getName().equals(name) && !"".equals(cookie.getValue())) { return cookie.getValue(); } } } return null; } /** * Sets a cookie to the response * * @param response response * @param name cookie name * @param value value * @param maxAge maxAge * @param path path * @deprecated use * {@link #setCookie(HttpServletResponse, String, String, int, String, String, boolean, boolean)} * instead */ @Deprecated public static void setCookie(HttpServletResponse response, String name, String value, int maxAge, String path) { setCookie(response, name, value, maxAge, path, null, false, false); } /** * Sets a cookie to the response * * @param response response * @param name cookie name * @param value value * @param maxAge maxAge * @param path path * @param domain The cookie domain or null to not set an * explicit domain on the cookie. * @param isHttpOnly Whether to set (true) or not the * HttpOnly attribute on the cookie. It is not * recommended to set this parameter to false unless * the cookie must support certain use cases where it is * essential for the client side to have access to the cookie * despite the inherent security risks. * @param isSecure Whether to set (true) or not the * Secure attribute on the cookie. The value for * this parameter should be derived from the current request, * namely the ServletRequest.isSecure() method. */ public static void setCookie(HttpServletResponse response, String name, String value, int maxAge, String path, String domain, boolean isHttpOnly, boolean isSecure) { /* * The Servlet Spec 2.5 does not allow us to set the commonly used * HttpOnly attribute on cookies (Servlet API 3.0 does) so we create the * Set-Cookie header manually. See * http://www.owasp.org/index.php/HttpOnly for information on what the * HttpOnly attribute is used for. */ final StringBuilder header = new StringBuilder(); // default setup with name, value, cookie path and HttpOnly header.append(name).append("=").append(value); header.append("; Path=").append(path); // don't allow JS access if requested so if (isHttpOnly) { header.append("; HttpOnly"); } // set the cookie domain if so configured if (domain != null) { header.append("; Domain=").append(domain); } // Only set the Max-Age attribute to remove the cookie if (maxAge >= 0) { header.append("; Max-Age=").append(maxAge); } // ensure the cookie is secured if this is an https request if (isSecure) { header.append("; Secure"); } response.addHeader("Set-Cookie", header.toString()); } /** * holds a token / workspace pair */ public static class Info { public static final Info INVALID = new Info(null, null); /** * name of the request attribute */ public static final String ATTR_NAME = Info.class.getName(); public final String token; public final String workspace; public Info(String token, String workspace) { this.workspace = workspace; this.token = token; } public boolean isValid() { return token != null && token.length() > 0; } @Override public String toString() { if (token == null) { return ""; } final StringBuilder sb = new StringBuilder(); sb.append(escape(token)).append(":"); sb.append(escape(workspace)); return sb.toString(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Info info = (Info) o; if (token != null ? !token.equals(info.token) : info.token != null) return false; if (workspace != null ? !workspace.equals(info.workspace) : info.workspace != null) return false; return true; } @Override public int hashCode() { int result = token != null ? token.hashCode() : 0; result = 31 * result + (workspace != null ? workspace.hashCode() : 0); return result; } } private static final BitSet URISaveEx = new BitSet(256); static { int i; for (i = 'a'; i <= 'z'; i++) { URISaveEx.set(i); } for (i = 'A'; i <= 'Z'; i++) { URISaveEx.set(i); } for (i = '0'; i <= '9'; i++) { URISaveEx.set(i); } URISaveEx.set('-'); URISaveEx.set('_'); URISaveEx.set('.'); URISaveEx.set('!'); URISaveEx.set('~'); URISaveEx.set('*'); URISaveEx.set('\''); URISaveEx.set('('); URISaveEx.set(')'); URISaveEx.set('/'); } /** * used for the md5 */ private final static char[] hexTable="0123456789abcdef".toCharArray(); /** * Does an URL encoding of the string using the * escape character. The characters that don't need encoding * are those defined 'unreserved' in section 2.3 of the 'URI genric syntax' * RFC 2396, but without the escape character. If isPath is * true, additionally the slash '/' is ignored, too. * * @param string the string to encode. * @param escape the escape character. * @param isPath if true, the string is treated as path * * @return the escaped string * * @throws NullPointerException if string is null. */ private static String escape(String string) { try { BitSet validChars = URISaveEx; byte[] bytes = string.getBytes("utf-8"); StringBuffer out = new StringBuffer(bytes.length); for (int i = 0; i < bytes.length; i++) { int c = bytes[i]&0xff; if (validChars.get(c) && c!='%') { out.append((char)c); } else { out.append('%'); out.append(hexTable[(c>>4) & 0x0f]); out.append(hexTable[(c ) & 0x0f]); } } return out.toString(); } catch (UnsupportedEncodingException e) { throw new InternalError(e.toString()); } } /** * Does a URL decoding of the string using the * escape character. Please note that in opposite to the * {@link java.net.URLDecoder} it does not transform the + into spaces. * @param string the string to decode * @param escape the escape character * @return the decoded string * * @throws NullPointerException if string is null. * @throws ArrayIndexOutOfBoundsException if not enough character follow an * escape character * @throws IllegalArgumentException if the 2 characters following the escape * character do not represent a hex-number. */ private static String unescape(String string) { ByteArrayOutputStream out = new ByteArrayOutputStream(string.length()*2); int lastPos=0; int pos; while ((pos=string.indexOf('%', lastPos))>=0) { try { out.write(string.substring(lastPos, pos).getBytes("utf-8")); } catch (IOException e) { throw new InternalError(e.toString()); } lastPos = pos+3; if (lastPos<=string.length()) { try { out.write(Integer.parseInt(string.substring(pos+1, lastPos),16)); } catch (NumberFormatException e) { throw new IllegalArgumentException(); } } else { // not enough characters, ignore rest } } if (lastPos>=0 && lastPos<=string.length()) { try { out.write(string.substring(lastPos).getBytes("utf-8")); } catch (IOException e) { throw new InternalError(e.toString()); } } try { return new String(out.toByteArray(), "utf-8"); } catch (UnsupportedEncodingException e) { throw new InternalError(e.toString()); } } /** * returns an array of strings decomposed of the original string, split at * every occurance of 'ch'. * @param str the string to decompose * @param ch the character to use a split pattern * @param respectEmpty if true, empty elements are generated * @return an array of strings */ private static String[] explode(final String str, final int ch, final boolean respectEmpty) { Vector strings = new Vector<>(); int pos; int lastpos = 0; // add snipples while ((pos = str.indexOf(ch, lastpos)) >= 0) { if (pos-lastpos>0 || respectEmpty) strings.add(str.substring(lastpos, pos)); lastpos = pos+1; } // add rest if (lastpos < str.length()) { strings.add(str.substring(lastpos)); } else if (respectEmpty && lastpos==str.length()) { strings.add(""); } // return stringarray return strings.toArray(new String[strings.size()]); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy