diff --git a/camel-frappe-api/src/main/java/com/ozonehis/camel/frappe/sdk/internal/security/DefaultFrappeAuthentication.java b/camel-frappe-api/src/main/java/com/ozonehis/camel/frappe/sdk/internal/security/DefaultFrappeAuthentication.java index 6f8434e..1dde377 100644 --- a/camel-frappe-api/src/main/java/com/ozonehis/camel/frappe/sdk/internal/security/DefaultFrappeAuthentication.java +++ b/camel-frappe-api/src/main/java/com/ozonehis/camel/frappe/sdk/internal/security/DefaultFrappeAuthentication.java @@ -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; @@ -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; @@ -33,10 +41,31 @@ public Response intercept(@NotNull Chain chain) throws IOException { } public List 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 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); } } @@ -57,15 +86,6 @@ private Response executeRequest(Request request) throws IOException { return client.newCall(request).execute(); } - private List 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(); diff --git a/camel-frappe-api/src/main/java/com/ozonehis/camel/frappe/sdk/internal/security/cookie/CookieCache.java b/camel-frappe-api/src/main/java/com/ozonehis/camel/frappe/sdk/internal/security/cookie/CookieCache.java new file mode 100644 index 0000000..a5c768f --- /dev/null +++ b/camel-frappe-api/src/main/java/com/ozonehis/camel/frappe/sdk/internal/security/cookie/CookieCache.java @@ -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 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); + } + }); + } +} diff --git a/camel-frappe-api/src/main/java/com/ozonehis/camel/frappe/sdk/internal/security/cookie/InMemoryCookieJar.java b/camel-frappe-api/src/main/java/com/ozonehis/camel/frappe/sdk/internal/security/cookie/InMemoryCookieJar.java index 5ce1788..f326c04 100644 --- a/camel-frappe-api/src/main/java/com/ozonehis/camel/frappe/sdk/internal/security/cookie/InMemoryCookieJar.java +++ b/camel-frappe-api/src/main/java/com/ozonehis/camel/frappe/sdk/internal/security/cookie/InMemoryCookieJar.java @@ -10,30 +10,28 @@ public class InMemoryCookieJar implements CookieJar { - private final List cookieCache = new ArrayList<>(); + private final List cookiesInMemoryStore = new ArrayList<>(); @NotNull @Override public List 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 cookies) { - List 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()); } } diff --git a/camel-frappe-api/src/main/java/com/ozonehis/camel/frappe/sdk/internal/security/cookie/WrappedCookie.java b/camel-frappe-api/src/main/java/com/ozonehis/camel/frappe/sdk/internal/security/cookie/WrappedCookie.java index d51a918..90a1246 100644 --- a/camel-frappe-api/src/main/java/com/ozonehis/camel/frappe/sdk/internal/security/cookie/WrappedCookie.java +++ b/camel-frappe-api/src/main/java/com/ozonehis/camel/frappe/sdk/internal/security/cookie/WrappedCookie.java @@ -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 cookies; - public Cookie unwrap() { - return cookie; + public boolean isExpired() { + return expiresAt < System.currentTimeMillis(); } - public boolean matches(HttpUrl url) { - return cookie.matches(url); + public List unwrap() { + return cookies; } } diff --git a/camel-frappe-api/src/test/java/com/ozonehis/camel/frappe/sdk/internal/security/cookie/CookieCacheTest.java b/camel-frappe-api/src/test/java/com/ozonehis/camel/frappe/sdk/internal/security/cookie/CookieCacheTest.java new file mode 100644 index 0000000..62263b1 --- /dev/null +++ b/camel-frappe-api/src/test/java/com/ozonehis/camel/frappe/sdk/internal/security/cookie/CookieCacheTest.java @@ -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")); + } +} diff --git a/camel-frappe-api/src/test/java/com/ozonehis/camel/frappe/sdk/internal/security/cookie/WrappedCookieTest.java b/camel-frappe-api/src/test/java/com/ozonehis/camel/frappe/sdk/internal/security/cookie/WrappedCookieTest.java index 7c4730d..36b61ce 100644 --- a/camel-frappe-api/src/test/java/com/ozonehis/camel/frappe/sdk/internal/security/cookie/WrappedCookieTest.java +++ b/camel-frappe-api/src/test/java/com/ozonehis/camel/frappe/sdk/internal/security/cookie/WrappedCookieTest.java @@ -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 originalCookies = List.of("cookie1", "cookie2"); + WrappedCookie wrappedCookie = new WrappedCookie(System.currentTimeMillis() + 1000, originalCookies); + assertEquals(originalCookies, wrappedCookie.unwrap()); } } diff --git a/camel-frappe-component/src/generated/resources/com/ozonehis/camel/frappe.json b/camel-frappe-component/src/generated/resources/com/ozonehis/camel/frappe.json index 1c40f4c..e9cb449 100644 --- a/camel-frappe-component/src/generated/resources/com/ozonehis/camel/frappe.json +++ b/camel-frappe-component/src/generated/resources/com/ozonehis/camel/frappe.json @@ -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",