Implement server (#3)
All checks were successful
continuous-integration/drone/push Build is passing

Use Actix as HTTP service

Reviewed-on: #3
This commit is contained in:
2023-04-28 07:15:09 +00:00
parent 001f2420e5
commit bf570c477f
16 changed files with 2708 additions and 58 deletions

View File

@ -1,50 +1,178 @@
use clap::Parser;
use actix_web::middleware::Logger;
use actix_web::{get, web, App, HttpResponse, HttpServer};
use askama::Template;
/// Basic OpenID test client
#[derive(Parser, Debug)]
struct AppConfig {
/// Listen URL
#[arg(short, long, env, default_value = "0.0.0.0:7510")]
listen_addr: String,
use oidc_test_client::app_config::AppConfig;
use oidc_test_client::openid_primitives::OpenIDConfig;
use oidc_test_client::remote_ip::RemoteIP;
use oidc_test_client::state_manager::StateManager;
/// Public URL, the URL where this service is accessible, without the trailing slash
#[arg(short, long, env, default_value = "http://localhost:7510")]
public_url: String,
/// URL where the OpenID configuration can be found
#[arg(short, long, env)]
configuration_url: String,
/// OpenID client ID
#[arg(long, env)]
client_id: String,
/// OpenID client secret
#[arg(long, env)]
client_secret: String,
#[get("/assets/bootstrap.min.css")]
async fn bootstrap() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/css")
.body(include_str!("../assets/bootstrap.min.css"))
}
lazy_static::lazy_static! {
static ref CONF: AppConfig = {
AppConfig::parse()
};
#[get("/assets/cover.css")]
async fn cover() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/css")
.body(include_str!("../assets/cover.css"))
}
fn main() {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
log::info!("Will listen on {}", CONF.listen_addr);
println!("Hello, world!");
#[derive(Template)]
#[template(path = "home.html")]
struct HomeTemplate {
redirect_url: String,
}
#[cfg(test)]
mod test {
use crate::AppConfig;
#[derive(Template)]
#[template(path = "result.html")]
struct ResultTemplate {
token: String,
user_info: String,
}
#[test]
fn verify_cli() {
use clap::CommandFactory;
AppConfig::command().debug_assert();
#[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() -> HttpResponse {
HttpResponse::Ok().content_type("text/html").body(
HomeTemplate {
redirect_url: AppConfig::get().redirect_url(),
}
.render()
.unwrap(),
)
}
#[get("/start")]
async fn start(remote_ip: RemoteIP) -> HttpResponse {
let config = match OpenIDConfig::load_from(&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 StateManager::gen_state(&remote_ip) {
Ok(s) => s,
Err(e) => {
log::error!("Failed to generate state! {:?}", e);
return ErrorTemplate::build("Failed to generate state!");
}
};
let authorization_url = config.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>) -> HttpResponse {
// First, validate state
if let Err(e) = StateManager::validate_state(&remote_ip, &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(&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");
StateManager::init();
log::info!("Will listen on {}", AppConfig::get().listen_addr);
HttpServer::new(|| {
App::new()
.wrap(Logger::default())
.service(bootstrap)
.service(cover)
.service(home)
.service(start)
.service(redirect)
})
.bind(&AppConfig::get().listen_addr)
.expect("Failed to bind server!")
.run()
.await
}