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


                                                                          
                                             
 
                                   
                    
                                           
         
                   
                                                                     


















                                                                                      









                                                                                     
         




                                                                                                                            
                                                                                    
 
                                                                        
                             
                                                                  
                                            
 


                                                                                  
                                                    


                                                                               
 
                           



                                                                                                                     
        
 
                                              
             
                                                   




                                                       
 
                                                                       









                                                                                               














                                                                                             
                                         
                                                     
                                               


                                                                      





                                                                    
          





                                                                       
                                  
                                   
                                                                                    


                                                                                                           
                                             



                                                   

                                               
                                                                             

                                       
                                                         
                                                   
                                                                                             










                                                                                                    
                                                     
 


                                                    
                                                                              
                                                                                                  
                          
                           
                            
                             
                      
                       

                         






                                                  
             
                                     
 








                                                                           
                                                                  





                                                              
















                                                                  


                                                      


                                                   
 
use log::{debug, error, info};
use std::{convert::Infallible, env, time::Duration};
use url::Url;
use hyper::client::{HttpConnector,connect::dns::GaiResolver};
use hyper_rustls::HttpsConnector;
use warp::{Filter, host::Authority};

#[tokio::main]
async fn main() {
    // TODO json logging in the future?
    let logger_env = env_logger::Env::new().filter_or("RUST_LOG", "info");
    env_logger::init_from_env(logger_env);

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

    let backend_uri: String;
    match env::var("BACKEND_URI") {
        Ok(val) => {
            debug!("Backend URI: {}", val);
            backend_uri = val
        }
        Err(_) => {
            error!("BACKEND_URI is not set, cannot find a database");
            std::process::exit(1);
        }
    };
    let token_endpoint: Url;
    match env::var("TOKEN_ENDPOINT") {
        Ok(val) => {
            debug!("Token endpoint: {}", val);
            match Url::parse(&val) {
                Ok(val) => token_endpoint = val,
                _ => {
                    error!("Token endpoint URL cannot be parsed, aborting.");
                    std::process::exit(1)
                }
            }
        }
        Err(_) => {
            error!("TOKEN_ENDPOINT is not set, will not be able to authorize users!");
            std::process::exit(1)
        }
    }
    let authorization_endpoint: Url;
    match env::var("AUTHORIZATION_ENDPOINT") {
        Ok(val) => {
            debug!("Auth endpoint: {}", val);
            match Url::parse(&val) {
                Ok(val) => authorization_endpoint = val,
                _ => {
                    error!("Authorization endpoint URL cannot be parsed, aborting.");
                    std::process::exit(1)
                }
            }
        }
        Err(_) => {
            error!("AUTHORIZATION_ENDPOINT is not set, will not be able to confirm token and ID requests using IndieAuth!");
            std::process::exit(1)
        }
    }

    //let internal_token: Option<String> = env::var("KITTYBOX_INTERNAL_TOKEN").ok();

    /*let cookie_secret: String = match env::var("COOKIE_SECRET").ok() {
        Some(value) => value,
        None => {
            if let Ok(filename) = env::var("COOKIE_SECRET_FILE") {
                use tokio::io::AsyncReadExt;

                let mut file = tokio::fs::File::open(filename).await.map_err(|e| {
                    error!("Couldn't open the cookie secret file: {}", e);
                    std::process::exit(1);
                }).unwrap();
                let mut temp_string = String::new();
                file.read_to_string(&mut temp_string).await.map_err(|e| {
                    error!("Couldn't read the cookie secret from file: {}", e);
                    std::process::exit(1);
                }).unwrap();

                temp_string
            } else {
                error!("COOKIE_SECRET or COOKIE_SECRET_FILE is not set, will not be able to log in users securely!");
                std::process::exit(1);
            }
        }
    };*/

    let listen_at = 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);
            }
        };

    // TODO remove this and see what screams to replace it with reqwest
    let http_client: hyper::Client<HttpsConnector<HttpConnector<GaiResolver>>, hyper::Body> = {
        let builder = hyper::Client::builder();
        let https = hyper_rustls::HttpsConnectorBuilder::new()
            .with_webpki_roots()
            .https_only()
            .enable_http1()
            .enable_http2()
            .build();
        builder.build(https)
    };

    // This thing handles redirects automatically but is type-incompatible with hyper::Client
    // Bonus: less generics to be aware of, this thing hides its complexity
    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 endpoints = kittybox::frontend::IndiewebEndpoints {
            authorization_endpoint: authorization_endpoint.to_string(),
            token_endpoint: token_endpoint.to_string(),
            webmention: None,
            microsub: None,
        };
        
        let homepage = warp::get()
            .and(warp::path::end())
            .and(kittybox::frontend::homepage(database.clone(), endpoints.clone()));

        let onboarding = warp::path("onboarding")
            .and(warp::path::end())
            .and(kittybox::frontend::onboarding(database.clone(), endpoints.clone(), http_client.clone()));
        
        let micropub = warp::path("micropub")
            .and(warp::path::end()
                 .and(kittybox::micropub::micropub(
                     database.clone(),
                     token_endpoint.to_string(),
                     http_client.clone()
                 ))
                 .or(warp::get()
                     .and(warp::path("client"))
                     .and(warp::path::end())
                     .map(|| warp::reply::html(kittybox::MICROPUB_CLIENT))));

        let media = warp::path("media")
            .and(warp::path::end()
                 .and(kittybox::micropub::media::media())
                 .or(kittybox::util::require_host()
                     .and(warp::path::param())
                     .map(|_host: Authority, path: String| format!("media file {}", path))));
        
        // TODO remember how login logic works because I forgor
        let login = warp::path("login")
            .and(warp::path("callback")
                 .map(|| "callback!")
                 // TODO form on GET and handler on POST
                 .or(warp::path::end().map(|| "login page!")));

        // TODO prettier error response
        let coffee = warp::path("coffee")
            .map(|| warp::reply::with_status("I'm a teapot!", warp::http::StatusCode::IM_A_TEAPOT));
        
        let static_files = warp::path("static")
            .and(kittybox::frontend::static_files());

        let catchall = kittybox::frontend::catchall(
            database.clone(),
            endpoints.clone()
        );

        let health = warp::path("health").and(warp::path::end()).map(|| "OK");
        let metrics = warp::path("metrics").and(warp::path::end()).map(kittybox::metrics::gather);

        let app = homepage
            .or(onboarding)
            .or(metrics
                .or(health))
            .or(static_files)
            .or(login)
            .or(coffee)
            .or(micropub)
            .or(media)
            .or(catchall)
            .with(warp::log("kittybox"))
            .with(kittybox::metrics::metrics(vec![
                "health".to_string(),
                "micropub".to_string(),
                "static".to_string(),
                "media".to_string(),
                "metrics".to_string()
            ]))
            ;

        let svc = warp::service(app);

        let tcp_listener: std::net::TcpListener;
        let mut listenfd = listenfd::ListenFd::from_env();
        if let Ok(Some(listener)) = listenfd.take_tcp_listener(0) {
            tcp_listener = listener;
        } else {
            tcp_listener = std::net::TcpListener::bind(listen_at).unwrap();
        }
        tcp_listener.set_nonblocking(true).unwrap();

        info!("Listening on {}", tcp_listener.local_addr().unwrap());
        let server = hyper::server::Server::from_tcp(tcp_listener)
            .unwrap()
            .tcp_keepalive(Some(Duration::from_secs(30 * 60)))
            .serve(hyper::service::make_service_fn(move |_| {
                let service = svc.clone();
                async move {
                    Ok::<_, Infallible>(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);
    }
}