about summary refs log blame commit diff
path: root/kittybox-rs/src/main.rs
blob: 395bb31b1a13c89c429b13a358d42bf823fef920 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11

                                    
 
              
                 



                                                                                                 
 
                                             
 
                                                             
                    
                                           
               
         
                   
                                                                     

                                  
 
                                                
             
                                                   






                                                   
 
                                 



                                                                        
                                                                                         



                                                                                          
                                         
                                                     
                                               

                                                                      





                                                                    
          






                                                                
          
 








                                                                      







                                                                     
                    


























                                                                                              



                                                                     

                                                                               
                                                                                                
 
                                                                                                  
                                           










                                                               
                                    
                                                                                    
                                                         

                                                     


                                                         
              



                                     
                             
                             
                                                                        
                                                                    

                                                                  
                                                              
 

                                                                                          
                                                                 
              
                                                                  


                                                        
                                                                     
 
                                                                  
                     
                                                                   
                                                              
                                           















                                                                  


                                                      


                                                   
 
use kittybox::database::FileStorage;
use std::{env, time::Duration};
use tracing::{debug, error, info};

#[tokio::main]
async fn main() {
    use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry};
    Registry::default()
        .with(EnvFilter::from_default_env())
        .with(tracing_subscriber::fmt::layer().json())
        .init();

    info!("Starting the kittybox server...");

    let backend_uri: String = match env::var("BACKEND_URI") {
        Ok(val) => {
            debug!("Backend URI: {}", val);
            val
        }
        Err(_) => {
            error!("BACKEND_URI is not set, cannot find a database");
            std::process::exit(1);
        }
    };

    let listen_addr = match env::var("SERVE_AT")
        .ok()
        .unwrap_or_else(|| "[::]:8080".to_string())
        .parse::<std::net::SocketAddr>()
    {
        Ok(addr) => addr,
        Err(e) => {
            error!("Cannot parse SERVE_AT: {}", e);
            std::process::exit(1);
        }
    };

    let http: reqwest::Client = {
        #[allow(unused_mut)]
        let mut builder = reqwest::Client::builder().user_agent(concat!(
            env!("CARGO_PKG_NAME"),
            "/",
            env!("CARGO_PKG_VERSION")
        ));
        // TODO: add a root certificate if there's an environment variable pointing at it
        //builder = builder.add_root_certificate(reqwest::Certificate::from_pem(todo!()));

        builder.build().unwrap()
    };

    if backend_uri.starts_with("redis") {
        println!("The Redis backend is deprecated.");
        std::process::exit(1);
    } else if backend_uri.starts_with("file") {
        let database = {
            let folder = backend_uri.strip_prefix("file://").unwrap();
            let path = std::path::PathBuf::from(folder);
            match kittybox::database::FileStorage::new(path).await {
                Ok(db) => db,
                Err(err) => {
                    error!("Error creating database: {:?}", err);
                    std::process::exit(1);
                }
            }
        };

        let blobstore = {
            let variable = std::env::var("BLOBSTORE_URI")
                .unwrap();
            let folder = variable
                .strip_prefix("file://")
                .unwrap();
            let path = std::path::PathBuf::from(folder);
            kittybox::media::storage::file::FileStore::new(path)
        };

        let auth_backend = {
            let variable = std::env::var("AUTH_STORE_URI")
                .unwrap();
            let folder = variable
                .strip_prefix("file://")
                .unwrap();
            kittybox::indieauth::backend::fs::FileBackend::new(folder)
        };


        // This code proves that different components of Kittybox can
        // be split up without hurting the app
        //
        // If needed, some features could be omitted from the binary
        // or just not spun up in the future
        //
        // For example, the frontend code could run spearately from
        // Micropub and only have read access to the database folder
        let frontend = axum::Router::new()
            .route(
                "/",
                axum::routing::get(kittybox::frontend::homepage::<FileStorage>)
                    .layer(axum::Extension(database.clone())))
            .route("/.kittybox/static/:path", axum::routing::get(kittybox::frontend::statics))
            .fallback(
                axum::routing::get(kittybox::frontend::catchall::<FileStorage>)
                    .layer(axum::Extension(database.clone())));

        // Onboarding is a bit of a special case. One might argue that
        // the onboarding makes Kittybox a monolith. This is wrong.
        // The "onboarding receiver" doesn't need any code from the
        // onboarding form - they're grouped in a single module for
        // convenience only, since modifying one usually requires
        // updating the other to match.
        //
        // For example, this "router" just groups two separate methods
        // in one request, because logically they live in the same
        // subtree. But one could manually construct only one but not
        // the other, to receive a "frontend-only" application. Of
        // course, in this scenario, one must employ a reverse proxy
        // to distinguish between GET and POST requests to the same
        // path, and route them to the correct set of endpoints with
        // write access.
        let onboarding = axum::Router::new()
            .route("/.kittybox/onboarding", kittybox::frontend::onboarding::router(
                database.clone(), http.clone()
            ));

        let micropub = axum::Router::new()
            .route("/.kittybox/micropub", kittybox::micropub::router(
                database.clone(),
                http.clone(),
                auth_backend.clone()
            ))
            .nest("/.kittybox/micropub/client", kittybox::companion::router());

        let media = axum::Router::new()
            .nest("/.kittybox/media", kittybox::media::router(blobstore, auth_backend.clone()));

        let indieauth = kittybox::indieauth::router(auth_backend, database.clone(), http.clone());

        let technical = axum::Router::new()
            .route(
                "/.kittybox/coffee",
                axum::routing::get(|| async {
                    use axum::http::{header, StatusCode};
                    (
                        StatusCode::IM_A_TEAPOT,
                        [(header::CONTENT_TYPE, "text/plain")],
                        "Sorry, can't brew coffee yet!",
                    )
                }),
            )
            .route(
                "/.kittybox/health",
                axum::routing::get(
                    |axum::Extension(db): axum::Extension<FileStorage>| async move {
                        // TODO health-check the database
                        "OK"
                    }
                )
                    .layer(axum::Extension(database))
            )
            .route(
                "/.kittybox/metrics",
                axum::routing::get(|| async { todo!() }),
            );

        let svc = axum::Router::new()
            .merge(frontend)
            .merge(onboarding)
            .merge(micropub)
            .merge(media)
            .merge(indieauth)
            .merge(technical)
            .layer(tower::ServiceBuilder::new()
                   .layer(tower_http::trace::TraceLayer::new_for_http())
                   .into_inner())
            .layer(tower_http::catch_panic::CatchPanicLayer::new());

        // A little dance to turn a potential file descriptor into
        // a guaranteed async network socket
        let tcp_listener: std::net::TcpListener = {
            let mut listenfd = listenfd::ListenFd::from_env();

            let tcp_listener = if let Ok(Some(listener)) = listenfd.take_tcp_listener(0) {
                listener
            } else {
                std::net::TcpListener::bind(listen_addr).unwrap()
            };
            // Set the socket to non-blocking so tokio can poll it
            // properly -- this is the async magic!
            tcp_listener.set_nonblocking(true).unwrap();

            tcp_listener
        };
        info!("Listening on {}", tcp_listener.local_addr().unwrap());

        let server = hyper::server::Server::from_tcp(tcp_listener)
            .unwrap()
            // Otherwise Chrome keeps connections open for too long
            .tcp_keepalive(Some(Duration::from_secs(30 * 60)))
            .serve(svc.into_make_service())
            .with_graceful_shutdown(async move {
                // Defer to C-c handler whenever we're not on Unix
                // TODO consider using a diverging future here
                #[cfg(not(unix))]
                return tokio::signal::ctrl_c().await.unwrap();
                #[cfg(unix)]
                {
                    use tokio::signal::unix::{signal, SignalKind};

                    signal(SignalKind::terminate())
                        .unwrap()
                        .recv()
                        .await
                        .unwrap()
                }
            });

        if let Err(err) = server.await {
            error!("Error serving requests: {}", err);
            std::process::exit(1);
        }
    } else {
        println!("Unknown backend, not starting.");
        std::process::exit(1);
    }
}