Skip to content

Commit

Permalink
OZ-579: Implement session cookie cache.
Browse files Browse the repository at this point in the history
  • Loading branch information
corneliouzbett committed May 23, 2024
1 parent 08cd414 commit e9fc074
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 85 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.ozonehis.camel.frappe.sdk.internal.security;

import com.ozonehis.camel.frappe.sdk.api.security.FrappeAuthentication;
import com.ozonehis.camel.frappe.sdk.internal.security.cookie.CookieCache;
import com.ozonehis.camel.frappe.sdk.internal.security.cookie.WrappedCookie;
import java.io.IOException;
import java.util.List;
import javax.security.sasl.AuthenticationException;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Cookie;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
Expand All @@ -18,6 +22,10 @@ public class DefaultFrappeAuthentication implements FrappeAuthentication {

private final String password;

private static final String SESSION_COOKIE_NAME = "session-cookie";

CookieCache cookieCache = CookieCache.getInstance();

public DefaultFrappeAuthentication(String username, String password) {
this.username = username;
this.password = password;
Expand All @@ -33,10 +41,31 @@ public Response intercept(@NotNull Chain chain) throws IOException {
}

public List<String> getSessionCookie(Request incomingRequest) throws IOException {
WrappedCookie sessionCookie = cookieCache.get(SESSION_COOKIE_NAME);
if (sessionCookie != null && !sessionCookie.isExpired()) {
log.debug("Session cookie found and not expired. Using it...");
return sessionCookie.unwrap();
} else {
log.debug("Session cookie not found or expired. Logging in...");
return login(incomingRequest, cookieCache);
}
}

private List<String> login(Request incomingRequest, CookieCache cookieCache) throws IOException {
var baseUrl = getBaseUrl(incomingRequest);
Request request = buildLoginRequest(baseUrl);
try (Response response = executeRequest(request)) {
return extractCookies(response);
var cookies = response.headers("set-cookie");
var cookieList = Cookie.parseAll(incomingRequest.url(), response.headers());
cookieList.forEach(cookie -> {
if (cookie.name().equalsIgnoreCase("sid") && !cookie.value().isEmpty()) {
cookieCache.clearExpired();
cookieCache.put(SESSION_COOKIE_NAME, new WrappedCookie(cookie.expiresAt(), cookies));
}
});
return cookies;
} catch (IOException e) {
throw new AuthenticationException("Error while logging in", e);
}
}

Expand All @@ -57,15 +86,6 @@ private Response executeRequest(Request request) throws IOException {
return client.newCall(request).execute();
}

private List<String> extractCookies(Response response) {
if (response.isSuccessful()) {
return response.headers("set-cookie");
} else {
log.error("Failed to login, {}", response.body());
return null;
}
}

public String getBaseUrl(Request request) {
int port = request.url().port();
String scheme = request.url().scheme();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.ozonehis.camel.frappe.sdk.internal.security.cookie;

import java.util.concurrent.ConcurrentHashMap;
import lombok.NoArgsConstructor;

/**
* A cache for storing cookies.
*/
@NoArgsConstructor
public class CookieCache {

private static CookieCache instance = null;

private final ConcurrentHashMap<String, WrappedCookie> cookieStore = new ConcurrentHashMap<>();

public static CookieCache getInstance() {
if (instance == null) {
instance = new CookieCache();
}
return instance;
}

/**
* Put cookies by name.
*
* @param cookieName the name of the cookie
* @param cookies the wrappedCookies
*/
public void put(String cookieName, WrappedCookie cookies) {
cookieStore.put(cookieName, cookies);
}

/**
* Get cookies by name.
*
* @param cookieName the name of the cookie
* @return the wrappedCookies
*/
public WrappedCookie get(String cookieName) {
return cookieStore.get(cookieName);
}

/**
* Clear all cookies from the cache.
*/
public void clear() {
cookieStore.clear();
}

/**
* Clear expired cookies from the cache.
*/
public void clearExpired() {
cookieStore.forEach((key, value) -> {
if (value.isExpired()) {
cookieStore.remove(key);
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,28 @@

public class InMemoryCookieJar implements CookieJar {

private final List<WrappedCookie> cookieCache = new ArrayList<>();
private final List<Cookie> cookiesInMemoryStore = new ArrayList<>();

@NotNull @Override
public List<Cookie> loadForRequest(@NotNull HttpUrl httpUrl) {
return this.cookieCache.stream()
.filter(cookie -> cookie.matches(httpUrl) && !cookie.isExpired())
.collect(ArrayList::new, (list, cookie) -> list.add(cookie.unwrap()), ArrayList::addAll);
this.clearExpired();
return this.cookiesInMemoryStore.stream()
.filter(cookie -> cookie.matches(httpUrl))
.toList();
}

@Override
public void saveFromResponse(@NotNull HttpUrl httpUrl, @NotNull List<Cookie> cookies) {
List<WrappedCookie> wrappedCookies =
cookies.stream().map(WrappedCookie::new).toList();
this.clear();
this.cookieCache.addAll(wrappedCookies);
this.cookiesInMemoryStore.addAll(cookies);
}

@Synchronized
public void clear() {
this.cookieCache.clear();
this.cookiesInMemoryStore.clear();
}

@Synchronized
public void clearExpired() {
this.cookieCache.removeIf(WrappedCookie::isExpired);
this.cookiesInMemoryStore.removeIf(cookie -> cookie.expiresAt() < System.currentTimeMillis());
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
package com.ozonehis.camel.frappe.sdk.internal.security.cookie;

import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import okhttp3.Cookie;
import okhttp3.HttpUrl;

@Data
@EqualsAndHashCode
@AllArgsConstructor
public class WrappedCookie {

private Cookie cookie;
private long expiresAt;

public boolean isExpired() {
return cookie.expiresAt() < System.currentTimeMillis();
}
private List<String> cookies;

public Cookie unwrap() {
return cookie;
public boolean isExpired() {
return expiresAt < System.currentTimeMillis();
}

public boolean matches(HttpUrl url) {
return cookie.matches(url);
public List<String> unwrap() {
return cookies;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.ozonehis.camel.frappe.sdk.internal.security.cookie;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

class CookieCacheTest {

@Test
@DisplayName("put should store the cookie in the cache")
void putShouldStoreTheCookieInTheCache() {
CookieCache cookieCache = CookieCache.getInstance();
WrappedCookie wrappedCookie =
new WrappedCookie(System.currentTimeMillis() + 1000, List.of("cookie1", "cookie2"));
cookieCache.put("testCookie", wrappedCookie);
assertEquals(wrappedCookie, cookieCache.get("testCookie"));
}

@Test
@DisplayName("get should return the correct cookie from the cache")
void getShouldReturnTheCorrectCookieFromTheCache() {
CookieCache cookieCache = CookieCache.getInstance();
WrappedCookie wrappedCookie =
new WrappedCookie(System.currentTimeMillis() + 1000, List.of("cookie1", "cookie2"));
cookieCache.put("testCookie", wrappedCookie);
assertEquals(wrappedCookie, cookieCache.get("testCookie"));
}

@Test
@DisplayName("clear should remove all cookies from the cache")
void clearShouldRemoveAllCookiesFromTheCache() {
CookieCache cookieCache = CookieCache.getInstance();
WrappedCookie wrappedCookie =
new WrappedCookie(System.currentTimeMillis() + 1000, List.of("cookie1", "cookie2"));
cookieCache.put("testCookie", wrappedCookie);
cookieCache.clear();
assertNull(cookieCache.get("testCookie"));
}

@Test
@DisplayName("clearExpired should remove only expired cookies from the cache")
void clearExpiredShouldRemoveOnlyExpiredCookiesFromTheCache() {
CookieCache cookieCache = CookieCache.getInstance();
WrappedCookie expiredCookie =
new WrappedCookie(System.currentTimeMillis() - 1000, List.of("cookie1", "cookie2"));
WrappedCookie validCookie = new WrappedCookie(System.currentTimeMillis() + 1000, List.of("cookie3", "cookie4"));
cookieCache.put("expiredCookie", expiredCookie);
cookieCache.put("validCookie", validCookie);
cookieCache.clearExpired();
assertNull(cookieCache.get("expiredCookie"));
assertEquals(validCookie, cookieCache.get("validCookie"));
}
}
Original file line number Diff line number Diff line change
@@ -1,73 +1,34 @@
package com.ozonehis.camel.frappe.sdk.internal.security.cookie;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.openMocks;
import static org.junit.jupiter.api.Assertions.*;

import java.util.Date;
import okhttp3.Cookie;
import okhttp3.HttpUrl;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;

class WrappedCookieTest {

@Mock
private Cookie cookie;

private WrappedCookie wrappedCookie;

private static AutoCloseable mocksCloser;

@BeforeEach
void setUp() {
mocksCloser = openMocks(this);
wrappedCookie = new WrappedCookie(cookie);
}

@AfterAll
static void closeMocks() throws Exception {
mocksCloser.close();
}

@Test
@DisplayName("isExpired should return true when cookie is expired")
void isExpiredShouldReturnTrueWhenCookieIsExpired() {
when(cookie.expiresAt()).thenReturn(new Date().getTime() - 1000);
@DisplayName("isExpired should return true when expiresAt is less than current time")
void isExpiredShouldReturnTrueWhenExpiresAtIsLessThanCurrentTime() {
WrappedCookie wrappedCookie =
new WrappedCookie(System.currentTimeMillis() - 1000, List.of("cookie1", "cookie2"));
assertTrue(wrappedCookie.isExpired());
}

@Test
@DisplayName("isExpired should return false when cookie is not expired")
void isExpiredShouldReturnFalseWhenCookieIsNotExpired() {
when(cookie.expiresAt()).thenReturn(new Date().getTime() + 1000);
@DisplayName("isExpired should return false when expiresAt is greater than current time")
void isExpiredShouldReturnFalseWhenExpiresAtIsGreaterThanCurrentTime() {
WrappedCookie wrappedCookie =
new WrappedCookie(System.currentTimeMillis() + 1000, List.of("cookie1", "cookie2"));
assertFalse(wrappedCookie.isExpired());
}

@Test
@DisplayName("unwrap should return the original cookie")
void unwrapShouldReturnTheOriginalCookie() {
assertEquals(cookie, wrappedCookie.unwrap());
}

@Test
@DisplayName("matches should return true when cookie matches the url")
void matchesShouldReturnTrueWhenCookieMatchesTheUrl() {
HttpUrl url = HttpUrl.parse("http://localhost");
when(cookie.matches(url)).thenReturn(true);
assertTrue(wrappedCookie.matches(url));
}

@Test
@DisplayName("matches should return false when cookie does not match the url")
void matchesShouldReturnFalseWhenCookieDoesNotMatchTheUrl() {
HttpUrl url = HttpUrl.parse("http://localhost");
when(cookie.matches(url)).thenReturn(false);
assertFalse(wrappedCookie.matches(url));
@DisplayName("unwrap should return the original list of cookies")
void unwrapShouldReturnTheOriginalListOfCookies() {
List<String> originalCookies = List.of("cookie1", "cookie2");
WrappedCookie wrappedCookie = new WrappedCookie(System.currentTimeMillis() + 1000, originalCookies);
assertEquals(originalCookies, wrappedCookie.unwrap());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"title": "Frappe",
"description": "Frappe component to integrate with Frappe REST API.",
"deprecated": false,
"firstVersion": "1.1.0-SNAPSHOT",
"firstVersion": "1.0.0",
"label": "api",
"javaType": "com.ozonehis.camel.FrappeComponent",
"supportLevel": "Preview",
Expand Down

0 comments on commit e9fc074

Please sign in to comment.