docserver/src/main.rs

547 lines
19 KiB
Rust

//! 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<String> = Lazy::new(|| {
format!("v{} [build: {}]",PKG_VERSION,GIT_VERSION)
});
static PKG_FULLNAME: Lazy<String> = 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::<ServerConfig>(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<HeaderValue> = 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::<SocketAddr>()
)
.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::<AppConnectInfo>()
)
.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::<SocketAddr>()
// 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::<SocketAddr>()
// // 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<Uri, BoxError> {
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::<SocketAddr>())
.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::<SocketAddr>()
// // web_router.into_make_service()
// )
// .await
// .unwrap();
}
*/