feat(desktop): add CA cert and HTTP proxy support for native interceptor (#4491)

This commit is contained in:
Shreyas
2024-10-29 17:13:32 +05:30
committed by GitHub
parent 4b2f04df82
commit 75bac21b46
3 changed files with 91 additions and 13 deletions

View File

@@ -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,

View File

@@ -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>

View File

@@ -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 {