* wip: fix manganis import path * upgrade tauri bundler, convert todo!() to unimplemented!() * feat: mobile hotreloading + ios asset configuration * remove oid demo and its associated env vars * nuke all the things that cause cache thrashing * swap to Asset type * add some more logs * display impl for attrvalue * dont panic on collect failure * cut down web deps to 150 * clean up deps in a lot of places, simplify build scripts * clean up asset resolution and cli-dev profile * wire up absolute paths for manganis asset in a particular mode * move document related things around to shorten compile times * move most things to `document::Item` and then decompose the cli-config * switch link to stylesheet for clarity in examples * move manganis workspace example to examples folder * decompose manganis-cli-support * dont need mobile demo anymore * remove gloo dialogs * html doesnt need document * rename hotreload to devtools * really clean up the html crate * fix weird rsx spacing * clean up desktop to use callback * clean up document a bit * re-wire up devsocket * fix utf css * yeeeet that js out of here * synthetic web system works * web crate almost done being cleaned up * desktop mostly cleaned up too * clean up cli a bit * more cli cleanup * cli builds again * clean up cli, inline structs, cut down number of unique types where possible * hotreload works again for desktop and this time, mobile too * cfg out tungstenite * devserver err * more refactor to cli builder * switch to unbounded_send * new structure is much cleaner * add http serve subcommand * bundled hotreload * kick stylsheets * clean up a bit more, split up eventloop * amazingly serve seems done * change from pub to pub(crate) in cli * remove tools * bit more polish to cli * fix issue with join * gracefully handle fullstack without a server * fullstack mobile demo * fix launch function, move projects into example-projects folder * hoist examples * add "run" command * clean up launch * remove old manual websocket receiver * doctor command * allow desktop to scroll * cut apart router crate * dont put launch in prelude * use dioxus::launch where possible * rename rsx, cut out hotreload tests * remove liveview project * bump native * fix compile for renderers * move sync event response out of interpreter * move render in serve * rollback settings change * cli compiles, huzzah! * change uris for asset * fix asset * new tui screen * new cargo-like tui works * very very verty close * it works! very small bug with incorrect grapheme calc * Clean up devserver a bit * status sytem * tidy up debug filters * clean up logging situation * Fix a number of bugs with log printing * new printing system is more reliable * wire up more stuff * things working but fullstack is having issues * fullstack works again! * hotreloading bundled assets works again * bundled hotreload and beginnings of macos bundling * Hotreload desktop * combined server builds * add build handle * fix fullstack assets * make open async, add some hooks for ios * migrate filemap to runner * wip global crate system * fixup bundles + organize asset * fix asset location bug * all the bundled reloading! * open ios simulator! * full hotreload support for mobile + serverfn * basic cleanups * clean up dx * Move filemap * fix cutting of newlines * assets workibg, some android * hoist wry/tao * use sync locks and headers to fix issues with android * desktop -> mac/win/linux with alias * better logging * feat: workspace (entire computer!) hotreload * should rebuild toggle, ios simulator bootup * proper mobile support in launch * more robust handling of assets * fix cargo * bring back some of tauri bundle * make warnings go away, clippy happy on cli * some final clippy cleanups * fmt * move manganis to its own folder * upgrade bundle to stable * drastically slim down manganis, prepping for merge * typos, failing test, docsrs config * remove static gen test * nix static gen test * we use --platform web instead of --platform fullstack now * only bind dev urls in desktop/mobile * install gtk * nix static gen * split build dir by app name
301 lines
9.6 KiB
Rust
301 lines
9.6 KiB
Rust
use crate::config::WebProxyConfig;
|
|
use crate::TraceSrc;
|
|
use crate::{Error, Result};
|
|
|
|
use anyhow::{anyhow, Context};
|
|
use axum::body::Body;
|
|
use axum::{body::Body as MyBody, response::IntoResponse};
|
|
use axum::{
|
|
http::StatusCode,
|
|
routing::{any, MethodRouter},
|
|
Router,
|
|
};
|
|
use hyper::{Request, Response, Uri};
|
|
use hyper_util::{
|
|
client::legacy::{self, connect::HttpConnector},
|
|
rt::TokioExecutor,
|
|
};
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct ProxyClient {
|
|
inner: legacy::Client<hyper_rustls::HttpsConnector<HttpConnector>, MyBody>,
|
|
url: Uri,
|
|
}
|
|
|
|
impl ProxyClient {
|
|
fn new(url: Uri) -> Self {
|
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
|
let https = hyper_rustls::HttpsConnectorBuilder::new()
|
|
.with_native_roots()
|
|
.unwrap()
|
|
.https_or_http()
|
|
.enable_all_versions()
|
|
.build();
|
|
Self {
|
|
inner: legacy::Client::builder(TokioExecutor::new()).build(https),
|
|
url,
|
|
}
|
|
}
|
|
|
|
async fn send(&self, mut req: Request<MyBody>) -> Result<Response<hyper::body::Incoming>> {
|
|
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(|err| crate::error::Error::Other(anyhow!(err)))
|
|
}
|
|
}
|
|
|
|
/// 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(crate) fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result<Router> {
|
|
let url: Uri = proxy.backend.parse()?;
|
|
let path = url.path().to_string();
|
|
let trimmed_path = path.trim_start_matches('/');
|
|
|
|
if trimmed_path.is_empty() {
|
|
return Err(crate::Error::ProxySetup(format!(
|
|
"Proxy backend URL must have a non-empty path, e.g. {}/api instead of {}",
|
|
proxy.backend.trim_end_matches('/'),
|
|
proxy.backend
|
|
)));
|
|
}
|
|
|
|
let method_router = proxy_to(url, false, handle_proxy_error);
|
|
|
|
// api/*path
|
|
router = router.route(
|
|
&format!("/{}/*path", trimmed_path.trim_end_matches('/')),
|
|
method_router.clone(),
|
|
);
|
|
|
|
// /api/
|
|
router = router.route(
|
|
&format!("/{}/", trimmed_path.trim_end_matches('/')),
|
|
method_router.clone(),
|
|
);
|
|
|
|
// /api
|
|
router = router.route(
|
|
&format!("/{}", trimmed_path.trim_end_matches('/')),
|
|
method_router,
|
|
);
|
|
|
|
Ok(router)
|
|
}
|
|
|
|
pub(crate) fn proxy_to(
|
|
url: Uri,
|
|
nocache: bool,
|
|
handle_error: fn(Error) -> Response<Body>,
|
|
) -> MethodRouter {
|
|
let client = ProxyClient::new(url.clone());
|
|
|
|
any(move |mut req: Request<MyBody>| async move {
|
|
// Prevent request loops
|
|
if req.headers().get("x-proxied-by-dioxus").is_some() {
|
|
return Err(Response::builder()
|
|
.status(StatusCode::NOT_FOUND)
|
|
.body(Body::from(
|
|
"API is sharing a loopback with the dev server. Try setting a different port on the API config.",
|
|
))
|
|
.unwrap());
|
|
}
|
|
|
|
req.headers_mut().insert(
|
|
"x-proxied-by-dioxus",
|
|
"true".parse().expect("header value is valid"),
|
|
);
|
|
|
|
// We have to throw a redirect for ws connections since the upgrade handler will not be called
|
|
// Our _dioxus handler will override this in the default case
|
|
if req.uri().scheme().map(|f| f.as_str()) == Some("ws")
|
|
|| req.uri().scheme().map(|f| f.as_str()) == Some("wss")
|
|
{
|
|
let new_host = url.host().unwrap_or("localhost");
|
|
let proxied_uri = format!(
|
|
"{scheme}://{host}:{port}{path_and_query}",
|
|
scheme = req.uri().scheme_str().unwrap_or("ws"),
|
|
port = url.port().unwrap(),
|
|
host = new_host,
|
|
path_and_query = req
|
|
.uri()
|
|
.path_and_query()
|
|
.map(|f| f.to_string())
|
|
.unwrap_or_default()
|
|
);
|
|
tracing::info!(dx_src = ?TraceSrc::Dev, "Proxied websocket request {req:?} to {proxied_uri}");
|
|
|
|
return Ok(axum::response::Redirect::permanent(&proxied_uri).into_response());
|
|
}
|
|
|
|
if nocache {
|
|
crate::serve::insert_no_cache_headers(req.headers_mut());
|
|
}
|
|
|
|
let uri = req.uri().clone();
|
|
|
|
// retry with backoff
|
|
|
|
let res = client.send(req).await.map_err(handle_error);
|
|
|
|
match res {
|
|
Ok(res) => {
|
|
// log assets at a different log level
|
|
if uri.path().starts_with("/assets")
|
|
|| uri.path().starts_with("/_dioxus")
|
|
|| uri.path().starts_with("/public")
|
|
|| uri.path().starts_with("/wasm")
|
|
{
|
|
tracing::trace!(dx_src = ?TraceSrc::Dev, "[{}] {}", res.status().as_u16(), uri);
|
|
} else {
|
|
tracing::info!(dx_src = ?TraceSrc::Dev, "[{}] {}", res.status().as_u16(), uri);
|
|
}
|
|
|
|
Ok(res.into_response())
|
|
}
|
|
Err(err) => {
|
|
tracing::error!(dx_src = ?TraceSrc::Dev, "[{}] {}", err.status().as_u16(), uri);
|
|
Err(err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fn handle_proxy_error(e: Error) -> axum::http::Response<axum::body::Body> {
|
|
tracing::error!(dx_src = ?TraceSrc::Dev, "Proxy error: {}", e);
|
|
axum::http::Response::builder()
|
|
.status(axum::http::StatusCode::INTERNAL_SERVER_ERROR)
|
|
.body(axum::body::Body::from(format!(
|
|
"Proxy connection failed: {:#?}",
|
|
e
|
|
)))
|
|
.unwrap()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
|
|
use super::*;
|
|
|
|
use axum_server::{Handle, Server};
|
|
|
|
async fn setup_servers(mut config: WebProxyConfig) -> String {
|
|
let backend_router =
|
|
Router::new().route(
|
|
"/*path",
|
|
any(|request: axum::extract::Request| async move {
|
|
format!("backend: {}", request.uri())
|
|
}),
|
|
);
|
|
|
|
// The API backend server
|
|
let backend_handle_handle = Handle::new();
|
|
let backend_handle_handle_ = backend_handle_handle.clone();
|
|
tokio::spawn(async move {
|
|
Server::bind("127.0.0.1:0".parse().unwrap())
|
|
.handle(backend_handle_handle_)
|
|
.serve(backend_router.into_make_service())
|
|
.await
|
|
.unwrap();
|
|
});
|
|
|
|
// Set the user's config to this dummy API we just built so we can test it
|
|
let backend_addr = backend_handle_handle.listening().await.unwrap();
|
|
config.backend = format!("http://{}{}", backend_addr, config.backend);
|
|
|
|
// Now set up our actual filesystem server
|
|
let router = super::add_proxy(Router::new(), &config);
|
|
let server_handle_handle = Handle::new();
|
|
let server_handle_handle_ = server_handle_handle.clone();
|
|
tokio::spawn(async move {
|
|
Server::bind("127.0.0.1:0".parse().unwrap())
|
|
.handle(server_handle_handle_)
|
|
.serve(router.unwrap().into_make_service())
|
|
.await
|
|
.unwrap();
|
|
});
|
|
|
|
// Expose *just* the filesystem web server's address
|
|
server_handle_handle.listening().await.unwrap().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 server_addr = setup_servers(config).await;
|
|
|
|
assert_eq!(
|
|
reqwest::get(format!("http://{}/api", server_addr))
|
|
.await
|
|
.unwrap()
|
|
.text()
|
|
.await
|
|
.unwrap(),
|
|
"backend: /api"
|
|
);
|
|
|
|
assert_eq!(
|
|
reqwest::get(format!("http://{}/api/", server_addr))
|
|
.await
|
|
.unwrap()
|
|
.text()
|
|
.await
|
|
.unwrap(),
|
|
"backend: /api/"
|
|
);
|
|
|
|
assert_eq!(
|
|
reqwest::get(format!("http://{server_addr}/api/subpath"))
|
|
.await
|
|
.unwrap()
|
|
.text()
|
|
.await
|
|
.unwrap(),
|
|
"backend: /api/subpath"
|
|
);
|
|
}
|
|
|
|
#[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;
|
|
}
|
|
|
|
#[test]
|
|
fn add_proxy_empty_path() {
|
|
let config = WebProxyConfig {
|
|
backend: "http://localhost:8000".to_string(),
|
|
};
|
|
let router = super::add_proxy(Router::new(), &config);
|
|
match router.unwrap_err() {
|
|
crate::Error::ProxySetup(e) => {
|
|
assert_eq!(
|
|
e,
|
|
"Proxy backend URL must have a non-empty path, e.g. http://localhost:8000/api instead of http://localhost:8000"
|
|
);
|
|
}
|
|
e => panic!("Unexpected error type: {}", e),
|
|
}
|
|
}
|
|
}
|