Add export to file payload button

See #362
This commit is contained in:
emanuele-f
2024-02-01 11:48:07 +01:00
parent 724da6a0f8
commit 36c99bc84b
6 changed files with 168 additions and 59 deletions
@@ -14,11 +14,14 @@
* 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
* 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<ConnUpdateListener> 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<Intent> 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;
}
}
@@ -66,7 +66,13 @@ public class PayloadAdapter extends RecyclerView.Adapter<PayloadAdapter.PayloadV
private final ArrayList<AdapterChunk> 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<PayloadAdapter.PayloadV
mContext = context;
mMode = mode;
mShowAsPrintable = showAsPrintable;
mSupportsFileDialog = Utils.supportsFileDialog(context);
// Note: in minimal mode, only the first chunk is captured, so don't reassemble them
boolean reassemble = (CaptureService.getCurPayloadMode() == Prefs.PayloadMode.FULL);
@@ -85,6 +92,10 @@ public class PayloadAdapter extends RecyclerView.Adapter<PayloadAdapter.PayloadV
handleChunksAdded(mConn.getNumPayloadChunks());
}
public void setExportPayloadHandler(ExportPayloadHandler handler) {
mExportHandler = handler;
}
private class AdapterChunk {
private final PayloadChunk mChunk;
private String mTheText;
@@ -203,6 +214,7 @@ public class PayloadAdapter extends RecyclerView.Adapter<PayloadAdapter.PayloadV
TextView dump;
MaterialButton expandButton;
MaterialButton copybutton;
MaterialButton exportbutton;
public PayloadViewHolder(View view) {
super(view);
@@ -213,6 +225,7 @@ public class PayloadAdapter extends RecyclerView.Adapter<PayloadAdapter.PayloadV
dumpBox = view.findViewById(R.id.dump_box);
expandButton = view.findViewById(R.id.expand_button);
copybutton = view.findViewById(R.id.copy_button);
exportbutton = view.findViewById(R.id.export_button);
}
}
@@ -239,67 +252,84 @@ public class PayloadAdapter extends RecyclerView.Adapter<PayloadAdapter.PayloadV
}
});
holder.copybutton.setOnClickListener(v -> {
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);
@@ -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)
@@ -0,0 +1,5 @@
<vector android:height="18dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="18dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,12v7L5,19v-7L3,12v7c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2zM13,12.67l2.59,-2.58L17,11.5l-5,5 -5,-5 1.41,-1.41L11,12.67L11,3h2z"/>
</vector>
+19
View File
@@ -49,6 +49,25 @@
app:iconPadding="0dp"
app:iconTint="@color/colorTabText"
style="@style/Widget.MaterialComponents.Button.TextButton" />
<com.google.android.material.button.MaterialButton
android:id="@+id/export_button"
android:insetTop="0dp"
android:insetBottom="0dp"
android:insetLeft="0dp"
android:insetRight="0dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:paddingVertical="2dp"
android:paddingHorizontal="2dp"
android:layout_marginHorizontal="2dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_save_alt_small"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:iconTint="@color/colorTabText"
style="@style/Widget.MaterialComponents.Button.TextButton" />
</LinearLayout>
<RelativeLayout
+1
View File
@@ -274,6 +274,7 @@
<string name="mitm_setup_wizard">Mitm setup wizard</string>
<string name="install_action">Install</string>
<string name="export_action">Export</string>
<string name="export_ellipsis">Export…</string>
<string name="install_the_mitm_addon">Install the PCAPdroid <a href='%1$s'>mitm addon</a></string>
<string name="configure_action">Configure</string>
<string name="export_ca_certificate">Export the PCAPdroid CA certificate, then open the Android \"Encryption &amp; Credentials\" settings and choose install it as a \"CA certificate\"</string>