Skip to content

Commit 6591f19

Browse files
feat(http): add SSL certificate verification setting (#810)
1 parent 40119b9 commit 6591f19

10 files changed

Lines changed: 121 additions & 4 deletions

File tree

src/main/i18n/locales/en_US/preferences.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@
163163
"autoSwitchToResponse": {
164164
"label": "Show Response After Send",
165165
"description": "Switch the bottom panel to Response when a request starts or finishes."
166+
},
167+
"sslCertificateVerification": {
168+
"label": "SSL Certificate Verification",
169+
"description": "Verify SSL certificates when sending HTTPS requests. Turn off to allow invalid or self-signed certificates."
166170
}
167171
},
168172
"api": {

src/main/i18n/locales/ru_RU/preferences.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@
130130
"autoSwitchToResponse": {
131131
"label": "Показывать ответ после отправки",
132132
"description": "Переключать нижнюю панель на Response, когда запрос начинается или завершается."
133+
},
134+
"sslCertificateVerification": {
135+
"label": "Проверка SSL-сертификата",
136+
"description": "Проверять SSL-сертификаты при отправке HTTPS-запросов. Отключите, чтобы разрешить недействительные или самоподписанные сертификаты."
133137
}
134138
},
135139
"api": {

src/main/ipc/handlers/__tests__/http.test.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,39 @@
1+
import { Readable } from 'node:stream'
12
import { describe, expect, it, vi } from 'vitest'
2-
import { formatHttpRequestError } from '../http'
3+
import { formatHttpRequestError, registerHttpHandlers } from '../http'
4+
5+
const { AgentMock, handleMock, requestMock } = vi.hoisted(() => ({
6+
AgentMock: class {
7+
options: unknown
8+
9+
constructor(options: unknown) {
10+
this.options = options
11+
}
12+
},
13+
handleMock: vi.fn(),
14+
requestMock: vi.fn(),
15+
}))
316

417
vi.mock('electron', () => ({
518
ipcMain: {
6-
handle: vi.fn(),
19+
handle: handleMock,
720
},
821
}))
922

23+
vi.mock('undici', () => ({
24+
Agent: vi.fn(AgentMock),
25+
request: requestMock,
26+
}))
27+
1028
vi.mock('../../../storage', () => ({
11-
useHttpStorage: vi.fn(),
29+
useHttpStorage: () => ({
30+
environments: {
31+
getEnvironments: () => [],
32+
},
33+
history: {
34+
appendEntry: vi.fn(),
35+
},
36+
}),
1237
}))
1338

1439
describe('formatHttpRequestError', () => {
@@ -67,3 +92,47 @@ describe('formatHttpRequestError', () => {
6792
)
6893
})
6994
})
95+
96+
describe('registerHttpHandlers', () => {
97+
it('uses an insecure dispatcher when certificate verification is skipped', async () => {
98+
requestMock.mockResolvedValueOnce({
99+
body: Readable.from([]),
100+
headers: {},
101+
statusCode: 200,
102+
})
103+
104+
registerHttpHandlers()
105+
const handler = handleMock.mock.calls.find(
106+
([channel]) => channel === 'spaces:http:execute',
107+
)?.[1]
108+
109+
await handler(null, {
110+
environmentId: null,
111+
request: {
112+
auth: { type: 'none' },
113+
body: null,
114+
bodyType: 'none',
115+
formData: [],
116+
headers: [],
117+
method: 'GET',
118+
query: [],
119+
url: 'https://example.test',
120+
},
121+
requestId: null,
122+
skipCertificateVerification: true,
123+
})
124+
125+
expect(requestMock).toHaveBeenCalledWith(
126+
'https://example.test/',
127+
expect.objectContaining({
128+
dispatcher: expect.objectContaining({
129+
options: {
130+
connect: {
131+
rejectUnauthorized: false,
132+
},
133+
},
134+
}),
135+
}),
136+
)
137+
})
138+
})

src/main/ipc/handlers/http.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@ import { Buffer } from 'node:buffer'
1616
import { readFileSync } from 'node:fs'
1717
import { basename } from 'node:path'
1818
import { ipcMain } from 'electron'
19-
import { request as undiciRequest } from 'undici'
19+
import { Agent, request as undiciRequest } from 'undici'
2020
import { interpolateHttpVariables } from '../../../shared/httpVariables'
2121
import { useHttpStorage } from '../../storage'
2222

2323
const RESPONSE_BODY_CAP_BYTES = 10 * 1024 * 1024
2424
const DEFAULT_TIMEOUT_MS = 30_000
25+
const insecureCertificateDispatcher = new Agent({
26+
connect: {
27+
rejectUnauthorized: false,
28+
},
29+
})
2530

2631
export function interpolate(
2732
template: string,
@@ -426,6 +431,9 @@ async function executeHttpRequest(
426431
body: built.body as Dispatcher.DispatchOptions['body'],
427432
signal: controller.signal,
428433
maxRedirections: 5,
434+
...(payload.skipCertificateVerification
435+
? { dispatcher: insecureCertificateDispatcher }
436+
: {}),
429437
})
430438

431439
const { buffer, sizeBytes, truncated } = await readBodyCapped(

src/main/store/__tests__/preferences.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ describe('preferences store sanitization', () => {
176176
wrapLines: false,
177177
defaultPreviewFormat: 'curl',
178178
autoSwitchToResponse: false,
179+
skipCertificateVerification: true,
179180
garbage: 'bad',
180181
},
181182
}
@@ -185,6 +186,9 @@ describe('preferences store sanitization', () => {
185186
expect(preferences.get('http.wrapLines' as any)).toBe(false)
186187
expect(preferences.get('http.defaultPreviewFormat' as any)).toBe('curl')
187188
expect(preferences.get('http.autoSwitchToResponse' as any)).toBe(false)
189+
expect(preferences.get('http.skipCertificateVerification' as any)).toBe(
190+
true,
191+
)
188192
expect(preferences.get('http.garbage' as any)).toBeUndefined()
189193
})
190194

@@ -194,6 +198,7 @@ describe('preferences store sanitization', () => {
194198
wrapLines: 'bad',
195199
defaultPreviewFormat: 'bad',
196200
autoSwitchToResponse: 'bad',
201+
skipCertificateVerification: 'bad',
197202
},
198203
}
199204

@@ -202,6 +207,9 @@ describe('preferences store sanitization', () => {
202207
expect(preferences.get('http.wrapLines' as any)).toBe(true)
203208
expect(preferences.get('http.defaultPreviewFormat' as any)).toBe('http')
204209
expect(preferences.get('http.autoSwitchToResponse' as any)).toBe(true)
210+
expect(preferences.get('http.skipCertificateVerification' as any)).toBe(
211+
false,
212+
)
205213
})
206214

207215
it('keeps sanitized API integration settings', async () => {

src/main/store/module/preferences.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const HTTP_DEFAULTS: HttpSettings = {
3232
wrapLines: true,
3333
defaultPreviewFormat: 'http',
3434
autoSwitchToResponse: true,
35+
skipCertificateVerification: false,
3536
}
3637

3738
const API_INTEGRATIONS_DEFAULTS: PreferencesStore['api']['integrations'] = {
@@ -226,6 +227,10 @@ function sanitizeHttpSettings(value: unknown): HttpSettings {
226227
typeof source.autoSwitchToResponse === 'boolean'
227228
? source.autoSwitchToResponse
228229
: HTTP_DEFAULTS.autoSwitchToResponse,
230+
skipCertificateVerification:
231+
typeof source.skipCertificateVerification === 'boolean'
232+
? source.skipCertificateVerification
233+
: HTTP_DEFAULTS.skipCertificateVerification,
229234
}
230235
}
231236

src/main/store/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ export interface HttpSettings {
192192
wrapLines: boolean
193193
defaultPreviewFormat: 'http' | 'curl'
194194
autoSwitchToResponse: boolean
195+
skipCertificateVerification: boolean
195196
}
196197

197198
export interface PreferencesStore {

src/main/types/http.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export interface HttpExecutePayload {
5858
request: HttpExecuteRequest
5959
requestId: number | null
6060
environmentId: number | null
61+
skipCertificateVerification?: boolean
6162
timeoutMs?: number
6263
}
6364

src/renderer/components/preferences/Http.vue

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ const { settings } = useHttpSettings()
5252
{{ i18n.t("preferences:http.autoSwitchToResponse.description") }}
5353
</template>
5454
</UiMenuFormItem>
55+
56+
<UiMenuFormItem
57+
:label="i18n.t('preferences:http.sslCertificateVerification.label')"
58+
>
59+
<Switch
60+
:checked="!settings.skipCertificateVerification"
61+
@update:checked="settings.skipCertificateVerification = !$event"
62+
/>
63+
<template #description>
64+
{{
65+
i18n.t("preferences:http.sslCertificateVerification.description")
66+
}}
67+
</template>
68+
</UiMenuFormItem>
5569
</UiMenuFormSection>
5670
</div>
5771
</template>

src/renderer/composables/spaces/http/useHttpExecute.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { markPersistedStorageMutation } from '@/composables/useStorageMutation'
88
import { ipc } from '@/electron'
99
import { useHttpEnvironments } from './useHttpEnvironments'
1010
import { useHttpRequests } from './useHttpRequests'
11+
import { useHttpSettings } from './useHttpSettings'
1112

1213
export type HttpResponse = HttpExecuteResult
1314

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

2224
function buildExecuteRequest(): HttpExecuteRequest | null {
2325
const draft = currentDraft.value
@@ -45,6 +47,7 @@ async function executeCurrentRequest(): Promise<HttpResponse | null> {
4547
request,
4648
requestId: currentRequest.value?.id ?? null,
4749
environmentId: activeEnvironmentId.value,
50+
skipCertificateVerification: settings.skipCertificateVerification,
4851
}
4952

5053
isExecuting.value = true

0 commit comments

Comments
 (0)