diff --git a/packages/insomnia/src/main/ipc/grpc.ts b/packages/insomnia/src/main/ipc/grpc.ts index 50bca4482d6..5c38e6f3b1f 100644 --- a/packages/insomnia/src/main/ipc/grpc.ts +++ b/packages/insomnia/src/main/ipc/grpc.ts @@ -11,7 +11,6 @@ import { ChannelCredentials, type ClientDuplexStream, type ClientReadableStream, - credentials, makeGenericClientConstructor, Metadata, type ServiceError, @@ -27,7 +26,6 @@ import type { } from '@grpc/proto-loader'; import * as protoLoader from '@grpc/proto-loader'; import electron, { type IpcMainEvent } from 'electron'; -import fs from 'fs'; import * as grpcReflection from 'grpc-reflection-js'; import { version } from '../../../package.json'; @@ -44,6 +42,9 @@ const grpcCalls = new Map(); export interface GrpcIpcRequestParams { request: RenderedGrpcRequest; + clientCert?: string; + clientKey?: string; + caCertificate?: string; } export interface GrpcIpcMessageParams { @@ -202,16 +203,19 @@ const getMethodsFromReflectionServer = async ( const getMethodsFromReflection = async ( host: string, metadata: GrpcRequestHeader[], - reflectionApi: GrpcRequest['reflectionApi'] + reflectionApi: GrpcRequest['reflectionApi'], + clientCert?: string, + clientKey?: string, + caCertificate?: string, ): Promise => { if (reflectionApi.enabled) { return getMethodsFromReflectionServer(reflectionApi); } try { - const { url, enableTls } = parseGrpcUrl(host); + const { url } = parseGrpcUrl(host); const client = new grpcReflection.Client( url, - enableTls ? credentials.createSsl() : credentials.createInsecure(), + getChannelCredentials({ url: host, caCertificate, clientCert, clientKey }), grpcOptions, filterDisabledMetaData(metadata) ); @@ -265,12 +269,18 @@ export const loadMethodsFromReflection = async (options: { url: string; metadata: GrpcRequestHeader[]; reflectionApi: GrpcRequest['reflectionApi']; + clientCert?: string; + clientKey?: string; + caCertificate?: string; }): Promise => { invariant(options.url, 'gRPC request url not provided'); const methods = await getMethodsFromReflection( options.url, options.metadata, - options.reflectionApi + options.reflectionApi, + options.clientCert, + options.clientKey, + options.caCertificate ); return methods.map(method => ({ type: getMethodType(method), @@ -345,27 +355,22 @@ const isEnumDefinition = (definition: AnyDefinition): definition is EnumTypeDefi return (definition as EnumTypeDefinition).format === 'Protocol Buffer 3 EnumDescriptorProto'; }; -const getChannelCredentialsByWorkspaceId = async (workspaceId: string): ChannelCredentials => { - const clientCerts = await models.clientCertificate.findByParentId(workspaceId); - const caCert = await models.caCertificate.findByParentId(workspaceId); - const caCertficatePath = caCert?.path || null; - const rootCert = (caCertficatePath && fs.readFileSync(caCertficatePath)); - // TODO: filter out disabled client certs and select approriate - const clientCert = clientCerts?.[0]?.cert; - const clientKey = clientCerts?.[0]?.key; - - if (!rootCert) { +const getChannelCredentials = ({ url, clientCert, clientKey, caCertificate }: { url: string; clientCert?: string; clientKey?: string; caCertificate?: string }): ChannelCredentials => { + if (url.toLowerCase().startsWith('grpc:')) { return ChannelCredentials.createInsecure(); } - if (rootCert && clientKey && clientCert) { - return ChannelCredentials.createSsl(rootCert, Buffer.from(clientKey, 'utf8'), Buffer.from(clientCert, 'utf8')); + if (caCertificate && clientKey && clientCert) { + return ChannelCredentials.createSsl(Buffer.from(caCertificate, 'utf8'), Buffer.from(clientKey, 'utf8'), Buffer.from(clientCert, 'utf8')); + } + if (caCertificate) { + return ChannelCredentials.createSsl(Buffer.from(caCertificate, 'utf8'),); } - return ChannelCredentials.createSsl(rootCert); + return ChannelCredentials.createInsecure(); }; export const start = ( event: IpcMainEvent, - { request }: GrpcIpcRequestParams, + { request, clientCert, clientKey, caCertificate }: GrpcIpcRequestParams, ) => { getSelectedMethod(request)?.then(method => { if (!method) { @@ -374,17 +379,15 @@ export const start = ( } const methodType = getMethodType(method); // Create client - const { url, enableTls } = parseGrpcUrl(request.url); - const workspaceId = request.parentId; + const { url } = parseGrpcUrl(request.url); if (!url) { event.reply('grpc.error', request._id, new Error('URL not specified')); return undefined; } - console.log(`[gRPC] connecting to url=${url} ${enableTls ? 'with' : 'without'} TLS`); // @ts-expect-error -- TSCONVERSION second argument should be provided, send an empty string? Needs testing const Client = makeGenericClientConstructor({}); - const creds = enableTls ? getChannelCredentialsByWorkspaceId(workspaceId) : credentials.createInsecure(); + const creds = getChannelCredentials({ url: request.url, clientCert, clientKey, caCertificate }); const client = new Client(url, creds); if (!client) { return; 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 8b829ce4014..94d9ac52574 100644 --- a/packages/insomnia/src/ui/components/panes/grpc-request-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/grpc-request-pane.tsx @@ -1,3 +1,4 @@ +import { readFile } from 'fs/promises'; import React, { type FunctionComponent, useRef, useState } from 'react'; import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components'; import { useParams, useRouteLoaderData } from 'react-router-dom'; @@ -11,7 +12,9 @@ import type { GrpcMethodType } from '../../../main/ipc/grpc'; import * as models from '../../../models'; import type { GrpcRequestHeader } from '../../../models/grpc-request'; import { queryAllWorkspaceUrls } from '../../../models/helpers/query-all-workspace-urls'; +import { urlMatchesCertHost } from '../../../network/url-matches-cert-host'; import { tryToInterpolateRequestOrShowRenderErrorModal } from '../../../utils/try-interpolate'; +import { setDefaultProtocol } from '../../../utils/url/protocol'; import { useRequestPatcher } from '../../hooks/use-request'; import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version'; import type { GrpcRequestState } from '../../routes/debug'; @@ -86,7 +89,15 @@ export const GrpcRequestPane: FunctionComponent = ({ purpose: 'send', skipBody: canClientStream(methodType), }); - window.main.grpc.start({ request }); + const workspaceClientCertificates = await models.clientCertificate.findByParentId(workspaceId); + const clientCertificate = workspaceClientCertificates.find(c => !c.disabled && urlMatchesCertHost(setDefaultProtocol(c.host, 'grpc:'), request.url, false)); + const caCertificatePath = (await models.caCertificate.findByParentId(workspaceId))?.path; + window.main.grpc.start({ + request, + clientCert: clientCertificate?.cert || undefined, + clientKey: clientCertificate?.key || undefined, + caCertificate: caCertificatePath ? await readFile(caCertificatePath, 'utf8') : undefined, + }); setGrpcState({ ...grpcState, requestMessages: [], @@ -177,7 +188,7 @@ export const GrpcRequestPane: FunctionComponent = ({ disabled={!activeRequest.url} onClick={async () => { try { - const rendered = + let rendered = await tryToInterpolateRequestOrShowRenderErrorModal({ request: activeRequest, environmentId, @@ -187,6 +198,15 @@ export const GrpcRequestPane: FunctionComponent = ({ reflectionApi: activeRequest.reflectionApi, }, }); + const workspaceClientCertificates = await models.clientCertificate.findByParentId(workspaceId); + const clientCertificate = workspaceClientCertificates.find(c => !c.disabled && urlMatchesCertHost(setDefaultProtocol(c.host, 'grpc:'), request.url, false)); + const caCertificatePath = (await models.caCertificate.findByParentId(workspaceId))?.path; + rendered = { + ...rendered, + clientCert: clientCertificate?.cert || undefined, + clientKey: clientCertificate?.key || undefined, + caCertificate: caCertificatePath ? await readFile(caCertificatePath, 'utf8') : undefined, + }; const methods = await window.main.grpc.loadMethodsFromReflection(rendered); setGrpcState({ ...grpcState, methods }); patchRequest(requestId, { protoFileId: '', protoMethodName: '' });