123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438 |
- use crate::{
- builder,
- serve::Serve,
- server::{
- output::{print_console_info, PrettierOptions, WebServerInfo},
- setup_file_watcher, HotReloadState,
- },
- BuildResult, CrateConfig, Result, WebHttpsConfig,
- };
- use axum::{
- body::{Full, HttpBody},
- extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
- http::{
- self,
- header::{HeaderName, HeaderValue},
- Method, Response, StatusCode,
- },
- response::IntoResponse,
- routing::{get, get_service},
- Router,
- };
- use axum_server::tls_rustls::RustlsConfig;
- use dioxus_html::HtmlCtx;
- use dioxus_rsx::hot_reload::*;
- use std::{
- net::UdpSocket,
- process::Command,
- sync::{Arc, Mutex},
- };
- use tokio::sync::broadcast::{self, Sender};
- use tower::ServiceBuilder;
- use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody};
- use tower_http::{
- cors::{Any, CorsLayer},
- ServiceBuilderExt,
- };
- #[cfg(feature = "plugin")]
- use plugin::PluginManager;
- mod proxy;
- mod hot_reload;
- use hot_reload::*;
- struct WsReloadState {
- update: broadcast::Sender<()>,
- }
- pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> {
- // ctrl-c shutdown checker
- let _crate_config = config.clone();
- let _ = ctrlc::set_handler(move || {
- #[cfg(feature = "plugin")]
- let _ = PluginManager::on_serve_shutdown(&_crate_config);
- std::process::exit(0);
- });
- let ip = get_ip().unwrap_or(String::from("0.0.0.0"));
- let hot_reload_state = match config.hot_reload {
- true => {
- let FileMapBuildResult { map, errors } =
- FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
- for err in errors {
- log::error!("{}", err);
- }
- let file_map = Arc::new(Mutex::new(map));
- let hot_reload_tx = broadcast::channel(100).0;
- Some(HotReloadState {
- messages: hot_reload_tx.clone(),
- file_map: file_map.clone(),
- })
- }
- false => None,
- };
- serve(ip, port, config, start_browser, hot_reload_state).await?;
- Ok(())
- }
- /// Start the server without hot reload
- pub async fn serve(
- ip: String,
- port: u16,
- config: CrateConfig,
- start_browser: bool,
- hot_reload_state: Option<HotReloadState>,
- ) -> Result<()> {
- let first_build_result = crate::builder::build(&config, true)?;
- log::info!("🚀 Starting development server...");
- // WS Reload Watching
- let (reload_tx, _) = broadcast::channel(100);
- // We got to own watcher so that it exists for the duration of serve
- // Otherwise full reload won't work.
- let _watcher = setup_file_watcher(
- {
- let config = config.clone();
- let reload_tx = reload_tx.clone();
- move || build(&config, &reload_tx)
- },
- &config,
- Some(WebServerInfo {
- ip: ip.clone(),
- port,
- }),
- hot_reload_state.clone(),
- )
- .await?;
- let ws_reload_state = Arc::new(WsReloadState {
- update: reload_tx.clone(),
- });
- // HTTPS
- // Before console info so it can stop if mkcert isn't installed or fails
- let rustls_config = get_rustls(&config).await?;
- // Print serve info
- print_console_info(
- &config,
- PrettierOptions {
- changed: vec![],
- warnings: first_build_result.warnings,
- elapsed_time: first_build_result.elapsed_time,
- },
- Some(crate::server::output::WebServerInfo {
- ip: ip.clone(),
- port,
- }),
- );
- // Router
- let router = setup_router(config, ws_reload_state, hot_reload_state).await?;
- // Start server
- start_server(port, router, start_browser, rustls_config).await?;
- Ok(())
- }
- const DEFAULT_KEY_PATH: &str = "ssl/key.pem";
- const DEFAULT_CERT_PATH: &str = "ssl/cert.pem";
- /// Returns an enum of rustls config and a bool if mkcert isn't installed
- async fn get_rustls(config: &CrateConfig) -> Result<Option<RustlsConfig>> {
- let web_config = &config.dioxus_config.web.https;
- if web_config.enabled != Some(true) {
- return Ok(None);
- }
- let (cert_path, key_path) = if let Some(true) = web_config.mkcert {
- // mkcert, use it
- get_rustls_with_mkcert(web_config)?
- } else {
- // if mkcert not specified or false, don't use it
- get_rustls_without_mkcert(web_config)?
- };
- Ok(Some(
- RustlsConfig::from_pem_file(cert_path, key_path).await?,
- ))
- }
- fn get_rustls_with_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
- // Get paths to store certs, otherwise use ssl/item.pem
- let key_path = web_config
- .key_path
- .clone()
- .unwrap_or(DEFAULT_KEY_PATH.to_string());
- let cert_path = web_config
- .cert_path
- .clone()
- .unwrap_or(DEFAULT_CERT_PATH.to_string());
- // Create ssl directory if using defaults
- if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH {
- _ = fs::create_dir("ssl");
- }
- let cmd = Command::new("mkcert")
- .args([
- "-install",
- "-key-file",
- &key_path,
- "-cert-file",
- &cert_path,
- "localhost",
- "::1",
- "127.0.0.1",
- ])
- .spawn();
- match cmd {
- Err(e) => {
- match e.kind() {
- io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."),
- e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()),
- };
- return Err("failed to generate mkcert certificates".into());
- }
- Ok(mut cmd) => {
- cmd.wait()?;
- }
- }
- Ok((cert_path, key_path))
- }
- fn get_rustls_without_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
- // get paths to cert & key
- if let (Some(key), Some(cert)) = (web_config.key_path.clone(), web_config.cert_path.clone()) {
- Ok((cert, key))
- } else {
- // missing cert or key
- Err("https is enabled but cert or key path is missing".into())
- }
- }
- /// Sets up and returns a router
- async fn setup_router(
- config: CrateConfig,
- ws_reload: Arc<WsReloadState>,
- hot_reload: Option<HotReloadState>,
- ) -> Result<Router> {
- // Setup cors
- let cors = CorsLayer::new()
- // allow `GET` and `POST` when accessing the resource
- .allow_methods([Method::GET, Method::POST])
- // allow requests from any origin
- .allow_origin(Any)
- .allow_headers(Any);
- let (coep, coop) = if config.cross_origin_policy {
- (
- HeaderValue::from_static("require-corp"),
- HeaderValue::from_static("same-origin"),
- )
- } else {
- (
- HeaderValue::from_static("unsafe-none"),
- HeaderValue::from_static("unsafe-none"),
- )
- };
- // Create file service
- let file_service_config = config.clone();
- let file_service = ServiceBuilder::new()
- .override_response_header(
- HeaderName::from_static("cross-origin-embedder-policy"),
- coep,
- )
- .override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
- .and_then(
- move |response: Response<ServeFileSystemResponseBody>| async move {
- let mut response = if file_service_config
- .dioxus_config
- .web
- .watcher
- .index_on_404
- .unwrap_or(false)
- && response.status() == StatusCode::NOT_FOUND
- {
- let body = Full::from(
- // TODO: Cache/memoize this.
- std::fs::read_to_string(
- file_service_config
- .crate_dir
- .join(file_service_config.out_dir)
- .join("index.html"),
- )
- .ok()
- .unwrap(),
- )
- .map_err(|err| match err {})
- .boxed();
- Response::builder()
- .status(StatusCode::OK)
- .body(body)
- .unwrap()
- } else {
- response.map(|body| body.boxed())
- };
- let headers = response.headers_mut();
- headers.insert(
- http::header::CACHE_CONTROL,
- HeaderValue::from_static("no-cache"),
- );
- headers.insert(http::header::PRAGMA, HeaderValue::from_static("no-cache"));
- headers.insert(http::header::EXPIRES, HeaderValue::from_static("0"));
- Ok(response)
- },
- )
- .service(ServeDir::new(config.crate_dir.join(&config.out_dir)));
- // Setup websocket
- let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
- // Setup proxy
- for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() {
- router = proxy::add_proxy(router, &proxy_config)?;
- }
- // Route file service
- router = router.fallback(get_service(file_service).handle_error(
- |error: std::io::Error| async move {
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- format!("Unhandled internal error: {}", error),
- )
- },
- ));
- // Setup routes
- router = router
- .route("/_dioxus/hot_reload", get(hot_reload_handler))
- .layer(cors)
- .layer(Extension(ws_reload));
- if let Some(hot_reload) = hot_reload {
- router = router.layer(Extension(hot_reload))
- }
- Ok(router)
- }
- /// Starts dx serve with no hot reload
- async fn start_server(
- port: u16,
- router: Router,
- start_browser: bool,
- rustls: Option<RustlsConfig>,
- ) -> Result<()> {
- // If plugins, call on_serve_start event
- #[cfg(feature = "plugin")]
- PluginManager::on_serve_start(&config)?;
- // Parse address
- let addr = format!("0.0.0.0:{}", port).parse().unwrap();
- // Open the browser
- if start_browser {
- match rustls {
- Some(_) => _ = open::that(format!("https://{}", addr)),
- None => _ = open::that(format!("http://{}", addr)),
- }
- }
- // Start the server with or without rustls
- match rustls {
- Some(rustls) => {
- axum_server::bind_rustls(addr, rustls)
- .serve(router.into_make_service())
- .await?
- }
- None => {
- axum::Server::bind(&addr)
- .serve(router.into_make_service())
- .await?
- }
- }
- Ok(())
- }
- /// Get the network ip
- fn get_ip() -> Option<String> {
- let socket = match UdpSocket::bind("0.0.0.0:0") {
- Ok(s) => s,
- Err(_) => return None,
- };
- match socket.connect("8.8.8.8:80") {
- Ok(()) => (),
- Err(_) => return None,
- };
- match socket.local_addr() {
- Ok(addr) => Some(addr.ip().to_string()),
- Err(_) => None,
- }
- }
- /// Handle websockets
- async fn ws_handler(
- ws: WebSocketUpgrade,
- _: Option<TypedHeader<headers::UserAgent>>,
- Extension(state): Extension<Arc<WsReloadState>>,
- ) -> impl IntoResponse {
- ws.on_upgrade(|mut socket| async move {
- let mut rx = state.update.subscribe();
- let reload_watcher = tokio::spawn(async move {
- loop {
- rx.recv().await.unwrap();
- // ignore the error
- if socket
- .send(Message::Text(String::from("reload")))
- .await
- .is_err()
- {
- break;
- }
- // flush the errors after recompling
- rx = rx.resubscribe();
- }
- });
- reload_watcher.await.unwrap();
- })
- }
- fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result<BuildResult> {
- let result = builder::build(config, true)?;
- // change the websocket reload state to true;
- // the page will auto-reload.
- if config
- .dioxus_config
- .web
- .watcher
- .reload_html
- .unwrap_or(false)
- {
- let _ = Serve::regen_dev_page(config);
- }
- let _ = reload_tx.send(());
- Ok(result)
- }
|