Просмотр исходного кода

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

Shreyas 4 месяцев назад
Родитель
Сommit
75bac21b46

+ 1 - 1
packages/hoppscotch-common/src/components/settings/Agent.vue

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

+ 52 - 7
packages/hoppscotch-selfhost-desktop/src/components/settings/NativeInterceptor.vue

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

+ 38 - 5
packages/hoppscotch-selfhost-desktop/src/platform/interceptors/native/index.ts

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