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

fix: paths in gRPC requests and proto state #8284

Merged
merged 7 commits into from
Jan 22, 2025
Merged
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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/insomnia/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"fastq": "^1.17.1",
"graphql": "^16.8.1",
"graphql-ws": "^5.16.0",
"grpc-reflection-js": "jackkav/grpc-reflection-js#remove-lodash-set",
"grpc-reflection-js": "Kong/grpc-reflection-js#master",
"hawk": "9.0.2",
"hkdf": "^0.0.2",
"hosted-git-info": "5.2.1",
Expand Down
16 changes: 9 additions & 7 deletions packages/insomnia/src/main/ipc/grpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,13 @@ const getMethodsFromReflection = async (
if (reflectionApi.enabled) {
return getMethodsFromReflectionServer(reflectionApi);
}
const { url } = parseGrpcUrl(host);
const { url, path } = parseGrpcUrl(host);
const client = new grpcReflection.Client(
url,
getChannelCredentials({ url: host, caCertificate, clientCert, clientKey, rejectUnauthorized }),
grpcOptions,
filterDisabledMetaData(metadata)
filterDisabledMetaData(metadata),
path
);
const services = await client.listServices();
const methodsPromises = services.map(async service => {
Expand Down Expand Up @@ -389,7 +390,7 @@ export const start = (
}
const methodType = getMethodType(method);
// Create client
const { url } = parseGrpcUrl(request.url);
const { url, path } = parseGrpcUrl(request.url);

if (!url) {
event.reply('grpc.error', request._id, new Error('URL not specified'));
Expand All @@ -405,10 +406,11 @@ export const start = (

try {
const messageBody = JSON.parse(request.body.text || '');
const requestPath = path + method.path;
switch (methodType) {
case 'unary':
const unaryCall = client.makeUnaryRequest(
method.path,
requestPath,
method.requestSerialize,
method.responseDeserialize,
messageBody,
Expand All @@ -420,7 +422,7 @@ export const start = (
break;
case 'client':
const clientCall = client.makeClientStreamRequest(
method.path,
requestPath,
method.requestSerialize,
method.responseDeserialize,
filterDisabledMetaData(request.metadata),
Expand All @@ -430,7 +432,7 @@ export const start = (
break;
case 'server':
const serverCall = client.makeServerStreamRequest(
method.path,
requestPath,
method.requestSerialize,
method.responseDeserialize,
messageBody,
Expand All @@ -441,7 +443,7 @@ export const start = (
break;
case 'bidi':
const bidiCall = client.makeBidiStreamRequest(
method.path,
requestPath,
method.requestSerialize,
method.responseDeserialize,
filterDisabledMetaData(request.metadata));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe('parseGrpcUrl', () => {
expect(parseGrpcUrl(input)).toStrictEqual({
url: expected,
enableTls: false,
path: '',
});
});

Expand All @@ -22,6 +23,7 @@ describe('parseGrpcUrl', () => {
expect(parseGrpcUrl(input)).toStrictEqual({
url: expected,
enableTls: true,
path: '',
});
});

Expand All @@ -33,13 +35,15 @@ describe('parseGrpcUrl', () => {
expect(parseGrpcUrl(input)).toStrictEqual({
url: expected,
enableTls: false,
path: '',
});
});

it.each([null, undefined, ''])('can handle falsey urls', input => {
expect(parseGrpcUrl(input)).toStrictEqual({
url: '',
enableTls: false,
path: '',
});
});
});
20 changes: 10 additions & 10 deletions packages/insomnia/src/network/grpc/parse-grpc-url.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
export const parseGrpcUrl = (grpcUrl: string): { url: string; enableTls: boolean } => {
export const parseGrpcUrl = (grpcUrl: string): { url: string; enableTls: boolean; path: string } => {
if (!grpcUrl) {
return { url: '', enableTls: false };
return { url: '', enableTls: false, path: '' };
}
const lower = grpcUrl.toLowerCase();
if (lower.startsWith('grpc://')) {
return { url: lower.slice(7), enableTls: false };
}
if (lower.startsWith('grpcs://')) {
return { url: lower.slice(8), enableTls: true };
}
return { url: lower, enableTls: false };
const url = new URL((grpcUrl.includes('://') ? '' : 'grpc://') + grpcUrl.toLowerCase());
return {
url: url.host,
enableTls: url.protocol === 'grpcs:',
// remove trailing slashes from pathname; the full request
// path is a concatenation of this parsed path + method path
path: url.pathname.endsWith('/') ? url.pathname.slice(0, -1) : url.pathname,
};
};
15 changes: 7 additions & 8 deletions packages/insomnia/src/ui/components/modals/proto-files-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ export interface Props {
reloadRequests: (requestIds: string[]) => void;
}

export const ProtoFilesModal: FC<Props> = ({ defaultId, onHide, onSave, reloadRequests }) => {
export const ProtoFilesModal: FC<Props> = ({ defaultId, onHide, onSave }) => {
const modalRef = useRef<ModalHandle>(null);
const { workspaceId } = useParams() as { workspaceId: string };
const { workspaceId } = useParams() as { workspaceId: string; requestId: string };

const [selectedId, setSelectedId] = useState(defaultId);
const [protoDirectories, setProtoDirectories] = useState<ExpandedProtoDirectory[]>([]);
Expand Down Expand Up @@ -205,7 +205,7 @@ export const ProtoFilesModal: FC<Props> = ({ defaultId, onHide, onSave, reloadRe
if (!filePath) {
return;
}
if (!await isProtofileValid(filePath)) {
if (!(await isProtofileValid(filePath))) {
return;
}
const contents = await fs.promises.readFile(filePath, 'utf-8');
Expand All @@ -216,8 +216,7 @@ export const ProtoFilesModal: FC<Props> = ({ defaultId, onHide, onSave, reloadRe
const impacted = await models.grpcRequest.findByProtoFileId(updatedFile._id);
const requestIds = impacted.map(g => g._id);
if (requestIds?.length) {
requestIds.forEach(requestId => window.main.grpc.cancel(requestId));
reloadRequests(requestIds);
requestIds.forEach(async requestId => window.main.grpc.cancel(requestId));
}
};

Expand Down Expand Up @@ -294,6 +293,7 @@ export const ProtoFilesModal: FC<Props> = ({ defaultId, onHide, onSave, reloadRe
protoDirectories={protoDirectories}
selectedId={selectedId}
handleSelect={id => setSelectedId(id)}
handleUnselect={() => setSelectedId('')}
handleUpdate={handleUpdate}
handleDelete={handleDeleteFile}
handleDeleteDirectory={handleDeleteDirectory}
Expand All @@ -305,11 +305,10 @@ export const ProtoFilesModal: FC<Props> = ({ defaultId, onHide, onSave, reloadRe
className="btn"
onClick={event => {
event.preventDefault();
if (typeof onSave === 'function' && selectedId) {
onSave(selectedId);
if (typeof onSave === 'function') {
onSave(selectedId || '');
}
}}
disabled={!selectedId}
>
Save
</button>
Expand Down
35 changes: 27 additions & 8 deletions packages/insomnia/src/ui/components/panes/grpc-request-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
reloadRequests,
}) => {
const { activeRequest } = useRouteLoaderData('request/:requestId') as GrpcRequestLoaderData;
const {
activeEnvironment,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const environmentId = activeEnvironment._id;
const { settings } = useRootLoaderData();
const [isProtoModalOpen, setIsProtoModalOpen] = useState(false);
const { requestMessages, running, methods } = grpcState;
Expand All @@ -77,7 +81,24 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
reflectionApi: activeRequest.reflectionApi,
},
});
const methods = await window.main.grpc.loadMethodsFromReflection(rendered);

const workspaceClientCertificates = await models.clientCertificate.findByParentId(workspaceId);
const clientCertificate = workspaceClientCertificates.find(c => !c.disabled && urlMatchesCertHost(setDefaultProtocol(c.host, 'grpc:'), rendered.url, false));
const caCertificate = (await models.caCertificate.findByParentId(workspaceId));
const caCertificatePath = caCertificate && !caCertificate.disabled ? caCertificate.path : undefined;
const clientCert = clientCertificate?.cert ? await readFile(clientCertificate?.cert, 'utf8') : undefined;
const clientKey = clientCertificate?.key ? await readFile(clientCertificate?.key, 'utf8') : undefined;

const renderedWithCertificates = {
...rendered,
rejectUnauthorized: settings.validateSSL,
...(activeRequest.url.toLowerCase().startsWith('grpcs:') ? {
clientCert,
clientKey,
caCertificate: caCertificatePath ? await readFile(caCertificatePath, 'utf8') : undefined,
} : {}),
};
const methods = await window.main.grpc.loadMethodsFromReflection(renderedWithCertificates);
setGrpcState({ ...grpcState, methods });
}
});
Expand All @@ -86,10 +107,6 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
const activeRequestSyncVersion = useActiveRequestSyncVCSVersion();
const { workspaceId, requestId } = useParams() as { workspaceId: string; requestId: string };
const patchRequest = useRequestPatcher();
const {
activeEnvironment,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const environmentId = activeEnvironment._id;
// Reset the response pane state when we switch requests, the environment gets modified, or the (Git|Sync)VCS version changes
const uniquenessKey = `${activeEnvironment.modified}::${requestId}::${gitVersion}::${activeRequestSyncVersion}`;
const method = methods.find(c => c.fullPath === activeRequest.protoMethodName);
Expand Down Expand Up @@ -381,7 +398,11 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
defaultId={activeRequest.protoFileId}
onHide={() => setIsProtoModalOpen(false)}
onSave={async (protoFileId: string) => {
if (activeRequest.protoFileId !== protoFileId) {
if (!protoFileId) {
patchRequest(requestId, { protoFileId: '', protoMethodName: '' });
setGrpcState({ ...grpcState, methods: [] });
setIsProtoModalOpen(false);
} else {
try {
const methods = await window.main.grpc.loadMethods(protoFileId);
patchRequest(requestId, { protoFileId, protoMethodName: '' });
Expand All @@ -394,8 +415,6 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
error,
});
}
} else {
setIsProtoModalOpen(false);
}
}}
/>}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface Props {
protoDirectories: ExpandedProtoDirectory[];
selectedId?: string;
handleSelect: SelectProtoFileHandler;
handleUnselect: SelectProtoFileHandler;
handleDelete: DeleteProtoFileHandler;
handleUpdate: UpdateProtoFileHandler;
handleDeleteDirectory: DeleteProtoDirectoryHandler;
Expand All @@ -29,6 +30,7 @@ const recursiveRender = (
indent: number,
{ dir, files, subDirs }: ExpandedProtoDirectory,
handleSelect: SelectProtoFileHandler,
handleUnselect: SelectProtoFileHandler,
handleUpdate: UpdateProtoFileHandler,
handleDelete: DeleteProtoFileHandler,
handleDeleteDirectory: DeleteProtoDirectoryHandler,
Expand Down Expand Up @@ -69,7 +71,17 @@ const recursiveRender = (
onClick={() => handleSelect(f._id)}
>
<>
<Checkbox className="py-0" isSelected={f._id === selectedId} onChange={isSelected => isSelected && handleSelect(f._id)}>
<Checkbox
className="py-0"
isSelected={f._id === selectedId}
onChange={isSelected => {
if (isSelected) {
handleSelect(f._id);
} else {
handleUnselect(f._id);
}
}}
>
{({ isSelected }) => {
return <>
{isSelected ?
Expand Down Expand Up @@ -115,6 +127,7 @@ const recursiveRender = (
indent + 1,
sd,
handleSelect,
handleUnselect,
handleUpdate,
handleDelete,
handleDeleteDirectory,
Expand All @@ -133,6 +146,7 @@ export const ProtoFileList: FunctionComponent<Props> = props => (
0,
dir,
props.handleSelect,
props.handleUnselect,
props.handleUpdate,
props.handleDelete,
props.handleDeleteDirectory,
Expand Down
9 changes: 9 additions & 0 deletions packages/insomnia/src/utils/grpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ const GRPC_CONNECTION_ERROR_STRINGS = {
SERVER_CANCELED: 'CANCELLED',
TLS_NOT_SUPPORTED: 'WRONG_VERSION_NUMBER',
BAD_LOCAL_ROOT_CERT: 'unable to get local issuer certificate',
UNIMPLEMENTED: 'UNIMPLEMENTED',
// this next one will break if we rename the call made from the pane
CANNOT_REFLECT: "'grpc.loadMethodsFromReflection': Error: 12",
};

export function isGrpcConnectionError(error: Error) {
Expand Down Expand Up @@ -34,6 +37,12 @@ export function getGrpcConnectionErrorDetails(error: Error) {
} else if (error.message.includes(GRPC_CONNECTION_ERROR_STRINGS.BAD_LOCAL_ROOT_CERT)) {
title = 'Local Root Certificate Error';
message = 'The local root certificate enabled for the host is not valid.\nEither disable the root certificate, or update it with a valid one.';
} else if (error.message.includes(GRPC_CONNECTION_ERROR_STRINGS.CANNOT_REFLECT)) {
title = 'Reflection Not Supported';
message = 'The server has indicated that it does not support reflection. You may need to manually enable it server-side.';
} else if (error.message.includes(GRPC_CONNECTION_ERROR_STRINGS.UNIMPLEMENTED)) {
title = 'Unimplemented Method';
message = 'The server does not support the requested method. Is the .proto file correct?';
}

return {
Expand Down
Loading