From 1bb9607c398453c89f75c3db4aef05363a86fc6b Mon Sep 17 00:00:00 2001 From: Jack Kavanagh Date: Wed, 14 Dec 2022 13:27:39 +0100 Subject: [PATCH] gRPC server reflection support (#5518) * eliminate grpc paths * add fake reflection ux * add grpc packages * basic ux * first working pass * package lock * reset selected protofile * fix types * ssl * improve naming * remove deprecated grpc url parse * remove broken test * replace grpc proto PR test with reflection --- .../insomnia-smoke-test/fixtures/grpc.yaml | 19 ++ .../insomnia-smoke-test/package-lock.json | 2 +- .../tests/smoke/grpc.test.ts | 10 +- packages/insomnia/package-lock.json | 198 +++++++++++------- packages/insomnia/package.json | 6 +- .../src/common/__tests__/grpc-paths.test.ts | 69 ------ packages/insomnia/src/common/grpc-paths.ts | 76 ------- packages/insomnia/src/main/ipc/grpc.ts | 131 +++++++++--- .../src/network/grpc/__tests__/method.test.ts | 59 ------ .../src/network/grpc/parse-grpc-url.ts | 19 +- packages/insomnia/src/preload.ts | 1 + .../grpc-method-dropdown.tsx | 48 ++++- .../ui/components/panes/grpc-request-pane.tsx | 17 +- packages/insomnia/src/ui/routes/debug.tsx | 2 +- 14 files changed, 319 insertions(+), 338 deletions(-) delete mode 100644 packages/insomnia/src/common/__tests__/grpc-paths.test.ts delete mode 100644 packages/insomnia/src/common/grpc-paths.ts delete mode 100644 packages/insomnia/src/network/grpc/__tests__/method.test.ts diff --git a/packages/insomnia-smoke-test/fixtures/grpc.yaml b/packages/insomnia-smoke-test/fixtures/grpc.yaml index d78234e5e..181e49123 100644 --- a/packages/insomnia-smoke-test/fixtures/grpc.yaml +++ b/packages/insomnia-smoke-test/fixtures/grpc.yaml @@ -3,6 +3,25 @@ __export_format: 4 __export_date: 2022-12-02T13:10:54.407Z __export_source: insomnia.desktop.app:v2022.7.0-beta.6 resources: + - _id: greq_70ceba59d634442891ee523389458a9c + parentId: fld_9f284a1171b84055959343e151fc6ce2 + modified: 1669986544274 + created: 1638200268776 + url: localhost:50051 + name: UnaryWithOutProtoFile + description: "" + protoFileId: "" + protoMethodName: "" + metadata: [] + body: + text: |- + { + "latitude":"409146138", + "longitude":"-746188906" + } + metaSortKey: -1638200268776 + isPrivate: false + _type: grpc_request - _id: greq_70ceba59d634442891ee523389458a8c parentId: fld_9f284a1171b84055959343e151fc6ce2 modified: 1669986544274 diff --git a/packages/insomnia-smoke-test/package-lock.json b/packages/insomnia-smoke-test/package-lock.json index 458bbd976..0f86b8ea3 100644 --- a/packages/insomnia-smoke-test/package-lock.json +++ b/packages/insomnia-smoke-test/package-lock.json @@ -13,7 +13,7 @@ "@grpc/proto-loader": "^0.6.11", "@jest/globals": "^28.1.0", "@playwright/test": "^1.26.1", - "@ravanallc/grpc-server-reflection": "0.1.6", + "@ravanallc/grpc-server-reflection": "^0.1.6", "@types/concurrently": "^6.0.1", "@types/express": "^4.17.11", "@types/express-graphql": "^0.9.0", diff --git a/packages/insomnia-smoke-test/tests/smoke/grpc.test.ts b/packages/insomnia-smoke-test/tests/smoke/grpc.test.ts index d9e906f92..09ea6ba1c 100644 --- a/packages/insomnia-smoke-test/tests/smoke/grpc.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/grpc.test.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test'; import { loadFixture } from '../../playwright/paths'; import { test } from '../../playwright/test'; -test('can send gRPC requests', async ({ app, page }) => { +test('can send gRPC requests with reflection', async ({ app, page }) => { test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms'); const statusTag = page.locator('[data-testid="response-status-tag"]:visible'); const responseBody = page.locator('[data-testid="response-pane"] >> [data-testid="CodeEditor"]:visible', { @@ -20,7 +20,13 @@ test('can send gRPC requests', async ({ app, page }) => { await page.click('div[role="dialog"] button:has-text("New")'); await page.click('text=CollectionPreRelease gRPCjust now'); await page.locator('button:has-text("Route Guide ExampleOPEN")').click(); - await page.click('button:has-text("gRPCUnary")'); + + await page.click('button:has-text("gRPCUnaryWithOutProtoFile")'); + await page.click('button:has-text("Select Method")'); + await page.click('button:has-text("Click to use server reflection")'); + await page.click('button:has-text("Select Method")'); + await page.click('button:has-text("U /RouteGuide/GetFeature")'); + await page.locator('[data-testid="request-pane"] >> text=Unary').click(); await page.click('text=Send'); diff --git a/packages/insomnia/package-lock.json b/packages/insomnia/package-lock.json index 62043188b..3f9090df6 100644 --- a/packages/insomnia/package-lock.json +++ b/packages/insomnia/package-lock.json @@ -13,7 +13,7 @@ "@apidevtools/swagger-parser": "10.1.0", "@getinsomnia/node-libcurl": "2.3.5-7", "@grpc/grpc-js": "^1.1.8", - "@grpc/proto-loader": "^0.5.5", + "@grpc/proto-loader": "^0.7.4", "@jest/globals": "^28.1.0", "@sentry/electron": "^3.0.7", "@stoplight/spectral-core": "^1.12.2", @@ -32,6 +32,7 @@ "electron-context-menu": "^3.1.1", "electron-log": "^4.4.3", "fs-extra": "^5.0.0", + "grpc-reflection-js": "0.1.2", "hawk": "9.0.1", "hkdf": "^0.0.2", "html-entities": "^1.2.0", @@ -1979,12 +1980,18 @@ } }, "node_modules/@grpc/proto-loader": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz", - "integrity": "sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", "dependencies": { + "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", - "protobufjs": "^6.8.6" + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" }, "engines": { "node": ">=6" @@ -3481,7 +3488,7 @@ "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", @@ -3496,12 +3503,12 @@ "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -3510,27 +3517,27 @@ "node_modules/@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, "node_modules/@react-aria/breadcrumbs": { "version": "3.3.1", @@ -6358,6 +6365,11 @@ "@types/node": "*" } }, + "node_modules/@types/google-protobuf": { + "version": "3.15.6", + "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.6.tgz", + "integrity": "sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==" + }, "node_modules/@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -6477,8 +6489,15 @@ "node_modules/@types/lodash": { "version": "4.14.182", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", - "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", - "dev": true + "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==" + }, + "node_modules/@types/lodash.set": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@types/lodash.set/-/lodash.set-4.3.7.tgz", + "integrity": "sha512-bS5Wkg/nrT82YUfkNYPSccFrNZRL+irl7Yt4iM6OTSQ0VZJED2oUIVm15NkNtUAQ8SRhCe+axqERUV6MJgkeEg==", + "dependencies": { + "@types/lodash": "*" + } }, "node_modules/@types/long": { "version": "4.0.1", @@ -8756,7 +8775,6 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -12129,6 +12147,11 @@ "node": ">= 6.0.0" } }, + "node_modules/google-protobuf": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", + "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==" + }, "node_modules/got": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", @@ -12326,6 +12349,21 @@ "graphql": ">=0.11 <=16" } }, + "node_modules/grpc-reflection-js": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/grpc-reflection-js/-/grpc-reflection-js-0.1.2.tgz", + "integrity": "sha512-ecLGJDCoRRyTNQ2jQYLsMQ0H9a/ZZ/6brU+6haGPS8GpQ93uYoFOc60ptO3bebS3xRPJQSIiSZClkBS5L9PYuw==", + "dependencies": { + "@types/google-protobuf": "^3.7.2", + "@types/lodash.set": "^4.3.6", + "google-protobuf": "^3.12.2", + "lodash.set": "^4.3.2", + "protobufjs": "^6.9.0" + }, + "peerDependencies": { + "@grpc/grpc-js": "^1.0.0" + } + }, "node_modules/gtoken": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.0.5.tgz", @@ -18553,9 +18591,9 @@ } }, "node_modules/protobufjs": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.1.tgz", - "integrity": "sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -18568,19 +18606,17 @@ "@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" + "@types/node": ">=13.7.0", + "long": "^5.0.0" }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" + "engines": { + "node": ">=12.0.0" } }, - "node_modules/protobufjs/node_modules/@types/node": { - "version": "13.13.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.30.tgz", - "integrity": "sha512-HmqFpNzp3TSELxU/bUuRK+xzarVOAsR00hzcvM0TXrMlt/+wcSLa5q6YhTb6/cA6wqDCZLDcfd8fSL95x5h7AA==" + "node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" }, "node_modules/prr": { "version": "1.0.1", @@ -21471,7 +21507,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -21488,7 +21523,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -21503,7 +21537,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -21514,8 +21547,7 @@ "node_modules/wrap-ansi/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/wrappy": { "version": "1.0.2", @@ -21676,7 +21708,6 @@ "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -21694,7 +21725,6 @@ "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, "engines": { "node": ">=10" } @@ -21703,7 +21733,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -23216,7 +23245,7 @@ "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", "long": "^4.0.0", - "protobufjs": "^6.9.0", + "protobufjs": "7.1.2", "yargs": "^15.3.1" } }, @@ -23301,12 +23330,15 @@ } }, "@grpc/proto-loader": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz", - "integrity": "sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", "requires": { + "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", - "protobufjs": "^6.8.6" + "long": "^4.0.0", + "protobufjs": "7.1.2", + "yargs": "^16.2.0" } }, "@hapi/b64": { @@ -24466,7 +24498,7 @@ "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" }, "@protobufjs/base64": { "version": "1.1.2", @@ -24481,12 +24513,12 @@ "@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" }, "@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "requires": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -24495,27 +24527,27 @@ "@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" }, "@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" }, "@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" }, "@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" }, "@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, "@react-aria/breadcrumbs": { "version": "3.3.1", @@ -26824,6 +26856,11 @@ "@types/node": "*" } }, + "@types/google-protobuf": { + "version": "3.15.6", + "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.6.tgz", + "integrity": "sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==" + }, "@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -26943,8 +26980,15 @@ "@types/lodash": { "version": "4.14.182", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", - "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", - "dev": true + "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==" + }, + "@types/lodash.set": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@types/lodash.set/-/lodash.set-4.3.7.tgz", + "integrity": "sha512-bS5Wkg/nrT82YUfkNYPSccFrNZRL+irl7Yt4iM6OTSQ0VZJED2oUIVm15NkNtUAQ8SRhCe+axqERUV6MJgkeEg==", + "requires": { + "@types/lodash": "*" + } }, "@types/long": { "version": "4.0.1", @@ -28759,7 +28803,6 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, "requires": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -31340,6 +31383,11 @@ } } }, + "google-protobuf": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", + "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==" + }, "got": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", @@ -31492,6 +31540,18 @@ "dev": true, "requires": {} }, + "grpc-reflection-js": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/grpc-reflection-js/-/grpc-reflection-js-0.1.2.tgz", + "integrity": "sha512-ecLGJDCoRRyTNQ2jQYLsMQ0H9a/ZZ/6brU+6haGPS8GpQ93uYoFOc60ptO3bebS3xRPJQSIiSZClkBS5L9PYuw==", + "requires": { + "@types/google-protobuf": "^3.7.2", + "@types/lodash.set": "^4.3.6", + "google-protobuf": "^3.12.2", + "lodash.set": "^4.3.2", + "protobufjs": "7.1.2" + } + }, "gtoken": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.0.5.tgz", @@ -36279,9 +36339,9 @@ } }, "protobufjs": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.1.tgz", - "integrity": "sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -36293,15 +36353,14 @@ "@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" + "@types/node": ">=13.7.0", + "long": "^5.0.0" }, "dependencies": { - "@types/node": { - "version": "13.13.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.30.tgz", - "integrity": "sha512-HmqFpNzp3TSELxU/bUuRK+xzarVOAsR00hzcvM0TXrMlt/+wcSLa5q6YhTb6/cA6wqDCZLDcfd8fSL95x5h7AA==" + "long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" } } }, @@ -38568,7 +38627,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -38579,7 +38637,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -38588,7 +38645,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -38596,8 +38652,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" } } }, @@ -38719,7 +38774,6 @@ "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, "requires": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -38733,16 +38787,14 @@ "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" } } }, "yargs-parser": { "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" }, "yauzl": { "version": "2.10.0", diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json index a5b7002e4..04a7aa84a 100644 --- a/packages/insomnia/package.json +++ b/packages/insomnia/package.json @@ -47,7 +47,7 @@ "@apidevtools/swagger-parser": "10.1.0", "@getinsomnia/node-libcurl": "2.3.5-7", "@grpc/grpc-js": "^1.1.8", - "@grpc/proto-loader": "^0.5.5", + "@grpc/proto-loader": "^0.7.4", "@jest/globals": "^28.1.0", "@sentry/electron": "^3.0.7", "@stoplight/spectral-core": "^1.12.2", @@ -66,6 +66,7 @@ "electron-context-menu": "^3.1.1", "electron-log": "^4.4.3", "fs-extra": "^5.0.0", + "grpc-reflection-js": "0.1.2", "hawk": "9.0.1", "hkdf": "^0.0.2", "html-entities": "^1.2.0", @@ -212,5 +213,8 @@ }, "dev": { "dev-server-port": 3334 + }, + "overrides": { + "protobufjs": "7.1.2" } } diff --git a/packages/insomnia/src/common/__tests__/grpc-paths.test.ts b/packages/insomnia/src/common/__tests__/grpc-paths.test.ts deleted file mode 100644 index b526d190e..000000000 --- a/packages/insomnia/src/common/__tests__/grpc-paths.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; - -import { - getGrpcPathSegments, - getShortGrpcPath, -} from '../grpc-paths'; - -describe('getGrpcPathSegments', () => { - it.each([ - ['package', 'service', 'method'], - ['nested.package', 'service', 'method'], - ['another.nested.package', 'service', 'method'], - ['.another.package', 'service', 'method'], - ])( - 'should extract package, service and method from "/%s.%s/%s"', - (packageName, serviceName, methodName) => { - expect(getGrpcPathSegments(`/${packageName}.${serviceName}/${methodName}`)).toStrictEqual({ - packageName, - serviceName, - methodName, - }); - }, - ); - - it.each([['service', 'method']])( - 'should extract service and method from "/%s/%s"', - (serviceName, methodName) => { - expect(getGrpcPathSegments(`/${serviceName}/${methodName}`)).toStrictEqual({ - packageName: undefined, - serviceName, - methodName, - }); - }, - ); -}); - -describe('getShortGrpcPath', () => { - it('should return shortened path', () => { - const packageName = 'package'; - const serviceName = 'service'; - const methodName = 'method'; - const fullPath = '/package.service/method'; - const shortPath = getShortGrpcPath( - { - packageName, - serviceName, - methodName, - }, - fullPath, - ); - expect(shortPath).toBe('/service/method'); - }); - - it('should return full path', () => { - const packageName = undefined; - const serviceName = 'service'; - const methodName = 'method'; - const fullPath = '/service/method'; - const shortPath = getShortGrpcPath( - { - packageName, - serviceName, - methodName, - }, - fullPath, - ); - expect(shortPath).toBe(fullPath); - }); -}); diff --git a/packages/insomnia/src/common/grpc-paths.ts b/packages/insomnia/src/common/grpc-paths.ts deleted file mode 100644 index 00f19d706..000000000 --- a/packages/insomnia/src/common/grpc-paths.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { MethodDefinition } from '@grpc/grpc-js'; - -import { GrpcMethodType } from '../main/ipc/grpc'; - -const PROTO_PATH_REGEX = /^\/(?:(?[\w.]+)\.)?(?\w+)\/(?\w+)$/; - -interface GrpcPathSegments { - packageName?: string; - serviceName?: string; - methodName?: string; -} - -// Split a full gRPC path into it's segments -export const getGrpcPathSegments = (path: string): GrpcPathSegments => { - const result = PROTO_PATH_REGEX.exec(path); - const packageName = result?.groups?.package; - const serviceName = result?.groups?.service; - const methodName = result?.groups?.method; - return { - packageName, - serviceName, - methodName, - }; -}; - -// If all segments are found, return a shorter path, otherwise the original path -export const getShortGrpcPath = ( - { packageName, serviceName, methodName }: GrpcPathSegments, - fullPath: string, -): string => { - return packageName && serviceName && methodName ? `/${serviceName}/${methodName}` : fullPath; -}; - -export interface GrpcMethodInfo { - segments: GrpcPathSegments; - type: GrpcMethodType; - fullPath: string; -} -export const getMethodType = ({ requestStream, responseStream }: MethodDefinition): GrpcMethodType => { - if (requestStream && responseStream) { - return 'bidi'; - } - if (requestStream) { - return 'client'; - } - if (responseStream) { - return 'server'; - } - return 'unary'; -}; - -export const getMethodInfo = (method: MethodDefinition): GrpcMethodInfo => ({ - segments: getGrpcPathSegments(method.path), - type: getMethodType(method), - fullPath: method.path, -}); - -export const NO_PACKAGE_KEY = 'no-package'; - -function groupBy(list: {}[], keyGetter: (item: any) => string):Record { - const map = new Map(); - list.forEach(item => { - const key = keyGetter(item); - const collection = map.get(key); - if (!collection) { - map.set(key, [item]); - } else { - collection.push(item); - } - }); - return Object.fromEntries(map); -} - -export const groupGrpcMethodsByPackage = (methodInfoList: GrpcMethodInfo[]): Record => { - return groupBy(methodInfoList, ({ segments }) => segments.packageName || NO_PACKAGE_KEY); -}; diff --git a/packages/insomnia/src/main/ipc/grpc.ts b/packages/insomnia/src/main/ipc/grpc.ts index 29febb8c2..9c0169e7f 100644 --- a/packages/insomnia/src/main/ipc/grpc.ts +++ b/packages/insomnia/src/main/ipc/grpc.ts @@ -1,10 +1,11 @@ -import { Call, ClientDuplexStream, ClientReadableStream, MethodDefinition, ServiceError, StatusObject } from '@grpc/grpc-js'; +import { Call, ClientDuplexStream, ClientReadableStream, ServiceError, StatusObject } from '@grpc/grpc-js'; import { credentials, makeGenericClientConstructor, Metadata, status } from '@grpc/grpc-js'; -import { AnyDefinition, EnumTypeDefinition, load, MessageTypeDefinition } from '@grpc/proto-loader'; +import * as protoLoader from '@grpc/proto-loader'; +import { AnyDefinition, EnumTypeDefinition, MessageTypeDefinition, PackageDefinition } from '@grpc/proto-loader'; import electron, { ipcMain } from 'electron'; import { IpcMainEvent } from 'electron'; +import * as grpcReflection from 'grpc-reflection-js'; -import { getMethodInfo, getMethodType, GrpcMethodInfo } from '../../common/grpc-paths'; import type { RenderedGrpcRequest, RenderedGrpcRequestBody } from '../../common/render'; import * as models from '../../models'; import type { GrpcRequest, GrpcRequestHeader } from '../../models/grpc-request'; @@ -28,6 +29,7 @@ export interface gRPCBridgeAPI { commit: typeof commit; cancel: typeof cancel; loadMethods: typeof loadMethods; + loadMethodsFromReflection: typeof loadMethodsFromReflection; closeAll: typeof closeAll; } export function registergRPCHandlers() { @@ -37,42 +39,107 @@ export function registergRPCHandlers() { ipcMain.on('grpc.cancel', (_, requestId) => cancel(requestId)); ipcMain.on('grpc.closeAll', closeAll); ipcMain.handle('grpc.loadMethods', (_, requestId) => loadMethods(requestId)); + ipcMain.handle('grpc.loadMethodsFromReflection', (_, url) => loadMethodsFromReflection(url)); } +const loadMethodsFromFilePath = async (filePath: string, includeDirs: string[]): Promise => { + try { + const definition = await protoLoader.load(filePath, { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + includeDirs, + }); + return getMethodsFromPackageDefinition(definition); + } catch (error) { + throw error; + } +}; const loadMethods = async (protoFileId: string): Promise => { const protoFile = await models.protoFile.getById(protoFileId); invariant(protoFile, `Proto file ${protoFileId} not found`); const { filePath, dirs } = await writeProtoFile(protoFile); - const definition = await load(filePath, { - keepCase: true, - longs: String, - enums: String, - defaults: true, - oneofs: true, - includeDirs: dirs, - }); - const methods = Object.values(definition).filter((obj: AnyDefinition): obj is EnumTypeDefinition | MessageTypeDefinition => !obj.format).flatMap(Object.values); - return methods.map(getMethodInfo); + const methods = await loadMethodsFromFilePath(filePath, dirs); + return methods.map(method => ({ + type: getMethodType(method), + fullPath: method.path, + })); +}; +interface MethodDefs { + path: string; + requestStream: boolean; + responseStream: boolean; + requestSerialize: (value: any) => Buffer; + responseDeserialize: (value: Buffer) => any; +} +const getMethodsFromReflection = async (host: string): Promise => { + try { + const { url, enableTls } = parseGrpcUrl(host); + const client = new grpcReflection.Client(url, enableTls ? credentials.createSsl() : credentials.createInsecure()); + const services = await client.listServices() as string[]; + const methodsPromises = services.map(async service => { + const fileContainingSymbol = await client.fileContainingSymbol(service); + const descriptorMessage = fileContainingSymbol.toDescriptor('proto3'); + const tryToGetMethods = () => { + try { + console.log('[grpc] loading service from reflection:', service); + const packageDefinition = protoLoader.loadFileDescriptorSetFromObject(descriptorMessage, {}); + return getMethodsFromPackageDefinition(packageDefinition); + } catch (e) { + console.error(e); + return []; + } + }; + const methods = tryToGetMethods(); + return methods; + }); + return (await Promise.all(methodsPromises)).flat(); + } catch (error) { + throw error; + } +}; +const loadMethodsFromReflection = async (url: string): Promise => { + const methods = await getMethodsFromReflection(url); + return methods.map(method => ({ + type: getMethodType(method), + fullPath: method.path, + })); +}; +export interface GrpcMethodInfo { + type: GrpcMethodType; + fullPath: string; +} +export const getMethodType = ({ requestStream, responseStream }: any): GrpcMethodType => { + if (requestStream && responseStream) { + return 'bidi'; + } + if (requestStream) { + return 'client'; + } + if (responseStream) { + return 'server'; + } + return 'unary'; }; -// TODO: instead of reloading the methods from the protoFile, -// just get it from what has already been loaded in the react component, -// or from the cache -// We can't send the method over IPC because of the following deprecation in Electron v9 -// https://www.electronjs.org/docs/breaking-changes#behavior-changed-sending-non-js-objects-over-ipc-now-throws-an-exception -export const getSelectedMethod = async (request: GrpcRequest): Promise | undefined> => { - invariant(request.protoFileId, 'protoFileId is required'); - const protoFile = await models.protoFile.getById(request.protoFileId); - invariant(protoFile?.protoText, `No proto file found for gRPC request ${request._id}`); - const { filePath, dirs } = await writeProtoFile(protoFile); - const definition = await load(filePath, { - keepCase: true, - longs: String, - enums: String, - defaults: true, - oneofs: true, - includeDirs: dirs, - }); - return Object.values(definition).filter((obj: AnyDefinition): obj is EnumTypeDefinition | MessageTypeDefinition => !obj.format).flatMap(Object.values).find(c => c.path === request.protoMethodName); +export const getSelectedMethod = async (request: GrpcRequest): Promise => { + if (request.protoFileId) { + const protoFile = await models.protoFile.getById(request.protoFileId); + invariant(protoFile?.protoText, `No proto file found for gRPC request ${request._id}`); + const { filePath, dirs } = await writeProtoFile(protoFile); + const methods = await loadMethodsFromFilePath(filePath, dirs); + invariant(methods, 'No methods found'); + return methods.find(c => c.path === request.protoMethodName); + } + const methods = await getMethodsFromReflection(request.url); + invariant(methods, 'No reflection methods found'); + return methods.find(c => c.path === request.protoMethodName); +}; +export const getMethodsFromPackageDefinition = (packageDefinition: PackageDefinition): MethodDefs[] => { + return Object.values(packageDefinition) + .filter((obj: AnyDefinition): obj is EnumTypeDefinition | MessageTypeDefinition => !obj.format) + .flatMap(Object.values); }; export const start = ( diff --git a/packages/insomnia/src/network/grpc/__tests__/method.test.ts b/packages/insomnia/src/network/grpc/__tests__/method.test.ts deleted file mode 100644 index 918f13d83..000000000 --- a/packages/insomnia/src/network/grpc/__tests__/method.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; - -import { getMethodType } from '../../../common/grpc-paths'; -import { GrpcMethodType } from '../../../main/ipc/grpc'; -import { canClientStream } from '../../../ui/components/panes/grpc-request-pane'; - -describe('getMethodType', () => { - it('should return unary', () => { - expect( - getMethodType({ - requestStream: false, - responseStream: false, - }), - ).toBe('unary'); - }); - - it('should return server', () => { - expect( - getMethodType({ - requestStream: false, - responseStream: true, - }), - ).toBe('server'); - }); - - it('should return client', () => { - expect( - getMethodType({ - requestStream: true, - responseStream: false, - }), - ).toBe('client'); - }); - - it('should return bidi', () => { - expect( - getMethodType({ - requestStream: true, - responseStream: true, - }), - ).toBe('bidi'); - }); -}); - -describe('canClientStream', () => { - it.each([ - 'unary', - 'server', - ])('should not support client streaming with %o', (type: GrpcMethodType) => - expect(canClientStream(type)).toBe(false), - ); - - it.each([ - 'client', - 'bidi', - ])('should support client streaming with %o', (type: GrpcMethodType) => - expect(canClientStream(type)).toBe(true), - ); -}); diff --git a/packages/insomnia/src/network/grpc/parse-grpc-url.ts b/packages/insomnia/src/network/grpc/parse-grpc-url.ts index 5254293ed..67765e5b5 100644 --- a/packages/insomnia/src/network/grpc/parse-grpc-url.ts +++ b/packages/insomnia/src/network/grpc/parse-grpc-url.ts @@ -1,12 +1,13 @@ -import { parse as urlParse } from 'url'; - -export const parseGrpcUrl = (grpcUrl: string) => { - const { protocol, host, href } = urlParse(grpcUrl?.toLowerCase() || ''); - if (protocol === 'grpcs:') { - return { url: host, enableTls: true }; +export const parseGrpcUrl = (grpcUrl: string): { url: string; enableTls: boolean } => { + if (!grpcUrl) { + return { url: '', enableTls: false }; } - if (protocol === 'grpc:') { - return { url: host, enableTls: false }; + const lower = grpcUrl.toLowerCase(); + if (lower.startsWith('grpc://')) { + return { url: lower.slice(7), enableTls: false }; } - return { url: href, enableTls: false }; + if (lower.startsWith('grpcs://')) { + return { url: lower.slice(8), enableTls: true }; + } + return { url: lower, enableTls: false }; }; diff --git a/packages/insomnia/src/preload.ts b/packages/insomnia/src/preload.ts index d4b9e2a0d..30d105b74 100644 --- a/packages/insomnia/src/preload.ts +++ b/packages/insomnia/src/preload.ts @@ -22,6 +22,7 @@ const grpc: gRPCBridgeAPI = { cancel: options => ipcRenderer.send('grpc.cancel', options), closeAll: () => ipcRenderer.send('grpc.closeAll'), loadMethods: options => ipcRenderer.invoke('grpc.loadMethods', options), + loadMethodsFromReflection: options => ipcRenderer.invoke('grpc.loadMethodsFromReflection', options), }; const main: Window['main'] = { restart: () => ipcRenderer.send('restart'), diff --git a/packages/insomnia/src/ui/components/dropdowns/grpc-method-dropdown/grpc-method-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/grpc-method-dropdown/grpc-method-dropdown.tsx index a4a1c0823..9223e9805 100644 --- a/packages/insomnia/src/ui/components/dropdowns/grpc-method-dropdown/grpc-method-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/grpc-method-dropdown/grpc-method-dropdown.tsx @@ -1,13 +1,7 @@ import React, { Fragment, FunctionComponent } from 'react'; import styled from 'styled-components'; -import { - getGrpcPathSegments, - getShortGrpcPath, - groupGrpcMethodsByPackage, - GrpcMethodInfo, - NO_PACKAGE_KEY, -} from '../../../../common/grpc-paths'; +import type { GrpcMethodInfo } from '../../../../main/ipc/grpc'; import { Dropdown } from '../../base/dropdown/dropdown'; import { DropdownButton } from '../../base/dropdown/dropdown-button'; import { DropdownDivider } from '../../base/dropdown/dropdown-divider'; @@ -40,8 +34,38 @@ interface Props { selectedMethod?: GrpcMethodInfo; handleChange: (arg0: string) => void; handleChangeProtoFile: () => void; + handleServerReflection: () => void; +} +const PROTO_PATH_REGEX = /^\/(?:(?[\w.]+)\.)?(?\w+)\/(?\w+)$/; + +export const NO_PACKAGE_KEY = 'no-package'; + +function groupBy(list: {}[], keyGetter: (item: any) => string):Record { + const map = new Map(); + list.forEach(item => { + const key = keyGetter(item); + const collection = map.get(key); + if (!collection) { + map.set(key, [item]); + } else { + collection.push(item); + } + }); + return Object.fromEntries(map); } +export const groupGrpcMethodsByPackage = (methodInfoList: GrpcMethodInfo[]): Record => { + return groupBy(methodInfoList, ({ fullPath }) => PROTO_PATH_REGEX.exec(fullPath)?.groups?.package || NO_PACKAGE_KEY); +}; + +// If all segments are found, return a shorter path, otherwise the original path +export const getShortGrpcPath = (fullPath: string): string => { + const result = PROTO_PATH_REGEX.exec(fullPath); + const packageName = result?.groups?.package; + const serviceName = result?.groups?.service; + const methodName = result?.groups?.method; + return packageName && serviceName && methodName ? `/${serviceName}/${methodName}` : fullPath; +}; const NormalCase = styled.span` text-transform: initial; `; @@ -52,6 +76,7 @@ export const GrpcMethodDropdown: FunctionComponent = ({ selectedMethod, handleChange, handleChangeProtoFile, + handleServerReflection, }) => { const groupedByPkg = groupGrpcMethodsByPackage(methods); const selectedPath = selectedMethod?.fullPath; @@ -64,10 +89,13 @@ export const GrpcMethodDropdown: FunctionComponent = ({ buttonClass={DropdownMethodButton} > - {!selectedPath ? 'Select Method' : getShortGrpcPath(getGrpcPathSegments(selectedPath), selectedPath)} + {!selectedPath ? 'Select Method' : getShortGrpcPath(selectedPath)} + + Click to use server reflection + Click to change proto file @@ -82,7 +110,7 @@ export const GrpcMethodDropdown: FunctionComponent = ({ {name !== NO_PACKAGE_KEY && pkg: {name}} - {pkg.map(({ segments, type, fullPath }) => ( + {pkg.map(({ type, fullPath }) => ( handleChange(fullPath)} @@ -90,7 +118,7 @@ export const GrpcMethodDropdown: FunctionComponent = ({ selected={fullPath === selectedPath} > - {getShortGrpcPath(segments, fullPath)} + {getShortGrpcPath(fullPath)} ))} diff --git a/packages/insomnia/src/ui/components/panes/grpc-request-pane.tsx b/packages/insomnia/src/ui/components/panes/grpc-request-pane.tsx index eba5d5d27..e6e978dce 100644 --- a/packages/insomnia/src/ui/components/panes/grpc-request-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/grpc-request-pane.tsx @@ -169,6 +169,11 @@ export const GrpcRequestPane: FunctionComponent = ({ }); }} handleChangeProtoFile={() => setIsProtoModalOpen(true)} + handleServerReflection={async () => { + models.grpcRequest.update(activeRequest, { protoMethodName: '', protoFileId: '' }); + const methods = await window.main.grpc.loadMethodsFromReflection(activeRequest.url); + setGrpcState({ ...grpcState, methods, reloadMethods: false }); + }} /> @@ -204,11 +209,13 @@ export const GrpcRequestPane: FunctionComponent = ({ requestId: activeRequest._id, }; window.main.grpc.sendMessage(preparedMessage); - setGrpcState({ ...grpcState, requestMessages:[...requestMessages, { - id: generateId(), - text:preparedMessage.body.text || '', - created: Date.now(), - }] }); + setGrpcState({ + ...grpcState, requestMessages: [...requestMessages, { + id: generateId(), + text: preparedMessage.body.text || '', + created: Date.now(), + }], + }); }} handleCommit={() => window.main.grpc.commit(activeRequest._id)} diff --git a/packages/insomnia/src/ui/routes/debug.tsx b/packages/insomnia/src/ui/routes/debug.tsx index 043d001b5..79eab0d2d 100644 --- a/packages/insomnia/src/ui/routes/debug.tsx +++ b/packages/insomnia/src/ui/routes/debug.tsx @@ -3,8 +3,8 @@ import React, { FC, Fragment, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { ChangeBufferEvent, database as db } from '../../common/database'; -import { GrpcMethodInfo } from '../../common/grpc-paths'; import { generateId } from '../../common/misc'; +import type { GrpcMethodInfo } from '../../main/ipc/grpc'; import * as models from '../../models'; import { isGrpcRequest } from '../../models/grpc-request'; import { getByParentId as getGrpcRequestMetaByParentId } from '../../models/grpc-request-meta';