forked from I2P_Developers/i2p.i2p
911 lines
36 KiB
Java
911 lines
36 KiB
Java
package net.i2p.router.transport;
|
|
/*
|
|
* free (adj.): unencumbered; not under the control of others
|
|
* Use at your own risk.
|
|
*/
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.BufferedWriter;
|
|
import java.io.IOException;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.io.OutputStreamWriter;
|
|
import java.util.Arrays;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.Date;
|
|
import java.util.Map;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
import com.maxmind.db.CHMCache;
|
|
import com.maxmind.geoip.InvalidDatabaseException;
|
|
import com.maxmind.geoip.LookupService;
|
|
import com.maxmind.geoip2.DatabaseReader;
|
|
|
|
import net.i2p.I2PAppContext;
|
|
import net.i2p.app.ClientAppManager;
|
|
import net.i2p.data.DataHelper;
|
|
import net.i2p.data.Hash;
|
|
import net.i2p.data.router.RouterAddress;
|
|
import net.i2p.data.router.RouterInfo;
|
|
import net.i2p.router.Blocklist;
|
|
import net.i2p.router.Router;
|
|
import net.i2p.router.RouterContext;
|
|
import net.i2p.router.transport.udp.UDPTransport;
|
|
import net.i2p.update.UpdateManager;
|
|
import net.i2p.update.UpdateType;
|
|
import net.i2p.util.Addresses;
|
|
import net.i2p.util.ConcurrentHashSet;
|
|
import net.i2p.util.Log;
|
|
import net.i2p.util.SecureFileOutputStream;
|
|
import net.i2p.util.SystemVersion;
|
|
|
|
/**
|
|
* Manage geoip lookup in a file with the Tor geoip format.
|
|
*
|
|
* The lookup is expensive, so a lookup is queued with add().
|
|
* The actual lookup of multiple IPs is fired with lookup().
|
|
* To get a country for an IP, use get() which returns a lower-case,
|
|
* generally two-letter country code or null.
|
|
*
|
|
* Everything here uses longs, since Java is signed-only, the file is
|
|
* sorted by unsigned, and we don't store the table in memory
|
|
* (unlike in Blocklist.java, where it's in-memory so we want to be
|
|
* space-efficient)
|
|
*
|
|
* @author zzz
|
|
*/
|
|
public class GeoIP {
|
|
private final Log _log;
|
|
private final I2PAppContext _context;
|
|
private final Map<String, String> _codeToName;
|
|
/** code to itself to prevent String proliferation */
|
|
private final Map<String, String> _codeCache;
|
|
|
|
// In the following structures, an IPv4 IP is stored as a non-negative long, 0 to 2**32 - 1,
|
|
// and the first 8 bytes of an IPv6 IP are stored as a signed long.
|
|
private final Map<Long, String> _IPToCountry;
|
|
private final Set<Long> _pendingSearch;
|
|
private final Set<Long> _pendingIPv6Search;
|
|
private final Set<Long> _notFound;
|
|
private final AtomicBoolean _lock;
|
|
private int _lookupRunCount;
|
|
private static final Map<String, List<String>> _associatedCountries;
|
|
|
|
static final String PROP_GEOIP_ENABLED = "routerconsole.geoip.enable";
|
|
public static final String PROP_GEOIP_DIR = "geoip.dir";
|
|
public static final String GEOIP_DIR_DEFAULT = "geoip";
|
|
static final String GEOIP_FILE_DEFAULT = "geoip.txt";
|
|
public static final String GEOIP2_FILE_DEFAULT = "GeoLite2-Country.mmdb";
|
|
static final String COUNTRY_FILE_DEFAULT = "countries.txt";
|
|
public static final String PROP_IP_COUNTRY = "i2np.lastCountry";
|
|
public static final String PROP_DEBIAN_GEOIP = "geoip.dat";
|
|
public static final String PROP_DEBIAN_GEOIPV6 = "geoip.v6.dat";
|
|
private static final String DEBIAN_GEOIP_FILE = "/usr/share/GeoIP/GeoIP.dat";
|
|
private static final String DEBIAN_GEOIPV6_FILE = "/usr/share/GeoIP/GeoIPv6.dat";
|
|
private static final boolean DISABLE_DEBIAN = false;
|
|
private static final boolean ENABLE_DEBIAN = !DISABLE_DEBIAN && !(SystemVersion.isWindows() || SystemVersion.isAndroid());
|
|
private static final String PROP_BLOCK_MY_COUNTRY = "i2np.blockMyCountry";
|
|
/** maxmind API */
|
|
private static final String UNKNOWN_COUNTRY_CODE = "--";
|
|
/** db-ip.com https://db-ip.com/faq.php */
|
|
private static final String UNKNOWN_COUNTRY_CODE2 = "ZZ";
|
|
|
|
static {
|
|
// To block additional countries b,c,d when blocking country a,
|
|
// put the list a,b,c,d for country a.
|
|
_associatedCountries = new HashMap<String, List<String>>(2);
|
|
List<String> c = new ArrayList<String>(2);
|
|
c.add("cn");
|
|
c.add("hk");
|
|
_associatedCountries.put("cn", c);
|
|
_associatedCountries.put("hk", c);
|
|
}
|
|
|
|
/**
|
|
* @param context RouterContext in production, I2PAppContext for testing only
|
|
*/
|
|
public GeoIP(I2PAppContext context) {
|
|
_context = context;
|
|
_log = context.logManager().getLog(GeoIP.class);
|
|
_codeToName = new ConcurrentHashMap<String, String>(512);
|
|
_codeCache = new ConcurrentHashMap<String, String>(512);
|
|
_IPToCountry = new ConcurrentHashMap<Long, String>();
|
|
_pendingSearch = new ConcurrentHashSet<Long>();
|
|
_pendingIPv6Search = new ConcurrentHashSet<Long>();
|
|
_notFound = new ConcurrentHashSet<Long>();
|
|
_lock = new AtomicBoolean();
|
|
readCountryFile();
|
|
}
|
|
|
|
/**
|
|
* @since 0.9.3
|
|
*/
|
|
public void shutdown() {
|
|
_codeToName.clear();
|
|
_codeCache.clear();
|
|
_IPToCountry.clear();
|
|
_pendingSearch.clear();
|
|
_pendingIPv6Search.clear();
|
|
_notFound.clear();
|
|
}
|
|
|
|
/**
|
|
* Fire off a thread to lookup all pending IPs.
|
|
* There is no indication of completion.
|
|
* Results will be added to the table and available via get() after completion.
|
|
*/
|
|
/******
|
|
public void lookup() {
|
|
if (! _context.getBooleanPropertyDefaultTrue(PROP_GEOIP_ENABLED)) {
|
|
_pendingSearch.clear();
|
|
return;
|
|
}
|
|
Thread t = new Thread(new LookupJob());
|
|
t.start();
|
|
}
|
|
******/
|
|
|
|
/**
|
|
* Blocking lookup of all pending IPs.
|
|
* Results will be added to the table and available via get() after completion.
|
|
*
|
|
* Public for BundleRouterInfos
|
|
*/
|
|
public void blockingLookup() {
|
|
if (! _context.getBooleanPropertyDefaultTrue(PROP_GEOIP_ENABLED)) {
|
|
_pendingSearch.clear();
|
|
_pendingIPv6Search.clear();
|
|
return;
|
|
}
|
|
int pri = Thread.currentThread().getPriority();
|
|
if (pri > Thread.MIN_PRIORITY)
|
|
Thread.currentThread().setPriority(pri - 1);
|
|
try {
|
|
LookupJob j = new LookupJob();
|
|
long ts = j.runit();
|
|
if (ts > 0)
|
|
updateOurCountry(ts);
|
|
} finally {
|
|
if (pri > Thread.MIN_PRIORITY)
|
|
Thread.currentThread().setPriority(pri);
|
|
}
|
|
}
|
|
|
|
private class LookupJob implements Runnable {
|
|
private static final int CLEAR = 8;
|
|
|
|
public void run() {
|
|
runit();
|
|
}
|
|
|
|
/**
|
|
* @return timestamp of the geoip ipv4 file used, or 0 on failure
|
|
*/
|
|
public long runit() {
|
|
if (_lock.getAndSet(true))
|
|
return 0;
|
|
int toSearch = 0;
|
|
int found = 0;
|
|
File geoip2 = getGeoIP2();
|
|
DatabaseReader dbr = null;
|
|
long rv = 0;
|
|
long start = _context.clock().now();
|
|
try {
|
|
// clear the negative cache every few runs, to prevent it from getting too big
|
|
if (((++_lookupRunCount) % CLEAR) == 0)
|
|
_notFound.clear();
|
|
// add our detected addresses
|
|
Set<String> addrs = Addresses.getAddresses(false, true);
|
|
for (String ip : addrs) {
|
|
add(ip);
|
|
}
|
|
String lastIP = _context.getProperty(UDPTransport.PROP_IP);
|
|
if (lastIP != null)
|
|
add(lastIP);
|
|
lastIP = _context.getProperty(UDPTransport.PROP_IPV6);
|
|
if (lastIP != null)
|
|
add(lastIP);
|
|
// IPv4
|
|
Long[] search = _pendingSearch.toArray(new Long[_pendingSearch.size()]);
|
|
_pendingSearch.clear();
|
|
if (search.length > 0) {
|
|
toSearch += search.length;
|
|
Arrays.sort(search);
|
|
File f = new File(_context.getProperty(PROP_DEBIAN_GEOIP, DEBIAN_GEOIP_FILE));
|
|
// if we have both, prefer the most recent.
|
|
// The Debian data can be pretty old.
|
|
// For now, we use the file date, we don't open it up to get the metadata.
|
|
if (ENABLE_DEBIAN && f.exists() &&
|
|
(geoip2 == null || f.lastModified() > geoip2.lastModified())) {
|
|
// Maxmind v1 database
|
|
LookupService ls = null;
|
|
try {
|
|
ls = new LookupService(f, LookupService.GEOIP_STANDARD);
|
|
Date date = ls.getDatabaseInfo().getDate();
|
|
if (date != null) {
|
|
long time = date.getTime();
|
|
notifyVersion("GeoIPv4", time);
|
|
rv = time;
|
|
}
|
|
for (int i = 0; i < search.length; i++) {
|
|
Long ipl = search[i];
|
|
long ip = ipl.longValue();
|
|
// returns upper case or "--"
|
|
String uc = ls.getCountry(ip).getCode();
|
|
if (!uc.equals(UNKNOWN_COUNTRY_CODE)) {
|
|
String lc = uc.toLowerCase(Locale.US);
|
|
String cached = _codeCache.get(lc);
|
|
if (cached == null)
|
|
cached = lc;
|
|
_IPToCountry.put(ipl, cached);
|
|
found++;
|
|
} else {
|
|
_notFound.add(ipl);
|
|
}
|
|
}
|
|
} catch (IOException ioe) {
|
|
_log.error("GeoIP failure", ioe);
|
|
} catch (InvalidDatabaseException ide) {
|
|
_log.error("GeoIP failure", ide);
|
|
} finally {
|
|
if (ls != null) ls.close();
|
|
}
|
|
} else if (geoip2 != null) {
|
|
// Maxmind v2 database
|
|
try {
|
|
dbr = openGeoIP2(geoip2);
|
|
long time = dbr.getMetadata().getBuildDate().getTime();
|
|
if (time > 0) {
|
|
notifyVersion("GeoIP2", time);
|
|
rv = time;
|
|
}
|
|
for (int i = 0; i < search.length; i++) {
|
|
Long ipl = search[i];
|
|
String ipv4 = toV4(ipl);
|
|
// returns upper case or null
|
|
String uc = dbr.country(ipv4);
|
|
if (uc != null && !uc.equals(UNKNOWN_COUNTRY_CODE2)) {
|
|
String lc = uc.toLowerCase(Locale.US);
|
|
String cached = _codeCache.get(lc);
|
|
if (cached == null)
|
|
cached = lc;
|
|
_IPToCountry.put(ipl, cached);
|
|
found++;
|
|
} else {
|
|
_notFound.add(ipl);
|
|
}
|
|
}
|
|
} catch (IOException ioe) {
|
|
_log.error("GeoIP2 failure", ioe);
|
|
}
|
|
} else {
|
|
// Tor-style database
|
|
String[] countries = readGeoIPFile(search);
|
|
if (countries.length > 0) {
|
|
String geoDir = _context.getProperty(PROP_GEOIP_DIR, GEOIP_DIR_DEFAULT);
|
|
File geoFile = new File(geoDir);
|
|
if (!geoFile.isAbsolute())
|
|
geoFile = new File(_context.getBaseDir(), geoDir);
|
|
geoFile = new File(geoFile, GEOIP_FILE_DEFAULT);
|
|
rv = geoFile.lastModified();
|
|
}
|
|
for (int i = 0; i < countries.length; i++) {
|
|
if (countries[i] != null) {
|
|
_IPToCountry.put(search[i], countries[i]);
|
|
found++;
|
|
} else {
|
|
_notFound.add(search[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// IPv6
|
|
search = _pendingIPv6Search.toArray(new Long[_pendingIPv6Search.size()]);
|
|
_pendingIPv6Search.clear();
|
|
if (search.length > 0) {
|
|
toSearch += search.length;
|
|
Arrays.sort(search);
|
|
File f = new File(_context.getProperty(PROP_DEBIAN_GEOIPV6, DEBIAN_GEOIPV6_FILE));
|
|
if (ENABLE_DEBIAN && f.exists() &&
|
|
(geoip2 == null || f.lastModified() > geoip2.lastModified())) {
|
|
// Maxmind v1 database
|
|
LookupService ls = null;
|
|
try {
|
|
ls = new LookupService(f, LookupService.GEOIP_STANDARD);
|
|
Date date = ls.getDatabaseInfo().getDate();
|
|
if (date != null)
|
|
notifyVersion("GeoIPv6", date.getTime());
|
|
for (int i = 0; i < search.length; i++) {
|
|
Long ipl = search[i];
|
|
long ip = ipl.longValue();
|
|
String ipv6 = toV6(ip);
|
|
// returns upper case or "--"
|
|
String uc = ls.getCountryV6(ipv6).getCode();
|
|
if (!uc.equals(UNKNOWN_COUNTRY_CODE)) {
|
|
String lc = uc.toLowerCase(Locale.US);
|
|
String cached = _codeCache.get(lc);
|
|
if (cached == null)
|
|
cached = lc;
|
|
_IPToCountry.put(ipl, cached);
|
|
found++;
|
|
} else {
|
|
_notFound.add(ipl);
|
|
}
|
|
}
|
|
} catch (IOException ioe) {
|
|
_log.error("GeoIP failure", ioe);
|
|
} catch (InvalidDatabaseException ide) {
|
|
_log.error("GeoIP failure", ide);
|
|
} finally {
|
|
if (ls != null) ls.close();
|
|
}
|
|
} else if (geoip2 != null) {
|
|
// Maxmind v2 database
|
|
try {
|
|
if (dbr == null)
|
|
dbr = openGeoIP2(geoip2);
|
|
for (int i = 0; i < search.length; i++) {
|
|
Long ipl = search[i];
|
|
String ipv6 = toV6(ipl);
|
|
// returns upper case or null
|
|
String uc = dbr.country(ipv6);
|
|
if (uc != null && !uc.equals(UNKNOWN_COUNTRY_CODE2)) {
|
|
String lc = uc.toLowerCase(Locale.US);
|
|
String cached = _codeCache.get(lc);
|
|
if (cached == null)
|
|
cached = lc;
|
|
_IPToCountry.put(ipl, cached);
|
|
found++;
|
|
} else {
|
|
_notFound.add(ipl);
|
|
}
|
|
}
|
|
} catch (IOException ioe) {
|
|
_log.error("GeoIP2 failure", ioe);
|
|
}
|
|
} else {
|
|
// I2P format IPv6 database
|
|
String[] countries = GeoIPv6.readGeoIPFile(_context, search, _codeCache);
|
|
for (int i = 0; i < countries.length; i++) {
|
|
if (countries[i] != null) {
|
|
_IPToCountry.put(search[i], countries[i]);
|
|
found++;
|
|
} else {
|
|
_notFound.add(search[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
if (dbr != null) try { dbr.close(); } catch (IOException ioe) {}
|
|
_lock.set(false);
|
|
}
|
|
if (_log.shouldInfo())
|
|
_log.info("GeoIP processing finished, looked up: " + toSearch + " found: " + found +
|
|
" time: " + (_context.clock().now() - start));
|
|
return rv;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write all IP ranges for country to blocklist-country.txt.
|
|
* Inline, blocking.
|
|
*
|
|
* @param two-letter lower-case country code
|
|
* @since 0.9.48
|
|
*/
|
|
private void countryToIP(String country) {
|
|
while (_lock.getAndSet(true)) {
|
|
try { Thread.sleep(1000); } catch (InterruptedException ie) { return; }
|
|
}
|
|
File geoip2 = getGeoIP2();
|
|
DatabaseReader dbr = null;
|
|
if (_log.shouldInfo())
|
|
_log.info("Generating blocklist for our country " + country);
|
|
long start = _context.clock().now();
|
|
File fout = new File(_context.getConfigDir(), Blocklist.BLOCKLIST_COUNTRY_FILE);
|
|
BufferedWriter out = null;
|
|
List<String> countries = _associatedCountries.get(country);
|
|
if (countries == null)
|
|
countries = Collections.singletonList(country);
|
|
try {
|
|
File f = new File(_context.getProperty(PROP_DEBIAN_GEOIP, DEBIAN_GEOIP_FILE));
|
|
// if we have both, prefer the most recent.
|
|
// The Debian data can be pretty old.
|
|
// For now, we use the file date, we don't open it up to get the metadata.
|
|
if (ENABLE_DEBIAN && f.exists() &&
|
|
(geoip2 == null || f.lastModified() > geoip2.lastModified())) {
|
|
// Maxmind v1 database
|
|
LookupService ls = null;
|
|
try {
|
|
out = new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(fout), "UTF-8"));
|
|
ls = new LookupService(f, LookupService.GEOIP_STANDARD);
|
|
for (String c : countries) {
|
|
ls.countryToIP(c, out);
|
|
}
|
|
out.close();
|
|
out = null;
|
|
RouterContext ctx = (RouterContext) _context;
|
|
ctx.blocklist().addCountryFile();
|
|
} catch (IOException ioe) {
|
|
_log.error("GeoIP failure", ioe);
|
|
} catch (InvalidDatabaseException ide) {
|
|
_log.error("GeoIP failure", ide);
|
|
} finally {
|
|
if (ls != null) ls.close();
|
|
}
|
|
} else if (geoip2 != null) {
|
|
// Maxmind v2 database
|
|
try {
|
|
out = new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(fout), "UTF-8"));
|
|
dbr = openGeoIP2(geoip2);
|
|
for (String c : countries) {
|
|
dbr.countryToIP(c, out);
|
|
}
|
|
out.close();
|
|
out = null;
|
|
RouterContext ctx = (RouterContext) _context;
|
|
ctx.blocklist().addCountryFile();
|
|
} catch (IOException ioe) {
|
|
_log.error("GeoIP2 failure", ioe);
|
|
}
|
|
} else {
|
|
// Tor-style database, unsupported
|
|
}
|
|
} finally {
|
|
if (out != null) try { out.close(); } catch (IOException e) {}
|
|
if (dbr != null) try { dbr.close(); } catch (IOException ioe) {}
|
|
_lock.set(false);
|
|
}
|
|
if (_log.shouldInfo())
|
|
_log.info("Finished generating blocklist for our country, time: " + (_context.clock().now() - start));
|
|
}
|
|
|
|
/**
|
|
* Get the GeoIP2 database file
|
|
*
|
|
* @return null if not found
|
|
* @since 0.9.38
|
|
*/
|
|
private File getGeoIP2() {
|
|
String geoDir = _context.getProperty(PROP_GEOIP_DIR, GEOIP_DIR_DEFAULT);
|
|
File geoFile = new File(geoDir);
|
|
if (!geoFile.isAbsolute())
|
|
geoFile = new File(_context.getBaseDir(), geoDir);
|
|
geoFile = new File(geoFile, GEOIP2_FILE_DEFAULT);
|
|
if (!geoFile.exists()) {
|
|
if (_log.shouldLog(Log.WARN))
|
|
_log.warn("GeoIP2 file not found: " + geoFile.getAbsolutePath());
|
|
return null;
|
|
}
|
|
return geoFile;
|
|
}
|
|
|
|
/**
|
|
* Open a GeoIP2 database
|
|
* @since 0.9.38
|
|
*/
|
|
private DatabaseReader openGeoIP2(File geoFile) throws IOException {
|
|
DatabaseReader.Builder b = new DatabaseReader.Builder(geoFile);
|
|
b.withCache(new CHMCache(256));
|
|
DatabaseReader rv = b.build();
|
|
if (_log.shouldDebug())
|
|
_log.debug("Opened GeoIP2 Database, Metadata: " + rv.getMetadata());
|
|
return rv;
|
|
}
|
|
|
|
/**
|
|
* Read in and parse the country file.
|
|
* The file need not be sorted.
|
|
*
|
|
* Acceptable formats:
|
|
* #comment (# must be in column 1)
|
|
* code,full name
|
|
*
|
|
* Example:
|
|
* US,UNITED STATES
|
|
*
|
|
* To create:
|
|
* wget http://ip-to-country.webhosting.info/downloads/ip-to-country.csv.zip
|
|
* unzip ip-to-country.csv.zip
|
|
* cut -d, -f3,5 < ip-to-country.csv|sed 's/"//g' | sort | uniq > countries.txt
|
|
*
|
|
*/
|
|
private void readCountryFile() {
|
|
InputStream is = GeoIP.class.getResourceAsStream("/net/i2p/util/resources/" + COUNTRY_FILE_DEFAULT);
|
|
if (is == null) {
|
|
if (_log.shouldLog(Log.WARN))
|
|
_log.warn("Country file not found");
|
|
return;
|
|
}
|
|
BufferedReader br = null;
|
|
try {
|
|
br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
|
|
String line = null;
|
|
while ( (line = br.readLine()) != null) {
|
|
try {
|
|
if (line.charAt(0) == '#') {
|
|
continue;
|
|
}
|
|
String[] s = DataHelper.split(line, ",");
|
|
String lc = s[0].toLowerCase(Locale.US);
|
|
_codeToName.put(lc, s[1]);
|
|
_codeCache.put(lc, lc);
|
|
} catch (IndexOutOfBoundsException ioobe) {
|
|
}
|
|
}
|
|
} catch (IOException ioe) {
|
|
if (_log.shouldLog(Log.ERROR))
|
|
_log.error("Error reading the Country File", ioe);
|
|
} finally {
|
|
try { is.close(); } catch (IOException ioe) {}
|
|
if (br != null) try { br.close(); } catch (IOException ioe) {}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read in and parse the geoip file.
|
|
* The geoip file must be sorted, and may not contain overlapping entries.
|
|
*
|
|
* Acceptable formats (IPV4 only):
|
|
* #comment (# must be in column 1)
|
|
* integer IP,integer IP, country code
|
|
*
|
|
* Example:
|
|
* 121195296,121195327,IT
|
|
*
|
|
* This is identical to the Tor geoip file, which can be found in
|
|
* src/config/geoip in their distribution, or /usr/local/lib/share/tor/geoip
|
|
* in their installation.
|
|
* Thanks to Tor for finding a source for the data, and the format script.
|
|
*
|
|
* To create:
|
|
* wget http://ip-to-country.webhosting.info/downloads/ip-to-country.csv.zip
|
|
* unzip ip-to-country.csv.zip
|
|
* cut -d, -f0-3 < ip-to-country.csv|sed 's/"//g' > geoip.txt
|
|
*
|
|
* @param search a sorted array of IPs to search
|
|
* @return an array of country codes, same order as the search param,
|
|
* or a zero-length array on failure
|
|
*
|
|
*/
|
|
private String[] readGeoIPFile(Long[] search) {
|
|
String geoDir = _context.getProperty(PROP_GEOIP_DIR, GEOIP_DIR_DEFAULT);
|
|
File geoFile = new File(geoDir);
|
|
if (!geoFile.isAbsolute())
|
|
geoFile = new File(_context.getBaseDir(), geoDir);
|
|
geoFile = new File(geoFile, GEOIP_FILE_DEFAULT);
|
|
if (!geoFile.exists()) {
|
|
if (_log.shouldLog(Log.WARN))
|
|
_log.warn("GeoIP file not found: " + geoFile.getAbsolutePath());
|
|
return new String[0];
|
|
}
|
|
String[] rv = new String[search.length];
|
|
int idx = 0;
|
|
BufferedReader br = null;
|
|
try {
|
|
String buf = null;
|
|
br = new BufferedReader(new InputStreamReader(
|
|
new FileInputStream(geoFile), "ISO-8859-1"));
|
|
notifyVersion("Torv4", geoFile.lastModified());
|
|
while ((buf = br.readLine()) != null && idx < search.length) {
|
|
try {
|
|
if (buf.charAt(0) == '#') {
|
|
continue;
|
|
}
|
|
String[] s = DataHelper.split(buf, ",");
|
|
long ip1 = Long.parseLong(s[0]);
|
|
long ip2 = Long.parseLong(s[1]);
|
|
while (idx < search.length && search[idx].longValue() < ip1) {
|
|
idx++;
|
|
}
|
|
while (idx < search.length && search[idx].longValue() >= ip1 && search[idx].longValue() <= ip2) {
|
|
String lc = s[2].toLowerCase(Locale.US);
|
|
// replace the new string with the identical one from the cache
|
|
String cached = _codeCache.get(lc);
|
|
if (cached == null)
|
|
cached = lc;
|
|
rv[idx++] = cached;
|
|
}
|
|
} catch (IndexOutOfBoundsException ioobe) {
|
|
} catch (NumberFormatException nfe) {
|
|
}
|
|
}
|
|
} catch (IOException ioe) {
|
|
if (_log.shouldLog(Log.ERROR))
|
|
_log.error("Error reading the geoFile", ioe);
|
|
} finally {
|
|
if (br != null) try { br.close(); } catch (IOException ioe) {}
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
/**
|
|
* Tell the update manager.
|
|
*
|
|
* @since 0.9.45
|
|
*/
|
|
private void notifyVersion(String subtype, long version) {
|
|
notifyVersion(_context, subtype, version);
|
|
}
|
|
|
|
/**
|
|
* Tell the update manager.
|
|
*
|
|
* @since 0.9.45
|
|
*/
|
|
static void notifyVersion(I2PAppContext ctx, String subtype, long version) {
|
|
if (version <= 0)
|
|
return;
|
|
ClientAppManager cmgr = ctx.clientAppManager();
|
|
if (cmgr != null) {
|
|
UpdateManager umgr = (UpdateManager) cmgr.getRegisteredApp(UpdateManager.APP_NAME);
|
|
if (umgr != null)
|
|
umgr.notifyInstalled(UpdateType.GEOIP, subtype, Long.toString(version));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Put our country code in the config, where others (such as Timestamper) can get it,
|
|
* and it will be there next time at startup.
|
|
*
|
|
* Does nothing in I2PAppContext
|
|
* @param ts the timestamp of the geoip file that was read, greater than zero
|
|
*/
|
|
private void updateOurCountry(long ts) {
|
|
if (! (_context instanceof RouterContext))
|
|
return;
|
|
RouterContext ctx = (RouterContext) _context;
|
|
String oldCountry = ctx.router().getConfigSetting(PROP_IP_COUNTRY);
|
|
RouterInfo us = ctx.router().getRouterInfo();
|
|
String country = null;
|
|
// we should always have a RouterInfo by now, but we had one report of an NPE here
|
|
if (us != null) {
|
|
// try our published addresses
|
|
for (RouterAddress ra : us.getAddresses()) {
|
|
byte[] ip = ra.getIP();
|
|
if (ip != null) {
|
|
country = get(ip);
|
|
if (country != null)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (country == null) {
|
|
// try our detected addresses
|
|
Set<String> addrs = Addresses.getAddresses(false, true);
|
|
for (String ip : addrs) {
|
|
country = get(ip);
|
|
if (country != null)
|
|
break;
|
|
}
|
|
if (country == null) {
|
|
String lastIP = _context.getProperty(UDPTransport.PROP_IP);
|
|
if (lastIP != null) {
|
|
country = get(lastIP);
|
|
if (country == null) {
|
|
lastIP = _context.getProperty(UDPTransport.PROP_IPV6);
|
|
if (lastIP != null)
|
|
country = get(lastIP);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (_log.shouldInfo())
|
|
_log.info("Old country was " + oldCountry + " new country is " + country);
|
|
if (country != null && !country.equals(oldCountry)) {
|
|
boolean wasStrict = ctx.commSystem().isInStrictCountry();
|
|
ctx.router().saveConfig(PROP_IP_COUNTRY, country);
|
|
boolean isStrict = ctx.commSystem().isInStrictCountry();
|
|
if (_log.shouldInfo())
|
|
_log.info("Old country was strict? " + wasStrict + "; new country is strict? " + isStrict);
|
|
if (isStrict || ctx.getBooleanProperty(Router.PROP_HIDDEN_HIDDEN) ||
|
|
ctx.getBooleanProperty(PROP_BLOCK_MY_COUNTRY)) {
|
|
// generate country blocklist
|
|
countryToIP(country);
|
|
// go thru the netdb
|
|
banCountry(ctx, country);
|
|
} else {
|
|
// remove country blocklist, won't take effect until restart
|
|
File bc = new File(_context.getConfigDir(), Blocklist.BLOCKLIST_COUNTRY_FILE);
|
|
bc.delete();
|
|
}
|
|
if (wasStrict != isStrict && ctx.getProperty(Router.PROP_HIDDEN_HIDDEN) == null) {
|
|
if (isStrict) {
|
|
String name = fullName(country);
|
|
if (name == null)
|
|
name = country;
|
|
_log.logAlways(Log.WARN, "Setting hidden mode to protect you in " + name +
|
|
", you may override on the network configuration page");
|
|
}
|
|
ctx.router().rebuildRouterInfo();
|
|
}
|
|
} else if (country != null) {
|
|
// No change, but we may need to update blocklist-country.txt
|
|
boolean isStrict = ctx.commSystem().isInStrictCountry();
|
|
if (isStrict || ctx.getBooleanProperty(Router.PROP_HIDDEN_HIDDEN) ||
|
|
ctx.getBooleanProperty(PROP_BLOCK_MY_COUNTRY)) {
|
|
// check country blocklist timestamp
|
|
File bc = new File(_context.getConfigDir(), Blocklist.BLOCKLIST_COUNTRY_FILE);
|
|
long lm = bc.lastModified();
|
|
if (lm < ts) {
|
|
// regenerate blocklist
|
|
countryToIP(country);
|
|
}
|
|
if (_lookupRunCount == 1) {
|
|
// go thru the netdb
|
|
banCountry(ctx, country);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param two-letter lower-case country code
|
|
* @since 0.9.48
|
|
*/
|
|
private static void banCountry(RouterContext ctx, String country) {
|
|
for (Hash h : ctx.netDb().getAllRouters()) {
|
|
String hisCountry = ctx.commSystem().getCountry(h);
|
|
if (country.equals(hisCountry)) {
|
|
ctx.banlist().banlistRouterForever(h, "In our country");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add to the list needing lookup
|
|
* Public for BundleRouterInfos
|
|
*
|
|
* @param ip IPv4 or IPv6
|
|
*/
|
|
public void add(String ip) {
|
|
byte[] pib = Addresses.getIP(ip);
|
|
if (pib == null) return;
|
|
add(pib);
|
|
}
|
|
|
|
/**
|
|
* Add to the list needing lookup
|
|
* Public for BundleRouterInfos
|
|
*
|
|
* @param ip IPv4 or IPv6
|
|
*/
|
|
public void add(byte ip[]) {
|
|
add(toLong(ip));
|
|
}
|
|
|
|
/** see above for ip-to-long mapping */
|
|
private void add(long ip) {
|
|
Long li = Long.valueOf(ip);
|
|
if (!(_IPToCountry.containsKey(li) || _notFound.contains(li))) {
|
|
if (ip >= 0 && ip < (1L << 32))
|
|
_pendingSearch.add(li);
|
|
else
|
|
_pendingIPv6Search.add(li);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the country for an IP from the cache.
|
|
* Public for BundleRouterInfos
|
|
*
|
|
* @param ip IPv4 or IPv6
|
|
* @return lower-case code, generally two letters, or null.
|
|
*/
|
|
public String get(String ip) {
|
|
byte[] pib = Addresses.getIP(ip);
|
|
if (pib == null) return null;
|
|
return get(pib);
|
|
}
|
|
|
|
/**
|
|
* Get the country for an IP from the cache.
|
|
* @param ip IPv4 or IPv6
|
|
* @return lower-case code, generally two letters, or null.
|
|
*/
|
|
String get(byte ip[]) {
|
|
return get(toLong(ip));
|
|
}
|
|
|
|
/** see above for ip-to-long mapping */
|
|
private String get(long ip) {
|
|
return _IPToCountry.get(Long.valueOf(ip));
|
|
}
|
|
|
|
/** see above for ip-to-long mapping */
|
|
private static long toLong(byte ip[]) {
|
|
long rv = 0;
|
|
if (ip.length == 16) {
|
|
for (int i = 0; i < 8; i++)
|
|
rv |= (ip[i] & 0xffL) << ((7-i)*8);
|
|
return rv;
|
|
} else {
|
|
for (int i = 0; i < 4; i++)
|
|
rv |= (ip[i] & 0xff) << ((3-i)*8);
|
|
return rv & 0xffffffffl;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return e.g. 1.2.3.4
|
|
* @since 0.9.38 for maxmind
|
|
*/
|
|
private static String toV4(long ip) {
|
|
StringBuilder buf = new StringBuilder(15);
|
|
for (int i = 0; i < 4; i++) {
|
|
buf.append(Long.toString((ip >> ((3-i)*8)) & 0xff));
|
|
if (i == 3)
|
|
break;
|
|
buf.append('.');
|
|
}
|
|
return buf.toString();
|
|
}
|
|
|
|
/**
|
|
* @return e.g. aabb:ccdd:eeff:1122::
|
|
* @since 0.9.26 for maxmind
|
|
*/
|
|
private static String toV6(long ip) {
|
|
StringBuilder buf = new StringBuilder(21);
|
|
for (int i = 0; i < 4; i++) {
|
|
buf.append(Long.toHexString((ip >> ((3-i)*16)) & 0xffff));
|
|
buf.append(':');
|
|
}
|
|
buf.append(':');
|
|
return buf.toString();
|
|
}
|
|
|
|
/**
|
|
* Get the country for a country code
|
|
* Public for BundleRouterInfos
|
|
*
|
|
* @param code two-letter lower case code
|
|
* @return untranslated name or null
|
|
*/
|
|
public String fullName(String code) {
|
|
return _codeToName.get(code);
|
|
}
|
|
|
|
public static void main(String args[]) {
|
|
if (args.length <= 0) {
|
|
System.out.println("Usage: GeoIP ip...");
|
|
System.exit(1);
|
|
}
|
|
GeoIP g = new GeoIP(I2PAppContext.getGlobalContext());
|
|
/***
|
|
String tests[] = {"0.0.0.0", "0.0.0.1", "0.0.0.2", "0.0.0.255", "1.0.0.0",
|
|
"94.3.3.3", "77.1.2.3", "127.0.0.0", "127.127.127.127", "128.0.0.0",
|
|
"89.8.9.3", "72.5.6.8", "217.4.9.7", "175.107.027.107", "135.6.5.2",
|
|
"129.1.2.3", "255.255.255.254", "255.255.255.255",
|
|
"::", "1", "2000:1:2:3::", "2001:200:1:2:3:4:5:6", "2001:208:7:8:9::",
|
|
"2c0f:fff0:1234:5678:90ab:cdef:0:0", "2c0f:fff1:0::"
|
|
};
|
|
for (int i = 0; i < tests.length; i++)
|
|
g.add(tests[i]);
|
|
***/
|
|
for (int i = 0; i < args.length; i++)
|
|
g.add(args[i]);
|
|
long start = System.currentTimeMillis();
|
|
g.blockingLookup();
|
|
System.out.println("Lookup took " + (System.currentTimeMillis() - start));
|
|
/***
|
|
g.countryToIP("af");
|
|
for (int i = 0; i < tests.length; i++)
|
|
System.out.println(tests[i] + " : " + g.get(tests[i]));
|
|
***/
|
|
for (int i = 0; i < args.length; i++)
|
|
System.out.println(args[i] + " : " + g.get(args[i]));
|
|
}
|
|
}
|