CookieStoreTest.java

/*
 *    Copyright (c) 2017-2023 AsyncHttpClient Project. All rights reserved.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.asynchttpclient;

import io.github.artsok.RepeatedIfExceptionsTest;
import io.netty.handler.codec.http.cookie.ClientCookieDecoder;
import io.netty.handler.codec.http.cookie.ClientCookieEncoder;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import org.asynchttpclient.cookie.CookieStore;
import org.asynchttpclient.cookie.ThreadSafeCookieStore;
import org.asynchttpclient.uri.Uri;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class CookieStoreTest {

    private static final Logger logger = LoggerFactory.getLogger(CookieStoreTest.class);

    @BeforeEach
    public void setUpGlobal() {
        logger.info("Local HTTP server started successfully");
        System.out.println("--Start");
    }

    @AfterEach
    public void tearDownGlobal() {
        System.out.println("--Stop");
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void runAllSequentiallyBecauseNotThreadSafe() throws Exception {
        addCookieWithEmptyPath();
        dontReturnCookieForAnotherDomain();
        returnCookieWhenItWasSetOnSamePath();
        returnCookieWhenItWasSetOnParentPath();
        dontReturnCookieWhenDomainMatchesButPathIsDifferent();
        dontReturnCookieWhenDomainMatchesButPathIsParent();
        returnCookieWhenDomainMatchesAndPathIsChild();
        returnCookieWhenItWasSetOnSubdomain();
        replaceCookieWhenSetOnSameDomainAndPath();
        dontReplaceCookiesWhenTheyHaveDifferentName();
        expireCookieWhenSetWithDateInThePast();
        cookieWithSameNameMustCoexistIfSetOnDifferentDomains();
        handleMissingDomainAsRequestHost();
        handleMissingPathAsSlash();
        returnTheCookieWheniTSissuedFromRequestWithSubpath();
        handleMissingPathAsRequestPathWhenFromRootDir();
        handleMissingPathAsRequestPathWhenPathIsNotEmpty();
        handleDomainInCaseInsensitiveManner();
        handleCookieNameInCaseInsensitiveManner();
        handleCookiePathInCaseSensitiveManner();
        ignoreQueryParametersInUri();
        shouldServerOnSubdomainWhenDomainMatches();
        replaceCookieWhenSetOnSamePathBySameUri();
        handleMultipleCookieOfSameNameOnDifferentPaths();
        handleTrailingSlashesInPaths();
        returnMultipleCookiesEvenIfTheyHaveSameName();
        shouldServeCookiesBasedOnTheUriScheme();
        shouldAlsoServeNonSecureCookiesBasedOnTheUriScheme();
        shouldNotServeSecureCookiesForDefaultRetrievedHttpUriScheme();
        shouldServeSecureCookiesForSpecificallyRetrievedHttpUriScheme();
        shouldCleanExpiredCookieFromUnderlyingDataStructure();
    }

    private static void addCookieWithEmptyPath() {
        CookieStore store = new ThreadSafeCookieStore();
        Uri uri = Uri.create("http://www.foo.com");
        store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; path="));
        assertFalse(store.get(uri).isEmpty());
    }

    private static void dontReturnCookieForAnotherDomain() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("http://www.foo.com"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; path="));
        assertTrue(store.get(Uri.create("http://www.bar.com")).isEmpty());
    }

    private static void returnCookieWhenItWasSetOnSamePath() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("http://www.foo.com"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; path=/bar/"));
        assertEquals(1, store.get(Uri.create("http://www.foo.com/bar/")).size());
    }

    private static void returnCookieWhenItWasSetOnParentPath() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("http://www.foo.com"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/bar"));
        assertEquals(1, store.get(Uri.create("http://www.foo.com/bar/baz")).size());
    }

    private static void dontReturnCookieWhenDomainMatchesButPathIsDifferent() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("http://www.foo.com/bar"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/bar"));
        assertTrue(store.get(Uri.create("http://www.foo.com/baz")).isEmpty());
    }

    private static void dontReturnCookieWhenDomainMatchesButPathIsParent() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("http://www.foo.com/bar"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/bar"));
        assertTrue(store.get(Uri.create("http://www.foo.com")).isEmpty());
    }

    private static void returnCookieWhenDomainMatchesAndPathIsChild() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("http://www.foo.com/bar"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/bar"));
        assertEquals(1, store.get(Uri.create("http://www.foo.com/bar/baz")).size());
    }

    private static void returnCookieWhenItWasSetOnSubdomain() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("http://www.foo.com"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=.foo.com"));
        assertEquals(1, store.get(Uri.create("http://bar.foo.com")).size());
    }

    private static void replaceCookieWhenSetOnSameDomainAndPath() {
        CookieStore store = new ThreadSafeCookieStore();
        Uri uri = Uri.create("http://www.foo.com/bar/baz");
        store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/bar"));
        store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=VALUE2; Domain=www.foo.com; path=/bar"));
        assertEquals(1, store.getAll().size());
        assertEquals("VALUE2", store.get(uri).get(0).value());
    }

    private static void dontReplaceCookiesWhenTheyHaveDifferentName() {
        CookieStore store = new ThreadSafeCookieStore();
        Uri uri = Uri.create("http://www.foo.com/bar/baz");
        store.add(uri, ClientCookieDecoder.LAX.decode("BETA=VALUE1; Domain=www.foo.com; path=/bar"));
        store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=VALUE2; Domain=www.foo.com; path=/bar"));
        assertEquals(2, store.get(uri).size());
    }

    private static void expireCookieWhenSetWithDateInThePast() {
        CookieStore store = new ThreadSafeCookieStore();
        Uri uri = Uri.create("http://www.foo.com/bar");
        store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/bar"));
        store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=EXPIRED; Domain=www.foo.com; Path=/bar; Expires=Sun, 06 Nov 1994 08:49:37 GMT"));
        assertTrue(store.getAll().isEmpty());
    }

    private static void cookieWithSameNameMustCoexistIfSetOnDifferentDomains() {
        CookieStore store = new ThreadSafeCookieStore();
        Uri uri1 = Uri.create("http://www.foo.com");
        store.add(uri1, ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com"));
        Uri uri2 = Uri.create("http://www.bar.com");
        store.add(uri2, ClientCookieDecoder.LAX.decode("ALPHA=VALUE2; Domain=www.bar.com"));

        assertEquals(1, store.get(uri1).size());
        assertEquals("VALUE1", store.get(uri1).get(0).value());

        assertEquals(1, store.get(uri2).size());
        assertEquals("VALUE2", store.get(uri2).get(0).value());
    }

    private static void handleMissingDomainAsRequestHost() {
        CookieStore store = new ThreadSafeCookieStore();
        Uri uri = Uri.create("http://www.foo.com");
        store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Path=/"));
        assertEquals(1, store.get(uri).size());
    }

    private static void handleMissingPathAsSlash() {
        CookieStore store = new ThreadSafeCookieStore();
        Uri uri = Uri.create("http://www.foo.com");
        store.add(uri, ClientCookieDecoder.LAX.decode("tooe_token=0b1d81dd02d207491a6e9b0a2af9470da9eb1dad"));
        assertEquals(1, store.get(uri).size());
    }

    private static void returnTheCookieWheniTSissuedFromRequestWithSubpath() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("http://www.foo.com/bar"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE; path=/"));
        assertEquals(1, store.get(Uri.create("http://www.foo.com")).size());
    }

    private static void handleMissingPathAsRequestPathWhenFromRootDir() {
        CookieStore store = new ThreadSafeCookieStore();
        Uri uri = Uri.create("http://www.foo.com");
        store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=VALUE1"));
        assertEquals(1, store.get(uri).size());
    }

    private static void handleMissingPathAsRequestPathWhenPathIsNotEmpty() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("http://www.foo.com/bar"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/bar"));
        assertTrue(store.get(Uri.create("http://www.foo.com/baz")).isEmpty());
    }

    // RFC 2965 sec. 3.3.3
    private static void handleDomainInCaseInsensitiveManner() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("http://www.foo.com/bar"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1"));
        assertEquals(1, store.get(Uri.create("http://www.FoO.com/bar")).size());
    }

    // RFC 2965 sec. 3.3.3
    private static void handleCookieNameInCaseInsensitiveManner() {
        CookieStore store = new ThreadSafeCookieStore();
        Uri uri = Uri.create("http://www.foo.com/bar/baz");
        store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/bar"));
        store.add(uri, ClientCookieDecoder.LAX.decode("alpha=VALUE2; Domain=www.foo.com; path=/bar"));
        assertEquals(1, store.getAll().size());
        assertEquals("VALUE2", store.get(uri).get(0).value());
    }

    // RFC 2965 sec. 3.3.3
    private static void handleCookiePathInCaseSensitiveManner() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("http://www.foo.com/foo/bar"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1"));
        assertTrue(store.get(Uri.create("http://www.FoO.com/Foo/bAr")).isEmpty());
    }

    private static void ignoreQueryParametersInUri() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("http://www.foo.com/bar?query1"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/"));
        assertEquals(1, store.get(Uri.create("http://www.foo.com/bar?query2")).size());
    }

    // RFC 6265, 5.1.3.  Domain Matching
    private static void shouldServerOnSubdomainWhenDomainMatches() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("https://x.foo.org/"), ClientCookieDecoder.LAX.decode("cookie1=VALUE1; Path=/; Domain=foo.org;"));
        assertEquals(1, store.get(Uri.create("https://y.x.foo.org/")).size());
    }

    // NOTE: Similar to replaceCookieWhenSetOnSameDomainAndPath()
    private static void replaceCookieWhenSetOnSamePathBySameUri() {
        CookieStore store = new ThreadSafeCookieStore();
        Uri uri = Uri.create("https://foo.org/");
        store.add(uri, ClientCookieDecoder.LAX.decode("cookie1=VALUE1; Path=/"));
        store.add(uri, ClientCookieDecoder.LAX.decode("cookie1=VALUE2; Path=/"));
        store.add(uri, ClientCookieDecoder.LAX.decode("cookie1=VALUE3; Path=/"));
        assertEquals(1, store.getAll().size());
        assertEquals("VALUE3", store.get(uri).get(0).value());
    }

    private static void handleMultipleCookieOfSameNameOnDifferentPaths() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("http://www.foo.com"), ClientCookieDecoder.LAX.decode("cookie=VALUE0; path=/"));
        store.add(Uri.create("http://www.foo.com/foo/bar"), ClientCookieDecoder.LAX.decode("cookie=VALUE1; path=/foo/bar/"));
        store.add(Uri.create("http://www.foo.com/foo/baz"), ClientCookieDecoder.LAX.decode("cookie=VALUE2; path=/foo/baz/"));

        Uri uri1 = Uri.create("http://www.foo.com/foo/bar/");
        List<Cookie> cookies1 = store.get(uri1);
        assertEquals(2, cookies1.size());
        assertEquals(2, cookies1.stream().filter(c -> "VALUE0".equals(c.value()) || "VALUE1".equals(c.value())).count());

        Uri uri2 = Uri.create("http://www.foo.com/foo/baz/");
        List<Cookie> cookies2 = store.get(uri2);
        assertEquals(2, cookies2.size());
        assertEquals(2, cookies2.stream().filter(c -> "VALUE0".equals(c.value()) || "VALUE2".equals(c.value())).count());
    }

    private static void handleTrailingSlashesInPaths() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(
                Uri.create("https://vagrant.moolb.com/app/consumer/j_spring_cas_security_check?ticket=ST-5-Q7gzqPpvG3N3Bb02bm3q-llinder-vagrantmgr.moolb.com"),
                ClientCookieDecoder.LAX.decode("JSESSIONID=211D17F016132BCBD31D9ABB31D90960; Path=/app/consumer/; HttpOnly"));
        assertEquals(1, store.getAll().size());
        assertEquals("211D17F016132BCBD31D9ABB31D90960", store.get(Uri.create("https://vagrant.moolb.com/app/consumer/")).get(0).value());
    }

    private static void returnMultipleCookiesEvenIfTheyHaveSameName() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("http://foo.com"), ClientCookieDecoder.LAX.decode("JSESSIONID=FOO; Domain=.foo.com"));
        store.add(Uri.create("http://sub.foo.com"), ClientCookieDecoder.LAX.decode("JSESSIONID=BAR; Domain=sub.foo.com"));

        Uri uri1 = Uri.create("http://sub.foo.com");
        List<Cookie> cookies1 = store.get(uri1);
        assertEquals(2, cookies1.size());
        assertEquals(2, cookies1.stream().filter(c -> "FOO".equals(c.value()) || "BAR".equals(c.value())).count());

        List<String> encodedCookieStrings = cookies1.stream().map(ClientCookieEncoder.LAX::encode).collect(Collectors.toList());
        assertTrue(encodedCookieStrings.contains("JSESSIONID=FOO"));
        assertTrue(encodedCookieStrings.contains("JSESSIONID=BAR"));
    }

    // rfc6265#section-1 Cookies for a given host are shared  across all the ports on that host
    private static void shouldServeCookiesBasedOnTheUriScheme() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("https://foo.org/moodle/"), ClientCookieDecoder.LAX.decode("cookie1=VALUE1; Path=/"));
        store.add(Uri.create("https://foo.org:443/moodle/login"), ClientCookieDecoder.LAX.decode("cookie1=VALUE2; Path=/"));
        store.add(Uri.create("https://foo.org:443/moodle/login"), ClientCookieDecoder.LAX.decode("cookie1=VALUE3; Path=/; Secure"));

        Uri uri = Uri.create("https://foo.org/moodle/login");
        assertEquals(1, store.getAll().size());
        assertEquals("VALUE3", store.get(uri).get(0).value());
        assertTrue(store.get(uri).get(0).isSecure());
    }

    // rfc6265#section-1 Cookies for a given host are shared  across all the ports on that host
    private static void shouldAlsoServeNonSecureCookiesBasedOnTheUriScheme() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("https://foo.org/moodle/"), ClientCookieDecoder.LAX.decode("cookie1=VALUE1; Path=/"));
        store.add(Uri.create("https://foo.org:443/moodle/login"), ClientCookieDecoder.LAX.decode("cookie1=VALUE2; Path=/"));
        store.add(Uri.create("https://foo.org:443/moodle/login"), ClientCookieDecoder.LAX.decode("cookie1=VALUE3; Path=/; HttpOnly"));

        Uri uri = Uri.create("https://foo.org/moodle/login");
        assertEquals(1, store.getAll().size());
        assertEquals("VALUE3", store.get(uri).get(0).value());
        assertFalse(store.get(uri).get(0).isSecure());
    }

    // rfc6265#section-1 Cookies for a given host are shared  across all the ports on that host
    private static void shouldNotServeSecureCookiesForDefaultRetrievedHttpUriScheme() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("https://foo.org/moodle/"), ClientCookieDecoder.LAX.decode("cookie1=VALUE1; Path=/"));
        store.add(Uri.create("https://foo.org:443/moodle/login"), ClientCookieDecoder.LAX.decode("cookie1=VALUE2; Path=/"));
        store.add(Uri.create("https://foo.org:443/moodle/login"), ClientCookieDecoder.LAX.decode("cookie1=VALUE3; Path=/; Secure"));

        Uri uri = Uri.create("http://foo.org/moodle/login");
        assertTrue(store.get(uri).isEmpty());
    }

    // rfc6265#section-1 Cookies for a given host are shared  across all the ports on that host
    private static void shouldServeSecureCookiesForSpecificallyRetrievedHttpUriScheme() {
        CookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("https://foo.org/moodle/"), ClientCookieDecoder.LAX.decode("cookie1=VALUE1; Path=/"));
        store.add(Uri.create("https://foo.org:443/moodle/login"), ClientCookieDecoder.LAX.decode("cookie1=VALUE2; Path=/"));
        store.add(Uri.create("https://foo.org:443/moodle/login"), ClientCookieDecoder.LAX.decode("cookie1=VALUE3; Path=/; Secure"));

        Uri uri = Uri.create("https://foo.org/moodle/login");
        assertEquals(1, store.get(uri).size());
        assertEquals("VALUE3", store.get(uri).get(0).value());
        assertTrue(store.get(uri).get(0).isSecure());
    }

    private static void shouldCleanExpiredCookieFromUnderlyingDataStructure() throws Exception {
        ThreadSafeCookieStore store = new ThreadSafeCookieStore();
        store.add(Uri.create("https://foo.org/moodle/"), getCookie("JSESSIONID", "FOO", 1));
        store.add(Uri.create("https://bar.org/moodle/"), getCookie("JSESSIONID", "BAR", 1));
        store.add(Uri.create("https://bar.org/moodle/"), new DefaultCookie("UNEXPIRED_BAR", "BAR"));
        store.add(Uri.create("https://foobar.org/moodle/"), new DefaultCookie("UNEXPIRED_FOOBAR", "FOOBAR"));


        assertEquals(4, store.getAll().size());
        Thread.sleep(2000);
        store.evictExpired();
        assertEquals(2, store.getUnderlying().size());
        Collection<String> unexpiredCookieNames = store.getAll().stream().map(Cookie::name).collect(Collectors.toList());
        assertTrue(unexpiredCookieNames.containsAll(Set.of("UNEXPIRED_BAR", "UNEXPIRED_FOOBAR")));
    }

    private static Cookie getCookie(String key, String value, int maxAge) {
        DefaultCookie cookie = new DefaultCookie(key, value);
        cookie.setMaxAge(maxAge);
        return cookie;
    }
}