serv4rs 0.1.7

serv4rs is a powerful, pragmatic, and extremely fast web framework for Rust
Documentation
pub mod commons;
pub mod config;
pub mod error;
pub mod errors;
pub mod mandelbrot;
pub mod middlewares;
pub mod simples;
pub mod wrapper;
pub mod request;
pub mod prelude;
pub mod templates;
pub mod flash;

use crate::prelude::*;
use lazy_static::lazy_static;
use actix_web::{dev, App, HttpServer, Responder, web::ServiceConfig };
use actix_session::{SessionMiddleware, storage::CookieSessionStore}; 
use clap::Parser;
use std::fmt;
use std::time::Duration;

use chrono::offset::Offset;
use chrono::Local;

// actix extens
use actix_extensible_rate_limit::{
    backend::memory::InMemoryBackend, backend::SimpleInput, backend::SimpleInputFunctionBuilder,
    backend::SimpleOutput, RateLimiter,
};

// middlewares
use crate::middlewares::access_filter;

/// Search for a pattern in a file and display the lines that contain it.
#[derive(Parser, Debug)]
#[clap(
    author = "keesh.zhang",
    version,
    about = "A powerful, pragmatic, and extremely fast web framework for Rust"
)]
pub struct Cli {
    /// The actived server profile
    #[arg(long, short)]
    profile: Option<String>,
    /// The env path to the folder to load environment files
    #[arg(long, short)]
    env_folder: Option<String>,
}

pub async fn favicon(_req: HttpRequest) -> std::io::Result<actix_files::NamedFile> {
    actix_files::NamedFile::open(format!("{}/favicon.ico", std::env::var("static_folder").unwrap()))
}

pub async fn favicon_svg() -> impl Responder {
    actix_files::NamedFile::open_async(format!("{}/favicon.svg", std::env::var("static_folder").unwrap()))
        .await
        .unwrap()
}

pub fn static_handler(config: &mut ServiceConfig) {
    let static_path = std::env::var("static_folder").unwrap();
    let fs = actix_files::Files::new("/static", static_path);
    config.service(fs);
}

pub async fn info(request: HttpRequest) -> impl Responder {
    request.text(200, "Hello, server is alive and kicking...")
}

pub async fn not_found() -> HttpResponse {
    HttpResponse::build(actix_web::http::StatusCode::NOT_FOUND)
        .content_type("text/html;charset=utf-8")
        .body("<h1>404 - Page not found</h1>")
}

#[rustfmt::skip]
pub async fn run<F>(server_name: &'static str, cfg: F)
where
    F: Fn(&mut actix_web::web::ServiceConfig) + Send + Clone + 'static,
{
    
    // 解析命令行参数
    let args = Cli::parse();

    // 加载环境变量
    config::load_env(&args.env_folder.expect("未指定env_folder"), vec![&(".env.".to_owned() + &(args.profile.expect("未指定profile"))), ".env"]);
    // 加载程序配置
    let server_config = config::load_config::<config::ServerConfig>();
    // 初始化日志系统
    log4rs::init_file(format!("{}/log4rs.yaml", server_config.resousrce_folder), Default::default()).expect("初始化日志配置异常");
    
    let temp_foder = format!("{}", server_config.temp_folder);

    // 设置环境变量
    std::env::set_var("RUST_LOG", "info");
    std::env::set_var("RUST_MIN_STACK", server_config.min_stack_size.to_string());

    let template_store = crate::templates::load();
    let templates = template_store.templates.clone();
    let signing_key = actix_web::cookie::Key::generate();

    let new_app = move || {
        App::new()
            .wrap(wrapper::cors())
            .wrap(wrapper::default_headers())
            .wrap(wrapper::normalize_path_config())
            .wrap(access_filter::Logger::get())
            .wrap(
                // assert_eq!(session_cookie.name(), "id");
                // assert_eq!(session_cookie.path().unwrap(), "/test");
                // assert!(session_cookie.secure().unwrap());
                // assert!(session_cookie.http_only().unwrap());
                // assert!(session_cookie.max_age().is_none());
                // assert_eq!(session_cookie.domain().unwrap(), "localhost");
                SessionMiddleware::builder(CookieSessionStore::default(), signing_key.clone())
                    .cookie_path("/".to_string())
                    // .cookie_domain(Some("localhost".to_string()))
                    .build(),
            )
            // .wrap(access_limiter())
            .app_data(templates.clone())
            .app_data(wrapper::validate_json_body()) // JsonBody 验证
            .app_data(wrapper::validate_query_string()) // QueryString 参数验证
            .app_data(wrapper::validate_path_variable()) // PathVariable 参数验证
            .app_data(wrapper::validate_form_data()) // FormData 参数验证
            .app_data(wrapper::multipart_form_config(52_428_800)) // 上传文件大小限制 50 MiB * 10
            .app_data(wrapper::multipart_temp_folder_config(&commons::temp_upload_folder(server_name))) // 上传文件临时文件夹设置
            .app_data(wrapper::multipart_temp_folder_config(&temp_foder.clone())) // 上传文件临时文件夹设置
            .configure(|wc| config(wc, server_config.enable_simples))
            .configure(|wc| cfg(wc))
            .configure(static_handler)
            .default_service(actix_web::web::route().to(not_found))
    };

    let server = HttpServer::new(new_app)
        .backlog(server_config.backlog_size)
        .workers(server_config.worker_count)
        .keep_alive(std::time::Duration::from_secs(server_config.keepalive_time));


    let bind_result = if server_config.protocal == "http" {
        server.bind(format!("0.0.0.0:{}", server_config.server_port))
    } else {
        server.bind_openssl(format!("0.0.0.0:{}", server_config.server_port), tls_builder())
    };

    match bind_result {
        Ok(svr) => {
            log::info!("Congratulations! Your server '{}' will be running at {}://0.0.0.0:{}", server_name, server_config.protocal, server_config.server_port);
            log::info!("Your temporary folder for uploader is: {}", &server_config.temp_folder);
            log::info!("Current time offset is: {:+02}:00", Local::now().offset().fix().local_minus_utc() / 3600);
            svr.run().await.expect("Failed to run server")
        }
        _ => log::info!("🔥 Couldn't start the server at port {}", server_config.server_port),
    }
}

pub fn tls_builder() -> openssl::ssl::SslAcceptorBuilder {
    let mut builder = openssl::ssl::SslAcceptor::mozilla_intermediate(openssl::ssl::SslMethod::tls()).expect("初始化ssl异常");
    builder.set_private_key_file("key.pem", openssl::ssl::SslFiletype::PEM).expect("初始化ssl异常: key.pem");
    builder.set_certificate_chain_file("cert.pem").expect("初始化ssl异常: cert.pem");
    return builder;
}

pub fn access_limiter() -> RateLimiter<
    InMemoryBackend,
    SimpleOutput,
    impl Fn(&dev::ServiceRequest) -> std::future::Ready<std::result::Result<SimpleInput, actix_web::Error>>,
> {
    return RateLimiter::builder(
        InMemoryBackend::builder().build(),
        SimpleInputFunctionBuilder::new(Duration::from_secs(1), 1)
            .real_ip_key()
            .build(),
    )
    .add_headers()
    .build();
}

// 类型别名,简化使用
pub type Result<T> = std::result::Result<T, crate::error::Error>;

#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct R<T> {
    pub code: u16,
    pub message: String,
    pub data: Option<T>,
}

impl<T: serde::Serialize> R<T> {
    pub fn ok(data: T) -> Self {
        R::<T> {
            code: 200,
            message: "Ok".to_string(),
            data: Some(data),
        }
    }

    pub fn success(data: T, message: String) -> Self {
        R::<T> {
            code: 200,
            message: message,
            data: Some(data),
        }
    }

    pub fn fail(code: u16) -> Self {
        R::<T> {
            code: code,
            message: "failed".to_string(),
            data: None,
        }
    }

    pub fn failed(code: u16, message: String) -> Self {
        R::<T> {
            code: code,
            message: message,
            data: None,
        }
    }
}

impl<T> fmt::Display for R<T>
where
    T: fmt::Display + fmt::Debug,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:?}", self)
    }
}

lazy_static! {
    pub static ref RS_CACHE: moka::sync::Cache<String, String> = moka::sync::Cache::builder()
    // Time to live (TTL): 30 minutes
    .time_to_live(Duration::from_secs(30 * 60))
    // Time to idle (TTI):  1 minutes
    .time_to_idle(Duration::from_secs( 1 * 60))
    // This cache will hold up to 32MiB of values.
    .max_capacity(32 * 1024 * 1024)
    // Create the cache.
    .build();
}

// 注册一个路由
pub fn reg_router<F, Args>(
    cfg: &mut actix_web::web::ServiceConfig,
    method: &str,
    path: &str,
    handler: F,
) where
    F: actix_web::Handler<Args>,
    Args: actix_web::FromRequest + 'static,
    F::Output: actix_web::Responder + 'static,
{
    let cache_key = format!("{}::{}", method.to_uppercase().trim(), path.to_string());

    // 避免启动多个 worker 时重复打印日志
    if RS_CACHE.get(&cache_key).is_none() {
        log::info!(
            "Reg-a-router: method={}, path={}, handler={}",
            method.to_uppercase().trim(),
            path,
            commons::get_function_name(&handler)
        );

        RS_CACHE.insert(cache_key, path.to_string());
    }

    cfg.route(
        path,
        actix_web::Route::new()
            .method(
                actix_web::http::Method::from_bytes(method.to_uppercase().trim().as_bytes())
                    .unwrap(),
            )
            .to(handler),
    );
}

#[rustfmt::skip]
pub fn config(cfg: &mut actix_web::web::ServiceConfig, enable_simples: bool) {

    // buildin handles
    reg_router(cfg, "GET", "/favicon.ico", favicon);
    reg_router(cfg, "GET", "/favicon.svg", favicon_svg);
    reg_router(cfg, "GET", "/info", info);

    // pages
    reg_router(cfg, "GET", "/", simples::default_page);
    reg_router(cfg, "GET", "/graphiql", simples::graphiql);

    if enable_simples {
        // enable simples 
        reg_router(cfg, "GET", "/simple1/{name}", simples::simple1);   
        reg_router(cfg, "POST", "/simple2/{project_id}", simples::simple2); 
        reg_router(cfg, "POST", "/simple3/{project_id}", simples::simple3); 
        reg_router(cfg, "POST", "/simple4", simples::simple4);     
        reg_router(cfg, "POST", "/simple5/{project_id}", simples::simple5);
        reg_router(cfg, "get", "/simple6", simples::simple6);
        reg_router(cfg, "get", "/simple/panic/{project_id}", simples::panic1); 
        reg_router(cfg, "POST", "/simple/upload_file", simples::save_files);       
        reg_router(cfg, "GET", "/simple/mandelbrot", simples::mandelbrot);    
    }    
}