diff --git a/.gitmodules b/.gitmodules
index 37ddf116..fac2d962 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -7,9 +7,6 @@
[submodule "submodules/libpcap"]
path = submodules/libpcap
url = https://github.com/the-tcpdump-group/libpcap
-[submodule "app/src/main/res/raw"]
- path = app/src/main/res/raw
- url = https://github.com/emanuele-f/PCAPdroid_res
[submodule "submodules/MaxMind-DB-Reader-java"]
path = submodules/MaxMind-DB-Reader-java
url = https://github.com/emanuele-f/MaxMind-DB-Reader-java
diff --git a/app/src/main/java/com/emanuelef/remote_capture/Geolocation.java b/app/src/main/java/com/emanuelef/remote_capture/Geolocation.java
index f9a923af..706b7d91 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/Geolocation.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/Geolocation.java
@@ -19,18 +19,21 @@
package com.emanuelef.remote_capture;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Log;
+import androidx.annotation.Nullable;
+
import com.emanuelef.remote_capture.model.Geomodel;
import com.maxmind.db.Reader;
-import java.io.BufferedOutputStream;
import java.io.File;
-import java.io.FileOutputStream;
+import java.io.FileInputStream;
import java.io.IOException;
-import java.io.InputStream;
import java.net.InetAddress;
+import java.text.SimpleDateFormat;
+import java.util.Date;
/* A class to query geolocation info from IP addresses. */
public class Geolocation {
@@ -46,67 +49,123 @@ public class Geolocation {
@Override
public void finalize() {
- try {
- mCountryReader.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- try {
- mAsnReader.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
+ Utils.safeClose(mCountryReader);
+ Utils.safeClose(mAsnReader);
+ mCountryReader = null;
+ mAsnReader = null;
}
private void openDb() {
try {
- File countryFile = new File(mContext.getCacheDir() + "/dbip_country_lite.mmdb");
- res_to_file(R.raw.dbip_country_lite, countryFile);
- mCountryReader = new Reader(countryFile);
+ mCountryReader = new Reader(getCountryFile(mContext));
Log.d(TAG, "Country DB loaded: " + mCountryReader.getMetadata());
- File asnFile = new File(mContext.getCacheDir() + "/dbip_asn_lite.mmdb");
- res_to_file(R.raw.dbip_asn_lite, asnFile);
- mAsnReader = new Reader(asnFile);
+ mAsnReader = new Reader(getAsnFile(mContext));
Log.d(TAG, "ASN DB loaded: " + mAsnReader.getMetadata());
} catch (IOException e) {
- e.printStackTrace();
- throw new IllegalStateException();
+ Log.i(TAG, "Geolocation is not available");
}
}
- // We need to get a File from the resource so that the Reader can mmap it
- private void res_to_file(int resid, File dst) throws IOException {
- try(InputStream is = mContext.getResources().openRawResource(resid)) {
- try(BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dst))) {
- byte[] bytesIn = new byte[4096];
- int read;
+ private static File getCountryFile(Context ctx) {
+ return new File(ctx.getFilesDir() + "/dbip_country_lite.mmdb");
+ }
- while((read = is.read(bytesIn)) != -1)
- bos.write(bytesIn, 0, read);
+ private static File getAsnFile(Context ctx) {
+ return new File(ctx.getFilesDir() + "/dbip_asn_lite.mmdb");
+ }
+
+ public static Date getDbDate(File file) throws IOException {
+ try(Reader reader = new Reader(file)) {
+ return reader.getMetadata().getBuildDate();
+ }
+ }
+
+ public static @Nullable Date getDbDate(Context ctx) {
+ try {
+ return getDbDate(getCountryFile(ctx));
+ } catch (IOException ignored) {
+ return null;
+ }
+ }
+
+ public static long getDbSize(Context ctx) {
+ return getCountryFile(ctx).length() + getAsnFile(ctx).length();
+ }
+
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ public static void deleteDb(Context ctx) {
+ getCountryFile(ctx).delete();
+ getAsnFile(ctx).delete();
+ }
+
+ @SuppressLint("SimpleDateFormat")
+ public static boolean downloadDb(Context ctx) {
+ String dateid = new SimpleDateFormat("yyyy-MM").format(new Date());
+ String country_url = "https://download.db-ip.com/free/dbip-country-lite-" + dateid + ".mmdb.gz";
+ String asn_url = "https://download.db-ip.com/free/dbip-asn-lite-" + dateid + ".mmdb.gz";
+
+ try {
+ return downloadAndUnzip(ctx, "country", country_url, getCountryFile(ctx)) &&
+ downloadAndUnzip(ctx, "asn", asn_url, getAsnFile(ctx));
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ private static boolean downloadAndUnzip(Context ctx, String label, String url, File dst) throws IOException {
+ File tmp_file = new File(ctx.getCacheDir() + "/geoip_db.zip");
+
+ boolean rv = Utils.downloadFile(url, tmp_file.getAbsolutePath());
+ if(!rv) {
+ Log.w(TAG, "Could not download " + label + " db from " + url);
+ return false;
+ }
+
+ try(FileInputStream is = new FileInputStream(tmp_file.getAbsolutePath())) {
+ if(!Utils.ungzip(is, dst.getAbsolutePath())) {
+ Log.w(TAG, "ungzip of " + tmp_file + " failed");
+ return false;
}
+
+ // Verify - throws IOException on error
+ getDbDate(dst);
+
+ return true;
+ } finally {
+ //noinspection ResultOfMethodCallIgnored
+ tmp_file.delete();
}
}
public String getCountryCode(InetAddress addr) {
- try {
- Geomodel.CountryResult res = mCountryReader.get(addr, Geomodel.CountryResult.class);
- if((res != null) && (res.country != null))
- return res.country.isoCode;
- } catch (IOException e) {
- e.printStackTrace();
+ if(mCountryReader != null) {
+ try {
+ Geomodel.CountryResult res = mCountryReader.get(addr, Geomodel.CountryResult.class);
+ if ((res != null) && (res.country != null))
+ return res.country.isoCode;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
}
+
+ // fallback
return "";
}
public Geomodel.ASN getASN(InetAddress addr) {
- try {
- Geomodel.ASN res = mAsnReader.get(addr, Geomodel.ASN.class);
- if(res != null)
- return res;
- } catch (IOException e) {
- e.printStackTrace();
+ if(mAsnReader != null) {
+ try {
+ Geomodel.ASN res = mAsnReader.get(addr, Geomodel.ASN.class);
+ if (res != null)
+ return res;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
}
+
+ // fallback
return new Geomodel.ASN();
}
}
diff --git a/app/src/main/java/com/emanuelef/remote_capture/Utils.java b/app/src/main/java/com/emanuelef/remote_capture/Utils.java
index 90ef4c69..84eb2eb3 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/Utils.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/Utils.java
@@ -113,6 +113,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Random;
+import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@@ -854,6 +855,21 @@ public class Utils {
}
}
+ public static boolean ungzip(InputStream is, String dst) {
+ try(GZIPInputStream gis = new GZIPInputStream(is)) {
+ try(BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dst))) {
+ byte[] bytesIn = new byte[4096];
+ int read;
+ while ((read = gis.read(bytesIn)) != -1)
+ bos.write(bytesIn, 0, read);
+ }
+ return true;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
public static boolean downloadFile(String _url, String path) {
boolean has_contents = false;
diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/SettingsActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/SettingsActivity.java
index f699e3f9..24d9a783 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/activities/SettingsActivity.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/activities/SettingsActivity.java
@@ -32,6 +32,9 @@ import android.util.Patterns;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
import androidx.preference.DropDownPreference;
import androidx.preference.EditTextPreference;
import androidx.preference.Preference;
@@ -43,6 +46,7 @@ import com.emanuelef.remote_capture.Billing;
import com.emanuelef.remote_capture.PCAPdroid;
import com.emanuelef.remote_capture.Utils;
import com.emanuelef.remote_capture.MitmAddon;
+import com.emanuelef.remote_capture.fragments.GeoipSettings;
import com.emanuelef.remote_capture.model.Prefs;
import com.emanuelef.remote_capture.R;
@@ -52,7 +56,7 @@ import java.util.ArrayList;
import java.util.Enumeration;
import java.util.regex.Matcher;
-public class SettingsActivity extends BaseActivity {
+public class SettingsActivity extends BaseActivity implements PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, FragmentManager.OnBackStackChangedListener {
private static final String TAG = "SettingsActivity";
private static final String ACTION_LANG_RESTART = "lang_restart";
public static final String TARGET_PREF_EXTRA = "target_pref";
@@ -66,19 +70,53 @@ public class SettingsActivity extends BaseActivity {
getSupportFragmentManager()
.beginTransaction()
- .replace(R.id.settings_container, new SettingsFragment())
+ .replace(R.id.settings_container, new SettingsFragment(), "root")
.commit();
+
+ getSupportFragmentManager().addOnBackStackChangedListener(this);
+ }
+
+ @Override
+ public boolean onPreferenceStartFragment(@NonNull PreferenceFragmentCompat caller, @NonNull Preference pref) {
+ PreferenceFragmentCompat targetFragment = null;
+ Log.d(TAG, "startFragment: " + pref.getKey());
+
+ if(pref.getKey().equals("geolocation")) {
+ targetFragment = new GeoipSettings();
+ setTitle(R.string.geolocation);
+ }
+
+ if(targetFragment != null) {
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.settings_container, targetFragment, pref.getKey())
+ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
+ .addToBackStack(pref.getKey())
+ .commit();
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onBackStackChanged() {
+ Fragment f = getSupportFragmentManager().findFragmentById(R.id.settings_container);
+ if(f instanceof SettingsFragment)
+ setTitle(R.string.title_activity_settings);
}
@Override
public void onBackPressed() {
- super.onBackPressed();
-
- // Use a custom intent to provide "up" navigation after ACTION_LANG_RESTART took place
- Intent intent = new Intent(this, MainActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- finish();
+ Fragment f = getSupportFragmentManager().findFragmentById(R.id.settings_container);
+ if(f instanceof SettingsFragment) {
+ // Use a custom intent to provide "up" navigation after ACTION_LANG_RESTART took place
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ finish();
+ } else
+ super.onBackPressed();
}
public static class SettingsFragment extends PreferenceFragmentCompat {
diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/GeoipSettings.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/GeoipSettings.java
new file mode 100644
index 00000000..cf2a24c9
--- /dev/null
+++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/GeoipSettings.java
@@ -0,0 +1,116 @@
+/*
+ * This file is part of PCAPdroid.
+ *
+ * PCAPdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PCAPdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PCAPdroid. If not, see .
+ *
+ * Copyright 2020-22 - Emanuele Faranda
+ */
+
+package com.emanuelef.remote_capture.fragments;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceFragmentCompat;
+
+import com.emanuelef.remote_capture.Geolocation;
+import com.emanuelef.remote_capture.R;
+import com.emanuelef.remote_capture.Utils;
+
+import java.util.Date;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class GeoipSettings extends PreferenceFragmentCompat {
+ private static final String TAG = "GeoipSettings";
+ private Preference mStatus;
+ private Preference mDelete;
+
+ @Override
+ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
+ setPreferencesFromResource(R.xml.geoip_preferences, rootKey);
+
+ mStatus = requirePreference("status");
+ mDelete = requirePreference("delete");
+ refreshStatus();
+
+ mDelete.setOnPreferenceClickListener(preference -> {
+ Geolocation.deleteDb(requireContext());
+ refreshStatus();
+ return true;
+ });
+
+ requirePreference("download")
+ .setOnPreferenceClickListener(preference -> {
+ downloadDatabases();
+ return true;
+ });
+ }
+
+ private void refreshStatus() {
+ Date builtDate = Geolocation.getDbDate(requireContext());
+ if(builtDate != null) {
+ String dateStr = Utils.formatEpochFull(requireContext(), builtDate.getTime() / 1000);
+ mStatus.setSummary("DB-IP Lite free\n" +
+ String.format(getString(R.string.built_on), dateStr) + "\n" +
+ String.format(getString(R.string.size_x), Utils.formatBytes(Geolocation.getDbSize(requireContext()))));
+ } else
+ mStatus.setSummary(R.string.geo_db_not_found);
+
+ mDelete.setVisible((builtDate != null));
+ }
+
+ private void downloadDatabases() {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ Handler handler = new Handler(Looper.getMainLooper());
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
+ builder.setTitle(R.string.downloading);
+ builder.setMessage(R.string.download_in_progress);
+
+ final AlertDialog alert = builder.create();
+ alert.setCanceledOnTouchOutside(false);
+ alert.show();
+
+ alert.setOnCancelListener(dialogInterface -> {
+ Log.i(TAG, "Abort download");
+ executor.shutdownNow();
+ });
+
+ executor.execute(() -> {
+ boolean result = Geolocation.downloadDb(requireContext());
+
+ handler.post(() -> {
+ if(!result)
+ Utils.showToastLong(requireContext(), R.string.download_failed);
+
+ alert.dismiss();
+ refreshStatus();
+ });
+ });
+ }
+
+ private @NonNull
+ T requirePreference(String key) {
+ T pref = findPreference(key);
+ if(pref == null)
+ throw new IllegalStateException();
+ return pref;
+ }
+}
diff --git a/app/src/main/res/raw b/app/src/main/res/raw
deleted file mode 160000
index c3ee5e68..00000000
--- a/app/src/main/res/raw
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit c3ee5e68cf4a436ceaa7f0df1dc8799c0a5a42ae
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a69e3ee0..58a7c62f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -318,4 +318,16 @@
Display as…
Printable text
Hexdump
+ Geolocation
+ Add country and ASN info by performing offline lookups
+ Downloading…
+ Download in progress, please wait
+ Download failed
+ Database not found. Geolocation is disabled
+ Database
+ Built on: %1$s
+ Tap to download the latest database. New databases are available monthly
+ Size: %1$s
+ Tap to delete the database and save space
+ Download
diff --git a/app/src/main/res/xml/geoip_preferences.xml b/app/src/main/res/xml/geoip_preferences.xml
new file mode 100644
index 00000000..c504fe1c
--- /dev/null
+++ b/app/src/main/res/xml/geoip_preferences.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
index 7702114c..ec7baa78 100644
--- a/app/src/main/res/xml/root_preferences.xml
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -163,6 +163,13 @@
app:defaultValue="system"
app:useSimpleSummaryProvider="true"/>
+
+