diff --git a/.changes/http-proxy-config.md b/.changes/http-proxy-config.md new file mode 100644 index 00000000..27e889a5 --- /dev/null +++ b/.changes/http-proxy-config.md @@ -0,0 +1,6 @@ +--- +"http": minor +"http-js": minor +--- + +Add `proxy` field to `fetch` options to configure proxy. \ No newline at end of file diff --git a/plugins/http/guest-js/index.ts b/plugins/http/guest-js/index.ts index 937388d1..f40ddea0 100644 --- a/plugins/http/guest-js/index.ts +++ b/plugins/http/guest-js/index.ts @@ -26,6 +26,45 @@ import { invoke } from "@tauri-apps/api/core"; +/** + * Configuration of a proxy that a Client should pass requests to. + * + * @since 2.0.0 + */ +export type Proxy = { + /** + * Proxy all traffic to the passed URL. + */ + all?: string | ProxyConfig; + /** + * Proxy all HTTP traffic to the passed URL. + */ + http?: string | ProxyConfig; + /** + * Proxy all HTTPS traffic to the passed URL. + */ + https?: string | ProxyConfig; +}; + +export interface ProxyConfig { + /** + * The URL of the proxy server. + */ + url: string; + /** + * Set the `Proxy-Authorization` header using Basic auth. + */ + basicAuth?: { + username: string; + password: string; + }; + /** + * A configuration for filtering out requests that shouldn’t be proxied. + * Entries are expected to be comma-separated (whitespace between entries is ignored) + */ + noProxy?: string; +} + /** * Options to configure the Rust client used to make fetch requests * @@ -39,6 +78,10 @@ export interface ClientOptions { maxRedirections?: number; /** Timeout in milliseconds */ connectTimeout?: number; + /** + * Configuration of a proxy that a Client should pass requests to. + */ + proxy?: Proxy; } /** @@ -61,11 +104,13 @@ export async function fetch( ): Promise { const maxRedirections = init?.maxRedirections; const connectTimeout = init?.maxRedirections; + const proxy = init?.proxy; // Remove these fields before creating the request if (init) { delete init.maxRedirections; delete init.connectTimeout; + delete init.proxy; } const req = new Request(input, init); @@ -73,12 +118,15 @@ export async function fetch( const reqData = buffer.byteLength ? Array.from(new Uint8Array(buffer)) : null; const rid = await invoke("plugin:http|fetch", { - method: req.method, - url: req.url, - headers: Array.from(req.headers.entries()), - data: reqData, - maxRedirections, - connectTimeout, + clientConfig: { + method: req.method, + url: req.url, + headers: Array.from(req.headers.entries()), + data: reqData, + maxRedirections, + connectTimeout, + proxy, + }, }); req.signal.addEventListener("abort", () => { diff --git a/plugins/http/src/api-iife.js b/plugins/http/src/api-iife.js index 90b0ae7d..b37e5c08 100644 --- a/plugins/http/src/api-iife.js +++ b/plugins/http/src/api-iife.js @@ -1 +1 @@ -if("__TAURI__"in window){var __TAURI_PLUGIN_HTTP__=function(e){"use strict";async function t(e,t={},r){return window.__TAURI_INTERNALS__.invoke(e,t,r)}return"function"==typeof SuppressedError&&SuppressedError,e.fetch=async function(e,r){const n=r?.maxRedirections,i=r?.maxRedirections;r&&(delete r.maxRedirections,delete r.connectTimeout);const a=new Request(e,r),s=await a.arrayBuffer(),o=s.byteLength?Array.from(new Uint8Array(s)):null,u=await t("plugin:http|fetch",{method:a.method,url:a.url,headers:Array.from(a.headers.entries()),data:o,maxRedirections:n,connectTimeout:i});a.signal.addEventListener("abort",(()=>{t("plugin:http|fetch_cancel",{rid:u})}));const{status:_,statusText:d,url:c,headers:f}=await t("plugin:http|fetch_send",{rid:u}),h=await t("plugin:http|fetch_read_body",{rid:u}),p=new Response(new Uint8Array(h),{headers:f,status:_,statusText:d});return Object.defineProperty(p,"url",{value:c}),p},e}({});Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_PLUGIN_HTTP__})} +if("__TAURI__"in window){var __TAURI_PLUGIN_HTTP__=function(e){"use strict";async function t(e,t={},r){return window.__TAURI_INTERNALS__.invoke(e,t,r)}return"function"==typeof SuppressedError&&SuppressedError,e.fetch=async function(e,r){const n=r?.maxRedirections,i=r?.maxRedirections,a=r?.proxy;r&&(delete r.maxRedirections,delete r.connectTimeout,delete r.proxy);const o=new Request(e,r),s=await o.arrayBuffer(),d=s.byteLength?Array.from(new Uint8Array(s)):null,u=await t("plugin:http|fetch",{clientConfig:{method:o.method,url:o.url,headers:Array.from(o.headers.entries()),data:d,maxRedirections:n,connectTimeout:i,proxy:a}});o.signal.addEventListener("abort",(()=>{t("plugin:http|fetch_cancel",{rid:u})}));const{status:_,statusText:c,url:p,headers:f}=await t("plugin:http|fetch_send",{rid:u}),l=await t("plugin:http|fetch_read_body",{rid:u}),h=new Response(new Uint8Array(l),{headers:f,status:_,statusText:c});return Object.defineProperty(h,"url",{value:p}),h},e}({});Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_PLUGIN_HTTP__})} diff --git a/plugins/http/src/commands.rs b/plugins/http/src/commands.rs index 898b180e..d277dd26 100644 --- a/plugins/http/src/commands.rs +++ b/plugins/http/src/commands.rs @@ -5,8 +5,8 @@ use std::{collections::HashMap, time::Duration}; use http::{header, HeaderName, HeaderValue, Method, StatusCode}; -use reqwest::redirect::Policy; -use serde::Serialize; +use reqwest::{redirect::Policy, NoProxy}; +use serde::{Deserialize, Serialize}; use tauri::{command, AppHandle, Runtime}; use crate::{Error, FetchRequest, HttpExt, RequestId}; @@ -20,16 +20,111 @@ pub struct FetchResponse { url: String, } -#[command] -pub async fn fetch( - app: AppHandle, +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientConfig { method: String, url: url::Url, headers: Vec<(String, String)>, data: Option>, connect_timeout: Option, max_redirections: Option, + proxy: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Proxy { + all: Option, + http: Option, + https: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum UrlOrConfig { + Url(String), + Config(ProxyConfig), +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProxyConfig { + url: String, + basic_auth: Option, + no_proxy: Option, +} + +#[derive(Deserialize)] +pub struct BasicAuth { + username: String, + password: String, +} + +#[inline] +fn proxy_creator( + url_or_config: UrlOrConfig, + proxy_fn: fn(String) -> reqwest::Result, +) -> reqwest::Result { + match url_or_config { + UrlOrConfig::Url(url) => Ok(proxy_fn(url)?), + UrlOrConfig::Config(ProxyConfig { + url, + basic_auth, + no_proxy, + }) => { + let mut proxy = proxy_fn(url)?; + if let Some(basic_auth) = basic_auth { + proxy = proxy.basic_auth(&basic_auth.username, &basic_auth.password); + } + if let Some(no_proxy) = no_proxy { + proxy = proxy.no_proxy(NoProxy::from_string(&no_proxy)); + } + Ok(proxy) + } + } +} + +fn attach_proxy( + proxy: Proxy, + mut builder: reqwest::ClientBuilder, +) -> crate::Result { + let Proxy { all, http, https } = proxy; + + if let Some(all) = all { + let proxy = proxy_creator(all, reqwest::Proxy::all)?; + builder = builder.proxy(proxy); + } + + if let Some(http) = http { + let proxy = proxy_creator(http, reqwest::Proxy::http)?; + builder = builder.proxy(proxy); + } + + if let Some(https) = https { + let proxy = proxy_creator(https, reqwest::Proxy::https)?; + builder = builder.proxy(proxy); + } + + Ok(builder) +} + +#[command] +pub async fn fetch( + app: AppHandle, + client_config: ClientConfig, ) -> crate::Result { + let ClientConfig { + method, + url, + headers, + data, + connect_timeout, + max_redirections, + proxy, + } = client_config; + let scheme = url.scheme(); let method = Method::from_bytes(method.as_bytes())?; let headers: HashMap = HashMap::from_iter(headers); @@ -51,6 +146,10 @@ pub async fn fetch( }); } + if let Some(proxy_config) = proxy { + builder = attach_proxy(proxy_config, builder)?; + } + let mut request = builder.build()?.request(method.clone(), url); for (key, value) in &headers {