proxy.rs 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. use crate::Result;
  2. use dioxus_cli_config::WebProxyConfig;
  3. use anyhow::{anyhow, Context};
  4. use axum::{http::StatusCode, routing::any, Router};
  5. use hyper::{Request, Response, Uri};
  6. use hyper_util::{
  7. client::legacy::{self, connect::HttpConnector},
  8. rt::TokioExecutor,
  9. };
  10. use axum::body::Body as MyBody;
  11. #[derive(Debug, Clone)]
  12. struct ProxyClient {
  13. inner: legacy::Client<hyper_rustls::HttpsConnector<HttpConnector>, MyBody>,
  14. url: Uri,
  15. }
  16. impl ProxyClient {
  17. fn new(url: Uri) -> Self {
  18. let https = hyper_rustls::HttpsConnectorBuilder::new()
  19. .with_native_roots()
  20. .unwrap()
  21. .https_or_http()
  22. .enable_http1()
  23. .build();
  24. Self {
  25. inner: legacy::Client::builder(TokioExecutor::new()).build(https),
  26. url,
  27. }
  28. }
  29. async fn send(&self, mut req: Request<MyBody>) -> Result<Response<hyper::body::Incoming>> {
  30. let mut uri_parts = req.uri().clone().into_parts();
  31. uri_parts.authority = self.url.authority().cloned();
  32. uri_parts.scheme = self.url.scheme().cloned();
  33. *req.uri_mut() = Uri::from_parts(uri_parts).context("Invalid URI parts")?;
  34. self.inner
  35. .request(req)
  36. .await
  37. .map_err(|err| crate::error::Error::Other(anyhow!(err)))
  38. }
  39. }
  40. /// Add routes to the router handling the specified proxy config.
  41. ///
  42. /// We will proxy requests directed at either:
  43. ///
  44. /// - the exact path of the proxy config's backend URL, e.g. /api
  45. /// - the exact path with a trailing slash, e.g. /api/
  46. /// - any subpath of the backend URL, e.g. /api/foo/bar
  47. pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result<Router> {
  48. let url: Uri = proxy.backend.parse()?;
  49. let path = url.path().to_string();
  50. let trimmed_path = path.trim_start_matches('/');
  51. if trimmed_path.is_empty() {
  52. return Err(crate::Error::ProxySetupError(format!(
  53. "Proxy backend URL must have a non-empty path, e.g. {}/api instead of {}",
  54. proxy.backend.trim_end_matches('/'),
  55. proxy.backend
  56. )));
  57. }
  58. let client = ProxyClient::new(url);
  59. router = router.route(
  60. // Always remove trailing /'s so that the exact route
  61. // matches.
  62. &format!("/*{}", trimmed_path.trim_end_matches('/')),
  63. any(move |req: Request<MyBody>| async move {
  64. client
  65. .send(req)
  66. .await
  67. .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
  68. }),
  69. );
  70. Ok(router)
  71. }
  72. #[cfg(test)]
  73. mod test {
  74. use super::*;
  75. use axum::Router;
  76. use axum_server::{Handle, Server};
  77. async fn setup_servers(mut config: WebProxyConfig) -> String {
  78. let backend_router =
  79. Router::new().route(
  80. "/*path",
  81. any(|request: axum::extract::Request| async move {
  82. format!("backend: {}", request.uri())
  83. }),
  84. );
  85. // The API backend server
  86. let backend_handle_handle = Handle::new();
  87. let backend_handle_handle_ = backend_handle_handle.clone();
  88. tokio::spawn(async move {
  89. Server::bind("127.0.0.1:0".parse().unwrap())
  90. .handle(backend_handle_handle_)
  91. .serve(backend_router.into_make_service())
  92. .await
  93. .unwrap();
  94. });
  95. // Set the user's config to this dummy API we just built so we can test it
  96. let backend_addr = backend_handle_handle.listening().await.unwrap();
  97. config.backend = format!("http://{}{}", backend_addr, config.backend);
  98. // Now set up our actual filesystem server
  99. let router = super::add_proxy(Router::new(), &config);
  100. let server_handle_handle = Handle::new();
  101. let server_handle_handle_ = server_handle_handle.clone();
  102. tokio::spawn(async move {
  103. Server::bind("127.0.0.1:0".parse().unwrap())
  104. .handle(server_handle_handle_)
  105. .serve(router.unwrap().into_make_service())
  106. .await
  107. .unwrap();
  108. });
  109. // Expose *just* the fileystem web server's address
  110. server_handle_handle.listening().await.unwrap().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 server_addr = setup_servers(config).await;
  122. assert_eq!(
  123. reqwest::get(format!("http://{}/api", server_addr))
  124. .await
  125. .unwrap()
  126. .text()
  127. .await
  128. .unwrap(),
  129. "backend: /api"
  130. );
  131. assert_eq!(
  132. reqwest::get(format!("http://{}/api/", server_addr))
  133. .await
  134. .unwrap()
  135. .text()
  136. .await
  137. .unwrap(),
  138. "backend: /api/"
  139. );
  140. assert_eq!(
  141. reqwest::get(format!("http://{server_addr}/api/subpath"))
  142. .await
  143. .unwrap()
  144. .text()
  145. .await
  146. .unwrap(),
  147. "backend: /api/subpath"
  148. );
  149. }
  150. #[tokio::test]
  151. async fn add_proxy() {
  152. test_proxy_requests("/api".to_string()).await;
  153. }
  154. #[tokio::test]
  155. async fn add_proxy_trailing_slash() {
  156. test_proxy_requests("/api/".to_string()).await;
  157. }
  158. #[test]
  159. fn add_proxy_empty_path() {
  160. let config = WebProxyConfig {
  161. backend: "http://localhost:8000".to_string(),
  162. };
  163. let router = super::add_proxy(Router::new(), &config);
  164. match router.unwrap_err() {
  165. crate::Error::ProxySetupError(e) => {
  166. assert_eq!(
  167. e,
  168. "Proxy backend URL must have a non-empty path, e.g. http://localhost:8000/api instead of http://localhost:8000"
  169. );
  170. }
  171. e => panic!("Unexpected error type: {}", e),
  172. }
  173. }
  174. }