From 2bb955ab90bdef287e14f4ee7bf382425de55bf6 Mon Sep 17 00:00:00 2001 From: emanuele-f Date: Mon, 5 Feb 2024 12:37:50 +0100 Subject: [PATCH] Improve payload export - HTTP body is now exported in binary form, with file name - fix export when HTTP body is empty - set "body" as the default HTTP export action - add new "Raw bytes" export option for non-HTTP See #362 --- .../remote_capture/HTTPReassembly.java | 19 ++++ .../activities/ConnectionDetailsActivity.java | 56 ++++++++++-- .../adapters/PayloadAdapter.java | 87 ++++++++++++++++--- .../remote_capture/model/PayloadChunk.java | 1 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 142 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/emanuelef/remote_capture/HTTPReassembly.java b/app/src/main/java/com/emanuelef/remote_capture/HTTPReassembly.java index 44f98265..d107f317 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/HTTPReassembly.java +++ b/app/src/main/java/com/emanuelef/remote_capture/HTTPReassembly.java @@ -40,6 +40,7 @@ public class HTTPReassembly { private boolean mChunkedEncoding; private ContentEncoding mContentEncoding; private String mContentType; + private String mPath; private int mContentLength; private int mHeadersSize; private final ArrayList mHeaders = new ArrayList<>(); @@ -68,6 +69,7 @@ public class HTTPReassembly { mChunkedEncoding = false; mContentLength = -1; mContentType = null; + mPath = null; mHeadersSize = 0; mHeaders.clear(); mBody.clear(); @@ -100,10 +102,26 @@ public class HTTPReassembly { // Reading the HTTP headers int headers_end = Utils.getEndOfHTTPHeaders(payload); int headers_size = (headers_end == 0) ? payload.length : headers_end; + boolean is_first_line = (mHeadersSize == 0); mHeadersSize += headers_size; try(BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(payload, 0, headers_size)))) { String line = reader.readLine(); + + if (is_first_line && (line != null)) { + if (line.startsWith("GET ") || line.startsWith("POST ") + || line.startsWith("HEAD ") || line.startsWith("PUT ")) { + int first_space = line.indexOf(' '); + int second_space = line.indexOf(' ', first_space + 1); + + if ((first_space > 0) && (second_space > 0)) { + mPath = line.substring(first_space + 1, second_space); + log_d("Path: " + mPath); + } + } + is_first_line = false; + } + while((line != null) && (line.length() > 0)) { line = line.toLowerCase(); //log_d("[HEADER] " + line); @@ -256,6 +274,7 @@ public class HTTPReassembly { to_add.type = PayloadChunk.ChunkType.RAW; to_add.contentType = mContentType; + to_add.path = mPath; mListener.onChunkReassembled(to_add); reset(); // mReadingHeaders = true } 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 f9ca0ccf..395eabc4 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 @@ -69,7 +69,8 @@ public class ConnectionDetailsActivity extends BaseActivity implements Connectio private boolean mHasPayload; private boolean mHasHttpTab; private boolean mHasWsTab; - private String mPayloadToExport; + private String mStringPayloadToExport; + private byte[] mRawPayloadToExport; private final ArrayList mListeners = new ArrayList<>(); private static final int POS_OVERVIEW = 0; @@ -345,35 +346,72 @@ public class ConnectionDetailsActivity extends BaseActivity implements Connectio @Override public void exportPayload(String payload) { - mPayloadToExport = payload; + mStringPayloadToExport = payload; + mRawPayloadToExport = null; 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); + } + + @Override + public void exportPayload(byte[] payload, String contentType, String fname) { + mStringPayloadToExport = null; + mRawPayloadToExport = payload; + + if (fname.isEmpty()) { + String ext; + + switch (contentType) { + case "text/html": + ext = "html"; + break; + case "application/octet-stream": + ext = "bin"; + break; + default: + ext = "txt"; + } + + fname = Utils.getUniqueFileName(this, ext); + } + + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(contentType); + intent.putExtra(Intent.EXTRA_TITLE, fname); + Utils.launchFileDialog(this, intent, payloadExportLauncher); } private void payloadExportResult(final ActivityResult result) { Log.d(TAG, "payloadExportResult"); - if (mPayloadToExport == null) + if ((mRawPayloadToExport == null) && (mStringPayloadToExport == 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); + if (out != null) { + if (mStringPayloadToExport != null) { + try (OutputStreamWriter writer = new OutputStreamWriter(out)) { + writer.write(mStringPayloadToExport); + } + } else + out.write(mRawPayloadToExport); + Utils.showToast(this, R.string.save_ok); + } else + Utils.showToastLong(this, R.string.export_failed); } catch (IOException e) { e.printStackTrace(); Utils.showToastLong(this, R.string.export_failed); } } - mPayloadToExport = null; + mRawPayloadToExport = null; + mStringPayloadToExport = 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 b633c14d..ecbd1b6d 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 @@ -46,7 +46,9 @@ import com.google.android.material.button.MaterialButton; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; +import java.util.List; import java.util.Locale; /* An adapter to show PayloadChunk items. @@ -72,6 +74,7 @@ public class PayloadAdapter extends RecyclerView.Adapter 0) { + // Try to match the HTTP request, to determine the file name + AdapterChunk req_chunk = getItem(payload_pos - 1).adaptChunk; + if (req_chunk.mChunk.is_sent && (req_chunk.mChunk.path != null)) + fname = req_chunk.mChunk.path; + } + + if (!fname.isEmpty()) { + int last_slash = fname.lastIndexOf('/'); + if (last_slash > 0) + fname = fname.substring(last_slash + 1); + } + + if (fname.contains(".")) + Log.d(TAG, "File name: " + fname); + else + fname = ""; + + String filename = fname; boolean has_body = (crlf_pos > 0) && (crlf_pos < (payload.length() - 4)); if (!has_body) { - Utils.copyToClipboard(mContext, payload); + // only HTTP headers + if (is_export) { + if (mExportHandler != null) + mExportHandler.exportPayload(payload); + } else + Utils.copyToClipboard(mContext, payload); return; } @@ -285,7 +324,7 @@ public class PayloadAdapter extends RecyclerView.Adapter {}); + builder.setSingleChoiceItems(choices, 1, (dialogInterface, i) -> {}); builder.setNeutralButton(R.string.cancel_action, (dialogInterface, i) -> {}); builder.setPositiveButton(positive_action, (dialogInterface, i) -> { int choice = ((AlertDialog)dialogInterface).getListView().getCheckedItemPosition(); @@ -299,32 +338,54 @@ public class PayloadAdapter extends RecyclerView.Adapter choices = new ArrayList<>(Arrays.asList( mContext.getString(R.string.printable_text), mContext.getString(R.string.hexdump) - }; + )); + if (is_export) + choices.add(mContext.getString(R.string.raw_bytes)); AlertDialog.Builder builder = new AlertDialog.Builder(mContext); builder.setTitle(title); - builder.setSingleChoiceItems(choices, mShowAsPrintable ? 0 : 1, (dialogInterface, i) -> {}); + builder.setSingleChoiceItems(choices.toArray(new String[]{}), 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 (choice == 2 /* raw bytes */) { + assert (is_export); + if (mExportHandler != null) - mExportHandler.exportPayload(payload); - } else - Utils.copyToClipboard(mContext, payload); + mExportHandler.exportPayload(chunk.mChunk.payload, "application/octet-stream", ""); + } else { + 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(); } diff --git a/app/src/main/java/com/emanuelef/remote_capture/model/PayloadChunk.java b/app/src/main/java/com/emanuelef/remote_capture/model/PayloadChunk.java index 5ce971a6..33dd0bf2 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/model/PayloadChunk.java +++ b/app/src/main/java/com/emanuelef/remote_capture/model/PayloadChunk.java @@ -28,6 +28,7 @@ public class PayloadChunk implements Serializable { public long timestamp; public ChunkType type; public String contentType; + public String path; // Serializable need in ConnectionPayload fragment public enum ChunkType implements Serializable { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a7127764..664786ba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -504,4 +504,5 @@ Body Both What\'s new + Raw bytes