//! Run //! ```not_rust //! cargo run -p example-static-file-server //! ``` // use axum_auth::AuthBasic; #![cfg_attr(docsrs, feature(doc_cfg))] #![doc(html_logo_url = "../images/docserver.svg")] #[doc = include_str!("../README.md")] // #![doc(html_no_source)] // TODO tasks https://docs.rs/async-sqlx-session/latest/async_sqlx_session/struct.SqliteSessionStore.html use rand_core::{SeedableRng,OsRng, RngCore}; use rand_chacha::ChaCha8Rng; use axum::{ extract::Host, handler::HandlerWithoutStateExt, routing::MethodRouter, http::{ StatusCode, Uri, header::HeaderValue, Method, }, BoxError, Extension, response::Redirect, // http::Request, handler::HandlerWithoutStateExt, http::StatusCode, routing::get, Router, Router, }; use tower::ServiceBuilder; use axum_server::tls_rustls::RustlsConfig; use std::{ net::SocketAddr, path::PathBuf, sync::{Arc, Mutex}, path::Path, }; // use std::net::SocketAddr; // use tower::ServiceExt; use tower_http::{ services::{ServeDir,ServeFile}, trace::TraceLayer, cors::CorsLayer, }; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use once_cell::sync::Lazy; // use once_cell::sync::{Lazy,OnceCell}; // use async_session::{Session, SessionStore, MemoryStore}; use tera::Context; use async_sqlx_session::SqliteSessionStore; use sqlx::AnyPool; mod tera_tpls; mod defs; mod users; mod login_password; mod handlers; mod tools; use defs::{ AppDBs, SessionStoreDB, FileStore, Config, load_from_file, parse_args, AppConnectInfo, }; use users::UserStore; use tera_tpls::init_tera; use tower_cookies::CookieManagerLayer; use handlers::{ handle_404, rewrite_request_uri, admin_router_handlers, users_router_handlers, pages_router_handlers, }; use crate::tools::get_socket_addr; pub const USER_AGENT: &str = "user-agent"; pub const SESSION_COOKIE_NAME: &str = "doc_session"; pub const CFG_FILE_EXTENSION: &str = ".toml"; pub const FILE_SCHEME: &str = "file:///"; pub const PKG_NAME: &str = env!("CARGO_PKG_NAME"); // static WEBSERVER: AtomicUsize = AtomicUsize::new(0); pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); // const PKG_VERSION: Option&<&'static str> = option_env!("CARGO_PKG_VERSION"); // const PKG_DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION"); // const COOKIE_NAME: &str = "lc_authz"; // const COOKIE_SEP: &str = ":tk:"; const GIT_VERSION: &str = ""; //git_version::git_version!(); static GIT_VERSION_NAME: Lazy = Lazy::new(|| { format!("v{} [build: {}]",PKG_VERSION,GIT_VERSION) }); static PKG_FULLNAME: Lazy = Lazy::new(|| { format!("{}: TII CL Rust",PKG_NAME) }); pub const USERS_TABLENAME: &str = "users"; pub const USERS_FILESTORE: &str = "users"; pub const DEFAULT_ROLES: &str = "user"; #[derive(Clone, Copy)] struct Ports { http: u16, https: u16, } pub fn route(path: &str, method_router: MethodRouter) -> Router { Router::new().route(path, method_router) } #[tokio::main] async fn main() { let config_path = parse_args(); if config_path.is_empty() { eprintln!("No config-file found"); std::process::exit(2) } // pretty_env_logger::init(); let config = { let mut server_cfg: Config = load_from_file(&config_path, "server-config").unwrap_or_else(|e|{ eprintln!("Settings error: {}",e); std::process::exit(2) }); server_cfg.load_items(); //config.fix_root_path::(String::new()); server_cfg }; if config.verbose > 1 { dbg!("{:?}",&config); } tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "example_static_file_server=debug,tower_http=debug".into()), ) .with(tracing_subscriber::fmt::layer()) .init(); // you can convert handler function to service // let store = FileStore { // sess_path: "store".to_owned(), // ses_file: "data".to_owned(), // // sess_path: sessions_config.session_store_uri.replace(FILE_SCHEME,"").to_owned(), // // ses_file: sessions_config.session_store_file.to_owned(), // }; // let session_store = SessionStoreDB::connect_file_store(store); // let session_store = SessionStoreDB::connect_memory_store(); let session_store = if config.session_store_uri.starts_with(FILE_SCHEME) { let store = FileStore { sess_path: config.session_store_uri.replace(FILE_SCHEME,"").to_owned(), ses_file: config.session_store_file.to_owned(), }; if let Err(e) = store.check_paths() { eprintln!("Error creation File Store: {}",e); std::process::exit(2) } SessionStoreDB::connect_file_store(store) } else if config.session_store_uri.starts_with("sqlite:") { let store = SqliteSessionStore::new(&config.session_store_uri).await.unwrap_or_else(|e|{ eprintln!("Error session database {}: {}", config.session_store_uri,e ); std::process::exit(2) }); let _ = store.migrate().await; let _ = store.cleanup().await; SessionStoreDB::connect_sqlite_store(store) } else if config.session_store_uri.starts_with("memory") { SessionStoreDB::connect_memory_store() } else { SessionStoreDB::None }; let user_store = if config.users_store_uri.starts_with(FILE_SCHEME) { let users_store_uri = config.users_store_uri.replace(FILE_SCHEME,"").to_owned(); if ! Path::new(&users_store_uri).exists() { if let Err(e) = std::fs::File::create(Path::new(&users_store_uri)) { eprintln!("Error creation Users store {}: {}", &users_store_uri,e); std::process::exit(2) } } UserStore::File(users_store_uri) // } else if config.users_store_uri.starts_with("sqlite:") { } else if config.users_store_uri.contains("sql") { //let m = Migrator::new(Path::new("./migrations")).await?; //} let pool = AnyPool::connect(&config.users_store_uri).await.unwrap_or_else(|e|{ eprintln!("Error pool database {}: {}", config.users_store_uri,e ); std::process::exit(2) }); // let pool = SqlitePool::connect(&config.users_store_uri).await.unwrap_or_else(|e|{ // eprintln!("Error pool database {}: {}", // config.users_store_uri,e // ); // std::process::exit(2) // }); UserStore::Sql(pool) } else { eprintln!("User store {}: Not defined",&config.users_store_uri); std::process::exit(2) }; #[cfg(feature = "casbin")] let enforcer = if !config.authz_model_path.is_empty() && ! config.authz_policy_path.is_empty() { AppDBs::create_enforcer( Box::leak(config.authz_model_path.to_owned().into_boxed_str()), Box::leak(config.authz_policy_path.to_owned().into_boxed_str()) ).await } else { eprintln!("Error auth enforcer {} + {}", config.authz_model_path, config.authz_policy_path ); std::process::exit(2); }; let ports = Ports { http: 7878, https: 8800, }; // optional: spawn a second server to redirect http requests to this server if config.protocol.contains("http") { tokio::spawn(redirect_http_to_https(ports)); } let mut origins: Vec = Vec::new(); for itm in config.allow_origin.to_owned() { match HeaderValue::from_str(itm.as_str()) { Ok(val) => origins.push(val), Err(e) => println!("error {} with {} header for allow_origin",e,itm), } } let mut context = Context::new(); context.insert("server_name","DOC Server"); context.insert("pkg_name",&PKG_NAME); context.insert("pkg_version",&PKG_VERSION); context.insert("git_version",&GIT_VERSION); context.insert("git_version_name",GIT_VERSION_NAME.as_str()); context.insert("pkg_fullname",PKG_FULLNAME.as_str()); // let app = Router::new().route("/", get(handler)); #[cfg(feature = "authstore")] let app_dbs = Arc::new( AppDBs::new(&config, session_store, user_store, init_tera(&config.templates_path), context ) ); #[cfg(feature = "casbin")] let app_dbs = Arc::new( AppDBs::new(&config, session_store, user_store, enforcer, init_tera(&config.templates_path), context ) ); let middleware = axum::middleware::from_fn_with_state(app_dbs.clone(),rewrite_request_uri); // apply the layer around the whole `Router` // this way the middleware will run before `Router` receives the request let mut web_router = Router::new(); // Parse serv_paths to add static paths as service for item in &config.serv_paths { // Try to check src_path ... let src_path: String; if Path::new(&item.src_path).exists() { src_path = format!("{}",item.src_path); } else { src_path = if item.src_path.starts_with("/") { format!("{}",item.src_path) } else { format!("{}/{}",&config.root_path,item.src_path) }; if ! Path::new(&src_path).exists() { eprintln!("File path {}: not found", &src_path); continue; } } // Add ServeDir with not_found page ... if item.not_found.is_empty() { web_router = web_router.nest_service( &item.url_path, ServeDir::new(&src_path).not_found_service(handle_404.into_service()) ); println!("Added path {} => {}", &src_path,&item.url_path); } else { web_router = web_router.nest_service( &item.url_path, ServeDir::new(&src_path).not_found_service(ServeFile::new(&item.not_found)) ); println!("Added path {} => {} ({})", &src_path,&item.url_path,&item.not_found); } } let mut key = [0u8; 16]; let mut os_rng = OsRng{}; os_rng.fill_bytes(&mut key); let random = ChaCha8Rng::seed_from_u64(OsRng.next_u64()); web_router = web_router .merge(users_router_handlers()) .merge(admin_router_handlers()) .merge(pages_router_handlers()) .layer(ServiceBuilder::new().layer(middleware)) .layer(CookieManagerLayer::new()) .layer(Extension(app_dbs)) .layer(Extension(Arc::new(Mutex::new(random)))) .fallback_service(handle_404.into_service()) ; // if !config.html_path.is_empty() && !config.html_url.is_empty() { // MAIN_URL.set(config.server.html_url.to_owned()).unwrap_or_default(); // println!("SpaRoutert local path {} to {}",&config.server.html_path,&config.server.html_url); // web_router = web_router.merge( // ) // .fallback(fallback); // } if config.verbose > 2 { dbg!("{:?}",&origins); } if config.allow_origin.len() > 0 { web_router = web_router.layer(CorsLayer::new() .allow_origin(origins) .allow_methods(vec![Method::GET, Method::POST]) .allow_headers(tower_http::cors::Any) ); } let addr = get_socket_addr(&config.bind,config.port); tracing::debug!("listening on {}", addr); println!("listening on {}", addr); if config.protocol.as_str() == "http" { // let app_with_middleware = middleware.layer(app); // run https server //let addr = SocketAddr::from(([127, 0, 0, 1], ports.http)); //tracing::debug!("listening on {}", addr); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); axum::serve(listener, web_router.layer(TraceLayer::new_for_http()) .into_make_service_with_connect_info::() ) .await .unwrap(); } else { // configure certificate and private key used by https // let tls_config = RustlsConfig::from_pem_file( // PathBuf::from(env!("CARGO_MANIFEST_DIR")) // .join("self_signed_certs") // .join("cert.pem"), // PathBuf::from(env!("CARGO_MANIFEST_DIR")) // .join("self_signed_certs") // .join("key.pem"), // ) let tls_config = RustlsConfig::from_pem_file( PathBuf::from(&config.cert_file), PathBuf::from(&config.key_file) ) .await .unwrap_or_else(|e|{ eprintln!("Error TLS config: {}",e); std::process::exit(2) }); //let addr = SocketAddr::from(([127, 0, 0, 1], ports.https)); //tracing::debug!("listening on {}", addr); //let app_with_middleware = middleware.layer(app); // apply the layer around the whole `Router`middleware.layer(app); // .serve(web_router.into_make_service()) axum_server::bind_rustls(addr, tls_config) .serve( // web_router.layer(TraceLayer::new_for_http()) web_router .into_make_service_with_connect_info::() ) .await .unwrap(); } /* // let port = 3002; // tokio::join!( // serve(using_serve_dir(), 3001), // serve(using_serve_dir_with_assets_fallback(), 3002), // serve(using_serve_dir_only_from_root_via_fallback(), 3003), // serve(using_serve_dir_with_handler_as_service(), 3004), // serve(two_serve_dirs(), 3005), // serve(calling_serve_dir_from_a_handler(), 3006), // ); //let addr = SocketAddr::from(([127, 0, 0, 1], port)); let addr = SocketAddr::from(([192,168,1,4], port)); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); tracing::debug!("listening on {}", listener.local_addr().unwrap()); // let _ = axum::Server::bind(&addr) // .serve( // // router.layer(TraceLayer::new_for_http()) // //router.into_make_service_with_connect_info::() // router.into_make_service() // ) // .await // .expect("server failed"); // axum_server::bind(addr) // .serve(router.into_make_service()) // .await // .unwrap(); axum::serve(listener, router.layer(TraceLayer::new_for_http())) .await .unwrap(); // let _ = axum::Server::bind(&addr) // .serve( // app.layer(TraceLayer::new_for_http()).into_make_service() // // web_router.into_make_service_with_connect_info::() // // web_router.into_make_service() // ) // .await // .unwrap(); */ } async fn redirect_http_to_https(ports: Ports) { fn make_https(host: String, uri: Uri, ports: Ports) -> Result { let mut parts = uri.into_parts(); parts.scheme = Some(axum::http::uri::Scheme::HTTPS); if parts.path_and_query.is_none() { parts.path_and_query = Some("/".parse().unwrap()); } let https_host = host.replace(&ports.http.to_string(), &ports.https.to_string()); parts.authority = Some(https_host.parse()?); Ok(Uri::from_parts(parts)?) } let redirect = move |Host(host): Host, uri: Uri| async move { match make_https(host, uri, ports) { Ok(uri) => Ok(Redirect::permanent(&uri.to_string())), Err(error) => { tracing::warn!(%error, "failed to convert URI to HTTPS"); Err(StatusCode::BAD_REQUEST) } } }; let addr = SocketAddr::from(([127, 0, 0, 1], ports.http)); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); tracing::debug!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, redirect.into_make_service_with_connect_info::()) .await .unwrap(); } /* fn using_serve_dir() -> Router { // serve the file in the "assets" directory under `/assets` Router::new().nest_service("/assets", ServeDir::new("assets")) } fn using_serve_dir_with_assets_fallback() -> Router { // `ServeDir` allows setting a fallback if an asset is not found // so with this `GET /assets/doesnt-exist.jpg` will return `index.html` // rather than a 404 let serve_dir = ServeDir::new("assets").not_found_service(ServeFile::new("assets/index.html")); Router::new() .route("/foo", get(|| async { "Hi from /foo" })) .nest_service("/assets", serve_dir.clone()) .fallback_service(serve_dir) } fn using_serve_dir_only_from_root_via_fallback() -> Router { // you can also serve the assets directly from the root (not nested under `/assets`) // by only setting a `ServeDir` as the fallback let serve_dir = ServeDir::new("assets").not_found_service(ServeFile::new("assets/index.html")); Router::new() .route("/foo", get(|| async { "Hi from /foo" })) .fallback_service(serve_dir) } fn using_serve_dir_with_handler_as_service() -> Router { async fn handle_404() -> (StatusCode, &'static str) { (StatusCode::NOT_FOUND, "Not found") } // you can convert handler function to service let service = handle_404.into_service(); let serve_dir = ServeDir::new("assets").not_found_service(service); Router::new() .route("/foo", get(|| async { "Hi from /foo" })) .fallback_service(serve_dir) } fn two_serve_dirs() -> Router { // you can also have two `ServeDir`s nested at different paths let serve_dir_from_assets = ServeDir::new("assets"); let serve_dir_from_dist = ServeDir::new("dist"); Router::new() .nest_service("/assets", serve_dir_from_assets) .nest_service("/dist", serve_dir_from_dist) } #[allow(clippy::let_and_return)] fn calling_serve_dir_from_a_handler() -> Router { // via `tower::Service::call`, or more conveniently `tower::ServiceExt::oneshot` you can // call `ServeDir` yourself from a handler Router::new().nest_service( "/foo", get(|request: Request<_>| async { let service = ServeDir::new("assets"); let result = service.oneshot(request).await; result }), ) } async fn serve(app: Router, port: u16) { let addr = SocketAddr::from(([127, 0, 0, 1], port)); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); tracing::debug!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app.layer(TraceLayer::new_for_http())) .await .unwrap(); // let _ = axum::Server::bind(&addr) // .serve( // app.layer(TraceLayer::new_for_http()).into_make_service() // // web_router.into_make_service_with_connect_info::() // // web_router.into_make_service() // ) // .await // .unwrap(); } */