//! Run with //! //! ```not_rust //! cargo run -p example-readme //! ``` use std::{ collections::{HashMap, HashSet}, sync::Arc, }; use axum::{ async_trait, extract::{FromRef, FromRequestParts, Query, State}, http::{request::Parts, StatusCode}, response::{Html, IntoResponse, Redirect}, routing::get, Router, }; use axum_extra::extract::{ cookie::{Cookie, Key, SameSite}, PrivateCookieJar, }; use base64::Engine; use color_eyre::eyre::Context; use reqwest::tls::Version; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; use tracing::{error, info}; macro_rules! unwrap_env { ($name:literal) => { std::env::var($name).expect(&format!("missing `{}` env var", $name)) }; } mod oauth2; #[derive(Clone)] struct AppState { _http: reqwest::Client, oauth: Arc, allowed: Arc>>, key: Key, } impl FromRef for Key { fn from_ref(input: &AppState) -> Self { input.key.clone() } } #[derive(Deserialize, Debug)] struct User42 { id: u64, } #[derive(Serialize, Deserialize, Debug, Clone)] struct GroupsUsers { id: u64, } const ALLOWED_USERS: &[u64] = &[/*maiboyer*/ 159559, /*nfelsemb*/ 95340]; #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); let local = tokio::task::LocalSet::new(); local .run_until(async { // initialize tracing let http = reqwest::ClientBuilder::new() // Following redirects opens the client up to SSRF vulnerabilities. .redirect(reqwest::redirect::Policy::none()) .user_agent("FftManager/1.0") .tls_info(true) .min_tls_version(Version::TLS_1_0) .max_tls_version(Version::TLS_1_2) .build() .expect("Client should build"); let cookie_secret = unwrap_env!("COOKIE_SECRET"); let base64_value = base64::engine::general_purpose::URL_SAFE .decode(cookie_secret) .unwrap(); let key: Key = Key::from(&base64_value); let oauth = oauth2::OauthClient::new( http.clone(), unwrap_env!("CLIENT_ID"), unwrap_env!("CLIENT_SECRET"), "https://fft.maix.me/manager/auth/callback", ) .await .unwrap(); let state = AppState { _http: http, key, oauth: Arc::new(oauth), allowed: Arc::new(Mutex::new(ALLOWED_USERS.iter().copied().collect())), }; // build our application with a route let app = Router::new() // `GET /` goes to `root` .route("/", get(root)) .route("/status", get(status)) .route("/status_container", get(status_container)) .route("/stop", get(stop)) .route("/start", get(start)) .route("/restart", get(restart)) .route("/pull", get(git_pull)) .route("/auth/callback", get(oauth2_callback)) .route("/auth/login", get(oauth2_login)) .route("/auth/error", get(auth_error)) .with_state(state); // run our app with hyper let listener = tokio::net::TcpListener::bind(format!( "0.0.0.0:{}", std::env::args() .nth(1) .and_then(|s| s.parse::().ok()) .unwrap_or(9912) )) .await .unwrap(); tracing::info!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); }) .await; } async fn oauth2_login(State(state): State) -> Result { Ok(Redirect::to( &(state .oauth .get_auth_url() .await .map_err(|e| { error!("{e}"); StatusCode::INTERNAL_SERVER_ERROR })? .to_string()), )) } #[axum::debug_handler] async fn oauth2_callback( State(state): State, Query(params): Query>, jar: PrivateCookieJar, ) -> Result { let inner = || async { let Some(code) = params.get("code") else { return Ok::<_, color_eyre::eyre::Report>((jar, Redirect::to("/"))); }; let Some(state_csrf) = params.get("state") else { return Ok((jar, Redirect::to("/"))); }; let token = state .oauth .get_user_token(code, state_csrf) .await .wrap_err("callback")?; let res: User42 = state .oauth .do_request( "https://api.intra.42.fr/v2/me", &[] as &[(String, String); 0], Some(&token), ) .await .wrap_err("Unable to get user self")?; let mut cookie = Cookie::new("manager_token", res.id.to_string()); cookie.set_same_site(SameSite::None); cookie.set_secure(false); cookie.set_path("/"); // cookie.set_domain("localhost:3000"); // cookie.set_http_only(Some(false)); let ujar = jar.add(cookie); Ok((ujar, Redirect::to("/"))) }; match inner().await { Ok(ret) => Ok(ret), Err(e) => { error!("{:?}", e); Err(StatusCode::INTERNAL_SERVER_ERROR) } } } #[derive(Clone, Debug)] struct UserLoggedIn; #[async_trait] impl FromRequestParts for UserLoggedIn { type Rejection = (StatusCode, PrivateCookieJar, Redirect); async fn from_request_parts( parts: &mut Parts, state: &AppState, ) -> Result { let jar = PrivateCookieJar::from_request_parts(parts, state) .await .unwrap(); let Some(id) = jar.get("manager_token") else { return Err(( StatusCode::TEMPORARY_REDIRECT, jar, Redirect::to("/manager/auth/login"), )); }; let Ok(user_id) = id.value().parse::() else { let jar = jar.remove("manager_token"); return Err(( StatusCode::TEMPORARY_REDIRECT, jar, Redirect::to("/manager/auth/login"), )); }; if state.allowed.lock().await.contains(&user_id) { Ok(UserLoggedIn) } else { let jar = jar.remove("manager_token"); Err(( StatusCode::TEMPORARY_REDIRECT, jar, Redirect::to("/manager/auth/error"), )) } } } // basic handler that responds with a static string async fn auth_error() -> Html<&'static str> { info!("Request auth_error page"); Html( r#"

Hello you aren't allowed here :) :D

"#, ) } // basic handler that responds with a static string async fn root(_user: UserLoggedIn) -> Html<&'static str> { info!("Request link page"); Html( r#" restart
stop
start
status
status for whole container
git pull (ask before!)
"#, ) } async fn restart(_user: UserLoggedIn) -> Redirect { info!("Requested to restart the bot"); tokio::spawn(async { tokio::process::Command::new("systemctl") .args(["restart", "container@fft.service"]) .spawn() .unwrap() }); Redirect::to("/") } async fn start(_user: UserLoggedIn) -> Redirect { info!("Requested to start the bot"); tokio::spawn(async { tokio::process::Command::new("systemctl") .args(["restart", "container@fft.service"]) .spawn() .unwrap() }); Redirect::to("/") } async fn stop(_user: UserLoggedIn) -> Redirect { info!("Requested to stop the bot"); tokio::spawn(async { tokio::process::Command::new("systemctl") .args(["stop", "container@fft.service"]) .spawn() .unwrap() }); Redirect::to("/") } async fn status() -> Result { info!("Requested status"); let mut output = tokio::process::Command::new("nixos-container") .args(["run", "fft", "--", "journalctl", "-xeu", "fft-1"]) .output() .await // let mut output = child.wait_with_output().await .map_err(|e| { error!("Error with systemctl status {e}"); StatusCode::INTERNAL_SERVER_ERROR })?; output.stdout.push(b'\n'); output.stdout.append(&mut output.stderr); String::from_utf8(output.stdout).map_err(|e| { error!("Error with systemctl status output {e}"); StatusCode::INTERNAL_SERVER_ERROR }) } async fn status_container() -> Result { info!("Requested status"); let mut output = tokio::process::Command::new("nixos-container") .args(["run", "fft", "--", "journalctl"]) .output() .await // let mut output = child.wait_with_output().await .map_err(|e| { error!("Error with systemctl status {e}"); StatusCode::INTERNAL_SERVER_ERROR })?; output.stdout.push(b'\n'); output.stdout.append(&mut output.stderr); String::from_utf8(output.stdout).map_err(|e| { error!("Error with systemctl status output {e}"); StatusCode::INTERNAL_SERVER_ERROR }) } async fn git_pull() -> Result { info!("Requested to pull"); let mut output = tokio::process::Command::new("/run/wrappers/bin/sudo") .current_dir(std::env::var("FFT_DIR").map_err(|e| { error!("Error with git pull command {e}"); ( StatusCode::INTERNAL_SERVER_ERROR, "Please set the BOTLOC_DIR variable", ) })?) .args(["-u", "maix", "/home/maix/.nix-profile/bin/git", "pull"]) .output() .await // let mut output = child.wait_with_output().await .map_err(|e| { error!("Error with git pull command {e}"); ( StatusCode::INTERNAL_SERVER_ERROR, "Error with the git pull command!", ) })?; output.stdout.push(b'\n'); output.stdout.append(&mut output.stderr); String::from_utf8(output.stdout).map_err(|e| { error!("Error with git pull output {e}"); ( StatusCode::INTERNAL_SERVER_ERROR, "Error with the git pull output!", ) }) }