mod.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. use crate::{
  2. builder,
  3. serve::Serve,
  4. server::{
  5. output::{print_console_info, PrettierOptions, WebServerInfo},
  6. setup_file_watcher, HotReloadState,
  7. },
  8. BuildResult, CrateConfig, Result, WebHttpsConfig,
  9. };
  10. use axum::{
  11. body::{Full, HttpBody},
  12. extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
  13. http::{
  14. self,
  15. header::{HeaderName, HeaderValue},
  16. Method, Response, StatusCode,
  17. },
  18. response::IntoResponse,
  19. routing::{get, get_service},
  20. Router,
  21. };
  22. use axum_server::tls_rustls::RustlsConfig;
  23. use dioxus_html::HtmlCtx;
  24. use dioxus_rsx::hot_reload::*;
  25. use std::{
  26. net::UdpSocket,
  27. process::Command,
  28. sync::{Arc, Mutex},
  29. };
  30. use tokio::sync::broadcast::{self, Sender};
  31. use tower::ServiceBuilder;
  32. use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody};
  33. use tower_http::{
  34. cors::{Any, CorsLayer},
  35. ServiceBuilderExt,
  36. };
  37. #[cfg(feature = "plugin")]
  38. use plugin::PluginManager;
  39. mod proxy;
  40. mod hot_reload;
  41. use hot_reload::*;
  42. struct WsReloadState {
  43. update: broadcast::Sender<()>,
  44. }
  45. pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> {
  46. // ctrl-c shutdown checker
  47. let _crate_config = config.clone();
  48. let _ = ctrlc::set_handler(move || {
  49. #[cfg(feature = "plugin")]
  50. let _ = PluginManager::on_serve_shutdown(&_crate_config);
  51. std::process::exit(0);
  52. });
  53. let ip = get_ip().unwrap_or(String::from("0.0.0.0"));
  54. let hot_reload_state = match config.hot_reload {
  55. true => {
  56. let FileMapBuildResult { map, errors } =
  57. FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
  58. for err in errors {
  59. log::error!("{}", err);
  60. }
  61. let file_map = Arc::new(Mutex::new(map));
  62. let hot_reload_tx = broadcast::channel(100).0;
  63. Some(HotReloadState {
  64. messages: hot_reload_tx.clone(),
  65. file_map: file_map.clone(),
  66. })
  67. }
  68. false => None,
  69. };
  70. serve(ip, port, config, start_browser, hot_reload_state).await?;
  71. Ok(())
  72. }
  73. /// Start the server without hot reload
  74. pub async fn serve(
  75. ip: String,
  76. port: u16,
  77. config: CrateConfig,
  78. start_browser: bool,
  79. hot_reload_state: Option<HotReloadState>,
  80. ) -> Result<()> {
  81. let first_build_result = crate::builder::build(&config, true)?;
  82. log::info!("🚀 Starting development server...");
  83. // WS Reload Watching
  84. let (reload_tx, _) = broadcast::channel(100);
  85. // We got to own watcher so that it exists for the duration of serve
  86. // Otherwise full reload won't work.
  87. let _watcher = setup_file_watcher(
  88. {
  89. let config = config.clone();
  90. let reload_tx = reload_tx.clone();
  91. move || build(&config, &reload_tx)
  92. },
  93. &config,
  94. Some(WebServerInfo {
  95. ip: ip.clone(),
  96. port,
  97. }),
  98. hot_reload_state.clone(),
  99. )
  100. .await?;
  101. let ws_reload_state = Arc::new(WsReloadState {
  102. update: reload_tx.clone(),
  103. });
  104. // HTTPS
  105. // Before console info so it can stop if mkcert isn't installed or fails
  106. let rustls_config = get_rustls(&config).await?;
  107. // Print serve info
  108. print_console_info(
  109. &config,
  110. PrettierOptions {
  111. changed: vec![],
  112. warnings: first_build_result.warnings,
  113. elapsed_time: first_build_result.elapsed_time,
  114. },
  115. Some(crate::server::output::WebServerInfo {
  116. ip: ip.clone(),
  117. port,
  118. }),
  119. );
  120. // Router
  121. let router = setup_router(config, ws_reload_state, hot_reload_state).await?;
  122. // Start server
  123. start_server(port, router, start_browser, rustls_config).await?;
  124. Ok(())
  125. }
  126. const DEFAULT_KEY_PATH: &str = "ssl/key.pem";
  127. const DEFAULT_CERT_PATH: &str = "ssl/cert.pem";
  128. /// Returns an enum of rustls config and a bool if mkcert isn't installed
  129. async fn get_rustls(config: &CrateConfig) -> Result<Option<RustlsConfig>> {
  130. let web_config = &config.dioxus_config.web.https;
  131. if web_config.enabled != Some(true) {
  132. return Ok(None);
  133. }
  134. let (cert_path, key_path) = if let Some(true) = web_config.mkcert {
  135. // mkcert, use it
  136. get_rustls_with_mkcert(web_config)?
  137. } else {
  138. // if mkcert not specified or false, don't use it
  139. get_rustls_without_mkcert(web_config)?
  140. };
  141. Ok(Some(
  142. RustlsConfig::from_pem_file(cert_path, key_path).await?,
  143. ))
  144. }
  145. fn get_rustls_with_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
  146. // Get paths to store certs, otherwise use ssl/item.pem
  147. let key_path = web_config
  148. .key_path
  149. .clone()
  150. .unwrap_or(DEFAULT_KEY_PATH.to_string());
  151. let cert_path = web_config
  152. .cert_path
  153. .clone()
  154. .unwrap_or(DEFAULT_CERT_PATH.to_string());
  155. // Create ssl directory if using defaults
  156. if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH {
  157. _ = fs::create_dir("ssl");
  158. }
  159. let cmd = Command::new("mkcert")
  160. .args([
  161. "-install",
  162. "-key-file",
  163. &key_path,
  164. "-cert-file",
  165. &cert_path,
  166. "localhost",
  167. "::1",
  168. "127.0.0.1",
  169. ])
  170. .spawn();
  171. match cmd {
  172. Err(e) => {
  173. match e.kind() {
  174. io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."),
  175. e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()),
  176. };
  177. return Err("failed to generate mkcert certificates".into());
  178. }
  179. Ok(mut cmd) => {
  180. cmd.wait()?;
  181. }
  182. }
  183. Ok((cert_path, key_path))
  184. }
  185. fn get_rustls_without_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
  186. // get paths to cert & key
  187. if let (Some(key), Some(cert)) = (web_config.key_path.clone(), web_config.cert_path.clone()) {
  188. Ok((cert, key))
  189. } else {
  190. // missing cert or key
  191. Err("https is enabled but cert or key path is missing".into())
  192. }
  193. }
  194. /// Sets up and returns a router
  195. async fn setup_router(
  196. config: CrateConfig,
  197. ws_reload: Arc<WsReloadState>,
  198. hot_reload: Option<HotReloadState>,
  199. ) -> Result<Router> {
  200. // Setup cors
  201. let cors = CorsLayer::new()
  202. // allow `GET` and `POST` when accessing the resource
  203. .allow_methods([Method::GET, Method::POST])
  204. // allow requests from any origin
  205. .allow_origin(Any)
  206. .allow_headers(Any);
  207. let (coep, coop) = if config.cross_origin_policy {
  208. (
  209. HeaderValue::from_static("require-corp"),
  210. HeaderValue::from_static("same-origin"),
  211. )
  212. } else {
  213. (
  214. HeaderValue::from_static("unsafe-none"),
  215. HeaderValue::from_static("unsafe-none"),
  216. )
  217. };
  218. // Create file service
  219. let file_service_config = config.clone();
  220. let file_service = ServiceBuilder::new()
  221. .override_response_header(
  222. HeaderName::from_static("cross-origin-embedder-policy"),
  223. coep,
  224. )
  225. .override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
  226. .and_then(
  227. move |response: Response<ServeFileSystemResponseBody>| async move {
  228. let mut response = if file_service_config
  229. .dioxus_config
  230. .web
  231. .watcher
  232. .index_on_404
  233. .unwrap_or(false)
  234. && response.status() == StatusCode::NOT_FOUND
  235. {
  236. let body = Full::from(
  237. // TODO: Cache/memoize this.
  238. std::fs::read_to_string(
  239. file_service_config
  240. .crate_dir
  241. .join(file_service_config.out_dir)
  242. .join("index.html"),
  243. )
  244. .ok()
  245. .unwrap(),
  246. )
  247. .map_err(|err| match err {})
  248. .boxed();
  249. Response::builder()
  250. .status(StatusCode::OK)
  251. .body(body)
  252. .unwrap()
  253. } else {
  254. response.map(|body| body.boxed())
  255. };
  256. let headers = response.headers_mut();
  257. headers.insert(
  258. http::header::CACHE_CONTROL,
  259. HeaderValue::from_static("no-cache"),
  260. );
  261. headers.insert(http::header::PRAGMA, HeaderValue::from_static("no-cache"));
  262. headers.insert(http::header::EXPIRES, HeaderValue::from_static("0"));
  263. Ok(response)
  264. },
  265. )
  266. .service(ServeDir::new(config.crate_dir.join(&config.out_dir)));
  267. // Setup websocket
  268. let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
  269. // Setup proxy
  270. for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() {
  271. router = proxy::add_proxy(router, &proxy_config)?;
  272. }
  273. // Route file service
  274. router = router.fallback(get_service(file_service).handle_error(
  275. |error: std::io::Error| async move {
  276. (
  277. StatusCode::INTERNAL_SERVER_ERROR,
  278. format!("Unhandled internal error: {}", error),
  279. )
  280. },
  281. ));
  282. // Setup routes
  283. router = router
  284. .route("/_dioxus/hot_reload", get(hot_reload_handler))
  285. .layer(cors)
  286. .layer(Extension(ws_reload));
  287. if let Some(hot_reload) = hot_reload {
  288. router = router.layer(Extension(hot_reload))
  289. }
  290. Ok(router)
  291. }
  292. /// Starts dx serve with no hot reload
  293. async fn start_server(
  294. port: u16,
  295. router: Router,
  296. start_browser: bool,
  297. rustls: Option<RustlsConfig>,
  298. ) -> Result<()> {
  299. // If plugins, call on_serve_start event
  300. #[cfg(feature = "plugin")]
  301. PluginManager::on_serve_start(&config)?;
  302. // Parse address
  303. let addr = format!("0.0.0.0:{}", port).parse().unwrap();
  304. // Open the browser
  305. if start_browser {
  306. match rustls {
  307. Some(_) => _ = open::that(format!("https://{}", addr)),
  308. None => _ = open::that(format!("http://{}", addr)),
  309. }
  310. }
  311. // Start the server with or without rustls
  312. match rustls {
  313. Some(rustls) => {
  314. axum_server::bind_rustls(addr, rustls)
  315. .serve(router.into_make_service())
  316. .await?
  317. }
  318. None => {
  319. axum::Server::bind(&addr)
  320. .serve(router.into_make_service())
  321. .await?
  322. }
  323. }
  324. Ok(())
  325. }
  326. /// Get the network ip
  327. fn get_ip() -> Option<String> {
  328. let socket = match UdpSocket::bind("0.0.0.0:0") {
  329. Ok(s) => s,
  330. Err(_) => return None,
  331. };
  332. match socket.connect("8.8.8.8:80") {
  333. Ok(()) => (),
  334. Err(_) => return None,
  335. };
  336. match socket.local_addr() {
  337. Ok(addr) => Some(addr.ip().to_string()),
  338. Err(_) => None,
  339. }
  340. }
  341. /// Handle websockets
  342. async fn ws_handler(
  343. ws: WebSocketUpgrade,
  344. _: Option<TypedHeader<headers::UserAgent>>,
  345. Extension(state): Extension<Arc<WsReloadState>>,
  346. ) -> impl IntoResponse {
  347. ws.on_upgrade(|mut socket| async move {
  348. let mut rx = state.update.subscribe();
  349. let reload_watcher = tokio::spawn(async move {
  350. loop {
  351. rx.recv().await.unwrap();
  352. // ignore the error
  353. if socket
  354. .send(Message::Text(String::from("reload")))
  355. .await
  356. .is_err()
  357. {
  358. break;
  359. }
  360. // flush the errors after recompling
  361. rx = rx.resubscribe();
  362. }
  363. });
  364. reload_watcher.await.unwrap();
  365. })
  366. }
  367. fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result<BuildResult> {
  368. let result = builder::build(config, true)?;
  369. // change the websocket reload state to true;
  370. // the page will auto-reload.
  371. if config
  372. .dioxus_config
  373. .web
  374. .watcher
  375. .reload_html
  376. .unwrap_or(false)
  377. {
  378. let _ = Serve::regen_dev_page(config);
  379. }
  380. let _ = reload_tx.send(());
  381. Ok(result)
  382. }