diff --git a/desktop/flipper-plugin/src/ui/Panel.tsx b/desktop/flipper-plugin/src/ui/Panel.tsx index c1d3ba3e9..7854bba47 100644 --- a/desktop/flipper-plugin/src/ui/Panel.tsx +++ b/desktop/flipper-plugin/src/ui/Panel.tsx @@ -82,8 +82,8 @@ const StyledCollapse = styled(Collapse)({ background: theme.backgroundWash, paddingTop: theme.space.tiny, paddingBottom: theme.space.tiny, - paddingLeft: 26, fontWeight: 'bold', + display: 'flex', '> .anticon': { padding: `5px 0px`, left: 8, diff --git a/desktop/plugins/public/network/RequestDetails.tsx b/desktop/plugins/public/network/RequestDetails.tsx index 3b40dd6ce..6e923ed9d 100644 --- a/desktop/plugins/public/network/RequestDetails.tsx +++ b/desktop/plugins/public/network/RequestDetails.tsx @@ -23,13 +23,7 @@ import { } from 'flipper-plugin'; import {Select, Typography} from 'antd'; -import { - bodyAsBinary, - bodyAsString, - formatBytes, - getHeaderValue, - isTextual, -} from './utils'; +import {bodyAsBinary, bodyAsString, formatBytes, getHeaderValue} from './utils'; import {Request, Header, Insights, RetryInsights} from './types'; import {BodyOptions} from './index'; import {ProtobufDefinitionsRepository} from './ProtobufDefinitionsRepository'; @@ -95,7 +89,7 @@ export default class RequestDetails extends Component { key="requestData" title={'Request Body'} extraActions={ - isTextual(request.requestHeaders) ? ( + typeof request.requestData === 'string' ? ( { @@ -129,7 +123,8 @@ export default class RequestDetails extends Component { request.responseIsMock ? ' (Mocked)' : '' }`} extraActions={ - isTextual(request.responseHeaders) && request.responseData ? ( + typeof request.responseData === 'string' && + request.responseData ? ( { diff --git a/desktop/plugins/public/network/__tests__/encoding.node.tsx b/desktop/plugins/public/network/__tests__/encoding.node.tsx index f3cc40a60..ed8677deb 100644 --- a/desktop/plugins/public/network/__tests__/encoding.node.tsx +++ b/desktop/plugins/public/network/__tests__/encoding.node.tsx @@ -9,7 +9,7 @@ import {readFile} from 'fs'; import path from 'path'; -import {decodeBody} from '../utils'; +import {decodeBody, isTextual} from '../utils'; import {ResponseInfo} from '../types'; import {promisify} from 'util'; import {readFileSync} from 'fs'; @@ -139,6 +139,19 @@ describe('network data encoding', () => { }); }); +test('detects utf8 strings in binary arrays', async () => { + const binaryBuffer = readFileSync( + path.join(__dirname, 'fixtures', 'tiny_logo.png'), + ); + const textBuffer = readFileSync( + path.join(__dirname, 'fixtures', 'donating.md'), + ); + const textBuffer2 = readFileSync(__filename); + expect(isTextual(undefined, binaryBuffer)).toBe(false); + expect(isTextual(undefined, textBuffer)).toBe(true); + expect(isTextual(undefined, textBuffer2)).toBe(true); +}); + test('binary data gets serialized correctly', async () => { const tinyLogoExpected = readFileSync( path.join(__dirname, 'fixtures', 'tiny_logo.png'), diff --git a/desktop/plugins/public/network/utils.tsx b/desktop/plugins/public/network/utils.tsx index 2560bd3f2..6e774dd9c 100644 --- a/desktop/plugins/public/network/utils.tsx +++ b/desktop/plugins/public/network/utils.tsx @@ -28,23 +28,66 @@ export function getHeaderValue( // Matches `application/json` and `application/vnd.api.v42+json` (see https://jsonapi.org/#mime-types) const jsonContentTypeRegex = new RegExp('application/(json|.+\\+json)'); +const binaryContentType = + /^(application\/(zip|octet-stream|pdf))|(video|audio)|(image\/(png|webp|jpeg|gif|avif))$/; -export function isTextual(headers?: Array
): boolean { +export function isTextual( + headers?: Array
, + body?: Uint8Array | string, +): 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') || - jsonContentTypeRegex.test(contentType) || - contentType.startsWith('multipart/') || - contentType.startsWith('message/') || - contentType.startsWith('image/svg') || - contentType.startsWith('application/xhtml+xml') - ); + if (contentType) { + if ( + contentType.startsWith('text/') || + contentType.startsWith('application/x-www-form-urlencoded') || + jsonContentTypeRegex.test(contentType) || + contentType.startsWith('multipart/') || + contentType.startsWith('message/') || + contentType.startsWith('image/svg') || + contentType.startsWith('application/xhtml+xml') || + contentType.startsWith('application/xml') + ) { + return true; + } + if (binaryContentType.test(contentType)) { + return false; + } + } + if ( + (body instanceof Buffer || body instanceof Uint8Array) && + isValidUtf8(body) + ) { + return true; + } + return false; +} + +function isValidUtf8(data: Uint8Array) { + if (data[0] === 0xef && data[1] === 0xbb && data[2] === 0xbf) { + return true; // valid utf8 BOM + } + // From https://weblog.rogueamoeba.com/2017/02/27/javascript-correctly-converting-a-byte-array-to-a-utf-8-string/ + const extraByteMap = [1, 1, 1, 1, 2, 2, 3, 0]; + const count = data.length; + + for (let index = 0; index < count; ) { + let ch = data[index++]; + if (ch & 0x80) { + let extra = extraByteMap[(ch >> 3) & 0x07]; + if (!(ch & 0x40) || !extra || index + extra > count) return false; + + ch &= 0x3f >> extra; + for (; extra > 0; extra -= 1) { + const chx = data[index++]; + if ((chx & 0xc0) != 0x80) return false; + + ch = (ch << 6) | (chx & 0x3f); + } + } + } + return true; } export function decodeBody( @@ -62,7 +105,7 @@ export function decodeBody( // The request is gzipped, so convert the raw bytes back to base64 first. const dataArr = Base64.toUint8Array(data); // then inflate. - return isTextual(headers) + return isTextual(headers, dataArr) ? // pako will detect the BOM headers and return a proper utf-8 string right away pako.inflate(dataArr, {to: 'string'}) : pako.inflate(dataArr); @@ -78,10 +121,11 @@ export function decodeBody( // 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 data directly // - either directly use data (for example) - if (isTextual(headers)) { + const bytes = Base64.toUint8Array(data); + if (isTextual(headers, bytes)) { return Base64.decode(data); } else { - return Base64.toUint8Array(data); + return bytes; } } catch (e) { console.warn(