diff --git a/package-lock.json b/package-lock.json index decb6c7cc..5a2b2a8e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/packages/bruno-requests/package.json b/packages/bruno-requests/package.json index 1d54c53e7..76d9749f8 100644 --- a/packages/bruno-requests/package.json +++ b/packages/bruno-requests/package.json @@ -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", diff --git a/packages/bruno-requests/src/grpc/grpc-client.js b/packages/bruno-requests/src/grpc/grpc-client.js index ab4c6cac1..55ecb5762 100644 --- a/packages/bruno-requests/src/grpc/grpc-client.js +++ b/packages/bruno-requests/src/grpc/grpc-client.js @@ -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)