use crate::ayb_db::db_interfaces::connect_to_ayb_db;
use crate::ayb_db::db_interfaces::AybDb;
use crate::email::create_email_backends;
use crate::error::AybError;
use crate::hosted_db::daemon_registry::DaemonRegistry;
use crate::server::config::read_config;
use crate::server::config::{AybConfig, AybConfigCors, WebHostingMethod};
use crate::server::snapshots::execution::schedule_periodic_snapshots;
use crate::server::tokens::retrieve_and_validate_api_token;
use crate::server::web_frontend::WebFrontendDetails;
use crate::server::{api_endpoints, ui_endpoints};
use actix_cors::Cors;
use actix_web::body::MessageBody;
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::middleware::Next;
use actix_web::{middleware, web, App, Error, HttpMessage, HttpServer};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use dyn_clone::clone_box;
use std::fs;
use std::path::Path;
pub fn config(cfg: &mut web::ServiceConfig, ayb_config: &AybConfig) {
cfg.service(api_endpoints::health_endpoint)
.service(api_endpoints::confirm_endpoint)
.service(api_endpoints::log_in_endpoint)
.service(api_endpoints::register_endpoint)
.service(api_endpoints::oauth_token_endpoint);
cfg.service(
web::scope("/v1")
.service(api_endpoints::create_database_endpoint)
.service(api_endpoints::database_details_endpoint)
.service(api_endpoints::update_database_endpoint)
.service(api_endpoints::query_endpoint)
.service(api_endpoints::entity_details_endpoint)
.service(api_endpoints::update_profile_endpoint)
.service(api_endpoints::list_snapshots_endpoint)
.service(api_endpoints::restore_snapshot_endpoint)
.service(api_endpoints::share_endpoint)
.service(api_endpoints::list_database_permissions_endpoint)
.service(api_endpoints::list_tokens_endpoint)
.service(api_endpoints::revoke_token_endpoint),
);
if let Some(web_config) = &ayb_config.web {
if web_config.hosting_method == WebHostingMethod::Local {
cfg.service(ui_endpoints::log_in_endpoint)
.service(ui_endpoints::log_in_submit_endpoint)
.service(ui_endpoints::log_out_endpoint)
.service(ui_endpoints::register_endpoint)
.service(ui_endpoints::register_submit_endpoint)
.service(ui_endpoints::confirm_endpoint)
.service(ui_endpoints::entity_tokens_endpoint)
.service(ui_endpoints::revoke_token_endpoint)
.service(ui_endpoints::oauth_authorize_endpoint)
.service(ui_endpoints::oauth_authorize_submit_endpoint)
.service(ui_endpoints::entity_details_endpoint)
.service(ui_endpoints::create_database_endpoint)
.service(ui_endpoints::update_profile_endpoint)
.service(ui_endpoints::database_endpoint)
.service(ui_endpoints::query_endpoint)
.service(ui_endpoints::update_public_sharing_endpoint)
.service(ui_endpoints::share_with_entity_endpoint)
.service(ui_endpoints::database_permissions_endpoint)
.service(ui_endpoints::database_snapshots_endpoint)
.service(ui_endpoints::restore_snapshot_endpoint);
}
}
}
async fn attach_validated_entity(token: &str, req: &ServiceRequest) -> Result<(), AybError> {
let ayb_db = req
.app_data::<web::Data<Box<dyn AybDb>>>()
.ok_or_else(|| AybError::Other {
message: "Misconfigured server: no database".to_string(),
})?;
let api_token = retrieve_and_validate_api_token(token, ayb_db).await?;
let entity = ayb_db.get_entity_by_id(api_token.entity_id).await?;
req.extensions_mut().insert(entity);
req.extensions_mut().insert(api_token);
Ok(())
}
pub async fn entity_validator(
req: ServiceRequest,
credentials: BearerAuth,
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
match attach_validated_entity(credentials.token(), &req).await {
Ok(()) => Ok(req),
Err(e) => Err((e.into(), req)),
}
}
pub async fn optional_entity_validator(
req: ServiceRequest,
next: Next<impl MessageBody + 'static>,
) -> Result<ServiceResponse<impl MessageBody>, Error> {
let auth_header = req
.headers()
.get(actix_web::http::header::AUTHORIZATION)
.and_then(|h| h.to_str().ok())
.map(str::to_string);
if let Some(header_value) = auth_header {
let token = header_value
.strip_prefix("Bearer ")
.or_else(|| header_value.strip_prefix("bearer "))
.or_else(|| header_value.strip_prefix("BEARER "))
.ok_or_else(|| AybError::Other {
message: "Invalid Authorization header: expected `Bearer <token>`".to_string(),
})?
.trim();
attach_validated_entity(token, &req).await?;
}
next.call(req).await
}
fn build_cors(ayb_cors: AybConfigCors) -> Cors {
let mut cors = Cors::default()
.allow_any_header()
.allow_any_method()
.max_age(7200);
if ayb_cors.origin.trim() == "*" {
cors = cors.allow_any_origin()
} else {
cors = cors.allowed_origin(ayb_cors.origin.trim());
}
cors
}
pub async fn run_server(config_path: &Path) -> std::io::Result<()> {
env_logger::init();
let ayb_conf = read_config(config_path)
.unwrap_or_else(|e| panic!("unable to read ayb.toml configuration file: {e}"));
let ayb_conf_for_server = ayb_conf.clone();
fs::create_dir_all(&ayb_conf.data_path).expect("unable to create data directory");
let ayb_db = connect_to_ayb_db(ayb_conf.database_url)
.await
.expect("unable to connect to ayb database");
let web_details = WebFrontendDetails::load(ayb_conf_for_server.clone())
.await
.expect("failed to load web frontend details");
let email_backends = create_email_backends(&ayb_conf.email);
let daemon_registry = DaemonRegistry::new();
let cleanup_daemon_registry = daemon_registry.clone();
schedule_periodic_snapshots(ayb_conf_for_server.clone(), ayb_db.clone())
.await
.expect("unable to start periodic snapshot scheduler");
println!("Starting server {}:{}...", ayb_conf.host, ayb_conf.port);
crate::hosted_db::sandbox::print_isolation_status(
crate::hosted_db::sandbox::detect_isolation_status(),
);
let server = HttpServer::new(move || {
let cors = build_cors(ayb_conf.cors.clone());
App::new()
.wrap(middleware::Logger::default())
.wrap(middleware::Compress::default())
.wrap(cors)
.app_data(web::Data::new(web_details.clone()))
.app_data(web::Data::new(clone_box(&*ayb_db)))
.app_data(web::Data::new(ayb_conf_for_server.clone()))
.app_data(web::Data::new(email_backends.clone()))
.app_data(web::Data::new(daemon_registry.clone()))
.configure(|cfg| config(cfg, &ayb_conf_for_server.clone()))
})
.bind((ayb_conf.host, ayb_conf.port))?
.run();
let server_handle = server.handle();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.ok();
println!("Shutting down server and cleaning up daemons...");
cleanup_daemon_registry.shut_down_all().await;
server_handle.stop(true).await;
});
server.await
}