Pierre Hubert
40eb16cef9
All checks were successful
continuous-integration/drone/push Build is passing
Use https://crates.io/crates/actix-remote-ip to remove some code redundancy Reviewed-on: #5
246 lines
6.5 KiB
Rust
246 lines
6.5 KiB
Rust
use actix_remote_ip::{RemoteIP, RemoteIPConfig};
|
|
use actix_web::middleware::Logger;
|
|
use actix_web::{get, web, App, HttpResponse, HttpServer};
|
|
use askama::Template;
|
|
use light_openid::basic_state_manager::BasicStateManager;
|
|
use light_openid::primitives::OpenIDConfig;
|
|
|
|
use clap::Parser;
|
|
|
|
const REDIRECT_URI: &str = "/redirect";
|
|
|
|
/// Basic OpenID test client
|
|
#[derive(Parser, Debug)]
|
|
pub struct AppConfig {
|
|
/// Listen URL
|
|
#[arg(short, long, env, default_value = "0.0.0.0:7510")]
|
|
pub listen_addr: String,
|
|
|
|
/// Public URL, the URL where this service is accessible, without the trailing slash
|
|
#[arg(short, long, env, default_value = "http://localhost:7510")]
|
|
pub public_url: String,
|
|
|
|
/// URL where the OpenID configuration can be found
|
|
#[arg(short, long, env)]
|
|
pub configuration_url: String,
|
|
|
|
/// OpenID client ID
|
|
#[arg(long, env)]
|
|
pub client_id: String,
|
|
|
|
/// OpenID client secret
|
|
#[arg(long, env)]
|
|
pub client_secret: String,
|
|
|
|
/// Proxy IP, might end with a "*"
|
|
#[clap(long, env)]
|
|
pub proxy_ip: Option<String>,
|
|
}
|
|
|
|
impl AppConfig {
|
|
pub fn get() -> &'static Self {
|
|
&CONF
|
|
}
|
|
|
|
pub fn redirect_url(&self) -> String {
|
|
format!("{}{}", self.public_url, REDIRECT_URI)
|
|
}
|
|
}
|
|
|
|
lazy_static::lazy_static! {
|
|
static ref CONF: AppConfig = {
|
|
AppConfig::parse()
|
|
};
|
|
}
|
|
|
|
#[get("/assets/bootstrap.min.css")]
|
|
async fn bootstrap() -> HttpResponse {
|
|
HttpResponse::Ok()
|
|
.content_type("text/css")
|
|
.body(include_str!("../assets/bootstrap.min.css"))
|
|
}
|
|
|
|
#[get("/assets/cover.css")]
|
|
async fn cover() -> HttpResponse {
|
|
HttpResponse::Ok()
|
|
.content_type("text/css")
|
|
.body(include_str!("../assets/cover.css"))
|
|
}
|
|
|
|
#[derive(Template)]
|
|
#[template(path = "home.html")]
|
|
struct HomeTemplate {
|
|
remote_ip: String,
|
|
redirect_url: String,
|
|
}
|
|
|
|
#[derive(Template)]
|
|
#[template(path = "result.html")]
|
|
struct ResultTemplate {
|
|
token: String,
|
|
user_info: String,
|
|
}
|
|
|
|
#[derive(Template)]
|
|
#[template(path = "error.html")]
|
|
struct ErrorTemplate<'a> {
|
|
message: &'a str,
|
|
}
|
|
|
|
impl<'a> ErrorTemplate<'a> {
|
|
pub fn build(message: &'a str) -> HttpResponse {
|
|
HttpResponse::Unauthorized()
|
|
.content_type("text/html")
|
|
.body(Self { message }.render().unwrap())
|
|
}
|
|
}
|
|
|
|
#[get("/")]
|
|
async fn home(remote_ip: RemoteIP) -> HttpResponse {
|
|
HttpResponse::Ok().content_type("text/html").body(
|
|
HomeTemplate {
|
|
remote_ip: remote_ip.0.to_string(),
|
|
redirect_url: AppConfig::get().redirect_url(),
|
|
}
|
|
.render()
|
|
.unwrap(),
|
|
)
|
|
}
|
|
|
|
#[get("/start")]
|
|
async fn start(remote_ip: RemoteIP, state_manager: web::Data<BasicStateManager>) -> HttpResponse {
|
|
let config = match OpenIDConfig::load_from_url(&AppConfig::get().configuration_url).await {
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
log::error!("Failed to load OpenID configuration! {e}");
|
|
return ErrorTemplate::build("Failed to load OpenID configuration!");
|
|
}
|
|
};
|
|
|
|
let state = match state_manager.gen_state(remote_ip.0) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
log::error!("Failed to generate state! {:?}", e);
|
|
return ErrorTemplate::build("Failed to generate state!");
|
|
}
|
|
};
|
|
|
|
let authorization_url = config.gen_authorization_url(
|
|
&AppConfig::get().client_id,
|
|
&state,
|
|
&AppConfig::get().redirect_url(),
|
|
);
|
|
|
|
HttpResponse::Found()
|
|
.append_header(("Location", authorization_url))
|
|
.finish()
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct RedirectQuery {
|
|
state: String,
|
|
code: String,
|
|
}
|
|
|
|
#[get("/redirect")]
|
|
async fn redirect(
|
|
remote_ip: RemoteIP,
|
|
query: web::Query<RedirectQuery>,
|
|
state_manager: web::Data<BasicStateManager>,
|
|
) -> HttpResponse {
|
|
// First, validate state
|
|
if let Err(e) = state_manager.validate_state(remote_ip.0, &query.state) {
|
|
log::error!("Failed to validate state {}: {:?}", query.state, e);
|
|
return ErrorTemplate::build("State could not be validated!");
|
|
}
|
|
|
|
// Then, load OpenID configuration
|
|
let config = match OpenIDConfig::load_from_url(&AppConfig::get().configuration_url).await {
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
log::error!("Failed to load OpenID configuration! {e}");
|
|
return ErrorTemplate::build("Failed to load OpenID configuration!");
|
|
}
|
|
};
|
|
|
|
// Query token endpoint
|
|
let (token, token_str) = match config
|
|
.request_token(
|
|
&AppConfig::get().client_id,
|
|
&AppConfig::get().client_secret,
|
|
&query.code,
|
|
&AppConfig::get().redirect_url(),
|
|
)
|
|
.await
|
|
{
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
log::error!("Failed to retrieve token! {}", e);
|
|
return ErrorTemplate::build("Failed to retrieve access token!");
|
|
}
|
|
};
|
|
|
|
// Query userinfo endpoint
|
|
let (_user_info, user_info_str) = match config.request_user_info(&token).await {
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
log::error!("Failed to retrieve user info! {}", e);
|
|
return ErrorTemplate::build("Failed to retrieve user info!");
|
|
}
|
|
};
|
|
|
|
HttpResponse::Ok().content_type("text/html").body(
|
|
ResultTemplate {
|
|
token: serde_json::to_string_pretty(
|
|
&serde_json::from_str::<serde_json::Value>(&token_str).unwrap(),
|
|
)
|
|
.unwrap(),
|
|
user_info: serde_json::to_string_pretty(
|
|
&serde_json::from_str::<serde_json::Value>(&user_info_str).unwrap(),
|
|
)
|
|
.unwrap(),
|
|
}
|
|
.render()
|
|
.unwrap(),
|
|
)
|
|
}
|
|
|
|
#[actix_web::main]
|
|
async fn main() -> std::io::Result<()> {
|
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
|
|
|
log::info!("Init state manager");
|
|
let state_manager = web::Data::new(BasicStateManager::new());
|
|
|
|
log::info!("Will listen on {}", AppConfig::get().listen_addr);
|
|
|
|
HttpServer::new(move || {
|
|
App::new()
|
|
.wrap(Logger::default())
|
|
.app_data(web::Data::new(RemoteIPConfig {
|
|
proxy: CONF.proxy_ip.clone(),
|
|
}))
|
|
.app_data(state_manager.clone())
|
|
.service(bootstrap)
|
|
.service(cover)
|
|
.service(home)
|
|
.service(start)
|
|
.service(redirect)
|
|
})
|
|
.bind(&AppConfig::get().listen_addr)
|
|
.expect("Failed to bind server!")
|
|
.run()
|
|
.await
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use crate::AppConfig;
|
|
|
|
#[test]
|
|
fn verify_cli() {
|
|
use clap::CommandFactory;
|
|
AppConfig::command().debug_assert();
|
|
}
|
|
}
|