2024-06-25 19:25:45 +02:00
|
|
|
//! Run with
|
|
|
|
|
//!
|
|
|
|
|
//! ```not_rust
|
|
|
|
|
//! cargo run -p example-readme
|
|
|
|
|
//! ```
|
|
|
|
|
|
2024-06-26 02:17:02 +02:00
|
|
|
use std::{
|
|
|
|
|
collections::{HashMap, HashSet},
|
2024-09-28 12:19:02 +02:00
|
|
|
sync::Arc,
|
2024-06-26 19:08:25 +02:00
|
|
|
time::Duration,
|
2024-06-26 02:17:02 +02:00
|
|
|
};
|
|
|
|
|
|
2024-06-25 19:25:45 +02:00
|
|
|
use axum::{
|
2024-06-26 19:08:25 +02:00
|
|
|
async_trait,
|
|
|
|
|
extract::{FromRef, FromRequestParts, Query, State},
|
|
|
|
|
http::{request::Parts, StatusCode},
|
2024-09-28 12:19:02 +02:00
|
|
|
response::{Html, IntoResponse, Redirect},
|
2024-06-26 02:17:02 +02:00
|
|
|
routing::get,
|
2024-09-28 12:19:02 +02:00
|
|
|
Router,
|
2024-06-25 19:25:45 +02:00
|
|
|
};
|
2024-06-26 02:17:02 +02:00
|
|
|
use axum_extra::extract::{
|
2024-09-28 12:19:02 +02:00
|
|
|
cookie::{Cookie, Key, SameSite},
|
2024-09-28 12:20:12 +02:00
|
|
|
PrivateCookieJar,
|
2024-06-26 02:17:02 +02:00
|
|
|
};
|
|
|
|
|
use base64::Engine;
|
2024-09-27 20:07:55 +02:00
|
|
|
use color_eyre::eyre::Context;
|
|
|
|
|
use reqwest::tls::Version;
|
2024-09-28 12:19:02 +02:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use serde_json::json;
|
|
|
|
|
use tokio::sync::Mutex;
|
2024-09-28 12:20:12 +02:00
|
|
|
use tracing::{error, info};
|
2024-06-25 19:25:45 +02:00
|
|
|
|
2024-06-26 02:17:02 +02:00
|
|
|
macro_rules! unwrap_env {
|
|
|
|
|
($name:literal) => {
|
|
|
|
|
std::env::var($name).expect(&format!("missing `{}` env var", $name))
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-27 20:07:55 +02:00
|
|
|
mod oauth2;
|
2024-06-26 19:08:25 +02:00
|
|
|
|
2024-06-26 02:17:02 +02:00
|
|
|
#[derive(Clone)]
|
|
|
|
|
struct AppState {
|
2024-09-28 12:20:12 +02:00
|
|
|
_http: reqwest::Client,
|
2024-09-27 20:07:55 +02:00
|
|
|
oauth: Arc<oauth2::OauthClient>,
|
|
|
|
|
tutors: Arc<Mutex<HashSet<u64>>>,
|
2024-06-26 02:17:02 +02:00
|
|
|
key: Key,
|
2024-06-26 19:08:25 +02:00
|
|
|
}
|
|
|
|
|
|
2024-09-27 20:07:55 +02:00
|
|
|
impl FromRef<AppState> for Key {
|
|
|
|
|
fn from_ref(input: &AppState) -> Self {
|
|
|
|
|
input.key.clone()
|
2024-06-26 19:08:25 +02:00
|
|
|
}
|
|
|
|
|
}
|
2024-06-26 02:17:02 +02:00
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
|
struct User42 {
|
2024-06-26 19:08:25 +02:00
|
|
|
id: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
|
|
|
struct GroupsUsers {
|
|
|
|
|
id: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn tutors(config: AppState) {
|
|
|
|
|
loop {
|
|
|
|
|
{
|
2024-09-27 20:07:55 +02:00
|
|
|
let mut lock = config.tutors.lock().await;
|
2024-06-26 19:08:25 +02:00
|
|
|
lock.clear();
|
|
|
|
|
let mut page_nb = 0;
|
|
|
|
|
loop {
|
|
|
|
|
info!("tutor request (page {page_nb})");
|
|
|
|
|
let res = config
|
2024-09-27 20:07:55 +02:00
|
|
|
.oauth
|
|
|
|
|
.do_request::<Vec<User42>>(
|
2024-06-26 19:08:25 +02:00
|
|
|
"https://api.intra.42.fr/v2/groups/166/users",
|
2024-09-27 20:07:55 +02:00
|
|
|
&json! ({
|
2024-06-26 19:08:25 +02:00
|
|
|
"page[number]": page_nb,
|
|
|
|
|
"page[size]": 100,
|
|
|
|
|
}),
|
2024-09-28 12:19:02 +02:00
|
|
|
Option::<&oauth2::Token>::None,
|
2024-06-26 19:08:25 +02:00
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
let do_next = res.len() == 100;
|
|
|
|
|
lock.extend(res.into_iter().map(|s| s.id));
|
|
|
|
|
if !do_next {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
page_nb += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
tokio::time::sleep(Duration::new(3600 * 24 /*tout les jours*/, 0)).await;
|
|
|
|
|
}
|
2024-06-26 02:17:02 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-25 19:25:45 +02:00
|
|
|
#[tokio::main]
|
|
|
|
|
async fn main() {
|
|
|
|
|
tracing_subscriber::fmt::init();
|
2024-06-26 19:08:25 +02:00
|
|
|
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())
|
2024-09-28 12:23:48 +02:00
|
|
|
.user_agent("AlertePoste/1.0")
|
2024-09-27 20:07:55 +02:00
|
|
|
.tls_info(true)
|
|
|
|
|
.min_tls_version(Version::TLS_1_0)
|
|
|
|
|
.max_tls_version(Version::TLS_1_2)
|
2024-06-26 19:08:25 +02:00
|
|
|
.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);
|
2024-09-27 20:07:55 +02:00
|
|
|
let oauth = oauth2::OauthClient::new(
|
|
|
|
|
http.clone(),
|
|
|
|
|
unwrap_env!("CLIENT_ID"),
|
|
|
|
|
unwrap_env!("CLIENT_SECRET"),
|
2024-09-28 12:23:48 +02:00
|
|
|
"https://t.maix.me/auth/callback",
|
2024-09-27 20:07:55 +02:00
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
2024-06-26 19:08:25 +02:00
|
|
|
|
|
|
|
|
let state = AppState {
|
2024-09-28 12:20:12 +02:00
|
|
|
_http: http,
|
2024-09-27 20:07:55 +02:00
|
|
|
key,
|
|
|
|
|
oauth: Arc::new(oauth),
|
|
|
|
|
tutors: Default::default(),
|
2024-06-26 19:08:25 +02:00
|
|
|
};
|
|
|
|
|
tokio::task::spawn_local(tutors(state.clone()));
|
|
|
|
|
|
|
|
|
|
// build our application with a route
|
|
|
|
|
let app = Router::new()
|
|
|
|
|
// `GET /` goes to `root`
|
|
|
|
|
.route("/", get(root))
|
|
|
|
|
.route("/status", get(status))
|
|
|
|
|
.route("/stop", get(stop))
|
|
|
|
|
.route("/start", get(start))
|
|
|
|
|
.route("/restart", get(restart))
|
2024-06-27 14:19:03 +02:00
|
|
|
.route("/pull", get(git_pull))
|
2024-06-26 19:08:25 +02:00
|
|
|
.route("/auth/callback", get(oauth2_callback))
|
|
|
|
|
.route("/auth/login", get(oauth2_login))
|
2024-09-28 14:24:11 +02:00
|
|
|
.route("/auth/error", get(auth_error))
|
2024-06-26 19:08:25 +02:00
|
|
|
.with_state(state);
|
|
|
|
|
|
|
|
|
|
// run our app with hyper
|
2024-09-27 20:07:55 +02:00
|
|
|
let listener = tokio::net::TcpListener::bind(format!(
|
2024-09-28 12:19:02 +02:00
|
|
|
"0.0.0.0:{}",
|
2024-09-27 20:07:55 +02:00
|
|
|
std::env::args()
|
|
|
|
|
.nth(1)
|
|
|
|
|
.and_then(|s| s.parse::<u16>().ok())
|
|
|
|
|
.unwrap_or(9911)
|
|
|
|
|
))
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
2024-06-26 19:08:25 +02:00
|
|
|
tracing::info!("listening on {}", listener.local_addr().unwrap());
|
|
|
|
|
axum::serve(listener, app).await.unwrap();
|
|
|
|
|
})
|
|
|
|
|
.await;
|
2024-06-25 19:25:45 +02:00
|
|
|
}
|
|
|
|
|
|
2024-09-27 20:07:55 +02:00
|
|
|
async fn oauth2_login(State(state): State<AppState>) -> Result<Redirect, StatusCode> {
|
|
|
|
|
Ok(Redirect::to(
|
|
|
|
|
&(state
|
|
|
|
|
.oauth
|
|
|
|
|
.get_auth_url()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
error!("{e}");
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR
|
|
|
|
|
})?
|
|
|
|
|
.to_string()),
|
|
|
|
|
))
|
2024-06-26 02:17:02 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-26 19:08:25 +02:00
|
|
|
#[axum::debug_handler]
|
2024-06-26 02:17:02 +02:00
|
|
|
async fn oauth2_callback(
|
2024-06-26 19:08:25 +02:00
|
|
|
State(state): State<AppState>,
|
2024-06-26 02:17:02 +02:00
|
|
|
Query(params): Query<HashMap<String, String>>,
|
2024-09-27 20:07:55 +02:00
|
|
|
jar: PrivateCookieJar,
|
|
|
|
|
) -> Result<impl IntoResponse, StatusCode> {
|
|
|
|
|
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")?;
|
|
|
|
|
|
2024-09-28 12:19:02 +02:00
|
|
|
let res: User42 = state
|
|
|
|
|
.oauth
|
|
|
|
|
.do_request("https://api.intra.42.fr/v2/me", &(), Some(&token))
|
2024-09-27 20:07:55 +02:00
|
|
|
.await
|
|
|
|
|
.wrap_err("Unable to get user self")?;
|
|
|
|
|
|
2024-09-28 12:19:02 +02:00
|
|
|
let mut cookie = Cookie::new("token", res.id.to_string());
|
2024-09-27 20:07:55 +02:00
|
|
|
cookie.set_same_site(SameSite::None);
|
2024-09-28 14:24:11 +02:00
|
|
|
cookie.set_secure(true);
|
2024-09-27 20:07:55 +02:00
|
|
|
cookie.set_path("/");
|
|
|
|
|
// cookie.set_domain("localhost:3000");
|
|
|
|
|
// cookie.set_http_only(Some(false));
|
|
|
|
|
let ujar = jar.add(cookie);
|
|
|
|
|
Ok((ujar, Redirect::to("/")))
|
2024-06-26 02:17:02 +02:00
|
|
|
};
|
2024-09-27 20:07:55 +02:00
|
|
|
match inner().await {
|
|
|
|
|
Ok(ret) => Ok(ret),
|
2024-06-26 02:17:02 +02:00
|
|
|
Err(e) => {
|
2024-09-27 20:07:55 +02:00
|
|
|
error!("{:?}", e);
|
|
|
|
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
2024-06-26 02:17:02 +02:00
|
|
|
}
|
|
|
|
|
}
|
2024-06-26 19:08:25 +02:00
|
|
|
}
|
2024-06-26 02:17:02 +02:00
|
|
|
|
2024-06-26 19:08:25 +02:00
|
|
|
#[derive(Clone, Debug)]
|
2024-09-28 12:19:02 +02:00
|
|
|
struct UserLoggedIn;
|
2024-06-26 19:08:25 +02:00
|
|
|
|
|
|
|
|
#[async_trait]
|
|
|
|
|
impl FromRequestParts<AppState> for UserLoggedIn {
|
2024-09-28 12:19:02 +02:00
|
|
|
type Rejection = (StatusCode, PrivateCookieJar, Redirect);
|
2024-06-26 19:08:25 +02:00
|
|
|
|
|
|
|
|
async fn from_request_parts(
|
|
|
|
|
parts: &mut Parts,
|
|
|
|
|
state: &AppState,
|
|
|
|
|
) -> Result<Self, Self::Rejection> {
|
2024-09-28 12:19:02 +02:00
|
|
|
let jar = PrivateCookieJar::from_request_parts(parts, state)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
2024-06-26 19:08:25 +02:00
|
|
|
let Some(id) = jar.get("token") else {
|
|
|
|
|
return Err((
|
|
|
|
|
StatusCode::TEMPORARY_REDIRECT,
|
|
|
|
|
jar,
|
|
|
|
|
Redirect::to("/auth/login"),
|
|
|
|
|
));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let Ok(user_id) = id.value().parse::<u64>() else {
|
|
|
|
|
let jar = jar.remove("token");
|
|
|
|
|
return Err((
|
|
|
|
|
StatusCode::TEMPORARY_REDIRECT,
|
|
|
|
|
jar,
|
|
|
|
|
Redirect::to("/auth/login"),
|
|
|
|
|
));
|
|
|
|
|
};
|
|
|
|
|
|
2024-09-27 20:07:55 +02:00
|
|
|
if state.tutors.lock().await.contains(&user_id) {
|
2024-09-28 12:19:02 +02:00
|
|
|
Ok(UserLoggedIn)
|
2024-06-26 19:08:25 +02:00
|
|
|
} else {
|
|
|
|
|
let jar = jar.remove("token");
|
|
|
|
|
Err((
|
|
|
|
|
StatusCode::TEMPORARY_REDIRECT,
|
|
|
|
|
jar,
|
2024-09-28 14:24:11 +02:00
|
|
|
Redirect::to("/auth/error"),
|
2024-06-26 19:08:25 +02:00
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-06-26 02:17:02 +02:00
|
|
|
}
|
2024-09-28 14:24:11 +02:00
|
|
|
// basic handler that responds with a static string
|
|
|
|
|
async fn auth_error() -> Html<&'static str> {
|
|
|
|
|
info!("Request auth_error page");
|
|
|
|
|
Html(
|
|
|
|
|
r#"
|
|
|
|
|
<h1>Hello TUTORS ONLY :D</h1>
|
|
|
|
|
"#,
|
|
|
|
|
)
|
|
|
|
|
}
|
2024-06-26 02:17:02 +02:00
|
|
|
|
2024-06-25 19:25:45 +02:00
|
|
|
// basic handler that responds with a static string
|
2024-06-26 19:08:25 +02:00
|
|
|
async fn root(_user: UserLoggedIn) -> Html<&'static str> {
|
2024-06-25 19:37:28 +02:00
|
|
|
info!("Request link page");
|
2024-06-25 19:25:45 +02:00
|
|
|
Html(
|
|
|
|
|
r#"
|
|
|
|
|
<a href="/restart">restart</a><br>
|
|
|
|
|
<a href="/stop">stop</a><br>
|
|
|
|
|
<a href="/start">start</a><br>
|
|
|
|
|
<a href="/status">status</a><br>
|
2024-06-27 14:19:03 +02:00
|
|
|
<a href="/pull">git pull (ask before!)</a><br>
|
2024-09-28 12:19:02 +02:00
|
|
|
"#,
|
2024-06-25 19:25:45 +02:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-26 19:08:25 +02:00
|
|
|
async fn restart(_user: UserLoggedIn) -> Redirect {
|
2024-06-25 19:25:45 +02:00
|
|
|
info!("Requested to restart the bot");
|
|
|
|
|
tokio::spawn(async {
|
|
|
|
|
tokio::process::Command::new("systemctl")
|
|
|
|
|
.args(["--user", "restart", "botloc.service"])
|
|
|
|
|
.spawn()
|
|
|
|
|
.unwrap()
|
|
|
|
|
});
|
|
|
|
|
Redirect::to("/")
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-26 19:08:25 +02:00
|
|
|
async fn start(_user: UserLoggedIn) -> Redirect {
|
2024-06-25 19:25:45 +02:00
|
|
|
info!("Requested to start the bot");
|
|
|
|
|
tokio::spawn(async {
|
|
|
|
|
tokio::process::Command::new("systemctl")
|
|
|
|
|
.args(["--user", "start", "botloc.service"])
|
|
|
|
|
.spawn()
|
|
|
|
|
.unwrap()
|
|
|
|
|
});
|
|
|
|
|
Redirect::to("/")
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-26 19:08:25 +02:00
|
|
|
async fn stop(_user: UserLoggedIn) -> Redirect {
|
2024-06-25 19:25:45 +02:00
|
|
|
info!("Requested to stop the bot");
|
|
|
|
|
tokio::spawn(async {
|
|
|
|
|
tokio::process::Command::new("systemctl")
|
|
|
|
|
.args(["--user", "stop", "botloc.service"])
|
|
|
|
|
.spawn()
|
|
|
|
|
.unwrap()
|
|
|
|
|
});
|
|
|
|
|
Redirect::to("/")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn status() -> Result<String, StatusCode> {
|
|
|
|
|
info!("Requested status");
|
2024-09-27 20:07:55 +02:00
|
|
|
let mut output = tokio::process::Command::new("journalctl")
|
2024-09-28 14:24:11 +02:00
|
|
|
.args(["--user", "-xeu", "botloc"])
|
2024-06-25 19:25:45 +02:00
|
|
|
.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
|
|
|
|
|
})
|
|
|
|
|
}
|
2024-06-27 14:15:50 +02:00
|
|
|
|
|
|
|
|
async fn git_pull() -> Result<String, (StatusCode, &'static str)> {
|
|
|
|
|
info!("Requested to pull");
|
2024-06-27 14:36:40 +02:00
|
|
|
let mut output = tokio::process::Command::new("/home/maix/.nix-profile/bin/git")
|
2024-06-27 14:15:50 +02:00
|
|
|
.current_dir(std::env::var("BOTLOC_DIR").map_err(|e| {
|
|
|
|
|
error!("Error with git pull command {e}");
|
|
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
"Please set the BOTLOC_DIR variable",
|
|
|
|
|
)
|
|
|
|
|
})?)
|
|
|
|
|
.args(["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!",
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}
|