// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. import { Base64 } from "js-base64"; import * as pako from "pako"; import { Request, Response, Header } from "../networkMessageData"; import { OutputChannelLogger } from "../../log/OutputChannelLogger"; import { notNullOrUndefined } from "../../../common/utils"; import { JSONFormatter } from "./jsonFormatter"; import { ImageFormatter } from "./imageFormatter"; import { GraphQLFormatter } from "./graphQLFormatter"; import { FormUrlencodedFormatter } from "./formUrlencodedFormatter"; export type FormattedBody = string | Record | Array>; export interface IFormatter { formatRequest?: (request: Request, contentType: string) => FormattedBody | null; formatResponse?: (response: Response, contentType: string) => FormattedBody | null; } export class RequestBodyFormatter { protected logger: OutputChannelLogger; private formatters: Array; constructor(logger: OutputChannelLogger) { this.logger = logger; this.formatters = [ new ImageFormatter(), new GraphQLFormatter(this.logger), new JSONFormatter(this.logger), new FormUrlencodedFormatter(this.logger), ]; } public formatBody(container: Request | Response): FormattedBody { const contentType = getHeaderValue(container.headers, "content-type"); for (const formatter of this.formatters) { try { let formattedRes = null; // if container is a response if ((container).status) { if (formatter.formatResponse) { formattedRes = formatter.formatResponse(container, contentType); } } else if (formatter.formatRequest) { formattedRes = formatter.formatRequest(container, contentType); } if (notNullOrUndefined(formattedRes)) { return formattedRes; } } catch (err) { this.logger.debug( `RequestBodyFormatter exception from ${formatter.constructor.name} ${String( (err as Error).message, )}`, ); } } return decodeBody(container, this.logger); } } /** * @preserve * Start region: the code is borrowed from https://github.com/facebook/flipper/blob/v0.79.1/desktop/plugins/network/utils.tsx#L23-L60 * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ export function decodeBody(container: Request | Response, logger?: OutputChannelLogger): string { if (!container.data) { return ""; } try { const isGzip = getHeaderValue(container.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" }); } 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. // In that case, we intentionally fall-through if (!(e as Error).toString().includes("incorrect header check")) { throw e; } } } // 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); } catch (err) { logger?.debug( `Network inspector failed to decode request/response body (size: ${ container.data.length }): ${String((err as Error).toString())}`, ); return ""; } } /** * @preserve * End region: https://github.com/facebook/flipper/blob/v0.79.1/desktop/plugins/network/utils.tsx#L23-L60 */ /** * @preserve * Start region: the code is borrowed from https://github.com/facebook/flipper/blob/v0.79.1/desktop/plugins/network/utils.tsx#L14-L21 * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ export function getHeaderValue(headers: Array
, key: string): string { for (const header of headers) { if (header.key.toLowerCase() === key.toLowerCase()) { return header.value; } } return ""; } /** * @preserve * End region: https://github.com/facebook/flipper/blob/v0.79.1/desktop/plugins/network/utils.tsx#L14-L21 */