From 36c99bc84bf2050af787bc9713da7d4fe3e091fa Mon Sep 17 00:00:00 2001 From: emanuele-f Date: Thu, 1 Feb 2024 11:48:07 +0100 Subject: [PATCH] Add export to file payload button See #362 --- .../activities/ConnectionDetailsActivity.java | 51 ++++++- .../adapters/PayloadAdapter.java | 144 +++++++++++------- .../fragments/ConnectionPayload.java | 7 + .../main/res/drawable/ic_save_alt_small.xml | 5 + app/src/main/res/layout/payload_item.xml | 19 +++ app/src/main/res/values/strings.xml | 1 + 6 files changed, 168 insertions(+), 59 deletions(-) create mode 100644 app/src/main/res/drawable/ic_save_alt_small.xml diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/ConnectionDetailsActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/ConnectionDetailsActivity.java index f62cbdfc..f9ca0ccf 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/ConnectionDetailsActivity.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/ConnectionDetailsActivity.java @@ -14,11 +14,14 @@ * You should have received a copy of the GNU General Public License * along with PCAPdroid. If not, see . * - * Copyright 2020-21 - Emanuele Faranda + * Copyright 2020-24 - Emanuele Faranda */ package com.emanuelef.remote_capture.activities; +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; @@ -26,6 +29,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import android.annotation.SuppressLint; +import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -36,6 +40,8 @@ import com.emanuelef.remote_capture.CaptureService; import com.emanuelef.remote_capture.ConnectionsRegister; import com.emanuelef.remote_capture.Log; import com.emanuelef.remote_capture.R; +import com.emanuelef.remote_capture.Utils; +import com.emanuelef.remote_capture.adapters.PayloadAdapter; import com.emanuelef.remote_capture.fragments.ConnectionOverview; import com.emanuelef.remote_capture.fragments.ConnectionPayload; import com.emanuelef.remote_capture.interfaces.ConnectionsListener; @@ -44,9 +50,12 @@ import com.emanuelef.remote_capture.model.PayloadChunk; import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.util.ArrayList; -public class ConnectionDetailsActivity extends BaseActivity implements ConnectionsListener { +public class ConnectionDetailsActivity extends BaseActivity implements ConnectionsListener, PayloadAdapter.ExportPayloadHandler { private static final String TAG = "ConnectionDetails"; public static final String CONN_ID_KEY = "conn_id"; private static final int MAX_CHUNKS_TO_CHECK = 10; @@ -60,6 +69,7 @@ public class ConnectionDetailsActivity extends BaseActivity implements Connectio private boolean mHasPayload; private boolean mHasHttpTab; private boolean mHasWsTab; + private String mPayloadToExport; private final ArrayList mListeners = new ArrayList<>(); private static final int POS_OVERVIEW = 0; @@ -67,6 +77,9 @@ public class ConnectionDetailsActivity extends BaseActivity implements Connectio private static final int POS_HTTP = 2; private static final int POS_RAW_PAYLOAD = 3; + private final ActivityResultLauncher payloadExportLauncher = + registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), this::payloadExportResult); + public interface ConnUpdateListener { void connectionUpdated(); } @@ -329,4 +342,38 @@ public class ConnectionDetailsActivity extends BaseActivity implements Connectio return super.onKeyDown(keyCode, event); } + + @Override + public void exportPayload(String payload) { + mPayloadToExport = payload; + + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_TITLE, Utils.getUniqueFileName(this, "txt")); + + Log.d(TAG, "exportPayload: launching dialog"); + Utils.launchFileDialog(this, intent, payloadExportLauncher); + } + + private void payloadExportResult(final ActivityResult result) { + Log.d(TAG, "payloadExportResult"); + + if (mPayloadToExport == null) + return; + + if((result.getResultCode() == RESULT_OK) && (result.getData() != null) && (result.getData().getData() != null)) { + try(OutputStream out = getContentResolver().openOutputStream(result.getData().getData(), "rwt")) { + try(OutputStreamWriter writer = new OutputStreamWriter(out)) { + writer.write(mPayloadToExport); + } + Utils.showToast(this, R.string.save_ok); + } catch (IOException e) { + e.printStackTrace(); + Utils.showToastLong(this, R.string.export_failed); + } + } + + mPayloadToExport = null; + } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/adapters/PayloadAdapter.java b/app/src/main/java/com/emanuelef/remote_capture/adapters/PayloadAdapter.java index 8c66fe67..b633c14d 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/adapters/PayloadAdapter.java +++ b/app/src/main/java/com/emanuelef/remote_capture/adapters/PayloadAdapter.java @@ -66,7 +66,13 @@ public class PayloadAdapter extends RecyclerView.Adapter mChunks = new ArrayList<>(); private final HTTPReassembly mHttpReq; private final HTTPReassembly mHttpRes; + private final boolean mSupportsFileDialog; private boolean mShowAsPrintable; + private ExportPayloadHandler mExportHandler; + + public interface ExportPayloadHandler { + void exportPayload(String payload); + } public PayloadAdapter(Context context, ConnectionDescriptor conn, ChunkType mode, boolean showAsPrintable) { mLayoutInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -74,6 +80,7 @@ public class PayloadAdapter extends RecyclerView.Adapter { - int payload_pos = holder.getAbsoluteAdapterPosition(); - - if(mMode == ChunkType.HTTP) { - String payload = getItem(payload_pos).adaptChunk.getExpandedText(true); - int crlf_pos = payload.indexOf("\r\n\r\n"); - - boolean has_body = (crlf_pos > 0) && (crlf_pos < (payload.length() - 4)); - if (!has_body) { - Utils.copyToClipboard(mContext, payload); - return; - } - - String[] choices = { - mContext.getString(R.string.headers), - mContext.getString(R.string.body), - mContext.getString(R.string.both), - }; - - AlertDialog.Builder builder = new AlertDialog.Builder(mContext); - builder.setTitle(R.string.copy_action); - builder.setSingleChoiceItems(choices, 2, (dialogInterface, i) -> {}); - builder.setNeutralButton(R.string.cancel_action, (dialogInterface, i) -> {}); - builder.setPositiveButton(R.string.copy_to_clipboard, (dialogInterface, i) -> { - int choice = ((AlertDialog)dialogInterface).getListView().getCheckedItemPosition(); - String to_copy = payload; - - if (choice != 2) { - if (choice == 0 /* Headers */) - to_copy = to_copy.substring(0, crlf_pos); - else /* body */ - to_copy = to_copy.substring(crlf_pos + 4); - } - - Utils.copyToClipboard(mContext, to_copy); - }); - builder.create().show(); - } else { - String[] choices = { - mContext.getString(R.string.printable_text), - mContext.getString(R.string.hexdump) - }; - - AlertDialog.Builder builder = new AlertDialog.Builder(mContext); - builder.setTitle(R.string.copy_action); - builder.setSingleChoiceItems(choices, mShowAsPrintable ? 0 : 1, (dialogInterface, i) -> {}); - - builder.setNeutralButton(R.string.cancel_action, (dialogInterface, i) -> {}); - builder.setPositiveButton(R.string.copy_to_clipboard, (dialogInterface, i) -> { - int choice = ((AlertDialog)dialogInterface).getListView().getCheckedItemPosition(); - String payload = getItem(payload_pos).adaptChunk.getExpandedText(choice == 0); - - Utils.copyToClipboard(mContext, payload); - }); - builder.create().show(); - } - }); + holder.copybutton.setOnClickListener(v -> handleCopyExportButtons(holder, false)); + holder.exportbutton.setOnClickListener(v -> handleCopyExportButtons(holder, true)); + holder.exportbutton.setVisibility(mSupportsFileDialog ? View.VISIBLE : View.GONE); return holder; } + private void handleCopyExportButtons(PayloadViewHolder holder, boolean is_export) { + if(is_export && (mExportHandler == null)) + return; + + int payload_pos = holder.getAbsoluteAdapterPosition(); + int title = is_export ? R.string.export_ellipsis : R.string.copy_action; + int positive_action = is_export ? R.string.export_action : R.string.copy_to_clipboard; + + if(mMode == ChunkType.HTTP) { + String payload = getItem(payload_pos).adaptChunk.getExpandedText(true); + int crlf_pos = payload.indexOf("\r\n\r\n"); + + boolean has_body = (crlf_pos > 0) && (crlf_pos < (payload.length() - 4)); + if (!has_body) { + Utils.copyToClipboard(mContext, payload); + return; + } + + String[] choices = { + mContext.getString(R.string.headers), + mContext.getString(R.string.body), + mContext.getString(R.string.both), + }; + + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + builder.setTitle(title); + builder.setSingleChoiceItems(choices, 2, (dialogInterface, i) -> {}); + builder.setNeutralButton(R.string.cancel_action, (dialogInterface, i) -> {}); + builder.setPositiveButton(positive_action, (dialogInterface, i) -> { + int choice = ((AlertDialog)dialogInterface).getListView().getCheckedItemPosition(); + String to_copy = payload; + + if (choice != 2) { + if (choice == 0 /* Headers */) + to_copy = to_copy.substring(0, crlf_pos); + else /* body */ + to_copy = to_copy.substring(crlf_pos + 4); + } + + if (is_export) { + if (mExportHandler != null) + mExportHandler.exportPayload(to_copy); + } else + Utils.copyToClipboard(mContext, to_copy); + }); + builder.create().show(); + } else { + String[] choices = { + mContext.getString(R.string.printable_text), + mContext.getString(R.string.hexdump) + }; + + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + builder.setTitle(title); + builder.setSingleChoiceItems(choices, mShowAsPrintable ? 0 : 1, (dialogInterface, i) -> {}); + + builder.setNeutralButton(R.string.cancel_action, (dialogInterface, i) -> {}); + builder.setPositiveButton(positive_action, (dialogInterface, i) -> { + int choice = ((AlertDialog)dialogInterface).getListView().getCheckedItemPosition(); + String payload = getItem(payload_pos).adaptChunk.getExpandedText(choice == 0); + + if (is_export) { + if (mExportHandler != null) + mExportHandler.exportPayload(payload); + } else + Utils.copyToClipboard(mContext, payload); + }); + builder.create().show(); + } + } + private String getHeaderTag(PayloadChunk chunk) { if(mMode == ChunkType.HTTP) return (chunk.is_sent) ? mContext.getString(R.string.request) : mContext.getString(R.string.response); diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/ConnectionPayload.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/ConnectionPayload.java index f3ce0a26..d8bf595c 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/fragments/ConnectionPayload.java +++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/ConnectionPayload.java @@ -76,6 +76,9 @@ public class ConnectionPayload extends Fragment implements ConnectionDetailsActi super.onAttach(context); mActivity = (ConnectionDetailsActivity) context; mActivity.addConnUpdateListener(this); + + if (mAdapter != null) + mAdapter.setExportPayloadHandler(mActivity); } @Override @@ -83,6 +86,9 @@ public class ConnectionPayload extends Fragment implements ConnectionDetailsActi super.onDetach(); mActivity.removeConnUpdateListener(this); mActivity = null; + + if (mAdapter != null) + mAdapter.setExportPayloadHandler(null); } @Override @@ -123,6 +129,7 @@ public class ConnectionPayload extends Fragment implements ConnectionDetailsActi else mShowAsPrintable = false; mAdapter = new PayloadAdapter(requireContext(), mConn, mode, mShowAsPrintable); + mAdapter.setExportPayloadHandler(mActivity); mJustCreated = true; // only set adapter after acknowledged (see setMenuVisibility below) diff --git a/app/src/main/res/drawable/ic_save_alt_small.xml b/app/src/main/res/drawable/ic_save_alt_small.xml new file mode 100644 index 00000000..6d7013df --- /dev/null +++ b/app/src/main/res/drawable/ic_save_alt_small.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/payload_item.xml b/app/src/main/res/layout/payload_item.xml index 147216a2..e131a769 100644 --- a/app/src/main/res/layout/payload_item.xml +++ b/app/src/main/res/layout/payload_item.xml @@ -49,6 +49,25 @@ app:iconPadding="0dp" app:iconTint="@color/colorTabText" style="@style/Widget.MaterialComponents.Button.TextButton" /> + + Mitm setup wizard Install Export + Export… Install the PCAPdroid mitm addon Configure Export the PCAPdroid CA certificate, then open the Android \"Encryption & Credentials\" settings and choose install it as a \"CA certificate\"