mirror of
https://github.com/DioxusLabs/dioxus
synced 2025-02-17 06:08:26 +00:00
Add minimal proxy capabilities to dioxus serve
This adds an MVP of some proxying capabilities to the `dioxus serve` server. The config is similar to that of `trunk serve`: the user can specify one or more proxy backends under `[[web.proxy]]` in Dioxus.toml, and the server will intercept requests targeted at the _path_ of that configured backend and forward them to the backend server. Example ------- For example, if the dev server is serving on port 8080 with this config: ``` [[web.proxy]] backend = "http://localhost:9000/api" ``` then requests to http://localhost:8080/api, http://localhost:8080/api/ and http://localhost:8080/api/any-subpath to be forwarded to the respective paths on http://localhost:9000. This PR doesn't handle path rewriting or anything yet but it would be fairly simple to add in future if anyone needs it.
This commit is contained in:
parent
6c2a51e453
commit
ead183dd2c
7 changed files with 240 additions and 12 deletions
15
Cargo.lock
generated
15
Cargo.lock
generated
|
@ -715,6 +715,7 @@ dependencies = [
|
|||
"headers",
|
||||
"html_parser",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"indicatif",
|
||||
"lazy_static",
|
||||
"log",
|
||||
|
@ -1329,7 +1330,9 @@ checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c"
|
|||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"log",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
]
|
||||
|
@ -2330,6 +2333,18 @@ dependencies = [
|
|||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pemfile",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.2"
|
||||
|
|
|
@ -36,6 +36,7 @@ regex = "1.5.4"
|
|||
chrono = "0.4.19"
|
||||
anyhow = "1.0.53"
|
||||
hyper = "0.14.17"
|
||||
hyper-rustls = "0.23.2"
|
||||
indicatif = "0.17.0-rc.11"
|
||||
subprocess = "0.2.9"
|
||||
|
||||
|
|
|
@ -119,6 +119,16 @@ Only include resources at `Dev` mode.
|
|||
]
|
||||
```
|
||||
|
||||
### Web.Proxy
|
||||
|
||||
Proxy requests matching a path to a backend server.
|
||||
|
||||
1. ***backend*** - the URL to the backend server.
|
||||
```
|
||||
backend = "http://localhost:8000/api/"
|
||||
```
|
||||
This will cause any requests made to the dev server with prefix /api/ to be redirected to the backend server at http://localhost:8000. The path and query parameters will be passed on as-is (path rewriting is not currently supported).
|
||||
|
||||
## Config example
|
||||
|
||||
```toml
|
||||
|
@ -168,4 +178,7 @@ style = []
|
|||
|
||||
# Javascript code file
|
||||
script = []
|
||||
|
||||
[[web.proxy]]
|
||||
backend = "http://localhost:8000/api/"
|
||||
```
|
||||
|
|
|
@ -63,6 +63,7 @@ impl Default for DioxusConfig {
|
|||
title: Some("dioxus | ⛺".into()),
|
||||
base_path: None,
|
||||
},
|
||||
proxy: Some(vec![]),
|
||||
watcher: WebWatcherConfig {
|
||||
watch_path: Some(vec![PathBuf::from("src")]),
|
||||
reload_html: Some(false),
|
||||
|
@ -97,6 +98,7 @@ pub struct ApplicationConfig {
|
|||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WebConfig {
|
||||
pub app: WebAppConfig,
|
||||
pub proxy: Option<Vec<WebProxyConfig>>,
|
||||
pub watcher: WebWatcherConfig,
|
||||
pub resource: WebResourceConfig,
|
||||
}
|
||||
|
@ -107,6 +109,11 @@ pub struct WebAppConfig {
|
|||
pub base_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WebProxyConfig {
|
||||
pub backend: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WebWatcherConfig {
|
||||
pub watch_path: Option<Vec<PathBuf>>,
|
||||
|
|
|
@ -32,6 +32,12 @@ pub enum Error {
|
|||
#[error("{0}")]
|
||||
CustomError(String),
|
||||
|
||||
#[error("Invalid proxy URL: {0}")]
|
||||
InvalidProxy(#[from] hyper::http::uri::InvalidUri),
|
||||
|
||||
#[error("Error proxying request: {0}")]
|
||||
ProxyRequestError(hyper::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ use tokio::sync::broadcast;
|
|||
use tower::ServiceBuilder;
|
||||
use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody};
|
||||
|
||||
mod proxy;
|
||||
|
||||
pub struct BuildManager {
|
||||
config: CrateConfig,
|
||||
reload_tx: broadcast::Sender<()>,
|
||||
|
@ -284,16 +286,18 @@ pub async fn startup_hot_reload(ip: String, port: u16, config: CrateConfig) -> R
|
|||
)
|
||||
.service(ServeDir::new(config.crate_dir.join(&dist_path)));
|
||||
|
||||
let router = Router::new()
|
||||
.route("/_dioxus/ws", get(ws_handler))
|
||||
.fallback(
|
||||
get_service(file_service).handle_error(|error: std::io::Error| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Unhandled internal error: {}", error),
|
||||
)
|
||||
}),
|
||||
);
|
||||
let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
|
||||
for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() {
|
||||
router = proxy::add_proxy(router, &proxy_config )?;
|
||||
}
|
||||
router = router.fallback(get_service(file_service).handle_error(
|
||||
|error: std::io::Error| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Unhandled internal error: {}", error),
|
||||
)
|
||||
},
|
||||
));
|
||||
|
||||
let router = router
|
||||
.route("/_dioxus/hot_reload", get(hot_reload_handler))
|
||||
|
@ -427,8 +431,9 @@ pub async fn startup_default(ip: String, port: u16, config: CrateConfig) -> Resu
|
|||
)
|
||||
.service(ServeDir::new(config.crate_dir.join(&dist_path)));
|
||||
|
||||
let router = Router::new()
|
||||
.route("/_dioxus/ws", get(ws_handler))
|
||||
let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
|
||||
|
||||
router = router
|
||||
.fallback(
|
||||
get_service(file_service).handle_error(|error: std::io::Error| async move {
|
||||
(
|
||||
|
@ -498,6 +503,8 @@ fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options: Pre
|
|||
"False"
|
||||
};
|
||||
|
||||
let proxies = config.dioxus_config.web.proxy.as_ref();
|
||||
|
||||
if options.changed.is_empty() {
|
||||
println!(
|
||||
"{} @ v{} [{}] \n",
|
||||
|
@ -528,6 +535,14 @@ fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options: Pre
|
|||
println!("");
|
||||
println!("\t> Profile : {}", profile.green());
|
||||
println!("\t> Hot Reload : {}", hot_reload.cyan());
|
||||
if let Some(proxies) = proxies {
|
||||
if !proxies.is_empty() {
|
||||
println!("\t> Proxies :");
|
||||
for proxy in proxies {
|
||||
println!("\t\t- {}", proxy.backend.blue());
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("\t> Index Template : {}", custom_html_file.green());
|
||||
println!("\t> URL Rewrite [index_on_404] : {}", url_rewrite.purple());
|
||||
println!("");
|
||||
|
|
171
src/server/proxy.rs
Normal file
171
src/server/proxy.rs
Normal file
|
@ -0,0 +1,171 @@
|
|||
use crate::{Result, WebProxyConfig};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{http::StatusCode, routing::any, Router};
|
||||
use hyper::{Request, Response, Uri};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ProxyClient {
|
||||
inner: hyper::Client<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>>,
|
||||
url: Uri,
|
||||
}
|
||||
|
||||
impl ProxyClient {
|
||||
fn new(url: Uri) -> Self {
|
||||
let https = hyper_rustls::HttpsConnectorBuilder::new()
|
||||
.with_native_roots()
|
||||
.https_or_http()
|
||||
.enable_http1()
|
||||
.build();
|
||||
Self {
|
||||
inner: hyper::Client::builder().build(https),
|
||||
url,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(
|
||||
&self,
|
||||
mut req: Request<hyper::body::Body>,
|
||||
) -> Result<Response<hyper::body::Body>> {
|
||||
let mut uri_parts = req.uri().clone().into_parts();
|
||||
uri_parts.authority = self.url.authority().cloned();
|
||||
uri_parts.scheme = self.url.scheme().cloned();
|
||||
*req.uri_mut() = Uri::from_parts(uri_parts).context("Invalid URI parts")?;
|
||||
self.inner
|
||||
.request(req)
|
||||
.await
|
||||
.map_err(crate::error::Error::ProxyRequestError)
|
||||
}
|
||||
}
|
||||
|
||||
/// Add routes to the router handling the specified proxy config.
|
||||
///
|
||||
/// We will proxy requests directed at either:
|
||||
///
|
||||
/// - the exact path of the proxy config's backend URL, e.g. /api
|
||||
/// - the exact path with a trailing slash, e.g. /api/
|
||||
/// - any subpath of the backend URL, e.g. /api/foo/bar
|
||||
pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result<Router> {
|
||||
let url: Uri = proxy.backend.parse()?;
|
||||
let path = url.path().to_string();
|
||||
let client = ProxyClient::new(url);
|
||||
|
||||
// We also match everything after the path using a wildcard matcher.
|
||||
let wildcard_client = client.clone();
|
||||
|
||||
router = router.route(
|
||||
// Always remove trailing /'s so that the exact route
|
||||
// matches.
|
||||
path.trim_end_matches('/'),
|
||||
any(move |req| async move {
|
||||
client
|
||||
.send(req)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
|
||||
}),
|
||||
);
|
||||
|
||||
// Wildcard match anything else _after_ the backend URL's path.
|
||||
// Note that we know `path` ends with a trailing `/` in this branch,
|
||||
// so `wildcard` will look like `http://localhost/api/*proxywildcard`.
|
||||
let wildcard = format!("{}/*proxywildcard", path.trim_end_matches('/'));
|
||||
router = router.route(
|
||||
&wildcard,
|
||||
any(move |req| async move {
|
||||
wildcard_client
|
||||
.send(req)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
|
||||
}),
|
||||
);
|
||||
Ok(router)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
|
||||
use axum::{extract::Path, Router};
|
||||
|
||||
fn setup_servers(
|
||||
mut config: WebProxyConfig,
|
||||
) -> (
|
||||
tokio::task::JoinHandle<()>,
|
||||
tokio::task::JoinHandle<()>,
|
||||
String,
|
||||
) {
|
||||
let backend_router = Router::new().route(
|
||||
"/*path",
|
||||
any(|path: Path<String>| async move { format!("backend: {}", path.0) }),
|
||||
);
|
||||
let backend_server = axum::Server::bind(&"127.0.0.1:0".parse().unwrap())
|
||||
.serve(backend_router.into_make_service());
|
||||
let backend_addr = backend_server.local_addr();
|
||||
let backend_handle = tokio::spawn(async move { backend_server.await.unwrap() });
|
||||
config.backend = format!("http://{}{}", backend_addr, config.backend);
|
||||
let router = super::add_proxy(Router::new(), &config);
|
||||
let server = axum::Server::bind(&"127.0.0.1:0".parse().unwrap())
|
||||
.serve(router.unwrap().into_make_service());
|
||||
let server_addr = server.local_addr();
|
||||
let server_handle = tokio::spawn(async move { server.await.unwrap() });
|
||||
(backend_handle, server_handle, server_addr.to_string())
|
||||
}
|
||||
|
||||
async fn test_proxy_requests(path: String) {
|
||||
let config = WebProxyConfig {
|
||||
// Normally this would be an absolute URL including scheme/host/port,
|
||||
// but in these tests we need to let the OS choose the port so tests
|
||||
// don't conflict, so we'll concatenate the final address and this
|
||||
// path together.
|
||||
// So in day to day usage, use `http://localhost:8000/api` instead!
|
||||
backend: path,
|
||||
};
|
||||
let (backend_handle, server_handle, server_addr) = setup_servers(config);
|
||||
let resp = hyper::Client::new()
|
||||
.get(format!("http://{}/api", server_addr).parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
hyper::body::to_bytes(resp.into_body()).await.unwrap(),
|
||||
"backend: /api"
|
||||
);
|
||||
|
||||
let resp = hyper::Client::new()
|
||||
.get(format!("http://{}/api/", server_addr).parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
hyper::body::to_bytes(resp.into_body()).await.unwrap(),
|
||||
"backend: /api/"
|
||||
);
|
||||
|
||||
let resp = hyper::Client::new()
|
||||
.get(
|
||||
format!("http://{}/api/subpath", server_addr)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
hyper::body::to_bytes(resp.into_body()).await.unwrap(),
|
||||
"backend: /api/subpath"
|
||||
);
|
||||
backend_handle.abort();
|
||||
server_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_proxy() {
|
||||
test_proxy_requests("/api".to_string()).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_proxy_trailing_slash() {
|
||||
test_proxy_requests("/api/".to_string()).await;
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue