mirror of
https://github.com/emanuele-f/PCAPdroid.git
synced 2026-06-19 21:05:25 +08:00
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
This commit is contained in:
parent
a0a3430f19
commit
2bb955ab90
@ -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<PayloadChunk> 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
|
||||
}
|
||||
|
||||
@ -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<ConnUpdateListener> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<PayloadAdapter.PayloadV
|
||||
|
||||
public interface ExportPayloadHandler {
|
||||
void exportPayload(String payload);
|
||||
void exportPayload(byte[] payload, String contentType, String fname);
|
||||
}
|
||||
|
||||
public PayloadAdapter(Context context, ConnectionDescriptor conn, ChunkType mode, boolean showAsPrintable) {
|
||||
@ -267,13 +270,49 @@ public class PayloadAdapter extends RecyclerView.Adapter<PayloadAdapter.PayloadV
|
||||
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;
|
||||
|
||||
AdapterChunk chunk = getItem(payload_pos).adaptChunk;
|
||||
if (chunk == null)
|
||||
return;
|
||||
|
||||
if(mMode == ChunkType.HTTP) {
|
||||
String payload = getItem(payload_pos).adaptChunk.getExpandedText(true);
|
||||
String payload = chunk.getExpandedText(true);
|
||||
int crlf_pos = payload.indexOf("\r\n\r\n");
|
||||
String content_type = ((chunk.mChunk.contentType != null) && (!chunk.mChunk.contentType.isEmpty())) ?
|
||||
chunk.mChunk.contentType : "text/plain";
|
||||
|
||||
Log.d(TAG, "Export body content type: " + content_type);
|
||||
|
||||
String fname = "";
|
||||
if (chunk.mChunk.is_sent && (chunk.mChunk.path != null))
|
||||
fname = chunk.mChunk.path;
|
||||
else if (payload_pos > 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<PayloadAdapter.PayloadV
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
|
||||
builder.setTitle(title);
|
||||
builder.setSingleChoiceItems(choices, 2, (dialogInterface, i) -> {});
|
||||
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<PayloadAdapter.PayloadV
|
||||
}
|
||||
|
||||
if (is_export) {
|
||||
if (mExportHandler != null)
|
||||
mExportHandler.exportPayload(to_copy);
|
||||
if (mExportHandler != null) {
|
||||
boolean only_body = (choice == 1);
|
||||
|
||||
if (only_body) {
|
||||
// export the raw body bytes
|
||||
byte[] payload_bytes = chunk.mChunk.payload;
|
||||
|
||||
if (crlf_pos < (payload_bytes.length - 4))
|
||||
payload_bytes = Arrays.copyOfRange(payload_bytes, crlf_pos + 4, payload_bytes.length);
|
||||
|
||||
mExportHandler.exportPayload(payload_bytes, content_type, filename);
|
||||
} else
|
||||
mExportHandler.exportPayload(to_copy);
|
||||
}
|
||||
} else
|
||||
Utils.copyToClipboard(mContext, to_copy);
|
||||
});
|
||||
builder.create().show();
|
||||
} else {
|
||||
String[] choices = {
|
||||
List<String> 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();
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -504,4 +504,5 @@
|
||||
<string name="body">Body</string>
|
||||
<string name="both">Both</string>
|
||||
<string name="whats_new">What\'s new</string>
|
||||
<string name="raw_bytes">Raw bytes</string>
|
||||
</resources>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user