package org.webpieces.router.impl; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.inject.Inject; import org.webpieces.ctx.api.CookieScope; import org.webpieces.ctx.api.RouterCookie; import org.webpieces.ctx.api.RouterRequest; import org.webpieces.router.api.RouterConfig; import org.webpieces.router.api.exceptions.BadCookieException; import org.webpieces.router.api.exceptions.CookieTooLargeException; import org.webpieces.router.impl.ctx.CookieScopeImpl; import org.webpieces.router.impl.ctx.SecureCookie; import org.webpieces.util.logging.Logger; import org.webpieces.util.logging.LoggerFactory; import org.webpieces.util.security.SecretKeyInfo; import org.webpieces.util.security.Security; public class CookieTranslator { private static final Logger log = LoggerFactory.getLogger(CookieTranslator.class); private static String VERSION = "1"; //private static final Logger log = LoggerFactory.getLogger(CookieTranslator.class); private RouterConfig config; private Security security; @Inject public CookieTranslator(RouterConfig config, Security security) { this.config = config; this.security = security; if(config.getSecretKey() == null) throw new IllegalArgumentException("secret key must be set"); } public void addScopeToCookieIfExist(List<RouterCookie> cookies, CookieScope cookie1) { if(!(cookie1 instanceof CookieScopeImpl)) throw new IllegalArgumentException("Cookie is not the right data type="+cookie1.getClass()+" needs to be of type "+CookieScopeImpl.class); CookieScopeImpl data = (CookieScopeImpl) cookie1; if(data.isNeedCreateSetCookie()) { log.debug(()->"translating cookie="+cookie1.getName()+" to send to browser"); RouterCookie cookie = translateScopeToCookie(data); cookies.add(cookie); } else if(data.isNeedCreateDeleteCookie()) { log.debug(()->"creating delete cookie for "+cookie1.getName()+" to send to browser"); RouterCookie cookie = createDeleteCookie(data.getName()); cookies.add(cookie); } else { log.debug(()->"not sending any cookie to browser for cookie="+cookie1.getName()); } } public RouterCookie translateScopeToCookie(CookieScopeImpl data) { try { return scopeToCookie(data); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } private RouterCookie scopeToCookie(CookieScopeImpl scopeData) throws UnsupportedEncodingException { Map<String, String> mapData = scopeData.getMapData(); RouterCookie cookie = createBase(scopeData.getName(), null); StringBuilder data = translateValuesToCookieFormat(mapData); String value = data.toString(); if(scopeData instanceof SecureCookie) { SecretKeyInfo key = config.getSecretKey(); String sign = security.sign(key, value); cookie.value = VERSION+"-"+sign+":"+value; } else { cookie.value = VERSION+":"+value; } if(cookie.value.length() > 4050) throw new CookieTooLargeException("Your webserver has put too many things into the session cookie and" + " browser will end up ignoring the cookie so we exception here to let you " + "know. Length of JUST the value(not whole cookie)="+cookie.value.length()+"\ncookie value="+cookie.value); return cookie; } public RouterCookie createDeleteCookie(String name) { return createBase(name, 0); } private RouterCookie createBase(String name, Integer maxAge) { RouterCookie cookie = new RouterCookie(); cookie.name= name; cookie.domain = null; cookie.path = "/"; cookie.maxAgeSeconds = maxAge; cookie.isHttpOnly = config.getIsCookiesHttpOnly(); cookie.isSecure = config.getIsCookiesSecure(); cookie.value = ""; return cookie; } private StringBuilder translateValuesToCookieFormat(Map<String, String> value) throws UnsupportedEncodingException { StringBuilder data = new StringBuilder(); String separator = ""; for (Map.Entry<String, String> entry : value.entrySet()) { String key = entry.getKey(); String val = entry.getValue(); String encodedKey = URLEncoder.encode(key, config.getUrlEncoding().name()); if(val == null) { continue; } else if (!"".equals(val)) { String encodedVal = URLEncoder.encode(val, config.getUrlEncoding().name()); data.append(separator) .append(encodedKey) .append("=") .append(encodedVal); } else { //append just key if null. must flash nulls or we would be resetting user changes in niche cases like clearing data or enums data.append(separator) .append(encodedKey); } separator = "&"; } return data; } public CookieScope translateCookieToScope(RouterRequest req, CookieScopeImpl data) { try { return cookieToScope(req, data); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } private CookieScope cookieToScope(RouterRequest req, CookieScopeImpl data) throws UnsupportedEncodingException { RouterCookie routerCookie = req.cookies.get(data.getName()); if(routerCookie == null) { data.setExisted(false); return data; } data.setExisted(true); Map<String, String> dataMap = new HashMap<>(); String value = routerCookie.value; int colonIndex = value.indexOf(":"); String version = value.substring(0, colonIndex); String keyValuePairs = value.substring(colonIndex+1); if(data instanceof SecureCookie) { String[] pair = version.split("-"); version = pair[0]; String expectedHash = pair[1]; String hash = security.sign(config.getSecretKey(), keyValuePairs); if(!hash.equals(expectedHash)) throw new BadCookieException("hashes don't match...This occurs if secret key" + " was switched, or loaded different webapp on same port or someone" + " created an invalid cookie and sent to your webserver", data.getName()); } if(!VERSION.equals(version)) throw new BadCookieException("versions don't match...This occurs if secret key" + " was switched, or loaded different webapp on same port or someone" + " created an invalid cookie and sent to your webserver", data.getName()); String[] pieces = keyValuePairs.split("&"); for(String piece : pieces) { String[] split = piece.split("="); if(split.length == 2) { String key = URLDecoder.decode(split[0], config.getUrlEncoding().name()); String val = URLDecoder.decode(split[1], config.getUrlEncoding().name()); dataMap.put(key, val); } else { String key = URLDecoder.decode(split[0], config.getUrlEncoding().name()); dataMap.put(key, ""); } } data.setMapData(dataMap); return data; } }