diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..653dfd7
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.project b/.project
new file mode 100644
index 0000000..f9ec62f
--- /dev/null
+++ b/.project
@@ -0,0 +1,34 @@
+
+
+ ipinfo-spring
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
+
+ 1768577910667
+
+ 30
+
+ org.eclipse.core.resources.regexFilterMatcher
+ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
+
+
+
+
diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..f9fe345
--- /dev/null
+++ b/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/test/java=UTF-8
+encoding/=UTF-8
diff --git a/.settings/org.eclipse.jdt.apt.core.prefs b/.settings/org.eclipse.jdt.apt.core.prefs
new file mode 100644
index 0000000..d4313d4
--- /dev/null
+++ b/.settings/org.eclipse.jdt.apt.core.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.apt.aptEnabled=false
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..1b6e1ef
--- /dev/null
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,9 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
+org.eclipse.jdt.core.compiler.processAnnotations=disabled
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 0000000..f897a7f
--- /dev/null
+++ b/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/pom.xml b/pom.xml
index 8018e70..3627764 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,7 +57,7 @@
io.ipinfo
ipinfo-api
- 3.2.0
+ 3.3.0
compile
diff --git a/src/main/java/io/ipinfo/spring/IPinfoResproxySpring.java b/src/main/java/io/ipinfo/spring/IPinfoResproxySpring.java
new file mode 100644
index 0000000..1eeef42
--- /dev/null
+++ b/src/main/java/io/ipinfo/spring/IPinfoResproxySpring.java
@@ -0,0 +1,111 @@
+package io.ipinfo.spring;
+
+import io.ipinfo.api.IPinfo;
+import io.ipinfo.api.model.ResproxyResponse;
+import io.ipinfo.spring.strategies.attribute.AttributeStrategy;
+import io.ipinfo.spring.strategies.attribute.SessionAttributeStrategy;
+import io.ipinfo.spring.strategies.interceptor.BotInterceptorStrategy;
+import io.ipinfo.spring.strategies.interceptor.InterceptorStrategy;
+import io.ipinfo.spring.strategies.ip.IPStrategy;
+import io.ipinfo.spring.strategies.ip.SimpleIPStrategy;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+public class IPinfoResproxySpring implements HandlerInterceptor {
+
+ public static final String ATTRIBUTE_KEY =
+ "IPinfoOfficialSparkWrapper.ResproxyResponse";
+ private final IPinfo ii;
+ private final AttributeStrategy attributeStrategy;
+ private final IPStrategy ipStrategy;
+ private final InterceptorStrategy interceptorStrategy;
+
+ IPinfoResproxySpring(
+ IPinfo ii,
+ AttributeStrategy attributeStrategy,
+ IPStrategy ipStrategy,
+ InterceptorStrategy interceptorStrategy
+ ) {
+ this.ii = ii;
+ this.attributeStrategy = attributeStrategy;
+ this.ipStrategy = ipStrategy;
+ this.interceptorStrategy = interceptorStrategy;
+ }
+
+ public static void main(String... args) {
+ System.out.println(
+ "This library is not meant to be run as a standalone jar."
+ );
+ System.exit(0);
+ }
+
+ @Override
+ public boolean preHandle(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ Object handler
+ ) throws Exception {
+ if (!interceptorStrategy.shouldRun(request)) {
+ return true;
+ }
+
+ // Don't waste an API call if we already have it.
+ // This should only happen for RequestAttributeStrategy and potentially
+ // other implementations.
+ if (attributeStrategy.hasResproxyAttribute(request)) {
+ return true;
+ }
+
+ String ip = ipStrategy.getIPAddress(request);
+ if (ip == null) {
+ return true;
+ }
+
+ ResproxyResponse resproxyResponse = ii.lookupResproxy(ip);
+ attributeStrategy.storeResproxyAttribute(request, resproxyResponse);
+
+ return true;
+ }
+
+ public static class Builder {
+
+ private IPinfo ii = new IPinfo.Builder().build();
+ private AttributeStrategy attributeStrategy =
+ new SessionAttributeStrategy();
+ private IPStrategy ipStrategy = new SimpleIPStrategy();
+ private InterceptorStrategy interceptorStrategy =
+ new BotInterceptorStrategy();
+
+ public Builder setIPinfo(IPinfo ii) {
+ this.ii = ii;
+ return this;
+ }
+
+ public Builder attributeStrategy(AttributeStrategy attributeStrategy) {
+ this.attributeStrategy = attributeStrategy;
+ return this;
+ }
+
+ public Builder ipStrategy(IPStrategy ipStrategy) {
+ this.ipStrategy = ipStrategy;
+ return this;
+ }
+
+ public Builder interceptorStrategy(
+ InterceptorStrategy interceptorStrategy
+ ) {
+ this.interceptorStrategy = interceptorStrategy;
+ return this;
+ }
+
+ public IPinfoResproxySpring build() {
+ return new IPinfoResproxySpring(
+ ii,
+ attributeStrategy,
+ ipStrategy,
+ interceptorStrategy
+ );
+ }
+ }
+}
diff --git a/src/main/java/io/ipinfo/spring/strategies/attribute/AttributeStrategy.java b/src/main/java/io/ipinfo/spring/strategies/attribute/AttributeStrategy.java
index 4b44cc5..a486202 100644
--- a/src/main/java/io/ipinfo/spring/strategies/attribute/AttributeStrategy.java
+++ b/src/main/java/io/ipinfo/spring/strategies/attribute/AttributeStrategy.java
@@ -4,6 +4,7 @@
import io.ipinfo.api.model.IPResponseCore;
import io.ipinfo.api.model.IPResponseLite;
import io.ipinfo.api.model.IPResponsePlus;
+import io.ipinfo.api.model.ResproxyResponse;
import jakarta.servlet.http.HttpServletRequest;
public interface AttributeStrategy {
@@ -75,4 +76,23 @@ default IPResponsePlus getPlusAttribute(HttpServletRequest request) {
default boolean hasPlusAttribute(HttpServletRequest request) {
return getPlusAttribute(request) != null;
}
+
+ default void storeResproxyAttribute(
+ HttpServletRequest request,
+ ResproxyResponse response
+ ) {
+ throw new UnsupportedOperationException(
+ "This strategy does not support ResproxyResponse."
+ );
+ }
+
+ default ResproxyResponse getResproxyAttribute(HttpServletRequest request) {
+ throw new UnsupportedOperationException(
+ "This strategy does not support ResproxyResponse."
+ );
+ }
+
+ default boolean hasResproxyAttribute(HttpServletRequest request) {
+ return getResproxyAttribute(request) != null;
+ }
}
diff --git a/src/main/java/io/ipinfo/spring/strategies/attribute/SessionAttributeStrategy.java b/src/main/java/io/ipinfo/spring/strategies/attribute/SessionAttributeStrategy.java
index 4af0377..5ee779a 100644
--- a/src/main/java/io/ipinfo/spring/strategies/attribute/SessionAttributeStrategy.java
+++ b/src/main/java/io/ipinfo/spring/strategies/attribute/SessionAttributeStrategy.java
@@ -4,9 +4,11 @@
import io.ipinfo.api.model.IPResponseCore;
import io.ipinfo.api.model.IPResponseLite;
import io.ipinfo.api.model.IPResponsePlus;
+import io.ipinfo.api.model.ResproxyResponse;
import io.ipinfo.spring.IPinfoCoreSpring;
import io.ipinfo.spring.IPinfoLiteSpring;
import io.ipinfo.spring.IPinfoPlusSpring;
+import io.ipinfo.spring.IPinfoResproxySpring;
import io.ipinfo.spring.IPinfoSpring;
import jakarta.servlet.http.HttpServletRequest;
@@ -77,4 +79,21 @@ public IPResponsePlus getPlusAttribute(HttpServletRequest request) {
.getSession()
.getAttribute(IPinfoPlusSpring.ATTRIBUTE_KEY);
}
+
+ @Override
+ public void storeResproxyAttribute(
+ HttpServletRequest request,
+ ResproxyResponse response
+ ) {
+ request
+ .getSession()
+ .setAttribute(IPinfoResproxySpring.ATTRIBUTE_KEY, response);
+ }
+
+ @Override
+ public ResproxyResponse getResproxyAttribute(HttpServletRequest request) {
+ return (ResproxyResponse) request
+ .getSession()
+ .getAttribute(IPinfoResproxySpring.ATTRIBUTE_KEY);
+ }
}
diff --git a/src/test/java/io/ipinfo/spring/IPinfoResproxySpringTest.java b/src/test/java/io/ipinfo/spring/IPinfoResproxySpringTest.java
new file mode 100644
index 0000000..bd77af0
--- /dev/null
+++ b/src/test/java/io/ipinfo/spring/IPinfoResproxySpringTest.java
@@ -0,0 +1,205 @@
+package io.ipinfo.spring;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import io.ipinfo.api.IPinfo;
+import io.ipinfo.api.errors.RateLimitedException;
+import io.ipinfo.api.model.ResproxyResponse;
+import io.ipinfo.spring.strategies.attribute.AttributeStrategy;
+import io.ipinfo.spring.strategies.interceptor.InterceptorStrategy;
+import io.ipinfo.spring.strategies.ip.IPStrategy;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+@ExtendWith(MockitoExtension.class)
+class IPinfoResproxySpringTest {
+
+ @Mock
+ private IPinfo mockIPinfoClient;
+
+ @Mock
+ private AttributeStrategy mockAttributeStrategy;
+
+ @Mock
+ private IPStrategy mockIpStrategy;
+
+ @Mock
+ private InterceptorStrategy mockInterceptorStrategy;
+
+ @InjectMocks
+ private IPinfoResproxySpring ipinfoSpring;
+
+ private MockHttpServletRequest request;
+ private MockHttpServletResponse response;
+ private Object handler;
+
+ private ResproxyResponse dummyResproxyResponse;
+
+ @BeforeEach
+ void setUp() {
+ request = new MockHttpServletRequest();
+ response = new MockHttpServletResponse();
+ handler = new Object();
+
+ dummyResproxyResponse = new ResproxyResponse(
+ "175.107.211.204",
+ "2026-01-15",
+ 100.0,
+ "test_service"
+ );
+ }
+
+ @Test
+ @DisplayName("should skip processing if interceptorStrategy returns false")
+ void preHandle_shouldSkipIfInterceptorStrategyFalse() throws Exception {
+ when(mockInterceptorStrategy.shouldRun(request)).thenReturn(false);
+
+ boolean result = ipinfoSpring.preHandle(request, response, handler);
+
+ assertTrue(result, "preHandle should return true to continue chain");
+ // Verify that no other strategies were called if shouldRun returned false
+ verify(mockInterceptorStrategy).shouldRun(request);
+ verifyNoInteractions(
+ mockAttributeStrategy,
+ mockIpStrategy,
+ mockIPinfoClient
+ );
+ }
+
+ @Test
+ @DisplayName(
+ "should skip processing if attributeStrategy already has resproxy attribute"
+ )
+ void preHandle_shouldSkipIfHasResproxyAttribute() throws Exception {
+ when(mockInterceptorStrategy.shouldRun(request)).thenReturn(true);
+ when(mockAttributeStrategy.hasResproxyAttribute(request)).thenReturn(
+ true
+ );
+
+ boolean result = ipinfoSpring.preHandle(request, response, handler);
+
+ assertTrue(result, "preHandle should return true to continue chain");
+ verify(mockInterceptorStrategy).shouldRun(request);
+ verify(mockAttributeStrategy).hasResproxyAttribute(request);
+ // Verify no IP lookup or storage occurred
+ verifyNoInteractions(mockIpStrategy, mockIPinfoClient);
+ }
+
+ @Test
+ @DisplayName("should skip processing if IPStrategy returns null IP")
+ void preHandle_shouldSkipIfIpIsNull() throws Exception {
+ when(mockInterceptorStrategy.shouldRun(request)).thenReturn(true);
+ when(mockAttributeStrategy.hasResproxyAttribute(request)).thenReturn(
+ false
+ );
+ when(mockIpStrategy.getIPAddress(request)).thenReturn(null);
+
+ boolean result = ipinfoSpring.preHandle(request, response, handler);
+
+ assertTrue(result, "preHandle should return true to continue chain");
+ verify(mockInterceptorStrategy).shouldRun(request);
+ verify(mockAttributeStrategy).hasResproxyAttribute(request);
+ verify(mockIpStrategy).getIPAddress(request);
+ // Verify no IP lookup or storage occurred
+ verifyNoInteractions(mockIPinfoClient);
+ verify(mockAttributeStrategy, never()).storeResproxyAttribute(
+ any(),
+ any()
+ );
+ }
+
+ @Test
+ @DisplayName(
+ "should perform resproxy lookup and store attribute if all conditions met"
+ )
+ void preHandle_shouldProcessAndStore() throws Exception {
+ String testIp = "175.107.211.204";
+ when(mockInterceptorStrategy.shouldRun(request)).thenReturn(true);
+ when(mockAttributeStrategy.hasResproxyAttribute(request)).thenReturn(
+ false
+ );
+ when(mockIpStrategy.getIPAddress(request)).thenReturn(testIp);
+ when(mockIPinfoClient.lookupResproxy(testIp)).thenReturn(
+ dummyResproxyResponse
+ );
+
+ boolean result = ipinfoSpring.preHandle(request, response, handler);
+
+ assertTrue(result, "preHandle should return true to continue chain");
+ verify(mockInterceptorStrategy).shouldRun(request);
+ verify(mockAttributeStrategy).hasResproxyAttribute(request);
+ verify(mockIpStrategy).getIPAddress(request);
+ verify(mockIPinfoClient).lookupResproxy(testIp);
+ verify(mockAttributeStrategy).storeResproxyAttribute(
+ request,
+ dummyResproxyResponse
+ );
+ }
+
+ @Test
+ @DisplayName("should rethrow RateLimitedException during lookup")
+ void preHandle_shouldRethrowRateLimitedException() throws Exception {
+ String testIp = "175.107.211.204";
+ when(mockInterceptorStrategy.shouldRun(request)).thenReturn(true);
+ when(mockAttributeStrategy.hasResproxyAttribute(request)).thenReturn(
+ false
+ );
+ when(mockIpStrategy.getIPAddress(request)).thenReturn(testIp);
+ // Simulate a RateLimitedException during lookup
+ when(mockIPinfoClient.lookupResproxy(testIp)).thenThrow(
+ new RateLimitedException()
+ );
+
+ assertThrows(RateLimitedException.class, () ->
+ ipinfoSpring.preHandle(request, response, handler)
+ );
+
+ verify(mockInterceptorStrategy).shouldRun(request);
+ verify(mockAttributeStrategy).hasResproxyAttribute(request);
+ verify(mockIpStrategy).getIPAddress(request);
+ verify(mockIPinfoClient).lookupResproxy(testIp);
+ verify(mockAttributeStrategy, never()).storeResproxyAttribute(
+ any(),
+ any()
+ );
+ }
+
+ @Test
+ @DisplayName(
+ "should pass through empty response when IP not in resproxy database"
+ )
+ void preHandle_shouldPassThroughEmptyResponse() throws Exception {
+ String testIp = "175.107.211.204";
+ // Empty response simulates IP not in resproxy database
+ ResproxyResponse emptyResponse = new ResproxyResponse(
+ null,
+ null,
+ null,
+ null
+ );
+
+ when(mockInterceptorStrategy.shouldRun(request)).thenReturn(true);
+ when(mockAttributeStrategy.hasResproxyAttribute(request)).thenReturn(
+ false
+ );
+ when(mockIpStrategy.getIPAddress(request)).thenReturn(testIp);
+ when(mockIPinfoClient.lookupResproxy(testIp)).thenReturn(emptyResponse);
+
+ boolean result = ipinfoSpring.preHandle(request, response, handler);
+
+ assertTrue(result, "preHandle should return true to continue chain");
+ verify(mockIPinfoClient).lookupResproxy(testIp);
+ verify(mockAttributeStrategy).storeResproxyAttribute(
+ request,
+ emptyResponse
+ );
+ }
+}