Skip to content

Commit

Permalink
OZ-579: Implement session cookie cache.
Browse files Browse the repository at this point in the history
Additional, implement shared container for integration tests
  • Loading branch information
corneliouzbett authored Jul 19, 2024
1 parent 08cd414 commit b1678b4
Show file tree
Hide file tree
Showing 10 changed files with 242 additions and 77 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
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 @@ -27,16 +30,37 @@ public DefaultFrappeAuthentication(String username, String password) {
public Response intercept(@NotNull Chain chain) throws IOException {
final Request original = chain.request();
final Request authorized = original.newBuilder()
.addHeader("Cookie", String.join("; ", getSessionCookie(original)))
.addHeader("Cookie", getSessionCookies(original))
.build();
return chain.proceed(authorized);
}

public List<String> getSessionCookie(Request incomingRequest) throws IOException {
var baseUrl = getBaseUrl(incomingRequest);
Request request = buildLoginRequest(baseUrl);
public String getSessionCookies(Request incomingRequest) throws IOException {
String[] cookieNames = {"sid", "system_user", "full_name", "user_id", "user_image"};
StringBuilder cookies = new StringBuilder();
for (String cookieName : cookieNames) {
WrappedCookie cookie = CookieCache.getInstance().get(cookieName);
if (cookie != null && !cookie.isExpired()) {
cookies.append(cookieName).append("=").append(cookie.unwrap()).append("; ");
}
}
// Make sure the sid cookie is included
if (!cookies.toString().contains("sid") || cookies.isEmpty()) {
if (log.isDebugEnabled()) log.debug("SID session cookie expired, logging in again...");
login(incomingRequest);
return getSessionCookies(incomingRequest);
}
return cookies.toString();
}

private void login(Request incomingRequest) throws IOException {
Request request = buildLoginRequest(getBaseUrl(incomingRequest));
try (Response response = executeRequest(request)) {
return extractCookies(response);
CookieCache.getInstance().clearExpired();
Cookie.parseAll(incomingRequest.url(), response.headers())
.forEach(cookie -> CookieCache.getInstance().put(cookie.name(), new WrappedCookie(cookie)));
} catch (IOException e) {
throw new AuthenticationException("Error while logging in", e);
}
}

Expand All @@ -57,15 +81,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,63 @@
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 synchronized 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) {
if (get(cookieName) != null) {
cookieStore.remove(cookieName);
}
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
Expand Up @@ -4,7 +4,6 @@
import lombok.Data;
import lombok.EqualsAndHashCode;
import okhttp3.Cookie;
import okhttp3.HttpUrl;

@Data
@EqualsAndHashCode
Expand All @@ -17,11 +16,7 @@ public boolean isExpired() {
return cookie.expiresAt() < System.currentTimeMillis();
}

public Cookie unwrap() {
return cookie;
}

public boolean matches(HttpUrl url) {
return cookie.matches(url);
public String unwrap() {
return cookie.value();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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 static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.openMocks;

import java.util.Objects;
import okhttp3.Cookie;
import okhttp3.HttpUrl;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class CookieCacheTest {

private static final Logger log = LoggerFactory.getLogger(CookieCacheTest.class);

private static AutoCloseable mocksCloser;

@BeforeAll
static void setUp() {
mocksCloser = openMocks(CookieCacheTest.class);
}

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

@Test
@DisplayName("put should store the cookie in the cache")
void putShouldStoreTheCookieInTheCache() {
CookieCache cookieCache = CookieCache.getInstance();
WrappedCookie wrappedCookie = new WrappedCookie(Cookie.parse(
Objects.requireNonNull(HttpUrl.parse("https://example.com")),
"testCookie=cookie1; testCookie2=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(Cookie.parse(
Objects.requireNonNull(HttpUrl.parse("https://example.com")),
"testCookie=cookie1; testCookie2=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(Cookie.parse(
Objects.requireNonNull(HttpUrl.parse("https://example.com")),
"testCookie=cookie1; testCookie2=cookie2"));
cookieCache.put("testCookie", wrappedCookie);
cookieCache.clear();
assertNull(cookieCache.get("testCookie"));
assertNull(cookieCache.get("testCookie2"));
}

@Test
@DisplayName("clearExpired should remove only expired cookies from the cache")
void clearExpiredShouldRemoveOnlyExpiredCookiesFromTheCache() {
CookieCache cookieCache = CookieCache.getInstance();

var expiredCookie = Mockito.mock(Cookie.class);
WrappedCookie expiredWrappedCookie = new WrappedCookie(expiredCookie);

var validCookie = Mockito.mock(Cookie.class);
WrappedCookie validWrappedCookie = new WrappedCookie(validCookie);

cookieCache.put("expiredCookie", expiredWrappedCookie);
cookieCache.put("validCookie", validWrappedCookie);

when(expiredCookie.expiresAt()).thenReturn(System.currentTimeMillis() - 1000);
when(validCookie.expiresAt()).thenReturn(System.currentTimeMillis() + 1000);

cookieCache.clearExpired();

assertNull(cookieCache.get("expiredCookie"));
assertEquals(validWrappedCookie, cookieCache.get("validCookie"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,26 @@
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.mock;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.openMocks;

import java.util.Date;
import okhttp3.Cookie;
import okhttp3.HttpUrl;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
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 Cookie mockCookie;

private static AutoCloseable mocksCloser;

@BeforeEach
void setUp() {
mocksCloser = openMocks(this);
wrappedCookie = new WrappedCookie(cookie);
mocksCloser = openMocks(WrappedCookieTest.class);
this.mockCookie = mock(Cookie.class);
}

@AfterAll
Expand All @@ -36,38 +31,32 @@ static void closeMocks() throws Exception {
}

@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() {
when(mockCookie.expiresAt()).thenReturn(System.currentTimeMillis() - 1000);

WrappedCookie wrappedCookie = new WrappedCookie(mockCookie);

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() {
when(mockCookie.expiresAt()).thenReturn(System.currentTimeMillis() + 1000);

WrappedCookie wrappedCookie = mock(WrappedCookie.class);

assertFalse(wrappedCookie.isExpired());
}

@Test
@DisplayName("unwrap should return the original cookie")
void unwrapShouldReturnTheOriginalCookie() {
assertEquals(cookie, wrappedCookie.unwrap());
}
@DisplayName("unwrap should return the original list of cookies")
void unwrapShouldReturnTheOriginalListOfCookies() {
when(mockCookie.value()).thenReturn("cookie1; cookie2");

@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));
}
WrappedCookie wrappedCookie = new WrappedCookie(mockCookie);

@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));
assertEquals(wrappedCookie.unwrap(), "cookie1; cookie2");
}
}
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
Loading

0 comments on commit b1678b4

Please sign in to comment.