Skip to content

Commit

Permalink
Merge pull request #109 from sd2k/add-serve-proxy
Browse files Browse the repository at this point in the history
Add minimal proxy capabilities to `dioxus serve`
  • Loading branch information
mrxiaozhuox authored Feb 28, 2023
2 parents 6c2a51e + ead183d commit 1587b68
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 12 deletions.
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
13 changes: 13 additions & 0 deletions docs/src/configure.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -168,4 +178,7 @@ style = []

# Javascript code file
script = []

[[web.proxy]]
backend = "http://localhost:8000/api/"
```
7 changes: 7 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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,
}
Expand All @@ -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>>,
Expand Down
6 changes: 6 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down
39 changes: 27 additions & 12 deletions src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()>,
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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 {
(
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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!("");
Expand Down
171 changes: 171 additions & 0 deletions src/server/proxy.rs
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit 1587b68

Please sign in to comment.