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 e4acc961..4ddfb9d6 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/HTTPReassembly.java +++ b/app/src/main/java/com/emanuelef/remote_capture/HTTPReassembly.java @@ -48,6 +48,7 @@ public class HTTPReassembly { private final ReassemblyListener mListener; private boolean mReassembleChunks; private boolean mInvalidHttp; + private boolean mIsTx; public HTTPReassembly(boolean reassembleChunks, ReassemblyListener listener) { mListener = listener; @@ -80,6 +81,10 @@ public class HTTPReassembly { void onChunkReassembled(PayloadChunk chunk); } + private void log_d(String msg) { + Log.d(TAG + "(" + (mIsTx ? "TX" : "RX") + ")", msg); + } + /* The request/response tab shows reassembled HTTP chunks. * Reassembling chunks is requires when using a content-encoding like gzip since we can only * decode the data when we have the full chunk and we cannot determine data bounds. @@ -89,6 +94,7 @@ public class HTTPReassembly { int body_start = 0; byte[] payload = chunk.payload; boolean chunked_complete = false; + mIsTx = chunk.is_sent; if(mReadingHeaders) { // Reading the HTTP headers @@ -100,11 +106,11 @@ public class HTTPReassembly { String line = reader.readLine(); while((line != null) && (line.length() > 0)) { line = line.toLowerCase(); - //Log.d(TAG, "[HEADER] " + line); + //log_d("[HEADER] " + line); if(line.startsWith("content-encoding: ")) { String contentEncoding = line.substring(18); - Log.d(TAG, "Content-Encoding: " + contentEncoding); + log_d("Content-Encoding: " + contentEncoding); switch (contentEncoding) { case "gzip": @@ -121,17 +127,17 @@ public class HTTPReassembly { } } else if(line.startsWith("content-type: ")) { String contentType = line.substring(14); - Log.d(TAG, "Content-Type: " + contentType); + log_d("Content-Type: " + contentType); } else if(line.startsWith("content-length: ")) { try { int contentLength = Integer.parseInt(line.substring(16)); - Log.d(TAG, "Content-Length: " + contentLength); + log_d("Content-Length: " + contentLength); } catch (NumberFormatException ignored) {} } else if(line.startsWith("upgrade: ")) { - Log.d(TAG, "Upgrade found, stop parsing"); + log_d("Upgrade found, stop parsing"); mReassembleChunks = false; } else if(line.equals("transfer-encoding: chunked")) { - Log.d(TAG, "Detected chunked encoding"); + log_d("Detected chunked encoding"); mChunkedEncoding = true; } @@ -145,6 +151,8 @@ public class HTTPReassembly { mHeaders.add(chunk.subchunk(0, body_start)); } else { if(mHeadersSize > MAX_HEADERS_SIZE) { + log_d("Assuming not HTTP"); + // Assume this is not valid HTTP traffic mReadingHeaders = false; mReassembleChunks = false; @@ -159,8 +167,10 @@ public class HTTPReassembly { // If not Content-Length provided and not using chunked encoding, then we cannot determine // chunks bounds, so disable reassembly - if(!mReadingHeaders && (mContentLength < 0) && (!mChunkedEncoding)) + if(!mReadingHeaders && (mContentLength < 0) && (!mChunkedEncoding) && mReassembleChunks) { + log_d("Cannot determine bounds, disable reassembly"); mReassembleChunks = false; + } // When mReassembleChunks is false, each chunk should be passed to the mListener if(!mReassembleChunks) @@ -181,7 +191,7 @@ public class HTTPReassembly { body_start += line.length() + 2; body_size -= line.length() + 2; - Log.d(TAG, "Chunk length: " + mContentLength); + log_d("Chunk length: " + mContentLength); if(mContentLength == 0) chunked_complete = true; @@ -193,6 +203,7 @@ public class HTTPReassembly { // NOTE: Content-Length is optional in HTTP/2.0, mitmproxy reconstructs the entire message if(body_size > 0) { if(mContentLength > 0) { + //log_d("body: " + body_size + " / " + mContentLength); if(body_size < mContentLength) mContentLength -= body_size; else { @@ -215,11 +226,14 @@ public class HTTPReassembly { if(chunked_complete || !mReassembleChunks) mChunkedEncoding = false; - if(((mContentLength <= 0) || !mReassembleChunks) && !mChunkedEncoding) { + if(((mContentLength <= 0) || !mReassembleChunks) + && !mChunkedEncoding) { // Reassemble the chunks (NOTE: gzip is applied only after all the chunks are collected) PayloadChunk headers = reassembleChunks(mHeaders); PayloadChunk body = mBody.size() > 0 ? reassembleChunks(mBody) : null; + //log_d("mContentLength=" + mContentLength + ", mReassembleChunks=" + mReassembleChunks + ", mChunkedEncoding=" + mChunkedEncoding); + // Decode body if((body != null) && (mContentEncoding != ContentEncoding.UNKNOWN)) decodeBody(body); @@ -245,7 +259,7 @@ public class HTTPReassembly { if((new_body_start > 0) && (chunk.payload.length > new_body_start)) { // Part of this chunk should be processed as a new chunk - Log.d(TAG, "Continue from " + new_body_start); + log_d("Continue from " + new_body_start); handleChunk(chunk.subchunk(new_body_start, chunk.payload.length - new_body_start)); } } @@ -254,7 +268,7 @@ public class HTTPReassembly { private void decodeBody(PayloadChunk body) { InputStream inputStream = null; - //Log.d(TAG, "Decoding as " + mContentEncoding.name().toLowerCase()); + //log_d("Decoding as " + mContentEncoding.name().toLowerCase()); try(ByteArrayInputStream bis = new ByteArrayInputStream(body.payload)) { switch (mContentEncoding) { @@ -282,7 +296,7 @@ public class HTTPReassembly { } } } catch (IOException ignored) { - Log.d(TAG, mContentEncoding.name().toLowerCase() + " decoding failed"); + log_d(mContentEncoding.name().toLowerCase() + " decoding failed"); //ignored.printStackTrace(); } finally { Utils.safeClose(inputStream); 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 12959607..667802f8 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 @@ -55,14 +55,14 @@ public class ConnectionDetailsActivity extends BaseActivity implements Connectio private int mCurChunks; private boolean mListenerSet; private boolean mHasPayload; - private boolean mHasRequestTab; - private boolean mHasResponseTab; + private boolean mHasHttpTab; + private boolean mHasWsTab; private final ArrayList mListeners = new ArrayList<>(); private static final int POS_OVERVIEW = 0; - private static final int POS_REQUEST = 1; - private static final int POS_RESPONSE = 2; - private static final int POS_PAYLOAD = 3; + private static final int POS_WEBSOCKET = 1; + private static final int POS_HTTP = 2; + private static final int POS_RAW_PAYLOAD = 3; public interface ConnUpdateListener { void connectionUpdated(); @@ -134,12 +134,12 @@ public class ConnectionDetailsActivity extends BaseActivity implements Connectio int pos = getVisibleTabsPositions()[position]; switch (pos) { - case POS_REQUEST: - return ConnectionPayload.newInstance(ConnectionPayload.Direction.REQUEST_ONLY); - case POS_RESPONSE: - return ConnectionPayload.newInstance(ConnectionPayload.Direction.RESPONSE_ONLY); - case POS_PAYLOAD: - return ConnectionPayload.newInstance(ConnectionPayload.Direction.BOTH); + case POS_WEBSOCKET: + return ConnectionPayload.newInstance(PayloadChunk.ChunkType.WEBSOCKET); + case POS_HTTP: + return ConnectionPayload.newInstance(PayloadChunk.ChunkType.HTTP); + case POS_RAW_PAYLOAD: + return ConnectionPayload.newInstance(PayloadChunk.ChunkType.RAW); case POS_OVERVIEW: default: return new ConnectionOverview(); @@ -147,18 +147,18 @@ public class ConnectionDetailsActivity extends BaseActivity implements Connectio } @Override - public int getItemCount() { return 1 + (mHasPayload ? 1 : 0) + (mHasRequestTab ? 1 : 0) + (mHasResponseTab ? 1 : 0); } + public int getItemCount() { return 1 + (mHasPayload ? 1 : 0) + (mHasHttpTab ? 1 : 0) + (mHasWsTab ? 1 : 0); } public int getPageTitle(final int position) { int pos = getVisibleTabsPositions()[position]; switch (pos) { - case POS_REQUEST: - return R.string.request; - case POS_RESPONSE: - return R.string.response; - case POS_PAYLOAD: - return R.string.payload; + case POS_WEBSOCKET: + return R.string.websocket; + case POS_HTTP: + return R.string.http; + case POS_RAW_PAYLOAD: + return R.string.raw_payload; case POS_OVERVIEW: default: return R.string.overview; @@ -171,12 +171,12 @@ public class ConnectionDetailsActivity extends BaseActivity implements Connectio visible[i++] = POS_OVERVIEW; - if(mHasRequestTab) - visible[i++] = POS_REQUEST; - if(mHasResponseTab) - visible[i++] = POS_RESPONSE; + if(mHasWsTab) + visible[i++] = POS_WEBSOCKET; + if(mHasHttpTab) + visible[i++] = POS_HTTP; if(mHasPayload) - visible[i] = POS_PAYLOAD; + visible[i] = POS_RAW_PAYLOAD; return visible; } @@ -254,7 +254,7 @@ public class ConnectionDetailsActivity extends BaseActivity implements Connectio @SuppressLint("NotifyDataSetChanged") private void recheckTabs() { - if(mHasRequestTab && mHasResponseTab) + if(mHasHttpTab && mHasWsTab) return; int max_check = Math.min(mConn.getNumPayloadChunks(), MAX_CHUNKS_TO_CHECK); @@ -268,14 +268,12 @@ public class ConnectionDetailsActivity extends BaseActivity implements Connectio for(int i=mCurChunks; i mChunks = new ArrayList<>(); - private final HTTPReassembly mHttp; + private final HTTPReassembly mHttpReq; + private final HTTPReassembly mHttpRes; - public PayloadAdapter(Context context, ConnectionDescriptor conn, Direction dir) { + public PayloadAdapter(Context context, ConnectionDescriptor conn, ChunkType mode) { mLayoutInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mConn = conn; mContext = context; - mDir = dir; + mMode = mode; // Note: in minimal mode, only the first chunk is captured, so don't reassemble them - mHttp = new HTTPReassembly(CaptureService.getCurPayloadMode() == Prefs.PayloadMode.FULL, this); + boolean reassemble = (CaptureService.getCurPayloadMode() == Prefs.PayloadMode.FULL); + + // each direction must have its separate reassembly + mHttpReq = new HTTPReassembly(reassemble, this); + mHttpRes = new HTTPReassembly(reassemble, this); + handleChunksAdded(mConn.getNumPayloadChunks()); } @@ -103,7 +108,7 @@ public class PayloadAdapter extends RecyclerView.AdapterThe connections payload will not be shown Only the initial bytes of the payload will be shown The full payload will be shown (e.g. the full HTTP request and response). This requires a lot of memory - Payload Connection Encrypted Cleartext @@ -310,4 +309,9 @@ HTTP response Payload visualization Only a portion of the actual payload is shown, see \"%1$s\" + WebSocket + HTTP + Raw + TX + RX