547 lines
19 KiB
Rust
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();
|
|
}
|
|
*/
|
|
|