proxy.rs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. use crate::Result;
  2. use dioxus_cli_config::WebProxyConfig;
  3. use anyhow::Context;
  4. use axum::{http::StatusCode, routing::any, Router};
  5. use hyper::{Request, Response, Uri};
  6. #[derive(Debug, Clone)]
  7. struct ProxyClient {
  8. inner: hyper::Client<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>>,
  9. url: Uri,
  10. }
  11. impl ProxyClient {
  12. fn new(url: Uri) -> Self {
  13. let https = hyper_rustls::HttpsConnectorBuilder::new()
  14. .with_native_roots()
  15. .https_or_http()
  16. .enable_http1()
  17. .build();
  18. Self {
  19. inner: hyper::Client::builder().build(https),
  20. url,
  21. }
  22. }
  23. async fn send(
  24. &self,
  25. mut req: Request<hyper::body::Body>,
  26. ) -> Result<Response<hyper::body::Body>> {
  27. let mut uri_parts = req.uri().clone().into_parts();
  28. uri_parts.authority = self.url.authority().cloned();
  29. uri_parts.scheme = self.url.scheme().cloned();
  30. *req.uri_mut() = Uri::from_parts(uri_parts).context("Invalid URI parts")?;
  31. self.inner
  32. .request(req)
  33. .await
  34. .map_err(crate::error::Error::ProxyRequestError)
  35. }
  36. }
  37. /// Add routes to the router handling the specified proxy config.
  38. ///
  39. /// We will proxy requests directed at either:
  40. ///
  41. /// - the exact path of the proxy config's backend URL, e.g. /api
  42. /// - the exact path with a trailing slash, e.g. /api/
  43. /// - any subpath of the backend URL, e.g. /api/foo/bar
  44. pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result<Router> {
  45. let url: Uri = proxy.backend.parse()?;
  46. let path = url.path().to_string();
  47. let trimmed_path = path.trim_end_matches('/');
  48. if trimmed_path.is_empty() {
  49. return Err(crate::Error::ProxySetupError(format!(
  50. "Proxy backend URL must have a non-empty path, e.g. {}/api instead of {}",
  51. proxy.backend.trim_end_matches('/'),
  52. proxy.backend
  53. )));
  54. }
  55. let client = ProxyClient::new(url);
  56. // We also match everything after the path using a wildcard matcher.
  57. let wildcard_client = client.clone();
  58. router = router.route(
  59. // Always remove trailing /'s so that the exact route
  60. // matches.
  61. trimmed_path,
  62. any(move |req| async move {
  63. client
  64. .send(req)
  65. .await
  66. .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
  67. }),
  68. );
  69. // Wildcard match anything else _after_ the backend URL's path.
  70. // Note that we know `path` ends with a trailing `/` in this branch,
  71. // so `wildcard` will look like `http://localhost/api/*proxywildcard`.
  72. let wildcard = format!("{}/*proxywildcard", trimmed_path);
  73. router = router.route(
  74. &wildcard,
  75. any(move |req| async move {
  76. wildcard_client
  77. .send(req)
  78. .await
  79. .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
  80. }),
  81. );
  82. Ok(router)
  83. }
  84. #[cfg(test)]
  85. mod test {
  86. use super::*;
  87. use axum::{extract::Path, Router};
  88. fn setup_servers(
  89. mut config: WebProxyConfig,
  90. ) -> (
  91. tokio::task::JoinHandle<()>,
  92. tokio::task::JoinHandle<()>,
  93. String,
  94. ) {
  95. let backend_router = Router::new().route(
  96. "/*path",
  97. any(|path: Path<String>| async move { format!("backend: {}", path.0) }),
  98. );
  99. let backend_server = axum::Server::bind(&"127.0.0.1:0".parse().unwrap())
  100. .serve(backend_router.into_make_service());
  101. let backend_addr = backend_server.local_addr();
  102. let backend_handle = tokio::spawn(async move { backend_server.await.unwrap() });
  103. config.backend = format!("http://{}{}", backend_addr, config.backend);
  104. let router = super::add_proxy(Router::new(), &config);
  105. let server = axum::Server::bind(&"127.0.0.1:0".parse().unwrap())
  106. .serve(router.unwrap().into_make_service());
  107. let server_addr = server.local_addr();
  108. let server_handle = tokio::spawn(async move { server.await.unwrap() });
  109. (backend_handle, server_handle, server_addr.to_string())
  110. }
  111. async fn test_proxy_requests(path: String) {
  112. let config = WebProxyConfig {
  113. // Normally this would be an absolute URL including scheme/host/port,
  114. // but in these tests we need to let the OS choose the port so tests
  115. // don't conflict, so we'll concatenate the final address and this
  116. // path together.
  117. // So in day to day usage, use `http://localhost:8000/api` instead!
  118. backend: path,
  119. };
  120. let (backend_handle, server_handle, server_addr) = setup_servers(config);
  121. let resp = hyper::Client::new()
  122. .get(format!("http://{}/api", server_addr).parse().unwrap())
  123. .await
  124. .unwrap();
  125. assert_eq!(resp.status(), StatusCode::OK);
  126. assert_eq!(
  127. hyper::body::to_bytes(resp.into_body()).await.unwrap(),
  128. "backend: /api"
  129. );
  130. let resp = hyper::Client::new()
  131. .get(format!("http://{}/api/", server_addr).parse().unwrap())
  132. .await
  133. .unwrap();
  134. assert_eq!(resp.status(), StatusCode::OK);
  135. assert_eq!(
  136. hyper::body::to_bytes(resp.into_body()).await.unwrap(),
  137. "backend: /api/"
  138. );
  139. let resp = hyper::Client::new()
  140. .get(
  141. format!("http://{}/api/subpath", server_addr)
  142. .parse()
  143. .unwrap(),
  144. )
  145. .await
  146. .unwrap();
  147. assert_eq!(resp.status(), StatusCode::OK);
  148. assert_eq!(
  149. hyper::body::to_bytes(resp.into_body()).await.unwrap(),
  150. "backend: /api/subpath"
  151. );
  152. backend_handle.abort();
  153. server_handle.abort();
  154. }
  155. #[tokio::test]
  156. async fn add_proxy() {
  157. test_proxy_requests("/api".to_string()).await;
  158. }
  159. #[tokio::test]
  160. async fn add_proxy_trailing_slash() {
  161. test_proxy_requests("/api/".to_string()).await;
  162. }
  163. #[test]
  164. fn add_proxy_empty_path() {
  165. let config = WebProxyConfig {
  166. backend: "http://localhost:8000".to_string(),
  167. };
  168. let router = super::add_proxy(Router::new(), &config);
  169. match router.unwrap_err() {
  170. crate::Error::ProxySetupError(e) => {
  171. assert_eq!(
  172. e,
  173. "Proxy backend URL must have a non-empty path, e.g. http://localhost:8000/api instead of http://localhost:8000"
  174. );
  175. }
  176. e => panic!("Unexpected error type: {}", e),
  177. }
  178. }
  179. }