proxy.rs 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  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. use hyper::client::legacy::Client;
  7. use axum::body::Body as MyBody;
  8. #[derive(Debug, Clone)]
  9. struct ProxyClient {
  10. inner: Client<hyper_rustls::HttpsConnector<Client::HttpConnector>>,
  11. url: Uri,
  12. }
  13. impl ProxyClient {
  14. fn new(url: Uri) -> Self {
  15. let https = hyper_rustls::HttpsConnectorBuilder::new()
  16. .with_native_roots()
  17. .unwrap()
  18. .https_or_http()
  19. .enable_http1()
  20. .build();
  21. Self {
  22. inner: Client::builder().build(https),
  23. url,
  24. }
  25. }
  26. async fn send(&self, mut req: Request<MyBody>) -> Result<Response<MyBody>> {
  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. use axum_server::Server;
  89. fn setup_servers(
  90. mut config: WebProxyConfig,
  91. ) -> (
  92. tokio::task::JoinHandle<()>,
  93. tokio::task::JoinHandle<()>,
  94. String,
  95. ) {
  96. let backend_router = Router::new().route(
  97. "/*path",
  98. any(|path: Path<String>| async move { format!("backend: {}", path.0) }),
  99. );
  100. let backend_server = Server::bind(&"127.0.0.1:0".parse().unwrap())
  101. .serve(backend_router.into_make_service());
  102. let backend_addr = backend_server.local_addr();
  103. let backend_handle = tokio::spawn(async move { backend_server.await.unwrap() });
  104. config.backend = format!("http://{}{}", backend_addr, config.backend);
  105. let router = super::add_proxy(Router::new(), &config);
  106. let server = Server::bind("127.0.0.1:0".parse().unwrap())
  107. .serve(router.unwrap().into_make_service());
  108. let server_addr = server.local_addr();
  109. let server_handle = tokio::spawn(async move { server.await.unwrap() });
  110. (backend_handle, server_handle, server_addr.to_string())
  111. }
  112. async fn test_proxy_requests(path: String) {
  113. let config = WebProxyConfig {
  114. // Normally this would be an absolute URL including scheme/host/port,
  115. // but in these tests we need to let the OS choose the port so tests
  116. // don't conflict, so we'll concatenate the final address and this
  117. // path together.
  118. // So in day to day usage, use `http://localhost:8000/api` instead!
  119. backend: path,
  120. };
  121. let (backend_handle, server_handle, server_addr) = setup_servers(config).await;
  122. let resp = Client::new()
  123. .get(format!("http://{}/api", server_addr).parse().unwrap())
  124. .await
  125. .unwrap();
  126. assert_eq!(resp.status(), StatusCode::OK);
  127. assert_eq!(
  128. axum::body::to_bytes(resp.into_body(), usize::MAX)
  129. .await
  130. .unwrap(),
  131. "backend: /api"
  132. );
  133. let resp = Client::new()
  134. .get(format!("http://{}/api/", server_addr).parse().unwrap())
  135. .await
  136. .unwrap();
  137. assert_eq!(resp.status(), StatusCode::OK);
  138. assert_eq!(
  139. axum::body::to_bytes(resp.into_body(), usize::MAX)
  140. .await
  141. .unwrap(),
  142. "backend: /api/"
  143. );
  144. let resp = Client::new()
  145. .get(
  146. format!("http://{}/api/subpath", server_addr)
  147. .parse()
  148. .unwrap(),
  149. )
  150. .await
  151. .unwrap();
  152. assert_eq!(resp.status(), StatusCode::OK);
  153. assert_eq!(
  154. axum::body::to_bytes(resp.into_body(), usize::MAX)
  155. .await
  156. .unwrap(),
  157. "backend: /api/subpath"
  158. );
  159. backend_handle.abort();
  160. server_handle.abort();
  161. }
  162. #[tokio::test]
  163. async fn add_proxy() {
  164. test_proxy_requests("/api".to_string()).await;
  165. }
  166. #[tokio::test]
  167. async fn add_proxy_trailing_slash() {
  168. test_proxy_requests("/api/".to_string()).await;
  169. }
  170. #[test]
  171. fn add_proxy_empty_path() {
  172. let config = WebProxyConfig {
  173. backend: "http://localhost:8000".to_string(),
  174. };
  175. let router = super::add_proxy(Router::new(), &config);
  176. match router.unwrap_err() {
  177. crate::Error::ProxySetupError(e) => {
  178. assert_eq!(
  179. e,
  180. "Proxy backend URL must have a non-empty path, e.g. http://localhost:8000/api instead of http://localhost:8000"
  181. );
  182. }
  183. e => panic!("Unexpected error type: {}", e),
  184. }
  185. }
  186. }