Add protobuf support to network inspector (#2080)

Summary:
Protobuf based APIs are becoming more common (i.e. gRPC) but are difficult to inspect. Unlike plain text data formats (JSON), Protobuf calls transmit binary data requiring the format to be known ahead of time, making ad-hoc inspection impossible. This PR allows for those format definitions (messages in protobuf terminology) to be transmitted from the client to the network inspector plugin. These definitions are then imported into ProtobufJS which enables the binary data transmitted to be inspected as easily as JSON data.

See Retrofit PR in https://github.com/facebook/flipper/pull/2084

## Changelog

* Add ProtobufJS library to network plugin
* New `ProtobufFormatter` UI in `RequestDetails`
* `ProtobufDefinitionsRepository` to cache and load protobuf defintions
* `addProtobufDefinitions` call in the Android network plugin

Pull Request resolved: https://github.com/facebook/flipper/pull/2080

Test Plan: ![screenshot](https://user-images.githubusercontent.com/745166/111652068-001a5e80-87c4-11eb-8c94-e19b46dd074c.png)

Reviewed By: mweststrate

Differential Revision: D27507451

Pulled By: passy

fbshipit-source-id: 586d891b74f2b17d28fe7a2a99074da755851f38
This commit is contained in:
Harold Martin
2021-04-20 05:08:42 -07:00
committed by Facebook GitHub Bot
parent 451c332260
commit 4d262c0da4
7 changed files with 290 additions and 1 deletions

View File

@@ -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;
};