Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ rootCA2.*
final.cpp
insomnia.ico
final.rc
.tmp*
46 changes: 46 additions & 0 deletions packages/insomnia/src/common/mime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const extensionToMimeType: Record<string, string> = {
csv: 'text/csv',
gif: 'image/gif',
html: 'text/html',
jpeg: 'image/jpeg',
jpg: 'image/jpeg',
js: 'application/javascript',
json: 'application/json',
pdf: 'application/pdf',
png: 'image/png',
svg: 'image/svg+xml',
txt: 'text/plain',
xml: 'application/xml',
yaml: 'application/yaml',
yml: 'application/yaml',
};

const mimeTypeToExtension: Record<string, string> = {
...Object.fromEntries(
Object.entries(extensionToMimeType).map(([extension, mimeType]) => [mimeType, extension]),
),
'application/octet-stream': 'bin',
};

export const lookupMimeType = (filePath: string) => {
const match = /\.([^.]+)$/.exec(filePath.trim().toLowerCase());
if (!match) {
return false;
}

return extensionToMimeType[match[1]] || false;
};

export const mimeTypeExtension = (contentType: string) => {
const normalizedType = contentType.split(';', 1)[0]?.trim().toLowerCase();
if (!normalizedType) {
return false;
}

if (mimeTypeToExtension[normalizedType]) {
return mimeTypeToExtension[normalizedType];
}

const subtype = normalizedType.split('/')[1];
return subtype?.split('+').pop() || false;
};
2 changes: 0 additions & 2 deletions packages/insomnia/src/main/window-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { isLinux, isMac } from '~/insomnia-data/common';
import { getAppBuildDate, getAppVersion, getProductName, isDevelopment, MNEMONIC_SYM } from '../common/constants';
import { docsBase } from '../common/documentation';
import { invariant } from '../utils/invariant';
import { AnalyticsEvent, trackAnalyticsEvent } from './analytics';
import { getElectronStorage } from './electron-storage';
import { ipcMainOn } from './ipc/electron';
import { getLogDirectory } from './log';
Expand Down Expand Up @@ -270,7 +269,6 @@ export function createWindow(): ElectronBrowserWindow {
{
label: `${MNEMONIC_SYM}Preferences`,
click: () => {
trackAnalyticsEvent(AnalyticsEvent.AppMenuPreferencesClicked);
mainBrowserWindow.webContents?.send('toggle-preferences');
},
},
Expand Down
10 changes: 4 additions & 6 deletions packages/insomnia/src/routes/auth.clear-vault-key.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import electron from 'electron';
import { getVault } from 'insomnia-api';
import { href } from 'react-router';

import { services } from '~/insomnia-data';
import { showToast } from '~/ui/components/toast-notification';
import { createFetcherSubmitHook } from '~/utils/router';

import type { Route } from './+types/auth.clear-vault-key';
Expand All @@ -23,11 +23,9 @@ export async function clientAction({ request }: Route.ClientActionArgs) {
// Update vault salt and delete vault key from session
await services.userSession.update({ vaultSalt: newVaultSalt, vaultKey: '' });
// show notification
electron.ipcRenderer.emit('show-toast', null, {
content: {
title: 'Your vault key has been reset, all you local secrets have been deleted.',
status: 'info',
},
showToast({
title: 'Your vault key has been reset, all you local secrets have been deleted.',
status: 'info',
});
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import clone from 'clone';
import { lookup } from 'mime-types';
import React, { type FC, useCallback } from 'react';
import { Toolbar } from 'react-aria-components';
import { useParams } from 'react-router';
Expand All @@ -10,6 +9,7 @@ import { CONTENT_TYPE_FORM_URLENCODED, CONTENT_TYPE_GRAPHQL, getContentTypeFromH

import { CONTENT_TYPE_FILE, CONTENT_TYPE_FORM_DATA } from '../../../../common/constants';
import { documentationLinks } from '../../../../common/documentation';
import { lookupMimeType } from '../../../../common/mime';
import { getContentTypeHeader } from '../../../../common/misc';
import { useRequestPatcher } from '../../../hooks/use-request';
import { ContentTypeDropdown } from '../../dropdowns/content-type-dropdown';
Expand Down Expand Up @@ -90,7 +90,7 @@ export const BodyEditor: FC<Props> = ({ request, environmentId }) => {

// Update Content-Type header if the user wants
const contentType = contentTypeHeader.value;
const newContentType = lookup(path) || CONTENT_TYPE_FILE;
const newContentType = lookupMimeType(path) || CONTENT_TYPE_FILE;

if (contentType !== newContentType && path) {
contentTypeHeader.value = newContentType;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { extension as mimeExtension } from 'mime-types';

import { mimeTypeExtension } from '~/common/mime';
import { jsonPrettify } from '~/utils/prettify/json';

export async function downloadResponseBody(
Expand All @@ -13,7 +12,7 @@ export async function downloadResponseBody(
}

const { contentType } = activeResponse;
const extension = mimeExtension(contentType) || 'unknown';
const extension = mimeTypeExtension(contentType) || 'unknown';
const { canceled, filePath: outputPath } = await window.dialog.showSaveDialog({
title: 'Save Response Body',
buttonLabel: 'Save',
Expand Down
7 changes: 4 additions & 3 deletions packages/insomnia/src/ui/components/tags/grpc-status-tag.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { status } from '@grpc/grpc-js';
import classnames from 'classnames';
import React, { type FC, memo } from 'react';

import { Tooltip } from '../tooltip';

const GRPC_STATUS_OK = 0;

interface Props {
statusCode?: number;
small?: boolean;
Expand All @@ -12,8 +13,8 @@ interface Props {
}

export const GrpcStatusTag: FC<Props> = memo(({ statusMessage, statusCode, small, tooltipDelay }) => {
const colorClass = statusCode === status.OK ? 'bg-success' : 'bg-danger';
const message = statusCode === status.OK ? 'OK' : statusMessage;
const colorClass = statusCode === GRPC_STATUS_OK ? 'bg-success' : 'bg-danger';
const message = statusCode === GRPC_STATUS_OK ? 'OK' : statusMessage;
return (
<div
className={classnames('tag', colorClass, {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import React, { type FC, useState } from 'react';
import { Cookie } from 'tough-cookie';

import { AnalyticsEvent } from '~/ui/analytics';
import { CookiesModal } from '~/ui/components/modals/cookies-modal';

const parseSetCookieHeader = (headerValue: string) => {
const [nameValue = ''] = headerValue.split(';');
const separatorIndex = nameValue.indexOf('=');

if (separatorIndex === -1) {
return null;
}

return {
key: nameValue.slice(0, separatorIndex).trim(),
value: nameValue.slice(separatorIndex + 1).trim(),
};
};

interface Props {
cookiesSent?: boolean | null;
cookiesStored?: boolean | null;
Expand All @@ -13,10 +26,10 @@ interface Props {
export const ResponseCookiesViewer: FC<Props> = props => {
const [isCookieModalOpen, setIsCookieModalOpen] = useState(false);
const renderRow = (h: any, i: number) => {
let cookie: Cookie | undefined | null = null;
let cookie: ReturnType<typeof parseSetCookieHeader> = null;

try {
cookie = h ? Cookie.parse(h.value || '', { loose: true }) : null;
cookie = h ? parseSetCookieHeader(h.value || '') : null;
} catch {
console.warn('Failed to parse set-cookie header', h);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { format } from 'date-fns';
import type { SaveDialogOptions } from 'electron';
import { extension as mimeExtension } from 'mime-types';
import React, { type FC, useCallback, useEffect, useState } from 'react';
import { Button } from 'react-aria-components';

import { getContentTypeFromHeaders, PREVIEW_MODE_FRIENDLY } from '~/insomnia-data/common';
import type { Part } from '~/main/multipart-buffer-to-array';

import { mimeTypeExtension } from '../../../common/mime';
import { Dropdown, DropdownItem, ItemContent } from '../base/dropdown';
import { showModal } from '../modals/index';
import { WrapperModal } from '../modals/wrapper-modal';
Expand Down Expand Up @@ -77,7 +77,7 @@ export const ResponseMultipartViewer: FC<Props> = ({
return;
}
const contentType = getContentTypeFromHeaders(selectedPart.headers, 'text/plain');
const extension = mimeExtension(contentType) || '.txt';
const extension = mimeTypeExtension(contentType) || 'txt';
const lastDir = window.localStorage.getItem('insomnia.lastExportPath');
const dir = lastDir || window.app.getPath('desktop');
const date = format(Date.now(), 'yyyy-MM-dd');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import iconv from 'iconv-lite';
import { Fragment, useCallback, useRef, useState } from 'react';

import { PREVIEW_MODE_FRIENDLY, PREVIEW_MODE_RAW } from '~/insomnia-data/common';
Expand Down Expand Up @@ -148,9 +147,9 @@ export const ResponseViewer = ({
// Show everything else as "source"
const match = _getContentType().match(/charset=([\w-]+)/);
const charset = match && match.length >= 2 ? match[1] : 'utf8';
// Sometimes iconv conversion fails so fallback to regular buffer
// Sometimes decoding fails so fallback to regular buffer
try {
return iconv.decode(overSizedBody, charset);
return new TextDecoder(charset).decode(overSizedBody);
} catch (err) {
console.warn('[response] Failed to decode body', err);
return overSizedBody.toString();
Expand Down
93 changes: 93 additions & 0 deletions packages/insomnia/src/utils/vault-crypto.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// @vitest-environment jsdom
import { describe, expect, it } from 'vitest';

import { decryptSecretValue, encryptSecretValue } from './vault-crypto';

const TEST_AES_KEY: JsonWebKey = {
kty: 'oct',
alg: 'A256GCM',
ext: true,
key_ops: ['encrypt', 'decrypt'],
k: '5hs1f2xuiNPHUp11i6SWlsqYpWe_hWPcEKucZlwBfFE',
};

describe('encryptSecretValue', () => {
it('returns rawValue when symmetricKey is not an object', () => {
expect(encryptSecretValue('secret', 'invalid' as unknown as JsonWebKey)).toBe('secret');
});

it('returns rawValue when symmetricKey is empty object', () => {
expect(encryptSecretValue('secret', {})).toBe('secret');
});

it('encrypts the value with a valid key', () => {
const encrypted = encryptSecretValue('my secret', TEST_AES_KEY);
expect(typeof encrypted).toBe('string');
expect(encrypted).not.toBe('my secret');
});

it('returns original value when encryption fails', () => {
// Use an invalid key format
const invalidKey = { kty: 'oct', k: 'invalid' };
const encrypted = encryptSecretValue('my secret', invalidKey as unknown as JsonWebKey);
expect(encrypted).toBe('my secret');
});
});

describe('decryptSecretValue', () => {
it('returns encryptedValue when symmetricKey is not an object', () => {
expect(decryptSecretValue('encrypted', 'invalid' as unknown as JsonWebKey)).toBe('encrypted');
});

it('returns encryptedValue when symmetricKey is empty object', () => {
expect(decryptSecretValue('encrypted', {})).toBe('encrypted');
});

it('round-trips encrypt then decrypt', () => {
const plaintext = 'my secret value';
const encrypted = encryptSecretValue(plaintext, TEST_AES_KEY);
const decrypted = decryptSecretValue(encrypted, TEST_AES_KEY);
expect(decrypted).toBe(plaintext);
});

it('returns original value when decryption fails', () => {
// Use an invalid encrypted value
const encrypted = encryptSecretValue('my secret', TEST_AES_KEY);
// Try to decrypt with wrong key
const wrongKey = {
kty: 'oct',
alg: 'A256GCM',
k: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
};
const result = decryptSecretValue(encrypted, wrongKey);
expect(result).toBe(encrypted);
});

it('handles special characters in plaintext', () => {
const plaintext = 'special chars: !@#$%^&*()_+-=[]{}|;:,.<>?/~`';
const encrypted = encryptSecretValue(plaintext, TEST_AES_KEY);
const decrypted = decryptSecretValue(encrypted, TEST_AES_KEY);
expect(decrypted).toBe(plaintext);
});

it('handles unicode characters in plaintext', () => {
const plaintext = 'unicode: 你好世界 🚀 مرحبا العالم';
const encrypted = encryptSecretValue(plaintext, TEST_AES_KEY);
const decrypted = decryptSecretValue(encrypted, TEST_AES_KEY);
expect(decrypted).toBe(plaintext);
});

it('handles empty string', () => {
const plaintext = '';
const encrypted = encryptSecretValue(plaintext, TEST_AES_KEY);
const decrypted = decryptSecretValue(encrypted, TEST_AES_KEY);
expect(decrypted).toBe(plaintext);
});

it('handles large plaintext', () => {
const plaintext = 'x'.repeat(10_000);
const encrypted = encryptSecretValue(plaintext, TEST_AES_KEY);
const decrypted = decryptSecretValue(encrypted, TEST_AES_KEY);
expect(decrypted).toBe(plaintext);
});
});
Loading
Loading