diff --git a/src/main/java/io/ipinfo/api/IPinfo.java b/src/main/java/io/ipinfo/api/IPinfo.java index d73f6de..458a3e5 100644 --- a/src/main/java/io/ipinfo/api/IPinfo.java +++ b/src/main/java/io/ipinfo/api/IPinfo.java @@ -10,12 +10,11 @@ import io.ipinfo.api.model.ASNResponse; import io.ipinfo.api.model.IPResponse; import io.ipinfo.api.model.MapResponse; +import io.ipinfo.api.model.ResproxyResponse; import io.ipinfo.api.request.ASNRequest; import io.ipinfo.api.request.IPRequest; import io.ipinfo.api.request.MapRequest; -import okhttp3.*; - -import javax.annotation.ParametersAreNonnullByDefault; +import io.ipinfo.api.request.ResproxyRequest; import java.io.IOException; import java.lang.reflect.Type; import java.time.Duration; @@ -26,15 +25,19 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; +import javax.annotation.ParametersAreNonnullByDefault; +import okhttp3.*; public class IPinfo { + private static final int batchMaxSize = 1000; private static final int batchReqTimeoutDefault = 5; - private static final BatchReqOpts defaultBatchReqOpts = new BatchReqOpts.Builder() + private static final BatchReqOpts defaultBatchReqOpts = + new BatchReqOpts.Builder() .setBatchSize(batchMaxSize) .setTimeoutPerBatch(batchReqTimeoutDefault) .build(); - private final static Gson gson = new Gson(); + private static final Gson gson = new Gson(); private final OkHttpClient client; private final Context context; @@ -49,7 +52,9 @@ public class IPinfo { } public static void main(String... args) { - System.out.println("This library is not meant to be run as a standalone jar."); + System.out.println( + "This library is not meant to be run as a standalone jar." + ); System.exit(0); } @@ -61,7 +66,7 @@ public static void main(String... args) { * @throws RateLimitedException an exception when your api key has been rate limited. */ public IPResponse lookupIP(String ip) throws RateLimitedException { - IPResponse response = (IPResponse)cache.get(cacheKey(ip)); + IPResponse response = (IPResponse) cache.get(cacheKey(ip)); if (response != null) { return response; } @@ -81,7 +86,7 @@ public IPResponse lookupIP(String ip) throws RateLimitedException { * @throws RateLimitedException an exception when your api key has been rate limited. */ public ASNResponse lookupASN(String asn) throws RateLimitedException { - ASNResponse response = (ASNResponse)cache.get(cacheKey(asn)); + ASNResponse response = (ASNResponse) cache.get(cacheKey(asn)); if (response != null) { return response; } @@ -93,6 +98,29 @@ public ASNResponse lookupASN(String asn) throws RateLimitedException { return response; } + /** + * Lookup residential proxy information using the IP. + * + * @param ip the ip string to lookup - accepts both ipv4 and ipv6. + * @return ResproxyResponse response from the api. + * @throws RateLimitedException an exception when your api key has been rate limited. + */ + public ResproxyResponse lookupResproxy(String ip) + throws RateLimitedException { + String cacheKeyStr = "resproxy:" + ip; + ResproxyResponse response = (ResproxyResponse) cache.get( + cacheKey(cacheKeyStr) + ); + if (response != null) { + return response; + } + + response = new ResproxyRequest(client, token, ip).handle(); + + cache.set(cacheKey(cacheKeyStr), response); + return response; + } + /** * Get a map of a list of IPs. * @@ -112,9 +140,8 @@ public String getMap(List ips) throws RateLimitedException { * @return the result where each URL is the key and the value is the data for that URL. * @throws RateLimitedException an exception when your API key has been rate limited. */ - public ConcurrentHashMap getBatch( - List urls - ) throws RateLimitedException { + public ConcurrentHashMap getBatch(List urls) + throws RateLimitedException { return this.getBatchGeneric(urls, defaultBatchReqOpts); } @@ -127,8 +154,8 @@ public ConcurrentHashMap getBatch( * @throws RateLimitedException an exception when your API key has been rate limited. */ public ConcurrentHashMap getBatch( - List urls, - BatchReqOpts opts + List urls, + BatchReqOpts opts ) throws RateLimitedException { return this.getBatchGeneric(urls, opts); } @@ -140,10 +167,11 @@ public ConcurrentHashMap getBatch( * @return the result where each IP is the key and the value is the data for that IP. * @throws RateLimitedException an exception when your API key has been rate limited. */ - public ConcurrentHashMap getBatchIps( - List ips - ) throws RateLimitedException { - return new ConcurrentHashMap(this.getBatchGeneric(ips, defaultBatchReqOpts)); + public ConcurrentHashMap getBatchIps(List ips) + throws RateLimitedException { + return new ConcurrentHashMap( + this.getBatchGeneric(ips, defaultBatchReqOpts) + ); } /** @@ -155,8 +183,8 @@ public ConcurrentHashMap getBatchIps( * @throws RateLimitedException an exception when your API key has been rate limited. */ public ConcurrentHashMap getBatchIps( - List ips, - BatchReqOpts opts + List ips, + BatchReqOpts opts ) throws RateLimitedException { return new ConcurrentHashMap(this.getBatchGeneric(ips, opts)); } @@ -169,9 +197,11 @@ public ConcurrentHashMap getBatchIps( * @throws RateLimitedException an exception when your API key has been rate limited. */ public ConcurrentHashMap getBatchAsns( - List asns + List asns ) throws RateLimitedException { - return new ConcurrentHashMap(this.getBatchGeneric(asns, defaultBatchReqOpts)); + return new ConcurrentHashMap( + this.getBatchGeneric(asns, defaultBatchReqOpts) + ); } /** @@ -183,15 +213,15 @@ public ConcurrentHashMap getBatchAsns( * @throws RateLimitedException an exception when your API key has been rate limited. */ public ConcurrentHashMap getBatchAsns( - List asns, - BatchReqOpts opts + List asns, + BatchReqOpts opts ) throws RateLimitedException { return new ConcurrentHashMap(this.getBatchGeneric(asns, opts)); } private ConcurrentHashMap getBatchGeneric( - List urls, - BatchReqOpts opts + List urls, + BatchReqOpts opts ) throws RateLimitedException { int batchSize; int timeoutPerBatch; @@ -201,7 +231,7 @@ private ConcurrentHashMap getBatchGeneric( // if the cache is available, filter out URLs already cached. result = new ConcurrentHashMap<>(urls.size()); if (this.cache != null) { - lookupUrls = new ArrayList<>(urls.size()/2); + lookupUrls = new ArrayList<>(urls.size() / 2); for (String url : urls) { Object val = cache.get(cacheKey(url)); if (val != null) { @@ -244,12 +274,14 @@ private ConcurrentHashMap getBatchGeneric( // prepare latch & common request. // each request, when complete, will countdown the latch. - CountDownLatch latch = new CountDownLatch((int)Math.ceil(lookupUrls.size()/1000.0)); + CountDownLatch latch = new CountDownLatch( + (int) Math.ceil(lookupUrls.size() / 1000.0) + ); Request.Builder reqCommon = new Request.Builder() - .url(postUrl) - .addHeader("Content-Type", "application/json") - .addHeader("Authorization", Credentials.basic(token, "")) - .addHeader("User-Agent", "IPinfoClient/Java/3.2.0"); + .url(postUrl) + .addHeader("Content-Type", "application/json") + .addHeader("Authorization", Credentials.basic(token, "")) + .addHeader("User-Agent", "IPinfoClient/Java/3.2.0"); for (int i = 0; i < lookupUrls.size(); i += batchSize) { // create chunk. @@ -263,49 +295,72 @@ private ConcurrentHashMap getBatchGeneric( String urlListJson = gson.toJson(urlsChunk); RequestBody requestBody = RequestBody.create(null, urlListJson); Request req = reqCommon.post(requestBody).build(); - OkHttpClient chunkClient = client.newBuilder() - .connectTimeout(timeoutPerBatch, TimeUnit.SECONDS) - .readTimeout(timeoutPerBatch, TimeUnit.SECONDS) - .build(); - chunkClient.newCall(req).enqueue(new Callback() { - @Override - @ParametersAreNonnullByDefault - public void onFailure(Call call, IOException e) { - latch.countDown(); - } - - @Override - @ParametersAreNonnullByDefault - public void onResponse(Call call, Response response) throws IOException { - if (response.body() == null || response.code() == 429) { - return; - } + OkHttpClient chunkClient = client + .newBuilder() + .connectTimeout(timeoutPerBatch, TimeUnit.SECONDS) + .readTimeout(timeoutPerBatch, TimeUnit.SECONDS) + .build(); + chunkClient + .newCall(req) + .enqueue( + new Callback() { + @Override + @ParametersAreNonnullByDefault + public void onFailure(Call call, IOException e) { + latch.countDown(); + } - Type respType = new TypeToken>() {}.getType(); - HashMap localResult - = gson.fromJson(response.body().string(), respType); - localResult.forEach(new BiConsumer() { @Override - public void accept(String k, Object v) { - if (k.startsWith("AS")) { - String vStr = gson.toJson(v); - ASNResponse vCasted = gson.fromJson(vStr, ASNResponse.class); - vCasted.setContext(context); - result.put(k, vCasted); - } else if (InetAddresses.isInetAddress(k)) { - String vStr = gson.toJson(v); - IPResponse vCasted = gson.fromJson(vStr, IPResponse.class); - vCasted.setContext(context); - result.put(k, vCasted); - } else { - result.put(k, v); + @ParametersAreNonnullByDefault + public void onResponse(Call call, Response response) + throws IOException { + if ( + response.body() == null || + response.code() == 429 + ) { + return; } - } - }); - latch.countDown(); - } - }); + Type respType = new TypeToken< + HashMap + >() {}.getType(); + HashMap localResult = gson.fromJson( + response.body().string(), + respType + ); + localResult.forEach( + new BiConsumer() { + @Override + public void accept(String k, Object v) { + if (k.startsWith("AS")) { + String vStr = gson.toJson(v); + ASNResponse vCasted = gson.fromJson( + vStr, + ASNResponse.class + ); + vCasted.setContext(context); + result.put(k, vCasted); + } else if ( + InetAddresses.isInetAddress(k) + ) { + String vStr = gson.toJson(v); + IPResponse vCasted = gson.fromJson( + vStr, + IPResponse.class + ); + vCasted.setContext(context); + result.put(k, vCasted); + } else { + result.put(k, v); + } + } + } + ); + + latch.countDown(); + } + } + ); } // wait for all requests to finish. @@ -313,7 +368,10 @@ public void accept(String k, Object v) { if (opts.timeoutTotal == 0) { latch.await(); } else { - boolean success = latch.await(opts.timeoutTotal, TimeUnit.SECONDS); + boolean success = latch.await( + opts.timeoutTotal, + TimeUnit.SECONDS + ); if (!success) { if (result.size() == 0) { return null; @@ -350,10 +408,11 @@ public void accept(String k, Object v) { * @return the versioned cache key. */ public static String cacheKey(String k) { - return k+":1"; + return k + ":1"; } public static class Builder { + private OkHttpClient client = new OkHttpClient.Builder().build(); private String token = ""; private Cache cache = new SimpleCache(Duration.ofDays(1)); @@ -379,16 +438,17 @@ public IPinfo build() { } public static class BatchReqOpts { + public final int batchSize; public final int timeoutPerBatch; public final int timeoutTotal; public final boolean filter; public BatchReqOpts( - int batchSize, - int timeoutPerBatch, - int timeoutTotal, - boolean filter + int batchSize, + int timeoutPerBatch, + int timeoutTotal, + boolean filter ) { this.batchSize = batchSize; this.timeoutPerBatch = timeoutPerBatch; @@ -397,6 +457,7 @@ public BatchReqOpts( } public static class Builder { + private int batchSize = 1000; private int timeoutPerBatch = 5; private int timeoutTotal = 0; @@ -462,7 +523,12 @@ public Builder setFilter(boolean filter) { } public IPinfo.BatchReqOpts build() { - return new IPinfo.BatchReqOpts(batchSize, timeoutPerBatch, timeoutTotal, filter); + return new IPinfo.BatchReqOpts( + batchSize, + timeoutPerBatch, + timeoutTotal, + filter + ); } } } diff --git a/src/main/java/io/ipinfo/api/model/ResproxyResponse.java b/src/main/java/io/ipinfo/api/model/ResproxyResponse.java new file mode 100644 index 0000000..553095f --- /dev/null +++ b/src/main/java/io/ipinfo/api/model/ResproxyResponse.java @@ -0,0 +1,50 @@ +package io.ipinfo.api.model; + +import com.google.gson.annotations.SerializedName; + +public class ResproxyResponse { + private final String ip; + @SerializedName("last_seen") + private final String lastSeen; + @SerializedName("percent_days_seen") + private final Double percentDaysSeen; + private final String service; + + public ResproxyResponse( + String ip, + String lastSeen, + Double percentDaysSeen, + String service + ) { + this.ip = ip; + this.lastSeen = lastSeen; + this.percentDaysSeen = percentDaysSeen; + this.service = service; + } + + public String getIp() { + return ip; + } + + public String getLastSeen() { + return lastSeen; + } + + public Double getPercentDaysSeen() { + return percentDaysSeen; + } + + public String getService() { + return service; + } + + @Override + public String toString() { + return "ResproxyResponse{" + + "ip='" + ip + '\'' + + ", lastSeen='" + lastSeen + '\'' + + ", percentDaysSeen=" + percentDaysSeen + + ", service='" + service + '\'' + + '}'; + } +} diff --git a/src/main/java/io/ipinfo/api/request/ResproxyRequest.java b/src/main/java/io/ipinfo/api/request/ResproxyRequest.java new file mode 100644 index 0000000..42c98e0 --- /dev/null +++ b/src/main/java/io/ipinfo/api/request/ResproxyRequest.java @@ -0,0 +1,37 @@ +package io.ipinfo.api.request; + +import io.ipinfo.api.errors.ErrorResponseException; +import io.ipinfo.api.errors.RateLimitedException; +import io.ipinfo.api.model.ResproxyResponse; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + + +public class ResproxyRequest extends BaseRequest { + private final static String URL_FORMAT = "https://ipinfo.io/resproxy/%s"; + private final String ip; + + public ResproxyRequest(OkHttpClient client, String token, String ip) { + super(client, token); + this.ip = ip; + } + + @Override + public ResproxyResponse handle() throws RateLimitedException { + String url = String.format(URL_FORMAT, ip); + Request.Builder request = new Request.Builder().url(url).get(); + + try (Response response = handleRequest(request)) { + if (response == null || response.body() == null) { + return null; + } + + try { + return gson.fromJson(response.body().string(), ResproxyResponse.class); + } catch (Exception ex) { + throw new ErrorResponseException(ex); + } + } + } +} diff --git a/src/test/java/io/ipinfo/IPinfoTest.java b/src/test/java/io/ipinfo/IPinfoTest.java index 26eeeb6..d546f46 100644 --- a/src/test/java/io/ipinfo/IPinfoTest.java +++ b/src/test/java/io/ipinfo/IPinfoTest.java @@ -1,15 +1,13 @@ package io.ipinfo; import io.ipinfo.api.IPinfo; -import io.ipinfo.api.errors.ErrorResponseException; import io.ipinfo.api.errors.RateLimitedException; import io.ipinfo.api.model.ASNResponse; import io.ipinfo.api.model.IPResponse; -import org.junit.jupiter.api.BeforeEach; +import io.ipinfo.api.model.ResproxyResponse; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -212,7 +210,7 @@ public void testGetBatchIps() { () -> assertFalse(res3.getPrivacy().getVpn(), "VPN mismatch"), () -> assertFalse(res3.getPrivacy().getTor(), "Tor mismatch"), () -> assertFalse(res3.getPrivacy().getRelay(), "relay mismatch"), - () -> assertFalse(res3.getPrivacy().getHosting(), "hosting mismatch"), + () -> assertTrue(res3.getPrivacy().getHosting(), "hosting mismatch"), () -> assertEquals("", res3.getPrivacy().getService(), "service mismatch"), () -> assertEquals(5, res3.getDomains().getDomains().size(), "domains size mismatch") ); @@ -267,4 +265,70 @@ public void testGetBatchAsns() { fail(e); } } + + @Test + public void testLookupResproxy() { + IPinfo ii = new IPinfo.Builder() + .setToken(System.getenv("IPINFO_TOKEN")) + .build(); + + try { + ResproxyResponse response = ii.lookupResproxy("175.107.211.204"); + assertAll( + "175.107.211.204", + () -> + assertEquals( + "175.107.211.204", + response.getIp(), + "IP mismatch" + ), + () -> + assertNotNull( + response.getLastSeen(), + "lastSeen should be set" + ), + () -> + assertNotNull( + response.getPercentDaysSeen(), + "percentDaysSeen should be set" + ), + () -> + assertNotNull( + response.getService(), + "service should be set" + ) + ); + } catch (RateLimitedException e) { + fail(e); + } + } + + @Test + public void testLookupResproxyEmpty() { + IPinfo ii = new IPinfo.Builder() + .setToken(System.getenv("IPINFO_TOKEN")) + .build(); + + try { + ResproxyResponse response = ii.lookupResproxy("8.8.8.8"); + assertAll( + "8.8.8.8 resproxy empty", + () -> assertNull(response.getIp(), "IP should be null"), + () -> + assertNull( + response.getLastSeen(), + "lastSeen should be null" + ), + () -> + assertNull( + response.getPercentDaysSeen(), + "percentDaysSeen should be null" + ), + () -> + assertNull(response.getService(), "service should be null") + ); + } catch (RateLimitedException e) { + fail(e); + } + } }