Rework PCAP dump to file

PCAP dump to file has been reworked as follows:

- File selection dialog is not shown anymore when the capture starts
- The PCAP filea are saved to the Downloads/PCAPdroid folder
- Simplified mechanism to dump to an arbitrary URI (pcap_uri param)
- Add pcap_name parameter to specify PCAP file name

Overall, this simplifies user interaction and make it easier to
access the PCAP file.

Closes #183
This commit is contained in:
emanuele-f
2022-12-27 13:59:02 +01:00
parent 3a89269f79
commit 46889738c7
12 changed files with 346 additions and 260 deletions
@@ -302,7 +302,6 @@ public class CaptureService extends VpnService implements Runnable {
last_connections = 0;
mLowMemory = false;
conn_reg = new ConnectionsRegister(this, CONNECTIONS_LOG_SIZE);
mPcapUri = null;
mDumper = null;
mDumpQueue = null;
mPendingUpdates.clear();
@@ -311,10 +310,17 @@ public class CaptureService extends VpnService implements Runnable {
if(mSettings.dump_mode == Prefs.DumpMode.HTTP_SERVER)
mDumper = new HTTPServer(this, mSettings.http_server_port);
else if(mSettings.dump_mode == Prefs.DumpMode.PCAP_FILE) {
if(mSettings.pcap_uri != null) {
if(!mSettings.pcap_uri.isEmpty())
mPcapUri = Uri.parse(mSettings.pcap_uri);
mDumper = new FileDumper(this, mPcapUri);
else {
String fname = !mSettings.pcap_name.isEmpty() ? mSettings.pcap_name : Utils.getUniquePcapFileName(this);
mPcapUri = Utils.getDownloadsUri(this, fname);
}
if(mPcapUri == null)
return abortStart();
mDumper = new FileDumper(this, mPcapUri);
} else if(mSettings.dump_mode == Prefs.DumpMode.UDP_EXPORTER) {
InetAddress addr;
@@ -566,7 +572,9 @@ public class CaptureService extends VpnService implements Runnable {
//INSTANCE = null;
unregisterNetworkCallbacks();
mBlacklists.abortUpdate();
if(mBlacklists != null)
mBlacklists.abortUpdate();
if(mCaptureThread != null)
mCaptureThread.interrupt();
@@ -928,6 +936,10 @@ public class CaptureService extends VpnService implements Runnable {
return ((INSTANCE != null) ? INSTANCE.mPcapUri : null);
}
public static boolean isUserDefinedPcapUri() {
return (INSTANCE == null || !INSTANCE.mSettings.pcap_uri.isEmpty());
}
public static long getBytes() {
return((INSTANCE != null) ? INSTANCE.last_bytes : 0);
}
@@ -0,0 +1,152 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
* Copyright 2020-21 - Emanuele Faranda
*/
package com.emanuelef.remote_capture;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.UriPermission;
import android.net.Uri;
import androidx.activity.ComponentActivity;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.preference.PreferenceManager;
// If an app asks to dump to a PCAP file, ensure that PCAPdroid has such a permission.
// When dumping to the external storage, the first time it's necessary to get a persistable
// permission, which requires the user to manually select the output file from the UI.
public class PersistableUriPermission {
private static final String TAG = "PersistableUriPermission";
private static final String PREF_KEY = "persistable_uri";
/* FLAG_GRANT_READ_URI_PERMISSION required for showPcapActionDialog (e.g. when auto-started at boot) */
private static int PERSIST_MODE = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
public String key = "";
public Uri persistableUri;
private String mNewKey;
private final Context mCtx;
private final SharedPreferences mPrefs;
private PupListener mListener;
private final ActivityResultLauncher<Intent> mPcapLauncher;
public interface PupListener {
void onUriChecked(Uri grantedUri);
}
public PersistableUriPermission(ComponentActivity activity) {
mCtx = activity;
mPrefs = PreferenceManager.getDefaultSharedPreferences(mCtx);
mPcapLauncher = activity.registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), this::pcapFileResult);
reload();
}
public void reload() {
String k = mPrefs.getString(PREF_KEY, "");
int sep = k.indexOf("|");
if(sep < 0)
return;
key = k.substring(0, sep);
persistableUri = Uri.parse(k.substring(sep + 1));
}
public void save() {
String serialized = key + "|" + persistableUri;
mPrefs.edit().putString(PREF_KEY, serialized).apply();
}
public void checkPermission(String perm_key, PupListener listener) {
boolean hasPermission = false;
boolean keyChanged = !perm_key.equals(key);
mNewKey = perm_key;
mListener = listener;
// Revoke the previous permissions and check
for(UriPermission permission : mCtx.getContentResolver().getPersistedUriPermissions()) {
if(keyChanged || !permission.getUri().equals(persistableUri)) {
Log.d(TAG, "Releasing URI permission: " + permission.getUri().toString());
mCtx.getContentResolver().releasePersistableUriPermission(permission.getUri(), PERSIST_MODE);
} else
hasPermission = true;
}
if(!hasPermission)
openFileSelector();
else
mListener.onUriChecked(persistableUri);
}
private void openFileSelector() {
boolean noFileDialog = false;
String fname = Utils.getUniquePcapFileName(mCtx);
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_TITLE, fname);
if(Utils.supportsFileDialog(mCtx, intent)) {
try {
mPcapLauncher.launch(intent);
} catch (ActivityNotFoundException e) {
noFileDialog = true;
}
} else
noFileDialog = true;
if(noFileDialog) {
Log.w(TAG, "No app found to handle file selection");
Utils.showToastLong(mCtx, R.string.no_activity_file_selection);
mListener.onUriChecked(null);
}
}
private void pcapFileResult(final ActivityResult result) {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
Uri uri = result.getData().getData();
boolean persistable = (result.getData().getFlags() & Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0;
/* Request a persistent permission to write this URI without invoking the system picker.
* This is needed to write to the URI when invoking PCAPdroid from other apps via Intents
* or when starting the capture at boot. */
if(persistable) {
try {
mCtx.getContentResolver().takePersistableUriPermission(uri, PERSIST_MODE);
// save the persistable uri to use it for the next capture
persistableUri = uri;
key = mNewKey;
save();
} catch (SecurityException e) {
// This should never occur
Log.e(TAG, "Could not get PersistableUriPermission");
e.printStackTrace();
}
}
mListener.onUriChecked(uri);
} else
mListener.onUriChecked(null);
}
}
@@ -30,6 +30,7 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ComponentCallbacks2;
import android.content.ComponentName;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
@@ -731,58 +732,53 @@ public class Utils {
return false;
}
@SuppressWarnings("deprecation")
public static Uri getInternalStorageFile(Context context, String fname) {
// Get a URI to write a file into the downloads folder, into a folder named "PCAPdroid"
// If the file exists, it's overwritten
public static Uri getDownloadsUri(Context context, String fname) {
ContentValues values = new ContentValues();
//values.put(MediaStore.MediaColumns.MIME_TYPE, "text/plain");
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fname);
String selectQuery = "";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// On Android Q+ cannot directly access the external dir. Must use RELATIVE_PATH instead.
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
// Important: trailing "/" required for the selectQuery
String relPath = Environment.DIRECTORY_DOWNLOADS + "/PCAPdroid/";
selectQuery = MediaStore.MediaColumns.RELATIVE_PATH + "='" + relPath + "' AND " +
MediaStore.MediaColumns.DISPLAY_NAME + "='" + fname + "'";
values.put(MediaStore.MediaColumns.RELATIVE_PATH, relPath);
} else {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if(context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
Log.w("getInternalStorageFile", "external storage permission was denied");
Utils.showToastLong(context, R.string.external_storage_perm_required);
return(null);
}
}
// NOTE: context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) returns an app internal folder
String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/" + fname;
Log.d("getInternalStorageFile", path);
String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/PCAPdroid/" + fname;
Log.d(TAG, "getDownloadsUri: path=" + path);
selectQuery = MediaStore.MediaColumns.DATA + "='" + path + "'";
values.put(MediaStore.MediaColumns.DATA, path);
}
return context.getContentResolver().insert(
MediaStore.Files.getContentUri("external"), values);
}
Uri externalUri = MediaStore.Files.getContentUri("external");
public static String getUriFname(Context context, Uri uri) {
Cursor cursor;
String fname;
// if the file with given name already exists, overwrite it
try (Cursor cursor = context.getContentResolver().query(externalUri, new String[]{MediaStore.MediaColumns._ID}, selectQuery, null, null)) {
if ((cursor != null) && cursor.moveToFirst()) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID));
Uri existingUri = ContentUris.withAppendedId(externalUri, id);
try {
String []projection = {OpenableColumns.DISPLAY_NAME};
cursor = context.getContentResolver().query(uri, projection, null, null, null);
} catch (Exception e) {
return null;
}
Log.d(TAG, "getDownloadsUri: overwriting file " + existingUri);
return existingUri;
}
} catch (Exception ignored) {}
if((cursor == null) || !cursor.moveToFirst())
return null;
try {
int idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if(idx < 0)
return null;
fname = cursor.getString(idx);
} finally {
cursor.close();
}
return fname;
Uri newUri = context.getContentResolver().insert(externalUri, values);
Log.d(TAG, "getDownloadsUri: new file " + newUri);
return newUri;
}
public static boolean isRootAvailable() {
@@ -1462,4 +1458,60 @@ public class Utils {
return false;
return true;
}
public static String uriToFilePath(Context ctx, Uri uri) {
String[] proj = { MediaStore.Images.Media.DATA };
try(Cursor cursor = ctx.getContentResolver().query(uri, proj, null, null, null)) {
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
if(cursor.moveToFirst())
return cursor.getString(column_index);
} catch (Exception ignored) {}
return null;
}
public static class UriStat {
public String name;
public long size;
}
public static UriStat getUriStat(Context ctx, Uri uri) {
String uri_str = uri.toString();
File uriFile = null;
// in some devices, cursor.isNull(sizeIndex) is true, which causes size to be 0
// try to resolve the original path and access it as a file
String fpath = uriToFilePath(ctx, uri);
if(fpath != null) {
Log.d(TAG, "getUriStat: resolved to file " + fpath);
uriFile = new File(fpath);
} else if(uri_str.startsWith("file://"))
uriFile = new File(uri_str.substring(7));
if((uriFile != null) && (uriFile.exists())) {
// retrieve via file
UriStat info = new UriStat();
info.name = uriFile.getName();
info.size = uriFile.length();
return info;
}
// retrieve via content uri
// https://developer.android.com/training/secure-file-sharing/retrieve-info.html#RetrieveFileInfo
try(Cursor cursor = ctx.getContentResolver().query(uri, null, null, null, null)) {
if((cursor == null) || !cursor.moveToFirst())
return null;
UriStat info = new UriStat();
int sizeIndex = cursor.getColumnIndexOrThrow(OpenableColumns.SIZE);
int idx = cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME);
info.name = (idx >= 0) ? cursor.getString(idx) : "*unknown*";
info.size = !cursor.isNull(sizeIndex) ? cursor.getLong(sizeIndex) : -1;
return info;
} catch (Exception e) {
return null;
}
}
}
@@ -49,6 +49,7 @@ import com.emanuelef.remote_capture.CaptureHelper;
import com.emanuelef.remote_capture.CaptureService;
import com.emanuelef.remote_capture.Log;
import com.emanuelef.remote_capture.PCAPdroid;
import com.emanuelef.remote_capture.PersistableUriPermission;
import com.emanuelef.remote_capture.R;
import com.emanuelef.remote_capture.Utils;
import com.emanuelef.remote_capture.model.AppDescriptor;
@@ -71,6 +72,8 @@ public class CaptureCtrl extends AppCompatActivity {
private CaptureHelper mCapHelper;
private CtrlPermissions mPermissions;
private PersistableUriPermission persistableUriPermission;
@Override
@SuppressWarnings("deprecation")
protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -79,6 +82,9 @@ public class CaptureCtrl extends AppCompatActivity {
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.ctrl_consent);
// define here since it calls registerForActivityResult
persistableUriPermission = new PersistableUriPermission(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
final WindowInsetsController insetsController = getWindow().getInsetsController();
if (insetsController != null)
@@ -238,8 +244,19 @@ public class CaptureCtrl extends AppCompatActivity {
return;
}
// will call the mCapHelper listener
mCapHelper.startCapture(settings);
if(!settings.pcap_uri.isEmpty()) {
persistableUriPermission.checkPermission(settings.pcap_uri, granted_uri -> {
Log.d(TAG, "persistable uri granted? " + granted_uri);
if(granted_uri != null) {
settings.pcap_uri = granted_uri.toString();
mCapHelper.startCapture(settings);
} else
abort();
});
} else
// will call the mCapHelper listener
mCapHelper.startCapture(settings);
return;
} else if(action.equals(ACTION_STOP)) {
Log.d(TAG, "Stopping capture");
@@ -21,12 +21,11 @@ package com.emanuelef.remote_capture.activities;
import android.Manifest;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.UriPermission;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import androidx.activity.result.ActivityResult;
@@ -50,8 +49,6 @@ import androidx.viewpager2.widget.ViewPager2;
import android.os.Build;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.provider.OpenableColumns;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
@@ -81,7 +78,6 @@ import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashSet;
@@ -91,14 +87,11 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
private ViewPager2 mPager;
private AppState mState;
private AppStateListener mListener;
private Uri mPcapUri;
private File mKeylogFile;
private String mPcapFname;
private DrawerLayout mDrawer;
private SharedPreferences mPrefs;
private NavigationView mNavView;
private CaptureHelper mCapHelper;
private boolean usingMediaStore;
private static final String TAG = "Main";
@@ -116,8 +109,6 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
public static final String FIREWALL_DOCS_URL = PAID_FEATURES_URL + "#51-firewall";
public static final String MALWARE_DETECTION_DOCS_URL = PAID_FEATURES_URL + "#52-malware-detection";
private final ActivityResultLauncher<Intent> pcapFileLauncher =
registerForActivityResult(new StartActivityForResult(), this::pcapFileResult);
private final ActivityResultLauncher<Intent> sslkeyfileExportLauncher =
registerForActivityResult(new StartActivityForResult(), this::sslkeyfileExportResult);
private final ActivityResultLauncher<String> requestPermissionLauncher =
@@ -152,7 +143,6 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
initAppState();
checkPermissions();
mPcapUri = CaptureService.isServiceActive() ? CaptureService.getPcapUri() : null;
mCapHelper = new CaptureHelper(this);
mCapHelper.setListener(success -> {
if(!success) {
@@ -181,10 +171,8 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
Log.d(TAG, "sslkeylog? " + (mKeylogFile != null));
if((mPcapUri != null) && (Prefs.getDumpMode(mPrefs) == Prefs.DumpMode.PCAP_FILE)) {
showPcapActionDialog(mPcapUri);
mPcapUri = null;
mPcapFname = null;
if((Prefs.getDumpMode(mPrefs) == Prefs.DumpMode.PCAP_FILE)) {
showPcapActionDialog();
// will export the keylogfile after saving/sharing pcap
} else if(mKeylogFile != null)
@@ -274,22 +262,14 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
}
private void checkPermissions() {
String fname = "test.pcap";
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_TITLE, fname);
if(!Utils.supportsFileDialog(this, intent)) {
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Needed to write file on devices which do not support ACTION_CREATE_DOCUMENT
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
try {
requestPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE);
} catch (ActivityNotFoundException e) {
Utils.showToastLong(this, R.string.no_intent_handler_found);
}
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Needed to write PCAP files
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
try {
requestPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE);
} catch (ActivityNotFoundException e) {
Utils.showToastLong(this, R.string.no_intent_handler_found);
}
}
}
@@ -617,54 +597,6 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
return super.onOptionsItemSelected(item);
}
private void pcapFileResult(final ActivityResult result) {
if (result.getResultCode() == RESULT_OK && result.getData() != null) {
startWithPcapFile(result.getData().getData(),
(result.getData().getFlags() & Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0);
} else {
mPcapUri = null;
}
}
private void startWithPcapFile(Uri uri, boolean persistable) {
mPcapUri = uri;
mPcapFname = null;
boolean hasPermission = false;
/* FLAG_GRANT_READ_URI_PERMISSION required for showPcapActionDialog (e.g. when auto-started at boot) */
int peristMode = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
// Revoke the previous permissions
for(UriPermission permission : getContentResolver().getPersistedUriPermissions()) {
if(!permission.getUri().equals(uri)) {
Log.d(TAG, "Releasing URI permission: " + permission.getUri().toString());
getContentResolver().releasePersistableUriPermission(permission.getUri(), peristMode);
} else
hasPermission = true;
}
/* Request a persistent permission to write this URI without invoking the system picker.
* This is needed to write to the URI when invoking PCAPdroid from other apps via Intents
* or when starting the capture at boot. */
if(persistable && !hasPermission) {
try {
getContentResolver().takePersistableUriPermission(uri, peristMode);
} catch (SecurityException e) {
// This should never occur
Log.e(TAG, "Could not get PersistableUriPermission");
e.printStackTrace();
persistable = false;
}
}
// Save the URI as a preference
mPrefs.edit().putString(Prefs.PREF_PCAP_URI, mPcapUri.toString()).apply();
// NOTE: part of app_api.md
Log.d(TAG, "PCAP URI to write [persistable=" + persistable + "]: " + mPcapUri.toString());
startCapture();
}
private void initAppState() {
boolean is_active = CaptureService.isServiceActive();
@@ -689,11 +621,6 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
return;
}
if((mPcapUri == null) && (Prefs.getDumpMode(mPrefs) == Prefs.DumpMode.PCAP_FILE)) {
openFileSelector();
return;
}
if(!Prefs.isRootCaptureEnabled(mPrefs) && Utils.hasVPNRunning(this)) {
new AlertDialog.Builder(this)
.setMessage(R.string.disconnect_vpn_confirm)
@@ -730,77 +657,24 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
return false;
}
private void openFileSelector() {
boolean noFileDialog = false;
String fname = Utils.getUniquePcapFileName(this);
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_TITLE, fname);
public void showPcapActionDialog() {
Log.d(TAG, "showPcapActionDialog called");
if(Utils.supportsFileDialog(this, intent)) {
try {
pcapFileLauncher.launch(intent);
} catch (ActivityNotFoundException e) {
noFileDialog = true;
}
} else
noFileDialog = true;
if(CaptureService.isUserDefinedPcapUri())
return;
if(noFileDialog) {
Log.w(TAG, "No app found to handle file selection");
Uri pcapUri = CaptureService.getPcapUri();
if(pcapUri == null)
return;
// Pick default path
Uri uri = Utils.getInternalStorageFile(this, fname);
if(uri != null) {
usingMediaStore = true;
// NOTE: cannot be persisted as it was not invoked via Intent
startWithPcapFile(uri, false);
} else
Utils.showToastLong(this, R.string.no_activity_file_selection);
}
}
public void showPcapActionDialog(Uri pcapUri) {
Cursor cursor;
Log.d(TAG, "showPcapActionDialog: " + pcapUri.toString());
try {
cursor = getContentResolver().query(pcapUri, null, null, null, null);
} catch (Exception e) {
Utils.UriStat pcapStat = Utils.getUriStat(this, pcapUri);
if((pcapStat == null) || (pcapStat.size == 0)) {
if(pcapStat != null)
deletePcapFile(pcapUri); // empty file, delete
return;
}
if((cursor == null) || !cursor.moveToFirst())
return;
int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
long file_size = !cursor.isNull(sizeIndex) ? cursor.getLong(sizeIndex) : -1;
int idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
String fname = (idx >= 0) ? cursor.getString(idx) : "*unknown*";
cursor.close();
// If file is empty, delete it
// NOTE: the user may want to get a PersistableUriPermission, so don't auto delete the file
/*if(file_size == 0) {
Log.d(TAG, "PCAP file is empty, deleting");
try {
if(usingMediaStore)
getContentResolver().delete(pcapUri, null, null);
else
DocumentsContract.deleteDocument(getContentResolver(), pcapUri);
} catch (FileNotFoundException | UnsupportedOperationException e) {
e.printStackTrace();
}
return;
}*/
String message = String.format(getResources().getString(R.string.pcap_file_action), fname, Utils.formatBytes(file_size));
String message = String.format(getResources().getString(R.string.pcap_file_action), pcapStat.name, Utils.formatBytes(pcapStat.size));
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setMessage(message);
@@ -809,28 +683,15 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.setType("application/cap");
sendIntent.putExtra(Intent.EXTRA_STREAM, pcapUri);
sendIntent.setClipData(ClipData.newRawUri("", pcapUri));
sendIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Utils.startActivity(this, Intent.createChooser(sendIntent, getResources().getString(R.string.share)));
});
builder.setNegativeButton(R.string.delete, (dialog, which) -> {
Log.d(TAG, "Deleting PCAP file" + pcapUri.getPath());
boolean deleted = false;
try {
if(usingMediaStore)
deleted = (getContentResolver().delete(pcapUri, null, null) == 1);
else
deleted = DocumentsContract.deleteDocument(getContentResolver(), pcapUri);
} catch (FileNotFoundException | UnsupportedOperationException | SecurityException e) {
e.printStackTrace();
}
if(!deleted)
Utils.showToast(MainActivity.this, R.string.delete_error);
dialog.cancel();
});
builder.setNeutralButton(R.string.ok, (dialog, which) -> dialog.cancel());
builder.setNegativeButton(R.string.delete, (dialog, which) -> deletePcapFile(pcapUri));
builder.setNeutralButton(R.string.ok, (dialog, which) -> {});
builder.setOnDismissListener(dialogInterface -> {
// also export the keylog
if(mKeylogFile != null)
startExportSslkeylogfile();
});
@@ -838,35 +699,22 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
builder.create().show();
}
public AppState getState() {
return(mState);
}
private void deletePcapFile(Uri pcapUri) {
Log.d(TAG, "Deleting PCAP file" + pcapUri.getPath());
boolean deleted = false;
public String getPcapFname() {
if((mState == AppState.running) && (mPcapUri != null)) {
if(mPcapFname != null)
return mPcapFname;
Cursor cursor;
try {
cursor = getContentResolver().query(mPcapUri, null, null, null, null);
} catch (Exception e) {
return null;
}
if((cursor == null) || !cursor.moveToFirst())
return null;
int idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
String fname = (idx >= 0) ? cursor.getString(idx) : "*unknown*";
cursor.close();
mPcapFname = fname;
return fname;
try {
deleted = (getContentResolver().delete(pcapUri, null, null) == 1);
} catch (UnsupportedOperationException | SecurityException e) {
e.printStackTrace();
}
return null;
if(!deleted)
Utils.showToast(MainActivity.this, R.string.delete_error);
}
public AppState getState() {
return(mState);
}
private void startExportSslkeylogfile() {
@@ -889,7 +737,6 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
Utils.showToastLong(this, R.string.export_failed);
}
}
mKeylogFile = null;
}
}
@@ -801,10 +801,10 @@ public class ConnectionsFragment extends Fragment implements ConnectionsListener
stream.close();
}
String fname = Utils.getUriFname(requireContext(), mCsvFname);
Utils.UriStat stat = Utils.getUriStat(requireContext(), mCsvFname);
if(fname != null) {
String msg = String.format(getString(R.string.file_saved_with_name), fname);
if(stat != null) {
String msg = String.format(getString(R.string.file_saved_with_name), stat.name);
Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show();
} else
Utils.showToast(requireContext(), R.string.save_ok);
@@ -842,7 +842,7 @@ public class ConnectionsFragment extends Fragment implements ConnectionsListener
Log.w(TAG, "No app found to handle file selection");
// Pick default path
Uri uri = Utils.getInternalStorageFile(requireContext(), fname);
Uri uri = Utils.getDownloadsUri(requireContext(), fname);
if(uri != null) {
mCsvFname = uri;
@@ -23,6 +23,7 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -273,10 +274,12 @@ public class StatusFragment extends Fragment implements AppStateListener, MenuPr
info = getString(R.string.pcap_file_info);
if(mActivity != null) {
String fname = mActivity.getPcapFname();
if(fname != null)
info = fname;
Uri pcap_uri = CaptureService.getPcapUri();
if(pcap_uri != null) {
Utils.UriStat uri_stat = Utils.getUriStat(mActivity, pcap_uri);
if(uri_stat != null)
info = uri_stat.name;
}
}
break;
case UDP_EXPORTER:
@@ -23,7 +23,8 @@ public class CaptureSettings implements Serializable {
public boolean block_quic;
public boolean auto_block_private_dns;
public String capture_interface;
public String pcap_uri;
public String pcap_uri = "";
public String pcap_name = "";
public int snaplen = 0;
public int max_pkts_per_flow = 0;
public int max_dump_size = 0;
@@ -42,7 +43,6 @@ public class CaptureSettings implements Serializable {
root_capture = Prefs.isRootCaptureEnabled(prefs);
pcapdroid_trailer = Prefs.isPcapdroidTrailerEnabled(prefs);
capture_interface = Prefs.getCaptureInterface(prefs);
pcap_uri = Prefs.getPCAPUri(prefs);
tls_decryption = Prefs.getTlsDecryptionEnabled(prefs);
full_payload = Prefs.getFullPayloadMode(prefs);
block_quic = Prefs.blockQuic(prefs);
@@ -63,7 +63,8 @@ public class CaptureSettings implements Serializable {
root_capture = getBool(intent, Prefs.PREF_ROOT_CAPTURE, false);
pcapdroid_trailer = getBool(intent, Prefs.PREF_PCAPDROID_TRAILER, false);
capture_interface = getString(intent, Prefs.PREF_CAPTURE_INTERFACE, "@inet");
pcap_uri = getString(intent, Prefs.PREF_PCAP_URI, "");
pcap_uri = getString(intent, "pcap_uri", "");
pcap_name = getString(intent, "pcap_name", "");
snaplen = getInt(intent, Prefs.PREF_SNAPLEN, 0);
max_pkts_per_flow = getInt(intent, Prefs.PREF_MAX_PKTS_PER_FLOW, 0);
max_dump_size = getInt(intent, Prefs.PREF_MAX_DUMP_SIZE, 0);
@@ -60,7 +60,6 @@ public class Prefs {
public static final String PREF_APP_FILTER = "app_filter";
public static final String PREF_HTTP_SERVER_PORT = "http_server_port";
public static final String PREF_PCAP_DUMP_MODE = "pcap_dump_mode_v2";
public static final String PREF_PCAP_URI = "pcap_uri";
public static final String PREF_IP_MODE = "ip_mode";
public static final String PREF_APP_LANGUAGE = "app_language";
public static final String PREF_APP_THEME = "app_theme";
@@ -185,7 +184,6 @@ public class Prefs {
&& p.getBoolean(PREF_FIREWALL, true));
}
public static boolean startAtBoot(SharedPreferences p) { return(p.getBoolean(PREF_START_AT_BOOT, false)); }
public static String getPCAPUri(SharedPreferences p) { return(p.getString(PREF_PCAP_URI, "")); }
public static boolean isTLSDecryptionSetupDone(SharedPreferences p) { return(p.getBoolean(PREF_TLS_DECRYPTION_SETUP_DONE, false)); }
public static boolean getFullPayloadMode(SharedPreferences p) { return(p.getBoolean(PREF_FULL_PAYLOAD, false)); }
public static boolean blockQuic(SharedPreferences p) { return(p.getBoolean(PREF_BLOCK_QUIC, false)); }
@@ -5,7 +5,6 @@ import android.net.Uri;
import com.emanuelef.remote_capture.CaptureService;
import com.emanuelef.remote_capture.Log;
import com.emanuelef.remote_capture.Utils;
import com.emanuelef.remote_capture.interfaces.PcapDumper;
import java.io.IOException;
+1
View File
@@ -447,4 +447,5 @@
<string name="decryption_whitelist_rules">Decryption whitelist rules</string>
<string name="decryption_whitelist_help">List of rules to exclude specific connections from the TLS decryption. Useful to avoid breaking apps/SDKs employing certificate pinning. Host matching only works if a prior DNS reply is seen</string>
<string name="decryption_whitelist">Decryption whitelist</string>
<string name="external_storage_perm_required">The external storage permission is required</string>
</resources>
+18 -14
View File
@@ -16,6 +16,13 @@ where ACTION is one of:
- `get_status`: get the capture status
The capture parameters are specified via Intent extras, which are discussed below.
For example, you can use the following command to start the capture and write the traffic dump to the PCAP file `Download/PCAPdroid/traffic.pcap`:
```bash
adb shell am start -e action start -e pcap_dump_mode pcap_file -e pcap_name traffic.pcap -n com.emanuelef.remote_capture/.activities.CaptureCtrl
```
A common task is to capture the traffic of a specific app to analyze it into your app. This can be easily accomplished by running PCAPdroid in the
[UDP Exporter mode](https://emanuele-f.github.io/PCAPdroid/dump_modes#24-udp-exporter):
@@ -76,7 +83,7 @@ As shown above, the capture settings can be specified by using intent extras. Th
| collector_ip_address | string | | | the IP address of the collector in udp_exporter mode |
| collector_port | int | | | the UDP port of the collector in udp_exporter mode |
| http_server_port | int | | | the HTTP server port in http_server mode |
| pcap_uri | string | | | the URI for the PCAP dump in pcap_file mode |
| pcap_uri | string | | | the URI for the PCAP dump in pcap_file mode (overrides pcap_name) |
| socks5_enabled | bool | | vpn | true to redirect the TCP connections to a SOCKS5 proxy |
| socks5_proxy_ip_address | string | | vpn | the IP address of the SOCKS5 proxy |
| socks5_proxy_port | int | | vpn | the TCP port of the SOCKS5 proxy |
@@ -91,13 +98,14 @@ As shown above, the capture settings can be specified by using intent extras. Th
| auto_block_private_dns | bool | 51 | vpn | true to detect and possibly block private DNS to inspect traffic |
| ip_mode | string | 56 | vpn | which IP addresses to use for the VPN: ipv4 \| ipv6 \| both |
| mitmproxy_opts | string | 62 | | additional options to provide to mitmproxy in decryption mode |
| pcap_name | string | 62 | | write the PCAP to Download/PCAPdroid/*pcap_name* in pcap_file mode |
The `Ver` column indicates the minimum PCAPdroid version required to use the given parameter. The PCAPdroid version can be queried via the `get_status` action as explained below.
The `Mode` column indicates if the option applies to any mode or only to the VPN or root mode.
*NOTE*: for security reasons, since version 1.5.3 you cannot specify a remote server IP address in `collector_ip_address` or in `socks5_proxy_ip_address`. If you really want to do this, you should first set such a remote IP address via the PCAPdroid gui and only then invoke the API.
*NOTE*: due to [file storage restrictions](https://developer.android.com/about/versions/11/privacy/storage), the `pcap_uri` must point to an app internal directory, e.g. `file:///data/user/0/com.emanuelef.remote_capture/cache/dump.pcap`.
*NOTE*: since version 1.6.0, the `pcap_uri` behavior is changed as described in the `Dumping PCAP to file` section below
## Query the Capture Status
@@ -147,17 +155,13 @@ In the result of the `stop` and `get_status` actions and in the broadcast of `Ca
## Dumping PCAP to file
Due to the restrictions introduced via the [scoped storage](https://developer.android.com/about/versions/11/privacy/storage), PCAPdroid can only create files inside its private directory, which is not accessible to you as a user. To dump the PCAP file to a publicly available directory, you must first perform the following steps:
[Scoped storage](https://developer.android.com/about/versions/11/privacy/storage) restrictions apply to PCAPdroid, which limits in which paths the PCAP file can be stored and how
other apps can access it.
1. Open PCAPdroid and select the "PCAP File" dump mode
2. Start the capture and select the path of the file to write. In this example I assume you select the `/sdcard/test.pcap` file
3. Stop the capture and choose to keep the generated PCAP file (don't delete it!)
4. Retrieve the internal URL which Android uses to reference this file. You can find this in the logcat output of PCAPdroid:
Since version 1.6.0, PCAPdroid dumps the PCAP file to the `Download/PCAPdroid` directory. By using the `pcap_name` parameter, you can get a predictable PCAP file path. The PCAP
file is overwritten if another file with the same `pcap_name` exists.
```
D/Main: PCAP URI to write [persistable=true]: content://com.android.externalstorage.documents/document/primary%3Atest.pcap
```
You should now be able to write the `test.pcap` file by setting the `pcap_uri` to this URI. You must repeat the steps above if you delete the file.
*NOTE*: if the messages shows `[persistable=false]` then it was not possible to get the permissions on the URI, so the `pcap_uri` paramter won't work. This occurs on devices without a file manager to select the PCAP destination path (e.g. on Android TV).
You can use the `pcap_uri` parameter to write the PCAP file to an arbitrary path. The URI specified in `pcap_uri`, however, is only used as an identifier, it's not the actual file
path. This choice has been made to cope with the scoped storage limitations. This means that, the first time the `pcap_uri` is set and every time it's changed, a file dialog is
displayed to the user to pick the actual file path. On subsequent runs, the same `pcap_uri` identifier can be used to write the selected file without the file dialog to be
presented to the user.