Parse request bodies eagerly

Summary:
Currently the network plugin was always storing the transfer format of our request/ response bodies: a base64 string. However, those are not searchable, and every formatter (and multiple can be invoked in a single view) was responsible for its own decompressing.

This diff changes parsing requests / responses into an accurate format: a decompressed, de-base64-ed utf8 string, or a Uint8array for binary data.

We will use this in the next diffs to do some more efficient searching

Reviewed By: passy

Differential Revision: D28200190

fbshipit-source-id: 33a71aeb7b340b58305e97fff4fa5ce472169b25
This commit is contained in:
Michel Weststrate
2021-05-06 04:26:41 -07:00
committed by Facebook GitHub Bot
parent fc4a08eb55
commit 72e34bbd0d
8 changed files with 189 additions and 130 deletions

View File

@@ -26,27 +26,42 @@ export function getHeaderValue(
return '';
}
export function decodeBody(container: {
headers?: Array<Header>;
data: string | null | undefined;
}): string {
if (!container.data) {
return '';
export function isTextual(headers?: Array<Header>): boolean {
const contentType = getHeaderValue(headers, 'Content-Type');
if (!contentType) {
return false;
}
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
return (
contentType.startsWith('text/') ||
contentType.startsWith('application/x-www-form-urlencoded') ||
contentType.startsWith('application/json') ||
contentType.startsWith('multipart/') ||
contentType.startsWith('message/') ||
contentType.startsWith('image/svg')
);
}
export function decodeBody(
headers?: Array<Header>,
data?: string | null,
): string | undefined | Uint8Array {
if (!data) {
return undefined;
}
try {
const isGzip =
getHeaderValue(container.headers, 'Content-Encoding') === 'gzip';
const isGzip = getHeaderValue(headers, 'Content-Encoding') === 'gzip';
if (isGzip) {
try {
const binStr = Base64.atob(container.data);
const dataArr = new Uint8Array(binStr.length);
for (let i = 0; i < binStr.length; i++) {
dataArr[i] = binStr.charCodeAt(i);
}
// The request is gzipped, so convert the base64 back to the raw bytes first,
// then inflate. pako will detect the BOM headers and return a proper utf-8 string right away
return pako.inflate(dataArr, {to: 'string'});
// The request is gzipped, so convert the raw bytes back to base64 first.
const dataArr = Base64.toUint8Array(data);
// then inflate.
return isTextual(headers)
? // pako will detect the BOM headers and return a proper utf-8 string right away
pako.inflate(dataArr, {to: 'string'})
: pako.inflate(dataArr);
} catch (e) {
// on iOS, the stream send to flipper is already inflated, so the content-encoding will not
// match the actual data anymore, and we should skip inflating.
@@ -57,14 +72,18 @@ export function decodeBody(container: {
}
}
// If this is not a gzipped request, assume we are interested in a proper utf-8 string.
// - If the raw binary data in is needed, in base64 form, use container.data directly
// - either directly use container.data (for example)
return Base64.decode(container.data);
// - If the raw binary data in is needed, in base64 form, use data directly
// - either directly use data (for example)
if (isTextual(headers)) {
return Base64.decode(data);
} else {
return Base64.toUint8Array(data);
}
} catch (e) {
console.warn(
`Flipper failed to decode request/response body (size: ${container.data.length}): ${e}`,
`Flipper failed to decode request/response body (size: ${data.length}): ${e}`,
);
return '';
return undefined;
}
}
@@ -78,17 +97,31 @@ export function convertRequestToCurlCommand(
const headerStr = `${header.key}: ${header.value}`;
command += ` -H ${escapedString(headerStr)}`;
});
// Add body. TODO: we only want this for non-binary data! See D23403095
const body = decodeBody({
headers: request.requestHeaders,
data: request.requestData,
});
if (body) {
command += ` -d ${escapedString(body)}`;
if (typeof request.requestData === 'string') {
command += ` -d ${escapedString(request.requestData)}`;
}
return command;
}
export function bodyAsString(body: undefined | string | Uint8Array): string {
if (body == undefined) {
return '(empty)';
}
if (body instanceof Uint8Array) {
return '(binary data)';
}
return body;
}
export function bodyAsBinary(
body: undefined | string | Uint8Array,
): Uint8Array | undefined {
if (body instanceof Uint8Array) {
return body;
}
return undefined;
}
function escapeCharacter(x: string) {
const code = x.charCodeAt(0);
return code < 16 ? '\\u0' + code.toString(16) : '\\u' + code.toString(16);
@@ -167,21 +200,8 @@ export function requestsToText(requests: Request[]): string {
.join('\n')}`;
// TODO: we want decoding only for non-binary data! See D23403095
const requestData = request.requestData
? decodeBody({
headers: request.requestHeaders,
data: request.requestData,
})
: null;
const responseData = request.responseData
? decodeBody({
headers: request.responseHeaders,
data: request.responseData,
})
: null;
if (requestData) {
copyText += `\n\n${requestData}`;
if (request.requestData) {
copyText += `\n\n${request.requestData}`;
}
if (request.status) {
copyText += `
@@ -198,8 +218,8 @@ export function requestsToText(requests: Request[]): string {
}`;
}
if (responseData) {
copyText += `\n\n${responseData}`;
if (request.responseData) {
copyText += `\n\n${request.responseData}`;
}
return copyText;
}