mirror of
https://github.com/emanuele-f/PCAPdroid.git
synced 2026-05-08 21:12:26 +00:00
+49
-2
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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 & Credentials\" settings and choose install it as a \"CA certificate\"</string>
|
||||
|
||||
Reference in New Issue
Block a user