Skip to content
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: 4 additions & 0 deletions src/main/i18n/locales/en_US/preferences.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@
"autoSwitchToResponse": {
"label": "Show Response After Send",
"description": "Switch the bottom panel to Response when a request starts or finishes."
},
"sslCertificateVerification": {
"label": "SSL Certificate Verification",
"description": "Verify SSL certificates when sending HTTPS requests. Turn off to allow invalid or self-signed certificates."
}
},
"api": {
Expand Down
4 changes: 4 additions & 0 deletions src/main/i18n/locales/ru_RU/preferences.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@
"autoSwitchToResponse": {
"label": "Показывать ответ после отправки",
"description": "Переключать нижнюю панель на Response, когда запрос начинается или завершается."
},
"sslCertificateVerification": {
"label": "Проверка SSL-сертификата",
"description": "Проверять SSL-сертификаты при отправке HTTPS-запросов. Отключите, чтобы разрешить недействительные или самоподписанные сертификаты."
}
},
"api": {
Expand Down
75 changes: 72 additions & 3 deletions src/main/ipc/handlers/__tests__/http.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
import { Readable } from 'node:stream'
import { describe, expect, it, vi } from 'vitest'
import { formatHttpRequestError } from '../http'
import { formatHttpRequestError, registerHttpHandlers } from '../http'

const { AgentMock, handleMock, requestMock } = vi.hoisted(() => ({
AgentMock: class {
options: unknown

constructor(options: unknown) {
this.options = options
}
},
handleMock: vi.fn(),
requestMock: vi.fn(),
}))

vi.mock('electron', () => ({
ipcMain: {
handle: vi.fn(),
handle: handleMock,
},
}))

vi.mock('undici', () => ({
Agent: vi.fn(AgentMock),
request: requestMock,
}))

vi.mock('../../../storage', () => ({
useHttpStorage: vi.fn(),
useHttpStorage: () => ({
environments: {
getEnvironments: () => [],
},
history: {
appendEntry: vi.fn(),
},
}),
}))

describe('formatHttpRequestError', () => {
Expand Down Expand Up @@ -67,3 +92,47 @@ describe('formatHttpRequestError', () => {
)
})
})

describe('registerHttpHandlers', () => {
it('uses an insecure dispatcher when certificate verification is skipped', async () => {
requestMock.mockResolvedValueOnce({
body: Readable.from([]),
headers: {},
statusCode: 200,
})

registerHttpHandlers()
const handler = handleMock.mock.calls.find(
([channel]) => channel === 'spaces:http:execute',
)?.[1]

await handler(null, {
environmentId: null,
request: {
auth: { type: 'none' },
body: null,
bodyType: 'none',
formData: [],
headers: [],
method: 'GET',
query: [],
url: 'https://example.test',
},
requestId: null,
skipCertificateVerification: true,
})

expect(requestMock).toHaveBeenCalledWith(
'https://example.test/',
expect.objectContaining({
dispatcher: expect.objectContaining({
options: {
connect: {
rejectUnauthorized: false,
},
},
}),
}),
)
})
})
10 changes: 9 additions & 1 deletion src/main/ipc/handlers/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@ import { Buffer } from 'node:buffer'
import { readFileSync } from 'node:fs'
import { basename } from 'node:path'
import { ipcMain } from 'electron'
import { request as undiciRequest } from 'undici'
import { Agent, request as undiciRequest } from 'undici'
import { interpolateHttpVariables } from '../../../shared/httpVariables'
import { useHttpStorage } from '../../storage'

const RESPONSE_BODY_CAP_BYTES = 10 * 1024 * 1024
const DEFAULT_TIMEOUT_MS = 30_000
const insecureCertificateDispatcher = new Agent({
connect: {
rejectUnauthorized: false,
},
})

export function interpolate(
template: string,
Expand Down Expand Up @@ -426,6 +431,9 @@ async function executeHttpRequest(
body: built.body as Dispatcher.DispatchOptions['body'],
signal: controller.signal,
maxRedirections: 5,
...(payload.skipCertificateVerification
? { dispatcher: insecureCertificateDispatcher }
: {}),
})

const { buffer, sizeBytes, truncated } = await readBodyCapped(
Expand Down
8 changes: 8 additions & 0 deletions src/main/store/__tests__/preferences.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ describe('preferences store sanitization', () => {
wrapLines: false,
defaultPreviewFormat: 'curl',
autoSwitchToResponse: false,
skipCertificateVerification: true,
garbage: 'bad',
},
}
Expand All @@ -185,6 +186,9 @@ describe('preferences store sanitization', () => {
expect(preferences.get('http.wrapLines' as any)).toBe(false)
expect(preferences.get('http.defaultPreviewFormat' as any)).toBe('curl')
expect(preferences.get('http.autoSwitchToResponse' as any)).toBe(false)
expect(preferences.get('http.skipCertificateVerification' as any)).toBe(
true,
)
expect(preferences.get('http.garbage' as any)).toBeUndefined()
})

Expand All @@ -194,6 +198,7 @@ describe('preferences store sanitization', () => {
wrapLines: 'bad',
defaultPreviewFormat: 'bad',
autoSwitchToResponse: 'bad',
skipCertificateVerification: 'bad',
},
}

Expand All @@ -202,6 +207,9 @@ describe('preferences store sanitization', () => {
expect(preferences.get('http.wrapLines' as any)).toBe(true)
expect(preferences.get('http.defaultPreviewFormat' as any)).toBe('http')
expect(preferences.get('http.autoSwitchToResponse' as any)).toBe(true)
expect(preferences.get('http.skipCertificateVerification' as any)).toBe(
false,
)
})

it('keeps sanitized API integration settings', async () => {
Expand Down
5 changes: 5 additions & 0 deletions src/main/store/module/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const HTTP_DEFAULTS: HttpSettings = {
wrapLines: true,
defaultPreviewFormat: 'http',
autoSwitchToResponse: true,
skipCertificateVerification: false,
}

const API_INTEGRATIONS_DEFAULTS: PreferencesStore['api']['integrations'] = {
Expand Down Expand Up @@ -226,6 +227,10 @@ function sanitizeHttpSettings(value: unknown): HttpSettings {
typeof source.autoSwitchToResponse === 'boolean'
? source.autoSwitchToResponse
: HTTP_DEFAULTS.autoSwitchToResponse,
skipCertificateVerification:
typeof source.skipCertificateVerification === 'boolean'
? source.skipCertificateVerification
: HTTP_DEFAULTS.skipCertificateVerification,
}
}

Expand Down
1 change: 1 addition & 0 deletions src/main/store/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export interface HttpSettings {
wrapLines: boolean
defaultPreviewFormat: 'http' | 'curl'
autoSwitchToResponse: boolean
skipCertificateVerification: boolean
}

export interface PreferencesStore {
Expand Down
1 change: 1 addition & 0 deletions src/main/types/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface HttpExecutePayload {
request: HttpExecuteRequest
requestId: number | null
environmentId: number | null
skipCertificateVerification?: boolean
timeoutMs?: number
}

Expand Down
14 changes: 14 additions & 0 deletions src/renderer/components/preferences/Http.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ const { settings } = useHttpSettings()
{{ i18n.t("preferences:http.autoSwitchToResponse.description") }}
</template>
</UiMenuFormItem>

<UiMenuFormItem
:label="i18n.t('preferences:http.sslCertificateVerification.label')"
>
<Switch
:checked="!settings.skipCertificateVerification"
@update:checked="settings.skipCertificateVerification = !$event"
/>
<template #description>
{{
i18n.t("preferences:http.sslCertificateVerification.description")
}}
</template>
</UiMenuFormItem>
</UiMenuFormSection>
</div>
</template>
3 changes: 3 additions & 0 deletions src/renderer/composables/spaces/http/useHttpExecute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { markPersistedStorageMutation } from '@/composables/useStorageMutation'
import { ipc } from '@/electron'
import { useHttpEnvironments } from './useHttpEnvironments'
import { useHttpRequests } from './useHttpRequests'
import { useHttpSettings } from './useHttpSettings'

export type HttpResponse = HttpExecuteResult

Expand All @@ -18,6 +19,7 @@ const lastError = ref<string | null>(null)
const { currentDraft, currentRequest } = useHttpRequests()
const { activeEnvironmentId } = useHttpEnvironments()
const { incrementSent } = useDonations()
const { settings } = useHttpSettings()

function buildExecuteRequest(): HttpExecuteRequest | null {
const draft = currentDraft.value
Expand Down Expand Up @@ -45,6 +47,7 @@ async function executeCurrentRequest(): Promise<HttpResponse | null> {
request,
requestId: currentRequest.value?.id ?? null,
environmentId: activeEnvironmentId.value,
skipCertificateVerification: settings.skipCertificateVerification,
}

isExecuting.value = true
Expand Down