Merge pull request #5653 from sanish-bruno/feat/support-grpc-v1-reflection

feat: support v1 reflection for grpc server reflection
This commit is contained in:
Bijin A B
2025-11-13 20:12:26 +05:30
committed by GitHub
3 changed files with 96 additions and 77 deletions

54
package-lock.json generated
View File

@@ -8232,12 +8232,6 @@
"@types/node": "*"
}
},
"node_modules/@types/google-protobuf": {
"version": "3.15.12",
"resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.12.tgz",
"integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==",
"license": "MIT"
},
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@@ -8373,9 +8367,9 @@
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz",
"integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==",
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
"license": "MIT"
},
"node_modules/@types/lodash-es": {
@@ -8388,15 +8382,6 @@
"@types/lodash": "*"
}
},
"node_modules/@types/lodash.set": {
"version": "4.3.9",
"resolved": "https://registry.npmjs.org/@types/lodash.set/-/lodash.set-4.3.9.tgz",
"integrity": "sha512-KOxyNkZpbaggVmqbpr82N2tDVTx05/3/j0f50Es1prxrWB0XYf9p3QNxqcbWb7P1Q9wlvsUSlCFnwlPCIJ46PQ==",
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/markdown-it": {
"version": "12.2.3",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
@@ -15183,9 +15168,9 @@
}
},
"node_modules/google-protobuf": {
"version": "3.21.4",
"resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz",
"integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-4.0.0.tgz",
"integrity": "sha512-b8wmenhUMf2WNL+xIJ/slvD/hEE6V3nRnG86O2bzkBrMweM9gnqZE1dfXlDjibY3aXJXDNbAHepevYyQ7qWKsQ==",
"license": "(BSD-3-Clause AND Apache-2.0)"
},
"node_modules/gopd": {
@@ -15312,20 +15297,18 @@
"node": ">= 6"
}
},
"node_modules/grpc-reflection-js": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/grpc-reflection-js/-/grpc-reflection-js-0.3.0.tgz",
"integrity": "sha512-3lhTlQluPxVgbowCXA3tAZC3RJW+GSOUkguLNYl1QffYRiutUB3RDfPkQFTcrCFJgNiIIxx+iJkr8s3uSp3zWA==",
"node_modules/grpc-js-reflection-client": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/grpc-js-reflection-client/-/grpc-js-reflection-client-1.3.0.tgz",
"integrity": "sha512-eJ5/m1pXpcheSjOGExktU69WPUKnL4Su3IxGJYYYjy3/w19vE8dH7Wi46G5T92bpM0eZWftjiM5HduX8CjPq9w==",
"license": "MIT",
"dependencies": {
"@types/google-protobuf": "^3.7.2",
"@types/lodash.set": "^4.3.6",
"google-protobuf": "^3.12.2",
"lodash.set": "^4.3.2",
"protobufjs": "^7.2.2"
"@types/lodash": "^4.17.15",
"lodash": "^4.17.21",
"protobufjs": "^7.4.0"
},
"peerDependencies": {
"@grpc/grpc-js": "^1.0.0"
"@grpc/grpc-js": "^1.12.6"
}
},
"node_modules/har-schema": {
@@ -18634,12 +18617,6 @@
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/lodash.set": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
"integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==",
"license": "MIT"
},
"node_modules/lodash.uniq": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
@@ -32129,7 +32106,8 @@
"@types/qs": "^6.9.18",
"axios": "^1.9.0",
"debug": "^4.4.3",
"grpc-reflection-js": "^0.3.0",
"google-protobuf": "^4.0.0",
"grpc-js-reflection-client": "^1.3.0",
"is-ip": "^5.0.1",
"system-ca": "^2.0.1",
"tough-cookie": "^6.0.0",

View File

@@ -26,11 +26,12 @@
"@types/qs": "^6.9.18",
"axios": "^1.9.0",
"debug": "^4.4.3",
"grpc-reflection-js": "^0.3.0",
"google-protobuf": "^4.0.0",
"grpc-js-reflection-client": "^1.3.0",
"is-ip": "^5.0.1",
"ws": "^8.18.3",
"system-ca": "^2.0.1",
"tough-cookie": "^6.0.0"
"tough-cookie": "^6.0.0",
"ws": "^8.18.3"
},
"devDependencies": {
"@babel/preset-env": "^7.22.0",

View File

@@ -1,5 +1,5 @@
import { makeGenericClientConstructor, ChannelCredentials, Metadata } from '@grpc/grpc-js';
import * as grpcReflection from 'grpc-reflection-js';
import { makeGenericClientConstructor, ChannelCredentials, Metadata, status, credentials, CallCredentials } from '@grpc/grpc-js';
import { GrpcReflection } from 'grpc-js-reflection-client';
import * as protoLoader from '@grpc/proto-loader';
import { generateGrpcSampleMessage } from './grpcMessageGenerator';
import * as tls from 'tls';
@@ -34,6 +34,8 @@ const configOptions = {
json: true
};
const reflectionServices = ['grpc.reflection.v1alpha.ServerReflection', 'grpc.reflection.v1.ServerReflection'];
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
if (!str || !str.length || !isString(str)) {
return '';
@@ -172,6 +174,51 @@ class GrpcClient {
this.eventCallback = eventCallback;
}
/**
* Creates call options from metadata for gRPC calls
* @param {grpc.Metadata} metadata - metadata to be sent with calls
* @returns {Object} callOptions object with credentials if metadata is provided
*/
#createCallOptions(metadata) {
if (metadata && Object.keys(metadata.getMap()).length > 0) {
// Create CallCredentials from metadata generator
const callCredentials = CallCredentials.createFromMetadataGenerator((options, callback) => {
callback(null, metadata);
});
return { credentials: callCredentials };
}
return {};
}
/**
* Creates a reflection client that works for v1, v1alpha, or both.
*
* @param {string} host - host:port of the gRPC server
* @param {grpc.ChannelCredentials} credentials - defaults to insecure
* @param {grpc.Metadata} metadata - metadata to be sent with reflection calls (used for insecure connections where credentials can't include metadata)
* @param {grpc.ChannelOptions} options - channel options
* @returns {Promise<{ client: GrpcReflection, services: string[], callOptions: Object }>}
*/
async #getReflectionClient(host, credentials = ChannelCredentials.createInsecure(), metadata = null, options = {}) {
const makeClient = (version) => new GrpcReflection(host, credentials, options, version);
const callOptions = this.#createCallOptions(metadata);
let client;
let services;
try {
client = makeClient('v1');
services = await client.listServices('*', callOptions);
return { client, services, callOptions };
} catch (e) {
console.warn(`gRPC reflection v1 failed:`, e);
}
client = makeClient('v1alpha');
services = await client.listServices('*', callOptions);
return { client, services, callOptions };
}
/**
* Get method type based on streaming configuration
*/
@@ -241,9 +288,7 @@ class GrpcClient {
return ChannelCredentials.createFromSecureContext(secureContext, sslOptions);
}
const credentials = ChannelCredentials.createSsl(rootCertBuffer, privateKeyBuffer, clientCertBuffer, sslOptions);
return credentials;
return ChannelCredentials.createSsl(rootCertBuffer, privateKeyBuffer, clientCertBuffer, sslOptions);
} catch (error) {
console.error('Error creating channel credentials:', error);
// Default to insecure as fallback
@@ -566,6 +611,11 @@ class GrpcClient {
verifyOptions,
sendEvent
}) {
const { host, path } = getParsedGrpcUrlObject(request.url);
const metadata = new Metadata();
Object.entries(request.headers).forEach(([name, value]) => {
metadata.add(name, value);
});
const credentials = this.#getChannelCredentials({
url: request.url,
rootCertificate,
@@ -575,38 +625,31 @@ class GrpcClient {
pfx,
verifyOptions
});
const { host, path } = getParsedGrpcUrlObject(request.url);
const metadata = new Metadata();
Object.entries(request.headers).forEach(([name, value]) => {
metadata.add(name, value);
});
try {
const client = new grpcReflection.Client(host, credentials, {}, metadata);
const { client, services, callOptions } = await this.#getReflectionClient(host, credentials, metadata, {});
const declarations = await client.listServices();
const methods = await Promise.all(
declarations.map(async (declaration) => {
const fileContainingSymbol = await client.fileContainingSymbol(declaration);
const descriptor = fileContainingSymbol.toDescriptor('proto3');
const protoDefinition = protoLoader.loadFileDescriptorSetFromObject(descriptor, configOptions);
const methods = [];
for (const service of services) {
if (reflectionServices.includes(service)) {
continue;
}
const m = await client.listMethods(service, callOptions);
methods.push(...m);
}
const serviceDefinition = protoDefinition[declaration];
if (!!serviceDefinition?.format) {
return [];
}
const methods = Object.values(serviceDefinition);
methods.forEach((method) => {
this.methods.set(method.path, method);
});
return methods;
})
);
const methodsWithType = methods.flat().map((method) => ({
...method,
type: this.#getMethodType(method)
}));
const methodsWithType = methods.map((method) => {
const { definition, ...rest } = method;
const modifiedMethod = {
...rest,
...definition
};
modifiedMethod.type = this.#getMethodType(modifiedMethod);
return modifiedMethod;
});
methodsWithType.forEach((method) => {
this.methods.set(method.path, method);
});
return methodsWithType;
} catch (error) {
console.error('Error in gRPC reflection:', error);
@@ -615,9 +658,6 @@ class GrpcClient {
}
}
/**
* Load methods from proto file
*/
async loadMethodsFromProtoFile(filePath, includeDirs = []) {
const protoDefinition = await protoLoader.load(filePath, { ...configOptions, includeDirs });
const methods = Object.values(protoDefinition)