From 1472eb06e7865a59046c9d230cdad9a8af05be82 Mon Sep 17 00:00:00 2001 From: Thierry De Leeuw Date: Wed, 11 Sep 2024 19:45:51 +0200 Subject: [PATCH] Issue #50: HTTP errors are ignored and result in empty responses with cache poisoning. WARNING This change is not a drop in replacement as if we get 403 because the token is incorrect, a checked exception is raised. Also note that, if an empty or null token is passed, it is now a fail fast (instead of getting a null pointer exception at the first request) --- src/main/java/io/ipinfo/api/IPinfo.java | 10 +++++++--- .../ipinfo/api/errors/ClientErrorException.java | 12 ++++++++++++ .../ipinfo/api/errors/HttpErrorException.java | 17 +++++++++++++++++ .../api/errors/InvalidTokenException.java | 13 +++++++++++++ .../ipinfo/api/errors/ServerErrorException.java | 10 ++++++++++ .../java/io/ipinfo/api/request/ASNRequest.java | 3 ++- .../java/io/ipinfo/api/request/BaseRequest.java | 13 +++++++++---- .../java/io/ipinfo/api/request/IPRequest.java | 3 ++- .../java/io/ipinfo/api/request/MapRequest.java | 3 ++- src/test/java/io/ipinfo/IPinfoTest.java | 5 +++-- 10 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 src/main/java/io/ipinfo/api/errors/ClientErrorException.java create mode 100644 src/main/java/io/ipinfo/api/errors/HttpErrorException.java create mode 100644 src/main/java/io/ipinfo/api/errors/InvalidTokenException.java create mode 100644 src/main/java/io/ipinfo/api/errors/ServerErrorException.java diff --git a/src/main/java/io/ipinfo/api/IPinfo.java b/src/main/java/io/ipinfo/api/IPinfo.java index 4cc4f9e..ba983d5 100644 --- a/src/main/java/io/ipinfo/api/IPinfo.java +++ b/src/main/java/io/ipinfo/api/IPinfo.java @@ -6,6 +6,7 @@ import io.ipinfo.api.cache.Cache; import io.ipinfo.api.cache.SimpleCache; import io.ipinfo.api.context.Context; +import io.ipinfo.api.errors.InvalidTokenException; import io.ipinfo.api.errors.RateLimitedException; import io.ipinfo.api.model.ASNResponse; import io.ipinfo.api.model.IPResponse; @@ -60,7 +61,7 @@ public static void main(String... args) { * @return IPResponse response from the api. * @throws RateLimitedException an exception when your api key has been rate limited. */ - public IPResponse lookupIP(String ip) throws RateLimitedException { + public IPResponse lookupIP(String ip) throws RateLimitedException, InvalidTokenException { IPResponse response = (IPResponse)cache.get(cacheKey(ip)); if (response != null) { return response; @@ -80,7 +81,7 @@ public IPResponse lookupIP(String ip) throws RateLimitedException { * @return ASNResponse response from the api. * @throws RateLimitedException an exception when your api key has been rate limited. */ - public ASNResponse lookupASN(String asn) throws RateLimitedException { + public ASNResponse lookupASN(String asn) throws RateLimitedException, InvalidTokenException { ASNResponse response = (ASNResponse)cache.get(cacheKey(asn)); if (response != null) { return response; @@ -100,7 +101,7 @@ public ASNResponse lookupASN(String asn) throws RateLimitedException { * @return String the URL to the map. * @throws RateLimitedException an exception when your API key has been rate limited. */ - public String getMap(List ips) throws RateLimitedException { + public String getMap(List ips) throws RateLimitedException, InvalidTokenException { MapResponse response = new MapRequest(client, token, ips).handle(); return response.getReportUrl(); } @@ -374,6 +375,9 @@ public Builder setCache(Cache cache) { } public IPinfo build() { + if (token == null || token.isEmpty()) { + throw new IllegalArgumentException("A token must be provided."); + } return new IPinfo(client, new Context(), token, cache); } } diff --git a/src/main/java/io/ipinfo/api/errors/ClientErrorException.java b/src/main/java/io/ipinfo/api/errors/ClientErrorException.java new file mode 100644 index 0000000..ce2fb26 --- /dev/null +++ b/src/main/java/io/ipinfo/api/errors/ClientErrorException.java @@ -0,0 +1,12 @@ +package io.ipinfo.api.errors; + +/** + *

This exception is raised when a client error is occurring (Http status code like 4xx).

+ * + *

Note that HTTP status 403 (Forbidden) is now reported as a checked exception of type @see InvalidTokenException .

+ */ +public class ClientErrorException extends HttpErrorException { + public ClientErrorException(int statusCode, String message) { + super(statusCode, message); + } +} diff --git a/src/main/java/io/ipinfo/api/errors/HttpErrorException.java b/src/main/java/io/ipinfo/api/errors/HttpErrorException.java new file mode 100644 index 0000000..89020b6 --- /dev/null +++ b/src/main/java/io/ipinfo/api/errors/HttpErrorException.java @@ -0,0 +1,17 @@ +package io.ipinfo.api.errors; + +/** + * This covers all non 403 and 429 Http error statuses. + */ +public class HttpErrorException extends RuntimeException { + private final int statusCode; + + public HttpErrorException(int statusCode, String message) { + super(message); + this.statusCode = statusCode; + } + + public int getStatusCode() { + return statusCode; + } +} diff --git a/src/main/java/io/ipinfo/api/errors/InvalidTokenException.java b/src/main/java/io/ipinfo/api/errors/InvalidTokenException.java new file mode 100644 index 0000000..1923838 --- /dev/null +++ b/src/main/java/io/ipinfo/api/errors/InvalidTokenException.java @@ -0,0 +1,13 @@ +package io.ipinfo.api.errors; + +/** + *

This exception is raised when the service returns a 403 (access denied).

+ * + *

That likely indicates that the token is either missing, incorrect or expired. + * It is a checked exception so, if you have multiple tokens (example during transition), you can fall back to another token.

+ */ +public class InvalidTokenException extends Exception { + public InvalidTokenException() { + super("The server did not accepted the provided token or no token was provided."); + } +} diff --git a/src/main/java/io/ipinfo/api/errors/ServerErrorException.java b/src/main/java/io/ipinfo/api/errors/ServerErrorException.java new file mode 100644 index 0000000..5f8a235 --- /dev/null +++ b/src/main/java/io/ipinfo/api/errors/ServerErrorException.java @@ -0,0 +1,10 @@ +package io.ipinfo.api.errors; + +/** + * This exception is raised when a server error is returned by IpInfo (Http status like 5xx) + */ +public class ServerErrorException extends HttpErrorException { + public ServerErrorException(int statusCode, String message) { + super(statusCode, message); + } +} diff --git a/src/main/java/io/ipinfo/api/request/ASNRequest.java b/src/main/java/io/ipinfo/api/request/ASNRequest.java index d5a83d7..6c0b68c 100644 --- a/src/main/java/io/ipinfo/api/request/ASNRequest.java +++ b/src/main/java/io/ipinfo/api/request/ASNRequest.java @@ -1,6 +1,7 @@ package io.ipinfo.api.request; import io.ipinfo.api.errors.ErrorResponseException; +import io.ipinfo.api.errors.InvalidTokenException; import io.ipinfo.api.errors.RateLimitedException; import io.ipinfo.api.model.ASNResponse; import okhttp3.OkHttpClient; @@ -18,7 +19,7 @@ public ASNRequest(OkHttpClient client, String token, String asn) { } @Override - public ASNResponse handle() throws RateLimitedException { + public ASNResponse handle() throws RateLimitedException, InvalidTokenException { String url = String.format(URL_FORMAT, asn); Request.Builder request = new Request.Builder().url(url).get(); diff --git a/src/main/java/io/ipinfo/api/request/BaseRequest.java b/src/main/java/io/ipinfo/api/request/BaseRequest.java index 0d9732a..ac7fd44 100644 --- a/src/main/java/io/ipinfo/api/request/BaseRequest.java +++ b/src/main/java/io/ipinfo/api/request/BaseRequest.java @@ -1,8 +1,7 @@ package io.ipinfo.api.request; import com.google.gson.Gson; -import io.ipinfo.api.errors.ErrorResponseException; -import io.ipinfo.api.errors.RateLimitedException; +import io.ipinfo.api.errors.*; import okhttp3.Credentials; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -18,9 +17,9 @@ protected BaseRequest(OkHttpClient client, String token) { this.token = token; } - public abstract T handle() throws RateLimitedException; + public abstract T handle() throws RateLimitedException, InvalidTokenException; - public Response handleRequest(Request.Builder request) throws RateLimitedException { + public Response handleRequest(Request.Builder request) throws RateLimitedException, InvalidTokenException { request .addHeader("Authorization", Credentials.basic(token, "")) .addHeader("user-agent", "IPinfoClient/Java/3.0.0") @@ -41,6 +40,12 @@ public Response handleRequest(Request.Builder request) throws RateLimitedExcepti if (response.code() == 429) { throw new RateLimitedException(); + } else if ((response.code() == 403)) { + throw new InvalidTokenException(); + } else if ((response.code() >= 400) && (response.code() <= 499)) { + throw new ClientErrorException(response.code(), "Http error " + response.code() + ". " + response.message()); + } else if ((response.code() >= 500) && (response.code() <= 599)) { + throw new ServerErrorException(response.code(), "Http error " + response.code() + ". " + response.message()); } return response; diff --git a/src/main/java/io/ipinfo/api/request/IPRequest.java b/src/main/java/io/ipinfo/api/request/IPRequest.java index 4472f7b..88fa362 100644 --- a/src/main/java/io/ipinfo/api/request/IPRequest.java +++ b/src/main/java/io/ipinfo/api/request/IPRequest.java @@ -1,6 +1,7 @@ package io.ipinfo.api.request; import io.ipinfo.api.errors.ErrorResponseException; +import io.ipinfo.api.errors.InvalidTokenException; import io.ipinfo.api.errors.RateLimitedException; import io.ipinfo.api.model.IPResponse; import okhttp3.OkHttpClient; @@ -19,7 +20,7 @@ public IPRequest(OkHttpClient client, String token, String ip) { } @Override - public IPResponse handle() throws RateLimitedException { + public IPResponse handle() throws RateLimitedException, InvalidTokenException { if (isBogon(ip)) { try { return new IPResponse(ip, true); diff --git a/src/main/java/io/ipinfo/api/request/MapRequest.java b/src/main/java/io/ipinfo/api/request/MapRequest.java index 3e6baf6..95a9bf5 100644 --- a/src/main/java/io/ipinfo/api/request/MapRequest.java +++ b/src/main/java/io/ipinfo/api/request/MapRequest.java @@ -1,6 +1,7 @@ package io.ipinfo.api.request; import io.ipinfo.api.errors.ErrorResponseException; +import io.ipinfo.api.errors.InvalidTokenException; import io.ipinfo.api.errors.RateLimitedException; import io.ipinfo.api.model.MapResponse; import okhttp3.*; @@ -17,7 +18,7 @@ public MapRequest(OkHttpClient client, String token, List ips) { } @Override - public MapResponse handle() throws RateLimitedException { + public MapResponse handle() throws RateLimitedException, InvalidTokenException { String jsonIpList = gson.toJson(ips); RequestBody requestBody = RequestBody.create(null, jsonIpList); Request.Builder request = new Request.Builder().url(URL).post(requestBody); diff --git a/src/test/java/io/ipinfo/IPinfoTest.java b/src/test/java/io/ipinfo/IPinfoTest.java index 0bd0fc7..1573abd 100644 --- a/src/test/java/io/ipinfo/IPinfoTest.java +++ b/src/test/java/io/ipinfo/IPinfoTest.java @@ -2,6 +2,7 @@ import io.ipinfo.api.IPinfo; import io.ipinfo.api.errors.ErrorResponseException; +import io.ipinfo.api.errors.InvalidTokenException; import io.ipinfo.api.errors.RateLimitedException; import io.ipinfo.api.model.ASNResponse; import io.ipinfo.api.model.IPResponse; @@ -56,7 +57,7 @@ public void testGoogleDNS() { () -> assertEquals(bogonResp.getIp(), "2001:0:c000:200::0:255:1"), () -> assertTrue(bogonResp.getBogon()) ); - } catch (RateLimitedException e) { + } catch (RateLimitedException | InvalidTokenException e) { fail(e); } } @@ -69,7 +70,7 @@ public void testGetMap() { try { String mapUrl = ii.getMap(Arrays.asList("1.1.1.1", "2.2.2.2", "8.8.8.8")); - } catch (RateLimitedException e) { + } catch (RateLimitedException | InvalidTokenException e) { fail(e); } }