Skip to content

Instantly share code, notes, and snippets.

@lemire
Created March 30, 2026 22:12
Show Gist options
  • Select an option

  • Save lemire/99a81fc77665184000f87d10db75e475 to your computer and use it in GitHub Desktop.

Select an option

Save lemire/99a81fc77665184000f87d10db75e475 to your computer and use it in GitHub Desktop.
IpCountryLookup - Reads a file of IP addresses and resolves each to a country using the free ip-api.com JSON API (no key required)
import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.stream.*;
import java.util.regex.*;
/**
* IpCountryLookup - Reads a file of IP addresses and resolves each to a country
* using the free ip-api.com JSON API (no key required, 45 req/min limit).
*
* Usage: java IpCountryLookup <input-file> [output-file]
*
* Input file format: one IP address per line (blank lines and #-comments ignored)
*
* Example input file:
* 8.8.8.8
* 1.1.1.1
* 185.220.101.45
*/
public class IpCountryLookup {
// Matches IPv4 addresses
private static final Pattern IPV4 =
Pattern.compile("^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$");
// ip-api.com free endpoint (HTTP only for free tier)
private static final String API_URL = "http://ip-api.com/json/%s?fields=status,message,country,countryCode,city,isp,query";
// Rate limit: free tier allows 45 requests/min → ~2000ms between requests to be safe
private static final int RATE_LIMIT_MS = 2000;
private static final AtomicLong nextAllowedTime = new AtomicLong(System.currentTimeMillis());
public static void main(String[] args) throws Exception {
if (args.length < 1) {
System.err.println("Usage: java IpCountryLookup <input-file> [output-file]");
System.exit(1);
}
String inputPath = args[0];
String outputPath = args.length >= 2 ? args[1] : null;
List<String> ips = loadIps(inputPath);
if (ips.isEmpty()) {
System.out.println("No valid IP addresses found in: " + inputPath);
return;
}
System.out.printf("Found %d IP address(es) to look up.%n%n", ips.size());
List<String> results = new ArrayList<>();
String header = String.format("%-18s %-5s %-30s %-20s %s",
"IP Address", "Code", "Country", "City", "ISP");
String divider = "-".repeat(header.length());
results.add(header);
results.add(divider);
System.out.println(header);
System.out.println(divider);
ExecutorService executor = Executors.newFixedThreadPool(2); // Limit concurrent requests
List<Future<Result>> futures = new ArrayList<>();
for (int i = 0; i < ips.size(); i++) {
int index = i;
Future<Result> future = executor.submit(() -> {
String ip = ips.get(index);
String line = lookup(ip);
System.out.println(line); // Print as soon as ready
return new Result(index, line);
});
futures.add(future);
}
executor.shutdown();
List<Result> resultList = new ArrayList<>();
for (Future<Result> f : futures) {
try {
resultList.add(f.get());
} catch (Exception e) {
System.err.println("Error processing: " + e.getMessage());
}
}
List<String> lookupResults = resultList.stream()
.sorted(Comparator.comparingInt(r -> r.index))
.map(r -> r.line)
.collect(Collectors.toList());
results.addAll(lookupResults);
Set<String> countries = resultList.stream()
.map(r -> extractCountry(r.line))
.filter(c -> !c.isEmpty())
.collect(Collectors.toCollection(TreeSet::new));
if (outputPath != null) {
Files.write(Paths.get(outputPath), results);
System.out.println("\nResults saved to: " + outputPath);
}
if (!countries.isEmpty()) {
System.out.println("\nCountries found:");
for (String c : countries) {
System.out.println(c);
}
}
}
/** Read and validate IP addresses from the input file. */
private static List<String> loadIps(String path) throws IOException {
List<String> valid = new ArrayList<>();
int lineNum = 0;
for (String raw : Files.readAllLines(Paths.get(path))) {
lineNum++;
String line = raw.trim();
if (line.isEmpty() || line.startsWith("#")) continue;
if (IPV4.matcher(line).matches()) {
valid.add(line);
} else {
System.err.printf("Line %d skipped (not a valid IPv4): %s%n", lineNum, line);
}
}
return valid;
}
/** Call ip-api.com and format the result row. */
private static String lookup(String ip) {
// Throttle requests to respect rate limit
long now;
while (true) {
now = System.currentTimeMillis();
long allowed = nextAllowedTime.get();
if (now >= allowed) {
if (nextAllowedTime.compareAndSet(allowed, now + RATE_LIMIT_MS)) {
break;
}
} else {
long wait = allowed - now;
try {
Thread.sleep(wait);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return formatRow(ip, "ERR", "Interrupted", "", "");
}
}
}
try {
URL url = new URL(String.format(API_URL, ip));
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setRequestProperty("User-Agent", "IpCountryLookup/1.0");
int status = conn.getResponseCode();
if (status != 200) {
return formatRow(ip, "ERR", "HTTP " + status, "", "");
}
StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(
new InputStreamReader(conn.getInputStream()))) {
String s;
while ((s = br.readLine()) != null) sb.append(s);
}
String json = sb.toString();
// Simple field extraction (no external JSON lib needed)
String apiStatus = extractField(json, "status");
String country = extractField(json, "country");
String countryCode = extractField(json, "countryCode");
String city = extractField(json, "city");
String isp = extractField(json, "isp");
if (!"success".equals(apiStatus)) {
String msg = extractField(json, "message");
return formatRow(ip, "??", "FAILED: " + msg, "", "");
}
return formatRow(ip, countryCode, country, city, isp);
} catch (Exception e) {
return formatRow(ip, "ERR", e.getMessage(), "", "");
}
}
private static String formatRow(String ip, String code, String country, String city, String isp) {
return String.format("%-18s %-5s %-30s %-20s %s", ip, code, country, city, isp);
}
private static String extractCountry(String line) {
String[] parts = line.trim().split("\\s+", 5);
if (parts.length < 3) return "";
String code = parts[1];
if ("??".equals(code) || "ERR".equals(code)) return "";
return parts[2];
}
/** Minimal JSON string-field extractor (no dependencies required). */
private static String extractField(String json, String key) {
String search = "\"" + key + "\"";
int idx = json.indexOf(search);
if (idx == -1) return "";
int colon = json.indexOf(":", idx + search.length());
if (colon == -1) return "";
int start = colon + 1;
while (start < json.length() && (json.charAt(start) == ' ' || json.charAt(start) == '\t')) start++;
if (start >= json.length()) return "";
char first = json.charAt(start);
if (first == '"') {
int end = json.indexOf('"', start + 1);
return end == -1 ? "" : json.substring(start + 1, end);
}
// Non-string value (number / boolean / null)
int end = start;
while (end < json.length() && ",}".indexOf(json.charAt(end)) == -1) end++;
return json.substring(start, end).trim();
}
private static class Result {
int index;
String line;
Result(int i, String l) { index = i; line = l; }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment