Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for gRPC mTLS #8002

Draft
wants to merge 6 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 44 additions & 3 deletions packages/insomnia-smoke-test/server/grpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
/* eslint-disable camelcase */
// inspiration: https://github.com/grpc/grpc/blob/master/examples/node/dynamic_codegen/route_guide/route_guide_server.js
import * as grpc from '@grpc/grpc-js';
import { ServerCredentials } from '@grpc/grpc-js';
import { HandleCall } from '@grpc/grpc-js/build/src/server-call';
import * as protoLoader from '@grpc/proto-loader';
import { addReflection } from '@ravanallc/grpc-server-reflection';
import fs from 'fs';
import fs, { readFileSync } from 'fs';
import path from 'path';

const PROTO_PATH = path.resolve('../../packages/insomnia/src/network/grpc/__fixtures__/library/route_guide.proto');
Expand Down Expand Up @@ -192,8 +193,7 @@ export const startGRPCServer = (port: number) => {
const server = new grpc.Server();

// Enable reflection
const descriptorSet = '../../packages/insomnia-smoke-test/fixtures/route_guide.bin';
addReflection(server, descriptorSet);
addReflection(server, '../../packages/insomnia-smoke-test/fixtures/route_guide.bin');

// @ts-expect-error generated from proto file
server.addService(routeguide.RouteGuide.service, {
Expand All @@ -218,5 +218,46 @@ export const startGRPCServer = (port: number) => {
resolve();
});
});
const serverWithTLS = new grpc.Server();

// Enable reflection
addReflection(serverWithTLS, '../../packages/insomnia-smoke-test/fixtures/route_guide.bin');

// @ts-expect-error generated from proto file
serverWithTLS.addService(routeguide.RouteGuide.service, {
getFeature: getFeature,
listFeatures: listFeatures,
recordRoute: recordRoute,
routeChat: routeChat,
});
const rootCert = readFileSync(path.join(__dirname, '../fixtures/certificates/rootCA.pem'));
const serverCert = readFileSync(path.join(__dirname, '../fixtures/certificates/localhost.pem'));
const serverKey = readFileSync(path.join(__dirname, '../fixtures/certificates/localhost-key.pem'));
const serverCredentials = ServerCredentials.createSsl(
rootCert,
[
{
cert_chain: serverCert,
private_key: serverKey,
},
],
true
);
serverWithTLS.bindAsync('localhost:50052', serverCredentials, error => {
if (error) {
return reject(error);
}

const dbPath = '../../packages/insomnia/src/network/grpc/__fixtures__/library/route_guide_db.json';
fs.readFile(path.resolve(dbPath), (err, data) => {
if (err) {
throw err;
}
featureList = JSON.parse(data.toString());
console.log('Listening at grpcs://localhost:50052 for route_guide.proto');
serverWithTLS.start();
resolve();
});
});
});
};
52 changes: 42 additions & 10 deletions packages/insomnia/src/main/ipc/grpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { Code, ConnectError, createPromiseClient } from '@connectrpc/connect';
import { createConnectTransport } from '@connectrpc/connect-node';
import {
type Call,
ChannelCredentials,
type ClientDuplexStream,
type ClientReadableStream,
credentials,
makeGenericClientConstructor,
Metadata,
type ServiceError,
Expand Down Expand Up @@ -42,6 +42,10 @@ const grpcCalls = new Map<string, Call>();

export interface GrpcIpcRequestParams {
request: RenderedGrpcRequest;
clientCert?: string;
clientKey?: string;
caCertificate?: string;
rejectUnauthorized: boolean;
}

export interface GrpcIpcMessageParams {
Expand Down Expand Up @@ -200,16 +204,20 @@ const getMethodsFromReflectionServer = async (
const getMethodsFromReflection = async (
host: string,
metadata: GrpcRequestHeader[],
reflectionApi: GrpcRequest['reflectionApi']
rejectUnauthorized: boolean,
reflectionApi: GrpcRequest['reflectionApi'],
clientCert?: string,
clientKey?: string,
caCertificate?: string,
): Promise<MethodDefs[]> => {
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, rejectUnauthorized }),
grpcOptions,
filterDisabledMetaData(metadata)
);
Expand Down Expand Up @@ -262,13 +270,21 @@ const getMethodsFromReflection = async (
export const loadMethodsFromReflection = async (options: {
url: string;
metadata: GrpcRequestHeader[];
rejectUnauthorized: boolean;
reflectionApi: GrpcRequest['reflectionApi'];
clientCert?: string;
clientKey?: string;
caCertificate?: string;
}): Promise<GrpcMethodInfo[]> => {
invariant(options.url, 'gRPC request url not provided');
const methods = await getMethodsFromReflection(
options.url,
options.metadata,
options.reflectionApi
options.rejectUnauthorized,
options.reflectionApi,
options.clientCert,
options.clientKey,
options.caCertificate,
);
return methods.map(method => ({
type: getMethodType(method),
Expand Down Expand Up @@ -313,10 +329,12 @@ export const getSelectedMethod = async (
invariant(methods, 'No methods found');
return methods.find(c => c.path === request.protoMethodName);
}
const settings = await models.settings.getOrCreate();
const methods = await getMethodsFromReflection(
request.url,
request.metadata,
request.reflectionApi
settings.validateSSL,
request.reflectionApi,
);
invariant(methods, 'No reflection methods found');
return methods.find(c => c.path === request.protoMethodName);
Expand All @@ -343,9 +361,22 @@ const isEnumDefinition = (definition: AnyDefinition): definition is EnumTypeDefi
return (definition as EnumTypeDefinition).format === 'Protocol Buffer 3 EnumDescriptorProto';
};

const getChannelCredentials = ({ url, rejectUnauthorized, clientCert, clientKey, caCertificate }: { url: string; rejectUnauthorized: boolean; clientCert?: string; clientKey?: string; caCertificate?: string }): ChannelCredentials => {
if (url.toLowerCase().startsWith('grpc:')) {
return ChannelCredentials.createInsecure();
}
if (caCertificate && clientKey && clientCert) {
return ChannelCredentials.createSsl(Buffer.from(caCertificate, 'utf8'), Buffer.from(clientKey, 'utf8'), Buffer.from(clientCert, 'utf8'), { rejectUnauthorized });
}
if (caCertificate) {
return ChannelCredentials.createSsl(Buffer.from(caCertificate, 'utf8'), null, null, { rejectUnauthorized });
}
return ChannelCredentials.createSsl(null, null, null, { rejectUnauthorized });
};

export const start = (
event: IpcMainEvent,
{ request }: GrpcIpcRequestParams,
{ request, rejectUnauthorized, clientCert, clientKey, caCertificate }: GrpcIpcRequestParams,
) => {
getSelectedMethod(request)?.then(method => {
if (!method) {
Expand All @@ -354,15 +385,16 @@ export const start = (
}
const methodType = getMethodType(method);
// Create client
const { url, enableTls } = parseGrpcUrl(request.url);
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 client = new Client(url, enableTls ? credentials.createSsl() : credentials.createInsecure());
const creds = getChannelCredentials({ url: request.url, rejectUnauthorized, clientCert, clientKey, caCertificate });
const client = new Client(url, creds);
if (!client) {
return;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/insomnia/src/network/grpc/parse-grpc-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export const parseGrpcUrl = (grpcUrl: string): { url: string; enableTls: boolean
if (lower.startsWith('grpcs://')) {
return { url: lower.slice(8), enableTls: true };
}
return { url: lower, enableTls: false };
return { url: lower, enableTls: true };
};
33 changes: 30 additions & 3 deletions packages/insomnia/src/ui/components/panes/grpc-request-pane.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,11 +12,14 @@ 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';
import type { GrpcRequestLoaderData } from '../../routes/request';
import { useRootLoaderData } from '../../routes/root';
import type { WorkspaceLoaderData } from '../../routes/workspace';
import { GrpcSendButton } from '../buttons/grpc-send-button';
import { CodeEditor, type CodeEditorHandle } from '../codemirror/code-editor';
Expand Down Expand Up @@ -53,7 +57,7 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
reloadRequests,
}) => {
const { activeRequest } = useRouteLoaderData('request/:requestId') as GrpcRequestLoaderData;

const { settings } = useRootLoaderData();
const [isProtoModalOpen, setIsProtoModalOpen] = useState(false);
const { requestMessages, running, methods } = grpcState;
useMount(async () => {
Expand Down Expand Up @@ -98,7 +102,18 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
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;
const clientCert = await readFile(clientCertificate?.cert || '', 'utf8');
const clientKey = await readFile(clientCertificate?.key || '', 'utf8');
window.main.grpc.start({
request,
rejectUnauthorized: settings.validateSSL,
clientCert,
clientKey,
caCertificate: caCertificatePath ? await readFile(caCertificatePath, 'utf8') : undefined,
});
setGrpcState({
...grpcState,
requestMessages: [],
Expand Down Expand Up @@ -189,7 +204,7 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
disabled={!activeRequest.url}
onClick={async () => {
try {
const rendered =
let rendered =
await tryToInterpolateRequestOrShowRenderErrorModal({
request: activeRequest,
environmentId,
Expand All @@ -199,6 +214,18 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
reflectionApi: activeRequest.reflectionApi,
},
});
const workspaceClientCertificates = await models.clientCertificate.findByParentId(workspaceId);
const clientCertificate = workspaceClientCertificates.find(c => !c.disabled);
const caCertificatePath = (await models.caCertificate.findByParentId(workspaceId))?.path;
const clientCert = await readFile(clientCertificate?.cert || '', 'utf8');
const clientKey = await readFile(clientCertificate?.key || '', 'utf8');
rendered = {
...rendered,
rejectUnauthorized: settings.validateSSL,
clientCert,
clientKey,
caCertificate: caCertificatePath ? await readFile(caCertificatePath, 'utf8') : undefined,
};
const methods = await window.main.grpc.loadMethodsFromReflection(rendered);
setGrpcState({ ...grpcState, methods });
patchRequest(requestId, { protoFileId: '', protoMethodName: '' });
Expand Down
Loading