/* GNU GENERAL PUBLIC LICENSE Copyright (C) 2006 The Lobo Project This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either verion 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Contact info: lobochief@users.sourceforge.net */ /* * Created on Jun 1, 2005 */ package org.lobobrowser.request; import java.io.IOException; import java.net.URI; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.javatuples.Pair; import org.jooq.DSLContext; import org.jooq.Result; import org.lobobrowser.main.PlatformInit; import org.lobobrowser.store.RestrictedStore; import org.lobobrowser.store.StorageManager; import info.gngr.db.tables.Cookies; import info.gngr.db.tables.records.CookiesRecord; /** * @author J. H. S. */ public class CookieStore { private static final String COOKIE_PATH_PREFIX = ".W$Cookies/"; private static final String COOKIE_PATH_PATTERN = "\\.W\\$Cookies/.*"; private static final CookieStore instance = new CookieStore(); private static final Logger logger = Logger.getLogger(CookieStore.class.getName()); private final Map<String, Map<Pair<String, String>, CookieValue>> transientMapByHost = new HashMap<>(); private CookieStore() { } public static CookieStore getInstance() { return instance; } public void saveCookie(final URI url, final String cookieSpec) { final String urlHostName = url.getHost(); if (logger.isLoggable(Level.INFO)) { logger.info("saveCookie(): host=" + urlHostName + ",cookieSpec=[" + cookieSpec + "]"); } final Optional<CookieDetails> cookieDetailsOpt = CookieParsing.parseCookieSpec(url, cookieSpec); if (cookieDetailsOpt.isPresent()) { final CookieDetails cookieDetails = cookieDetailsOpt.get(); if (PlatformInit.getInstance().debugOn) { System.out.println("Cookie details: " + cookieDetails); } if (cookieDetails.name == null) { logger.log(Level.SEVERE, "saveCookie(): Invalid name in cookie spec from '" + urlHostName + "'"); return; } if (!cookieDetails.isValidDomain()) { logger.log(Level.SEVERE, "saveCookie(): Invalid domain in cookie spec from '" + urlHostName + "'"); return; } this.saveCookie(cookieDetails); } } private void saveCookie(final CookieDetails cookieDetails) { final String name = cookieDetails.name; final String domain = cookieDetails.getEffectiveDomain(); final String domainTL = domain.toLowerCase(); final Optional<java.util.Date> expiresOpt = cookieDetails.getExpiresDate(); if (logger.isLoggable(Level.INFO)) { logger.info("saveCookie(): " + cookieDetails); } final Optional<Long> expiresLongOpt = expiresOpt.map(e -> e.getTime()); final CookieValue cookieValue = new CookieValue(cookieDetails.name, cookieDetails.value, cookieDetails.getEffectivePath(), expiresLongOpt, cookieDetails.secure, cookieDetails.httpOnly, getMonotonicTime()); synchronized (this) { // Always save a transient cookie. It acts as a cache. Map<Pair<String, String>, CookieValue> hostMap = this.transientMapByHost.get(domainTL); if (hostMap == null) { hostMap = new HashMap<>(2); this.transientMapByHost.put(domainTL, hostMap); } hostMap.put(new Pair<>(cookieDetails.name, cookieDetails.getEffectivePath()), cookieValue); } if (expiresLongOpt.isPresent()) { final DSLContext userDB = StorageManager.getInstance().getDB(); userDB .mergeInto(Cookies.COOKIES) .values(domainTL, name, cookieValue.getValue(), cookieValue.getPath(), true, true, cookieValue.getCreationTime(), cookieValue.getExpires().orElse(null)) .execute(); } } // This should be 1000 * 1000, but for optimization has been converted to 1024*1024 // TODO: This seems like premature optimisation, since cookies are not that frequently stored. private static final long MILLION_LIKE = 1024 * 1024; private long previousTimeNanos = System.currentTimeMillis() * MILLION_LIKE; private long getMonotonicTime() { final long previousTimeMillis = previousTimeNanos / MILLION_LIKE; final long currentMillis = System.currentTimeMillis(); if (previousTimeMillis == currentMillis) { previousTimeNanos += 1; return previousTimeNanos; } else { final long currentNanos = currentMillis * MILLION_LIKE; previousTimeNanos = currentNanos; return currentNanos; } } private static String getPathFromCookieName(final String cookieName) { return COOKIE_PATH_PREFIX + cookieName; } private static String getCookieNameFromPath(final String path) { if (!path.startsWith(COOKIE_PATH_PREFIX)) { throw new IllegalArgumentException("Invalid path: " + path); } return path.substring(COOKIE_PATH_PREFIX.length()); } /* Path-match algorithm as per section 5.4.1 of RFC 6264. */ private static boolean pathMatch(final String cookiePath, final String requestPath) { if (cookiePath.equals(requestPath)) { return true; } else if (requestPath.startsWith(cookiePath)) { return ((cookiePath.charAt(cookiePath.length() - 1) == '/') || (requestPath.charAt(cookiePath.length()) == '/')); } else { return false; } } /** * Gets cookies belonging exactly to the host name given, not to a broader * domain. */ private List<CookieValue> getCookiesStrict(final String protocol, final String hostName, String path) { final String hostNameTL = hostName.toLowerCase(); if ((path == null) || (path.length() == 0)) { path = "/"; // TODO: Confirm that this is correct. Issue #14 in browserTesting } final boolean secureProtocol = "https".equalsIgnoreCase(protocol); final boolean liflag = logger.isLoggable(Level.INFO); // final Set<String> transientCookieNames = new HashSet<>(); final Set<Pair<String, String>> transientCookieNames = new HashSet<>(); final List<CookieValue> selectedCookies = new LinkedList<>(); synchronized (this) { final Map<Pair<String, String>, CookieValue> hostMap = this.transientMapByHost.get(hostNameTL); if (hostMap != null) { final Iterator<Map.Entry<Pair<String, String>, CookieValue>> i = hostMap.entrySet().iterator(); while (i.hasNext()) { final Map.Entry<Pair<String, String>, CookieValue> entry = i.next(); final CookieValue cookieValue = entry.getValue(); if (cookieValue.isExpired()) { if (liflag) { logger.info("getCookiesStrict(): Cookie " + entry.getKey() + " from " + hostNameTL + " expired: " + cookieValue.getExpires()); } } else { if (pathMatch(cookieValue.getPath(), path)) { if (cookieValue.checkSecure(secureProtocol)) { final Pair<String, String> cookieNameAndPath = entry.getKey(); // transientCookieNames.add(new Pair<>(cookieName, cookieValue.getPath())); transientCookieNames.add(cookieNameAndPath); selectedCookies.add(cookieValue); } } else { if (liflag) { logger.info("getCookiesStrict(): Skipping cookie " + cookieValue + " since it does not match path " + path); } } } } } } try { final RestrictedStore store = StorageManager.getInstance().getRestrictedStore(hostNameTL, false); if (store != null) { final DSLContext userDB = StorageManager.getInstance().getDB(); final Result<CookiesRecord> cookieResult = userDB.selectFrom(Cookies.COOKIES) .where(Cookies.COOKIES.HOSTNAME.eq(hostNameTL)) .fetch(); if (cookieResult.isNotEmpty()) { final CookiesRecord cookiesRecord = cookieResult.get(0); final String cookieName = cookiesRecord.getName(); final CookieValue cookieValue = new CookieValue( cookiesRecord.getName(), cookiesRecord.getValue(), cookiesRecord.getPath(), Optional.ofNullable(cookiesRecord.getExpirationtime()), cookiesRecord.getSecure(), cookiesRecord.getHttponly(), cookiesRecord.getCreationtime() ); if (!transientCookieNames.contains(new Pair<>(cookieName, cookieValue.getPath()))) { if (cookieValue.isExpired()) { if (logger.isLoggable(Level.INFO)) { logger.info("getCookiesStrict(): Cookie " + cookieName + " from " + hostName + " expired: " + cookieValue.getExpires()); } cookiesRecord.delete(); } else { if (pathMatch(cookieValue.getPath(), path)) { // Found one that is not in main memory. Cache it. synchronized (this) { Map<Pair<String, String>, CookieValue> hostMap = this.transientMapByHost.get(hostName); if (hostMap == null) { hostMap = new HashMap<>(); this.transientMapByHost.put(hostName, hostMap); } hostMap.put(new Pair<>(cookieName, cookieValue.getPath()), cookieValue); } if (cookieValue.checkSecure(secureProtocol)) { // Now add cookie to the collection. selectedCookies.add(cookieValue); } } else { if (logger.isLoggable(Level.INFO)) { logger.info("getCookiesStrict(): Skipping cookie " + cookieValue + " since it does not match path " + path); } } } } } } } catch (final IOException ioe) { logger.log(Level.SEVERE, "getCookiesStrict()", ioe); } return selectedCookies; } public Collection<Cookie> getCookies(final String protocol, final String hostName, final String path) { // Security provided by RestrictedStore. final Collection<String> possibleDomains = DomainValidation.getPossibleDomains(hostName); final List<CookieValue> allCookies = new LinkedList<>(); for (final String domain : possibleDomains) { allCookies.addAll(this.getCookiesStrict(protocol, domain, path)); } allCookies.sort(null); final List<Cookie> cookies = new LinkedList<>(); for (final CookieValue cookieValue : allCookies) { cookies.add(new Cookie(cookieValue.getName(), cookieValue.getValue())); } if (logger.isLoggable(Level.INFO)) { logger.info("getCookies(): For host=" + hostName + ", found " + cookies.size() + " cookies: " + cookies); } return cookies; } }