mod.rs 13 KB

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