Skip to content

Commit

Permalink
fix: pre-request script variable hostname certificate resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
ryan-willis committed Dec 12, 2024
1 parent eca35d8 commit 67e40d0
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 28 deletions.
37 changes: 22 additions & 15 deletions packages/insomnia-sdk/src/objects/insomnia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expect } from 'chai';
import type { ClientCertificate } from 'insomnia/src/models/client-certificate';
import type { RequestHeader } from 'insomnia/src/models/request';
import type { Settings } from 'insomnia/src/models/settings';
import { filterClientCertificates } from 'insomnia/src/network/certificate';

import { toPreRequestAuth } from './auth';
import { CookieObject } from './cookies';
Expand Down Expand Up @@ -167,16 +168,29 @@ export async function initInsomniaObject(
localVars: localVariables,
});

const existClientCert = rawObj.clientCertificates != null && rawObj.clientCertificates.length > 0;
const certificate = existClientCert && rawObj.clientCertificates[0] ?
const reqUrl = toUrlObject(rawObj.request.url);
reqUrl.addQueryParams(
rawObj.request.parameters
.filter(param => !param.disabled)
.map(param => ({ key: param.name, value: param.value }))
);

// hack: sanitize a template reference if it exists in the hostname
// so we can filter certificates without a full render on the request
const host = reqUrl.getHost().replaceAll(/{{\s*\_\./g, '{{');
const renderedHost = variables.replaceIn(host);

const filteredCerts = filterClientCertificates(rawObj.clientCertificates || [], renderedHost, 'https:');
const existingClientCert = filteredCerts != null && filteredCerts.length > 0 && filteredCerts[0];
const certificate = existingClientCert ?
{
disabled: rawObj.clientCertificates[0].disabled,
disabled: existingClientCert.disabled,
name: 'The first certificate from Settings',
matches: [rawObj.clientCertificates[0].host],
key: { src: rawObj.clientCertificates[0].key || '' },
cert: { src: rawObj.clientCertificates[0].cert || '' },
passphrase: rawObj.clientCertificates[0].passphrase || undefined,
pfx: { src: rawObj.clientCertificates[0].pfx || '' }, // PFX or PKCS12 Certificate
matches: [existingClientCert.host],
key: { src: existingClientCert.key || '' },
cert: { src: existingClientCert.cert || '' },
passphrase: existingClientCert.passphrase || undefined,
pfx: { src: existingClientCert.pfx || '' }, // PFX or PKCS12 Certificate
} :
{ disabled: true };

Expand All @@ -187,13 +201,6 @@ export async function initInsomniaObject(
rawObj.settings.noProxy,
);

const reqUrl = toUrlObject(rawObj.request.url);
reqUrl.addQueryParams(
rawObj.request.parameters
.filter(param => !param.disabled)
.map(param => ({ key: param.name, value: param.value }))
);

const reqOpt: RequestOptions = {
name: rawObj.request.name,
url: reqUrl,
Expand Down
6 changes: 2 additions & 4 deletions packages/insomnia-sdk/src/objects/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,10 +534,8 @@ export function mergeClientCertificates(
modified: 0,
created: 0,
isPrivate: false,
name: updatedReq.name || '',
host: updatedReq.url.getHost() || '',
disabled: updatedReq.disabled || false,

name: updatedReq.certificate.name || '',
disabled: updatedReq.certificate.disabled || false,
key: updatedReq.certificate.key?.src,
cert: updatedReq.certificate.cert?.src,
passphrase: updatedReq.certificate.passphrase || null,
Expand Down
91 changes: 91 additions & 0 deletions packages/insomnia-smoke-test/fixtures/client-certs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
_type: export
__export_format: 4
__export_date: 2024-12-11T21:49:50.826Z
__export_source: insomnia.desktop.app:v10.2.1-beta.1
resources:
- _id: req_23f466953bff448faaf482e59624b17d
parentId: wrk_78f64a950d1248599ddbfcb2dbe4a4e0
modified: 1733953779068
created: 1733930967254
url: https://localhost:4011/protected/pets/2
name: pet 2
description: ""
method: GET
body: {}
parameters: []
headers:
- name: User-Agent
value: insomnia/10.2.1-beta.1
authentication: {}
preRequestScript: console.log('yippee')
metaSortKey: -1733930967254
isPrivate: false
pathParameters: []
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global
_type: request
- _id: wrk_78f64a950d1248599ddbfcb2dbe4a4e0
parentId: null
modified: 1733930502550
created: 1733930502550
name: client-certs
description: ""
scope: collection
_type: workspace
- _id: req_01d6681fd6434cc7a75c8c4e3deee713
parentId: wrk_78f64a950d1248599ddbfcb2dbe4a4e0
modified: 1733953775398
created: 1733953597632
url: "{{_.srvr}}/protected/pets/2"
name: pet 2 with url var
description: ""
method: GET
body: {}
parameters: []
headers:
- name: User-Agent
value: insomnia/10.2.1-beta.1
authentication: {}
preRequestScript: console.log("yeehaw")
metaSortKey: -1732678181446
isPrivate: false
pathParameters: []
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global
_type: request
- _id: env_1140e4f10f8a7e3ae858474a594d0bc440e35c99
parentId: wrk_78f64a950d1248599ddbfcb2dbe4a4e0
modified: 1733953690814
created: 1733930502551
name: Base Environment
data:
srvr: https://localhost:4011
dataPropertyOrder:
"&":
- srvr
color: null
isPrivate: false
metaSortKey: 1733930502551
environmentType: kv
kvPairData:
- id: envPair_6117101ea3704c85bc3deab101603717
name: srvr
value: https://localhost:4011
type: str
enabled: true
_type: environment
- _id: jar_1140e4f10f8a7e3ae858474a594d0bc440e35c99
parentId: wrk_78f64a950d1248599ddbfcb2dbe4a4e0
modified: 1733953690813
created: 1733930502552
name: Default Jar
cookies: []
_type: cookie_jar
9 changes: 7 additions & 2 deletions packages/insomnia-smoke-test/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import crypto from 'node:crypto';

import * as bodyParser from 'body-parser';
import * as cookieParser from 'cookie-parser';
import cookieParser from 'cookie-parser';
import express from 'express';
import { readFileSync } from 'fs';
import { createHandler } from 'graphql-http/lib/use/http';
Expand All @@ -14,11 +14,12 @@ import gitlabApi from './gitlab-api';
import { schema } from './graphql';
import { startGRPCServer } from './grpc';
import insomniaApi from './insomnia-api';
import { mtlsRouter } from './mtls';
import { oauthRoutes } from './oauth';
import { startWebSocketServer } from './websocket';

const app = express();
app.use(cookieParser.default());
app.use(cookieParser());
const port = 4010;
const httpsPort = 4011;
const grpcPort = 50051;
Expand Down Expand Up @@ -64,6 +65,7 @@ app.get('/cookies', (_req, res) => {

app.use('/file', express.static('fixtures/files'));
app.use('/auth/basic', basicAuthRouter);
app.use('/protected', mtlsRouter);

githubApi(app);
gitlabApi(app);
Expand Down Expand Up @@ -129,6 +131,9 @@ startWebSocketServer(app.listen(port, () => {
startWebSocketServer(createServer({
cert: readFileSync(join(__dirname, '../fixtures/certificates/localhost.pem')),
key: readFileSync(join(__dirname, '../fixtures/certificates/localhost-key.pem')),
ca: readFileSync(join(__dirname, '../fixtures/certificates/rootCA.pem')),
requestCert: true,
rejectUnauthorized: false,
}, app).listen(httpsPort, () => {
console.log(`Listening at https://localhost:${httpsPort}`);
console.log(`Listening at wss://localhost:${httpsPort}`);
Expand Down
17 changes: 17 additions & 0 deletions packages/insomnia-smoke-test/server/mtls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import express, { NextFunction, Response } from 'express';

export const mtlsRouter = express.Router();

mtlsRouter.use(clientCertificateAuth);

async function clientCertificateAuth(req: any, res: Response, next: NextFunction) {
if (!req.client.authorized) {
return res.status(401).send({ error: 'Client certificate required' });
}

next();
}

mtlsRouter.get('/pets/:id', (req, res) => {
res.status(200).send({ id: req.params.id });
});
73 changes: 73 additions & 0 deletions packages/insomnia-smoke-test/tests/smoke/mtls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@

import path from 'node:path';

import { expect } from '@playwright/test';

import { getFixturePath, loadFixture } from '../../playwright/paths';
import { test } from '../../playwright/test';

test('can use client certificate for mTLS', async ({ app, page }) => {
const statusTag = page.locator('[data-testid="response-status-tag"]:visible');
const responseBody = page.locator('[data-testid="response-pane"] >> [data-testid="CodeEditor"]:visible', {
has: page.locator('.CodeMirror-activeline'),
});

const clientCertsCollectionText = await loadFixture('client-certs.yaml');
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), clientCertsCollectionText);

await page.getByLabel('Import').click();
await page.locator('[data-test-id="import-from-clipboard"]').click();
await page.getByRole('button', { name: 'Scan' }).click();
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
await page.getByLabel('client-certs').click();

await page.getByLabel('Request Collection').getByTestId('pet 2 with url var').press('Enter');

await page.getByRole('button', { name: 'Send', exact: true }).click();
await page.getByText('Error: SSL peer certificate or SSH remote key was not OK').click();

const fixturePath = getFixturePath('certificates');

await page.getByRole('button', { name: 'Add Certificates' }).click();

let fileChooser = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Add CA Certificate' }).click();
await (await fileChooser).setFiles(path.join(fixturePath, 'rootCA.pem'));

await page.getByRole('button', { name: 'Done' }).click();
await page.getByRole('button', { name: 'Send', exact: true }).click();

await expect(statusTag).toContainText('401 Unauthorized');
await expect(responseBody).toContainText('Client certificate required');

await page.getByRole('button', { name: 'Add Certificates' }).click();
await page.getByRole('button', { name: 'Add client certificate' }).click();
await page.locator('[name="host"]').fill('localhost');

fileChooser = page.waitForEvent('filechooser');
await page.locator('[data-test-id="add-client-certificate-file-chooser"]').click();
await (await fileChooser).setFiles(path.join(fixturePath, 'client.crt'));

fileChooser = page.waitForEvent('filechooser');
await page.locator('[data-test-id="add-client-certificate-key-file-chooser"]').click();
await (await fileChooser).setFiles(path.join(fixturePath, 'client.key'));

await page.getByRole('button', { name: 'Add certificate' }).click();
await page.getByRole('button', { name: 'Done' }).click();

await page.getByRole('button', { name: 'Send', exact: true }).click();

await expect(statusTag).toContainText('200 OK');
await expect(responseBody).toContainText('"id": "2"');

// ensure disabling the cert actually disables it
await page.getByRole('button', { name: 'Add Certificates' }).click();
await page.locator('[data-test-id="client-certificate-toggle"]').click();
await page.getByRole('button', { name: 'Done' }).click();
await page.getByLabel('Request Collection').getByTestId('pet 2').press('Enter');

await page.getByRole('button', { name: 'Send', exact: true }).click();
await expect(statusTag).toContainText('401 Unauthorized');
await expect(responseBody).toContainText('Client certificate required');

});
5 changes: 3 additions & 2 deletions packages/insomnia/src/network/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export const tryToExecutePreRequestScript = async (
globals: activeGlobalEnvironment,
userUploadEnvironment,
requestTestResults: new Array<RequestTestResult>(),
transientVariables,
};
}
const joinedScript = [...folderScripts].join('\n');
Expand Down Expand Up @@ -550,8 +551,8 @@ export const tryToInterpolateRequest = async ({
purpose?: RenderPurpose;
extraInfo?: ExtraRenderInfo;
baseEnvironment?: Environment;
userUploadEnvironment?: UserUploadEnvironment;
transientVariables?: Environment;
userUploadEnvironment?: UserUploadEnvironment;
transientVariables?: Environment;
ignoreUndefinedEnvVariable?: boolean;
}
) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ const ClientCertificateGridListItem = ({ certificate }: {
)}
<div className='flex items-center gap-2 h-6'>
<ToggleButton
data-test-id="client-certificate-toggle"
onChange={isSelected => {
updateClientCertificateFetcher.submit({ ...certificate, disabled: !isSelected }, {
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/clientcert/update`,
Expand Down
10 changes: 5 additions & 5 deletions packages/insomnia/src/ui/routes/request.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -483,14 +483,14 @@ export const sendActionImplementation = async ({
iterationCount,
transientVariables,
}: {
requestId: string;
requestId: string;
shouldPromptForPathAfterResponse: boolean | undefined;
ignoreUndefinedEnvVariable: boolean | undefined;
testResultCollector?: RunnerContextForRequest;
iteration?: number;
iterationCount?: number;
userUploadEnvironment?: UserUploadEnvironment;
transientVariables?: Environment;
iteration?: number;
iterationCount?: number;
userUploadEnvironment?: UserUploadEnvironment;
transientVariables?: Environment;
}) => {
window.main.startExecution({ requestId });
const requestData = await fetchRequestData(requestId);
Expand Down

0 comments on commit 67e40d0

Please sign in to comment.