mirror of
https://github.com/emanuele-f/PCAPdroid.git
synced 2026-05-08 21:12:26 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user