diff --git a/android/plugins/network/src/main/java/com/facebook/flipper/plugins/network/NetworkFlipperPlugin.java b/android/plugins/network/src/main/java/com/facebook/flipper/plugins/network/NetworkFlipperPlugin.java
index 35f1db1aa..f2a381800 100644
--- a/android/plugins/network/src/main/java/com/facebook/flipper/plugins/network/NetworkFlipperPlugin.java
+++ b/android/plugins/network/src/main/java/com/facebook/flipper/plugins/network/NetworkFlipperPlugin.java
@@ -127,6 +127,19 @@ public class NetworkFlipperPlugin extends BufferingFlipperPlugin implements Netw
job.run();
}
+ public void addProtobufDefinitions(
+ final String baseUrl, final FlipperArray callNestedMessagesPayloads) {
+ (new ErrorReportingRunnable(getConnection()) {
+ @Override
+ protected void runOrThrow() throws Exception {
+ send(
+ "addProtobufDefinitions",
+ new FlipperObject.Builder().put(baseUrl, callNestedMessagesPayloads).build());
+ }
+ })
+ .run();
+ }
+
private String toBase64(@Nullable byte[] bytes) {
if (bytes == null) {
return null;
diff --git a/desktop/plugins/public/network/ProtobufDefinitionsRepository.tsx b/desktop/plugins/public/network/ProtobufDefinitionsRepository.tsx
new file mode 100644
index 000000000..94c76318a
--- /dev/null
+++ b/desktop/plugins/public/network/ProtobufDefinitionsRepository.tsx
@@ -0,0 +1,93 @@
+/**
+ * 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
+ */
+
+import {ProtobufDefinition} from './types';
+import protobuf, {Type} from 'protobufjs';
+
+export class ProtobufDefinitionsRepository {
+ private static instance: ProtobufDefinitionsRepository;
+ private rawDefinitions: {[path: string]: ProtobufDefinition} = {};
+ private cachedDecodedDefinitions: {
+ [path: string]: DecodedProtobufDefinition;
+ } = {};
+
+ private constructor() {}
+
+ public static getInstance(): ProtobufDefinitionsRepository {
+ if (!ProtobufDefinitionsRepository.instance) {
+ ProtobufDefinitionsRepository.instance = new ProtobufDefinitionsRepository();
+ }
+ return ProtobufDefinitionsRepository.instance;
+ }
+
+ public addDefinitions(baseUrl: string, definitions: ProtobufDefinition[]) {
+ for (const d of definitions) {
+ if (!baseUrl.endsWith('/') && d.path.substr(0, 1) != '/') {
+ this.rawDefinitions[this.key(d.method, baseUrl + '/' + d.path)] = d;
+ } else {
+ this.rawDefinitions[this.key(d.method, baseUrl + d.path)] = d;
+ }
+ }
+ }
+
+ public getResponseType(method: string, path: string): Type | undefined {
+ const key = this.key(method, path);
+ this.generateRoots(key);
+ const messageFullName = this.rawDefinitions[key]?.responseMessageFullName;
+ if (messageFullName) {
+ return this.cachedDecodedDefinitions[key]?.responseRoot?.lookupType(
+ messageFullName,
+ );
+ } else {
+ return undefined;
+ }
+ }
+
+ public getRequestType(method: string, path: string): Type | undefined {
+ const key = this.key(method, path);
+ this.generateRoots(key);
+ const messageFullName = this.rawDefinitions[key]?.requestMessageFullName;
+ if (messageFullName) {
+ return this.cachedDecodedDefinitions[key]?.requestRoot?.lookupType(
+ messageFullName,
+ );
+ } else {
+ return undefined;
+ }
+ }
+
+ private generateRoots(key: string) {
+ if (key in this.cachedDecodedDefinitions) {
+ return;
+ }
+ const rawDefinition = this.rawDefinitions[key];
+ if (rawDefinition === undefined) return;
+
+ let responseRoot = undefined;
+ if (rawDefinition.responseDefinitions) {
+ responseRoot = protobuf.Root.fromJSON(rawDefinition.responseDefinitions);
+ }
+
+ let requestRoot = undefined;
+ if (rawDefinition.requestDefinitions) {
+ requestRoot = protobuf.Root.fromJSON(rawDefinition.requestDefinitions);
+ }
+
+ this.cachedDecodedDefinitions[key] = {responseRoot, requestRoot};
+ }
+
+ private key(method: string, path: string): string {
+ return method + '::' + path.split('?')[0];
+ }
+}
+
+type DecodedProtobufDefinition = {
+ responseRoot: protobuf.Root | undefined;
+ requestRoot: protobuf.Root | undefined;
+};
diff --git a/desktop/plugins/public/network/RequestDetails.tsx b/desktop/plugins/public/network/RequestDetails.tsx
index d43113869..7807ae55f 100644
--- a/desktop/plugins/public/network/RequestDetails.tsx
+++ b/desktop/plugins/public/network/RequestDetails.tsx
@@ -27,6 +27,8 @@ import React from 'react';
import querystring from 'querystring';
import xmlBeautifier from 'xml-beautifier';
+import {ProtobufDefinitionsRepository} from './ProtobufDefinitionsRepository';
+import {Base64} from 'js-base64';
const WrappingText = styled(Text)({
wordWrap: 'break-word',
@@ -804,6 +806,75 @@ class BinaryFormatter {
}
}
+class ProtobufFormatter {
+ private protobufDefinitionRepository = ProtobufDefinitionsRepository.getInstance();
+
+ formatRequest(request: Request) {
+ if (
+ getHeaderValue(request.headers, 'content-type') ===
+ 'application/x-protobuf'
+ ) {
+ const protobufDefinition = this.protobufDefinitionRepository.getRequestType(
+ request.method,
+ request.url,
+ );
+ if (protobufDefinition == undefined) {
+ return (
+
+ Could not locate protobuf definition for request body of{' '}
+ {request.url}
+
+ );
+ }
+
+ if (request?.data) {
+ const data = protobufDefinition.decode(
+ Base64.toUint8Array(request.data),
+ );
+ return {data.toJSON()};
+ } else {
+ return (
+ Could not locate request body data for {request.url}
+ );
+ }
+ }
+ return undefined;
+ }
+
+ formatResponse(request: Request, response: Response) {
+ if (
+ getHeaderValue(response.headers, 'content-type') ===
+ 'application/x-protobuf' ||
+ request.url.endsWith('.proto')
+ ) {
+ const protobufDefinition = this.protobufDefinitionRepository.getResponseType(
+ request.method,
+ request.url,
+ );
+ if (protobufDefinition == undefined) {
+ return (
+
+ Could not locate protobuf definition for response body of{' '}
+ {request.url}
+
+ );
+ }
+
+ if (response?.data) {
+ const data = protobufDefinition.decode(
+ Base64.toUint8Array(response.data),
+ );
+ return {data.toJSON()};
+ } else {
+ return (
+ Could not locate response body data for {request.url}
+ );
+ }
+ }
+ return undefined;
+ }
+}
+
const BodyFormatters: Array = [
new ImageFormatter(),
new VideoFormatter(),
@@ -813,6 +884,7 @@ const BodyFormatters: Array = [
new JSONFormatter(),
new FormUrlencodedFormatter(),
new XMLTextFormatter(),
+ new ProtobufFormatter(),
new BinaryFormatter(),
];
diff --git a/desktop/plugins/public/network/index.tsx b/desktop/plugins/public/network/index.tsx
index 58b25e9bb..f32363a2c 100644
--- a/desktop/plugins/public/network/index.tsx
+++ b/desktop/plugins/public/network/index.tsx
@@ -36,8 +36,10 @@ import {
ResponseFollowupChunk,
Header,
MockRoute,
+ AddProtobufEvent,
PartialResponses,
} from './types';
+import {ProtobufDefinitionsRepository} from './ProtobufDefinitionsRepository';
import {convertRequestToCurlCommand, getHeaderValue, decodeBody} from './utils';
import RequestDetails from './RequestDetails';
import {clipboard} from 'electron';
@@ -68,6 +70,7 @@ type Events = {
newRequest: Request;
newResponse: Response;
partialResponse: Response | ResponseFollowupChunk;
+ addProtobufDefinitions: AddProtobufEvent;
};
type Methods = {
@@ -228,6 +231,13 @@ export function plugin(client: PluginClient) {
});
});
+ client.onMessage('addProtobufDefinitions', (data) => {
+ const repository = ProtobufDefinitionsRepository.getInstance();
+ for (const [baseUrl, definitions] of Object.entries(data)) {
+ repository.addDefinitions(baseUrl, definitions);
+ }
+ });
+
client.onMessage('partialResponse', (data) => {
/* Some clients (such as low end Android devices) struggle to serialise large payloads in one go, so partial responses allow them
to split payloads into chunks and serialise each individually.
@@ -240,7 +250,7 @@ export function plugin(client: PluginClient) {
The remaining chunks will be sent in ResponseFollowupChunks, which each contain another piece of the payload, along with their index from 1 onwards.
The payload of each chunk is individually encoded in the same way that full responses are.
- The order that initialResponse, and followup chunks are recieved is not guaranteed to be in index order.
+ The order that initialResponse, and followup chunks are received is not guaranteed to be in index order.
*/
const message: Response | ResponseFollowupChunk = data as
| Response
diff --git a/desktop/plugins/public/network/package.json b/desktop/plugins/public/network/package.json
index e35eb5b5e..4e0c437f3 100644
--- a/desktop/plugins/public/network/package.json
+++ b/desktop/plugins/public/network/package.json
@@ -19,6 +19,7 @@
"dependencies": {
"lodash": "^4.17.21",
"pako": "^2.0.3",
+ "protobufjs": "^6.10.2",
"xml-beautifier": "^0.4.0"
},
"peerDependencies": {
diff --git a/desktop/plugins/public/network/types.tsx b/desktop/plugins/public/network/types.tsx
index d3fda6a53..4a5ed3710 100644
--- a/desktop/plugins/public/network/types.tsx
+++ b/desktop/plugins/public/network/types.tsx
@@ -7,6 +7,8 @@
* @format
*/
+import {AnyNestedObject} from 'protobufjs';
+
export type RequestId = string;
export type Request = {
@@ -31,6 +33,17 @@ export type Response = {
index?: number;
};
+export type ProtobufDefinition = {
+ path: string;
+ method: string;
+ requestMessageFullName: string | null | undefined;
+ requestDefinitions: {[k: string]: AnyNestedObject} | null | undefined;
+ responseMessageFullName: string | null | undefined;
+ responseDefinitions: {[k: string]: AnyNestedObject} | null | undefined;
+};
+
+export type AddProtobufEvent = {[baseUrl: string]: ProtobufDefinition[]};
+
export type ResponseFollowupChunk = {
id: string;
totalChunks: number;
diff --git a/desktop/plugins/public/yarn.lock b/desktop/plugins/public/yarn.lock
index 4b952bdd6..04ecce5b0 100644
--- a/desktop/plugins/public/yarn.lock
+++ b/desktop/plugins/public/yarn.lock
@@ -56,6 +56,59 @@
"@types/yargs" "^15.0.0"
chalk "^4.0.0"
+"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
+ integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78=
+
+"@protobufjs/base64@^1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
+ integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
+
+"@protobufjs/codegen@^2.0.4":
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
+ integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
+
+"@protobufjs/eventemitter@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
+ integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A=
+
+"@protobufjs/fetch@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
+ integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=
+ dependencies:
+ "@protobufjs/aspromise" "^1.1.1"
+ "@protobufjs/inquire" "^1.1.0"
+
+"@protobufjs/float@^1.0.2":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
+ integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=
+
+"@protobufjs/inquire@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
+ integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=
+
+"@protobufjs/path@^1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
+ integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=
+
+"@protobufjs/pool@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
+ integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=
+
+"@protobufjs/utf8@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
+ integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
+
"@testing-library/dom@^7.28.1":
version "7.29.4"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.29.4.tgz#1647c2b478789621ead7a50614ad81ab5ae5b86c"
@@ -146,11 +199,21 @@
dependencies:
"@types/istanbul-lib-report" "*"
+"@types/long@^4.0.1":
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
+ integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
+
"@types/node@*":
version "14.14.31"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055"
integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==
+"@types/node@^13.7.0":
+ version "13.13.48"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.48.tgz#46a3df718aed5217277f2395a682e055a487e341"
+ integrity sha512-z8wvSsgWQzkr4sVuMEEOvwMdOQjiRY2Y/ZW4fDfjfe3+TfQrZqFKOthBgk2RnVEmtOKrkwdZ7uTvsxTBLjKGDQ==
+
"@types/pako@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/pako/-/pako-1.0.1.tgz#33b237f3c9aff44d0f82fe63acffa4a365ef4a61"
@@ -964,6 +1027,11 @@ lodash@^4.17.21:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+long@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
+ integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
+
loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@@ -1176,6 +1244,25 @@ prop-types@^15.6.2, prop-types@^15.7.2:
object-assign "^4.1.1"
react-is "^16.8.1"
+protobufjs@^6.10.2:
+ version "6.10.2"
+ resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.10.2.tgz#b9cb6bd8ec8f87514592ba3fdfd28e93f33a469b"
+ integrity sha512-27yj+04uF6ya9l+qfpH187aqEzfCF4+Uit0I9ZBQVqK09hk/SQzKa2MUqUpXaVa7LOFRg1TSSr3lVxGOk6c0SQ==
+ dependencies:
+ "@protobufjs/aspromise" "^1.1.2"
+ "@protobufjs/base64" "^1.1.2"
+ "@protobufjs/codegen" "^2.0.4"
+ "@protobufjs/eventemitter" "^1.1.0"
+ "@protobufjs/fetch" "^1.1.0"
+ "@protobufjs/float" "^1.0.2"
+ "@protobufjs/inquire" "^1.1.0"
+ "@protobufjs/path" "^1.1.2"
+ "@protobufjs/pool" "^1.1.0"
+ "@protobufjs/utf8" "^1.1.0"
+ "@types/long" "^4.0.1"
+ "@types/node" "^13.7.0"
+ long "^4.0.0"
+
raf@^3.4.0:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"