Skip to content

Commit bfa87da

Browse files
authored
feat(http): expose proxy configuration (#824)
* feat(http): add proxy config * chore: build iife api * chore: allow `too_many_arguments` * improvement * refactor: restructure code * improvement * format
1 parent c2115d8 commit bfa87da

4 files changed

Lines changed: 165 additions & 12 deletions

File tree

.changes/http-proxy-config.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"http": minor
3+
"http-js": minor
4+
---
5+
6+
Add `proxy` field to `fetch` options to configure proxy.

plugins/http/guest-js/index.ts

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,45 @@
2626

2727
import { invoke } from "@tauri-apps/api/core";
2828

29+
/**
30+
* Configuration of a proxy that a Client should pass requests to.
31+
*
32+
* @since 2.0.0
33+
*/
34+
export type Proxy = {
35+
/**
36+
* Proxy all traffic to the passed URL.
37+
*/
38+
all?: string | ProxyConfig;
39+
/**
40+
* Proxy all HTTP traffic to the passed URL.
41+
*/
42+
http?: string | ProxyConfig;
43+
/**
44+
* Proxy all HTTPS traffic to the passed URL.
45+
*/
46+
https?: string | ProxyConfig;
47+
};
48+
49+
export interface ProxyConfig {
50+
/**
51+
* The URL of the proxy server.
52+
*/
53+
url: string;
54+
/**
55+
* Set the `Proxy-Authorization` header using Basic auth.
56+
*/
57+
basicAuth?: {
58+
username: string;
59+
password: string;
60+
};
61+
/**
62+
* A configuration for filtering out requests that shouldn’t be proxied.
63+
* Entries are expected to be comma-separated (whitespace between entries is ignored)
64+
*/
65+
noProxy?: string;
66+
}
67+
2968
/**
3069
* Options to configure the Rust client used to make fetch requests
3170
*
@@ -39,6 +78,10 @@ export interface ClientOptions {
3978
maxRedirections?: number;
4079
/** Timeout in milliseconds */
4180
connectTimeout?: number;
81+
/**
82+
* Configuration of a proxy that a Client should pass requests to.
83+
*/
84+
proxy?: Proxy;
4285
}
4386

4487
/**
@@ -61,24 +104,29 @@ export async function fetch(
61104
): Promise<Response> {
62105
const maxRedirections = init?.maxRedirections;
63106
const connectTimeout = init?.maxRedirections;
107+
const proxy = init?.proxy;
64108

65109
// Remove these fields before creating the request
66110
if (init) {
67111
delete init.maxRedirections;
68112
delete init.connectTimeout;
113+
delete init.proxy;
69114
}
70115

71116
const req = new Request(input, init);
72117
const buffer = await req.arrayBuffer();
73118
const reqData = buffer.byteLength ? Array.from(new Uint8Array(buffer)) : null;
74119

75120
const rid = await invoke<number>("plugin:http|fetch", {
76-
method: req.method,
77-
url: req.url,
78-
headers: Array.from(req.headers.entries()),
79-
data: reqData,
80-
maxRedirections,
81-
connectTimeout,
121+
clientConfig: {
122+
method: req.method,
123+
url: req.url,
124+
headers: Array.from(req.headers.entries()),
125+
data: reqData,
126+
maxRedirections,
127+
connectTimeout,
128+
proxy,
129+
},
82130
});
83131

84132
req.signal.addEventListener("abort", () => {

plugins/http/src/api-iife.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/http/src/commands.rs

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
use std::{collections::HashMap, time::Duration};
66

77
use http::{header, HeaderName, HeaderValue, Method, StatusCode};
8-
use reqwest::redirect::Policy;
9-
use serde::Serialize;
8+
use reqwest::{redirect::Policy, NoProxy};
9+
use serde::{Deserialize, Serialize};
1010
use tauri::{command, AppHandle, Runtime};
1111

1212
use crate::{Error, FetchRequest, HttpExt, RequestId};
@@ -20,16 +20,111 @@ pub struct FetchResponse {
2020
url: String,
2121
}
2222

23-
#[command]
24-
pub async fn fetch<R: Runtime>(
25-
app: AppHandle<R>,
23+
#[derive(Deserialize)]
24+
#[serde(rename_all = "camelCase")]
25+
pub struct ClientConfig {
2626
method: String,
2727
url: url::Url,
2828
headers: Vec<(String, String)>,
2929
data: Option<Vec<u8>>,
3030
connect_timeout: Option<u64>,
3131
max_redirections: Option<usize>,
32+
proxy: Option<Proxy>,
33+
}
34+
35+
#[derive(Deserialize)]
36+
#[serde(rename_all = "camelCase")]
37+
pub struct Proxy {
38+
all: Option<UrlOrConfig>,
39+
http: Option<UrlOrConfig>,
40+
https: Option<UrlOrConfig>,
41+
}
42+
43+
#[derive(Deserialize)]
44+
#[serde(rename_all = "camelCase")]
45+
#[serde(untagged)]
46+
pub enum UrlOrConfig {
47+
Url(String),
48+
Config(ProxyConfig),
49+
}
50+
51+
#[derive(Deserialize)]
52+
#[serde(rename_all = "camelCase")]
53+
pub struct ProxyConfig {
54+
url: String,
55+
basic_auth: Option<BasicAuth>,
56+
no_proxy: Option<String>,
57+
}
58+
59+
#[derive(Deserialize)]
60+
pub struct BasicAuth {
61+
username: String,
62+
password: String,
63+
}
64+
65+
#[inline]
66+
fn proxy_creator(
67+
url_or_config: UrlOrConfig,
68+
proxy_fn: fn(String) -> reqwest::Result<reqwest::Proxy>,
69+
) -> reqwest::Result<reqwest::Proxy> {
70+
match url_or_config {
71+
UrlOrConfig::Url(url) => Ok(proxy_fn(url)?),
72+
UrlOrConfig::Config(ProxyConfig {
73+
url,
74+
basic_auth,
75+
no_proxy,
76+
}) => {
77+
let mut proxy = proxy_fn(url)?;
78+
if let Some(basic_auth) = basic_auth {
79+
proxy = proxy.basic_auth(&basic_auth.username, &basic_auth.password);
80+
}
81+
if let Some(no_proxy) = no_proxy {
82+
proxy = proxy.no_proxy(NoProxy::from_string(&no_proxy));
83+
}
84+
Ok(proxy)
85+
}
86+
}
87+
}
88+
89+
fn attach_proxy(
90+
proxy: Proxy,
91+
mut builder: reqwest::ClientBuilder,
92+
) -> crate::Result<reqwest::ClientBuilder> {
93+
let Proxy { all, http, https } = proxy;
94+
95+
if let Some(all) = all {
96+
let proxy = proxy_creator(all, reqwest::Proxy::all)?;
97+
builder = builder.proxy(proxy);
98+
}
99+
100+
if let Some(http) = http {
101+
let proxy = proxy_creator(http, reqwest::Proxy::http)?;
102+
builder = builder.proxy(proxy);
103+
}
104+
105+
if let Some(https) = https {
106+
let proxy = proxy_creator(https, reqwest::Proxy::https)?;
107+
builder = builder.proxy(proxy);
108+
}
109+
110+
Ok(builder)
111+
}
112+
113+
#[command]
114+
pub async fn fetch<R: Runtime>(
115+
app: AppHandle<R>,
116+
client_config: ClientConfig,
32117
) -> crate::Result<RequestId> {
118+
let ClientConfig {
119+
method,
120+
url,
121+
headers,
122+
data,
123+
connect_timeout,
124+
max_redirections,
125+
proxy,
126+
} = client_config;
127+
33128
let scheme = url.scheme();
34129
let method = Method::from_bytes(method.as_bytes())?;
35130
let headers: HashMap<String, String> = HashMap::from_iter(headers);
@@ -51,6 +146,10 @@ pub async fn fetch<R: Runtime>(
51146
});
52147
}
53148

149+
if let Some(proxy_config) = proxy {
150+
builder = attach_proxy(proxy_config, builder)?;
151+
}
152+
54153
let mut request = builder.build()?.request(method.clone(), url);
55154

56155
for (key, value) in &headers {

0 commit comments

Comments
 (0)