feat(desktop): add CA cert and HTTP proxy support for native interceptor (#4491)
This commit is contained in:
@@ -56,11 +56,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- TODO: i18n -->
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from "vue"
|
import { computed, ref } from "vue"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import IconLucideFileKey from "~icons/lucide/file-key"
|
import IconLucideFileKey from "~icons/lucide/file-key"
|
||||||
|
import IconLucideFileBadge from "~icons/lucide/file-badge"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
import {
|
import {
|
||||||
RequestDef,
|
RequestDef,
|
||||||
|
|||||||
@@ -9,14 +9,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<!--
|
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:icon="IconLucideFileBadge"
|
:icon="IconLucideFileBadge"
|
||||||
:label="'CA Certificates'"
|
:label="'CA Certificates'"
|
||||||
outline
|
outline
|
||||||
@click="showCACertificatesModal = true"
|
@click="showCACertificatesModal = true"
|
||||||
/>
|
/>
|
||||||
-->
|
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:icon="IconLucideFileKey"
|
:icon="IconLucideFileKey"
|
||||||
:label="'Client Certificates'"
|
:label="'Client Certificates'"
|
||||||
@@ -25,31 +23,78 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--
|
|
||||||
<ModalsNativeCACertificates
|
<ModalsNativeCACertificates
|
||||||
:show="showCACertificatesModal"
|
:show="showCACertificatesModal"
|
||||||
@hide-modal="showCACertificatesModal = false"
|
@hide-modal="showCACertificatesModal = false"
|
||||||
/>
|
/>
|
||||||
-->
|
|
||||||
<ModalsNativeClientCertificates
|
<ModalsNativeClientCertificates
|
||||||
:show="showClientCertificatesModal"
|
:show="showClientCertificatesModal"
|
||||||
@hide-modal="showClientCertificatesModal = false"
|
@hide-modal="showClientCertificatesModal = false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="pt-4 space-y-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<HoppSmartToggle :on="allowProxy" @change="allowProxy = !allowProxy" />
|
||||||
|
Use HTTP Proxy
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HoppSmartInput
|
||||||
|
v-if="allowProxy"
|
||||||
|
v-model="proxyURL"
|
||||||
|
:autofocus="false"
|
||||||
|
styles="flex-1"
|
||||||
|
placeholder=" "
|
||||||
|
:label="'Proxy URL'"
|
||||||
|
input-styles="input floating-input"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p class="my-1 text-secondaryLight">
|
||||||
|
Hoppscotch native interceptor supports HTTP/HTTPS/SOCKS proxies along with NTLM and Basic Auth in those proxies. Include the username and password for the proxy authentication in the URL itself.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- TODO: i18n -->
|
<!-- TODO: i18n -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue"
|
import { computed, ref } from "vue"
|
||||||
import IconLucideFileBadge from "~icons/lucide/file-badge"
|
import IconLucideFileBadge from "~icons/lucide/file-badge"
|
||||||
import IconLucideFileKey from "~icons/lucide/file-key"
|
import IconLucideFileKey from "~icons/lucide/file-key"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
import { NativeInterceptorService } from "@platform/interceptors/native"
|
import { RequestDef, NativeInterceptorService } from "@platform/interceptors/native"
|
||||||
|
import { syncRef } from "@vueuse/core"
|
||||||
|
|
||||||
|
type RequestProxyInfo = RequestDef["proxy"]
|
||||||
|
|
||||||
const nativeInterceptorService = useService(NativeInterceptorService)
|
const nativeInterceptorService = useService(NativeInterceptorService)
|
||||||
|
|
||||||
const allowSSLVerification = nativeInterceptorService.validateCerts
|
const allowSSLVerification = nativeInterceptorService.validateCerts
|
||||||
|
|
||||||
// const showCACertificatesModal = ref(false)
|
const showCACertificatesModal = ref(false)
|
||||||
const showClientCertificatesModal = ref(false)
|
const showClientCertificatesModal = ref(false)
|
||||||
|
|
||||||
|
const allowProxy = ref(false)
|
||||||
|
const proxyURL = ref("")
|
||||||
|
|
||||||
|
const proxyInfo = computed<RequestProxyInfo>({
|
||||||
|
get() {
|
||||||
|
if (allowProxy.value) {
|
||||||
|
return {
|
||||||
|
url: proxyURL.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
|
set(newData) {
|
||||||
|
if (newData) {
|
||||||
|
allowProxy.value = true
|
||||||
|
proxyURL.value = newData.url
|
||||||
|
} else {
|
||||||
|
allowProxy.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
syncRef(nativeInterceptorService.proxyInfo, proxyInfo, { direction: "both" })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ type ClientCertDef =
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Figure out a way to autogen this from the interceptor definition on the Rust side
|
// TODO: Figure out a way to autogen this from the interceptor definition on the Rust side
|
||||||
type RequestDef = {
|
export type RequestDef = {
|
||||||
req_id: number
|
req_id: number
|
||||||
|
|
||||||
method: string
|
method: string
|
||||||
@@ -65,6 +65,10 @@ type RequestDef = {
|
|||||||
validate_certs: boolean,
|
validate_certs: boolean,
|
||||||
root_cert_bundle_files: number[],
|
root_cert_bundle_files: number[],
|
||||||
client_cert: ClientCertDef | null
|
client_cert: ClientCertDef | null
|
||||||
|
|
||||||
|
proxy?: {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type RunRequestResponse = {
|
type RunRequestResponse = {
|
||||||
@@ -177,7 +181,8 @@ async function convertToRequestDef(
|
|||||||
reqID: number,
|
reqID: number,
|
||||||
caCertificates: CACertificateEntry[],
|
caCertificates: CACertificateEntry[],
|
||||||
clientCertificates: Map<string, ClientCertificateEntry>,
|
clientCertificates: Map<string, ClientCertificateEntry>,
|
||||||
validateCerts: boolean
|
validateCerts: boolean,
|
||||||
|
proxyInfo: RequestDef["proxy"]
|
||||||
): Promise<RequestDef> {
|
): Promise<RequestDef> {
|
||||||
const clientCertDomain = getURLDomain(axiosReq.url!)
|
const clientCertDomain = getURLDomain(axiosReq.url!)
|
||||||
|
|
||||||
@@ -188,14 +193,21 @@ async function convertToRequestDef(
|
|||||||
method: axiosReq.method ?? "GET",
|
method: axiosReq.method ?? "GET",
|
||||||
endpoint: axiosReq.url ?? "",
|
endpoint: axiosReq.url ?? "",
|
||||||
headers: Object.entries(axiosReq.headers ?? {})
|
headers: Object.entries(axiosReq.headers ?? {})
|
||||||
.filter(([key, value]) => !(key.toLowerCase() === "content-type" && value.toLowerCase() === "multipart/form-data")) // Removing header, because this header will be set by reqwest
|
.filter(
|
||||||
|
([key, value]) =>
|
||||||
|
!(
|
||||||
|
key.toLowerCase() === "content-type" &&
|
||||||
|
value.toLowerCase() === "multipart/form-data"
|
||||||
|
)
|
||||||
|
) // Removing header, because this header will be set by relay.
|
||||||
.map(([key, value]): KeyValuePair => ({ key, value })),
|
.map(([key, value]): KeyValuePair => ({ key, value })),
|
||||||
parameters: Object.entries(axiosReq.params as Record<string, string> ?? {})
|
parameters: Object.entries(axiosReq.params as Record<string, string> ?? {})
|
||||||
.map(([key, value]): KeyValuePair => ({ key, value })),
|
.map(([key, value]): KeyValuePair => ({ key, value })),
|
||||||
body: await processBody(axiosReq),
|
body: await processBody(axiosReq),
|
||||||
root_cert_bundle_files: caCertificates.map((cert) => Array.from(cert.certificate)),
|
root_cert_bundle_files: caCertificates.map((cert) => Array.from(cert.certificate)),
|
||||||
validate_certs: validateCerts,
|
validate_certs: validateCerts,
|
||||||
client_cert: clientCert ? convertClientCertToDefCert(clientCert) : null
|
client_cert: clientCert ? convertClientCertToDefCert(clientCert) : null,
|
||||||
|
proxy: proxyInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +248,7 @@ export type ClientCertificateEntry = z.infer<typeof ClientCertificateEntry>
|
|||||||
const CA_STORE_PERSIST_KEY = "native_interceptor_ca_store"
|
const CA_STORE_PERSIST_KEY = "native_interceptor_ca_store"
|
||||||
const CLIENT_CERTS_PERSIST_KEY = "native_interceptor_client_certs_store"
|
const CLIENT_CERTS_PERSIST_KEY = "native_interceptor_client_certs_store"
|
||||||
const VALIDATE_SSL_KEY = "native_interceptor_validate_ssl"
|
const VALIDATE_SSL_KEY = "native_interceptor_validate_ssl"
|
||||||
|
const PROXY_INFO_PERSIST_KEY = "native_interceptor_proxy_info"
|
||||||
|
|
||||||
export class NativeInterceptorService extends Service implements Interceptor {
|
export class NativeInterceptorService extends Service implements Interceptor {
|
||||||
public static readonly ID = "NATIVE_INTERCEPTOR_SERVICE"
|
public static readonly ID = "NATIVE_INTERCEPTOR_SERVICE"
|
||||||
@@ -262,6 +275,7 @@ export class NativeInterceptorService extends Service implements Interceptor {
|
|||||||
|
|
||||||
public clientCertificates = ref<Map<string, ClientCertificateEntry>>(new Map())
|
public clientCertificates = ref<Map<string, ClientCertificateEntry>>(new Map())
|
||||||
public validateCerts = ref(true)
|
public validateCerts = ref(true)
|
||||||
|
public proxyInfo = ref<RequestDef["proxy"]>(undefined)
|
||||||
|
|
||||||
override onServiceInit() {
|
override onServiceInit() {
|
||||||
// Load SSL Validation
|
// Load SSL Validation
|
||||||
@@ -273,6 +287,17 @@ export class NativeInterceptorService extends Service implements Interceptor {
|
|||||||
this.validateCerts.value = persistedValidateSSL
|
this.validateCerts.value = persistedValidateSSL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const persistedProxyInfo = this.persistenceService.getLocalConfig(
|
||||||
|
PROXY_INFO_PERSIST_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
if (persistedProxyInfo && persistedProxyInfo !== "null") {
|
||||||
|
try {
|
||||||
|
const proxyInfo = JSON.parse(persistedProxyInfo)
|
||||||
|
this.proxyInfo.value = proxyInfo
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
watch(this.validateCerts, () => {
|
watch(this.validateCerts, () => {
|
||||||
this.persistenceService.setLocalConfig(VALIDATE_SSL_KEY, JSON.stringify(this.validateCerts.value))
|
this.persistenceService.setLocalConfig(VALIDATE_SSL_KEY, JSON.stringify(this.validateCerts.value))
|
||||||
})
|
})
|
||||||
@@ -390,6 +415,13 @@ export class NativeInterceptorService extends Service implements Interceptor {
|
|||||||
|
|
||||||
this.persistenceService.setLocalConfig(CLIENT_CERTS_PERSIST_KEY, JSON.stringify(storableValue))
|
this.persistenceService.setLocalConfig(CLIENT_CERTS_PERSIST_KEY, JSON.stringify(storableValue))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(this.proxyInfo, (newProxyInfo) => {
|
||||||
|
this.persistenceService.setLocalConfig(
|
||||||
|
PROXY_INFO_PERSIST_KEY,
|
||||||
|
JSON.stringify(newProxyInfo) ?? "null"
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public runRequest(req: AxiosRequestConfig): RequestRunResult<InterceptorError> {
|
public runRequest(req: AxiosRequestConfig): RequestRunResult<InterceptorError> {
|
||||||
@@ -417,7 +449,8 @@ export class NativeInterceptorService extends Service implements Interceptor {
|
|||||||
reqID,
|
reqID,
|
||||||
this.caCertificates.value,
|
this.caCertificates.value,
|
||||||
this.clientCertificates.value,
|
this.clientCertificates.value,
|
||||||
this.validateCerts.value
|
this.validateCerts.value,
|
||||||
|
this.proxyInfo.value
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user