diff --git a/src/errors.rs b/src/errors.rs index 56d53a6e..3bbc2aa2 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,5 +1,6 @@ use actix_web::http::StatusCode; use askama::Template; +use thiserror::Error; #[derive(Template)] #[template(path = "error.html")] @@ -53,3 +54,29 @@ macro_rules! impl_api_error { }; } pub(crate) use impl_api_error; + +use crate::search::SearchError; + +#[derive(Error, Debug)] +pub enum RedirectError { + #[error("search error: `{0}`")] + Search(#[from] SearchError), + #[error("serialization error: `{0}`")] + Serialization(#[from] serde_json::Error), + #[error("urlencode error: `{0}`")] + Base64Decode(#[from] base64::DecodeError), + #[error("url parse error: `{0}`")] + UrlParse(#[from] url::ParseError), +} + +impl_api_error!(RedirectError, + status => { + RedirectError::Search(_) => StatusCode::INTERNAL_SERVER_ERROR, + RedirectError::Serialization(_) => StatusCode::INTERNAL_SERVER_ERROR, + RedirectError::Base64Decode(_) => StatusCode::INTERNAL_SERVER_ERROR, + RedirectError::UrlParse(_) => StatusCode::INTERNAL_SERVER_ERROR, + }, + data => { + _ => None, + } +); diff --git a/src/filters.rs b/src/filters.rs new file mode 100644 index 00000000..8145fb45 --- /dev/null +++ b/src/filters.rs @@ -0,0 +1,7 @@ +use crate::crawler::CrawledInstance; + +pub fn sort_crawled_instances(l: &[CrawledInstance]) -> askama::Result> { + let mut new = l.to_owned(); + new.sort_by(|a, b| a.status.as_isize().cmp(&b.status.as_isize())); + Ok(new) +} diff --git a/src/main.rs b/src/main.rs index 031ea0bf..f2467584 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,12 @@ mod config; mod crawler; mod errors; +mod filters; mod log_setup; mod routes; mod search; mod serde_types; +mod utils; use crate::crawler::Crawler; use crate::serde_types::ServicesData; diff --git a/src/routes/config.rs b/src/routes/config.rs new file mode 100644 index 00000000..e20d6f0f --- /dev/null +++ b/src/routes/config.rs @@ -0,0 +1,57 @@ +use actix_web::{cookie::Cookie, get, http::header::LOCATION, web, HttpRequest, Responder, Scope}; +use askama::Template; +use base64::prelude::*; + +use crate::{ + config::AppConfig, + errors::RedirectError, + serde_types::{LoadedData, UserConfig}, + utils::user_config::load_settings_cookie, +}; + +pub fn scope(_config: &AppConfig) -> Scope { + web::scope("/configure") + .service(configure_page) + .service(configure_save) +} + +#[derive(Template)] +#[template(path = "configure.html")] +pub struct ConfigureTemplate<'a> { + current_config: &'a str, +} + +#[get("")] +async fn configure_page( + req: HttpRequest, + loaded_data: web::Data, +) -> actix_web::Result { + let user_config = load_settings_cookie(&req, &loaded_data.default_settings); + let json: String = serde_json::to_string(&user_config).map_err(RedirectError::Serialization)?; + let data = BASE64_STANDARD.encode(json.as_bytes()); + + let template = ConfigureTemplate { + current_config: &data, + }; + + Ok(actix_web::HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(template.render().expect("failed to render error page"))) +} + +#[get("/save")] +async fn configure_save(req: HttpRequest) -> actix_web::Result { + let query_string = req.query_string(); + let b64_decoded = BASE64_STANDARD + .decode(query_string.as_bytes()) + .map_err(RedirectError::Base64Decode)?; + let user_config: UserConfig = + serde_json::from_slice(&b64_decoded).map_err(RedirectError::Serialization)?; + let json: String = serde_json::to_string(&user_config).map_err(RedirectError::Serialization)?; + let data = BASE64_STANDARD.encode(json.as_bytes()); + let cookie = Cookie::build("config", data).path("/").finish(); + Ok(actix_web::HttpResponse::TemporaryRedirect() + .cookie(cookie) + .insert_header((LOCATION, "/configure?success")) + .finish()) +} diff --git a/src/routes/index.rs b/src/routes/index.rs new file mode 100644 index 00000000..ddb89174 --- /dev/null +++ b/src/routes/index.rs @@ -0,0 +1,51 @@ +use std::collections::HashMap; + +use actix_web::{get, web, Responder, Scope}; +use askama::Template; +use chrono::{DateTime, Utc}; + +use crate::{ + config::AppConfig, + crawler::{CrawledService, Crawler}, + errors::RedirectError, + filters, + search::SearchError, + serde_types::{LoadedData, ServicesData}, +}; + +use super::{config, redirect}; + +pub fn scope(app_config: &AppConfig) -> Scope { + web::scope("") + .service(index) + .service(config::scope(app_config)) + .service(redirect::scope(app_config)) +} + +#[derive(Template)] +#[template(path = "index.html")] +pub struct IndexTemplate<'a> { + pub crawled_services: &'a HashMap, + pub services: &'a ServicesData, + pub time: &'a DateTime, +} + +#[get("/")] +async fn index( + crawler: web::Data, + loaded_data: web::Data, +) -> actix_web::Result { + let data = crawler.read().await; + let Some(crawled_services) = data.as_ref() else { + return Err(RedirectError::from(SearchError::CrawlerNotFetchedYet))?; + }; + let template = IndexTemplate { + services: &loaded_data.services, + crawled_services: &crawled_services.services, + time: &crawled_services.time, + }; + + Ok(actix_web::HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(template.render().expect("failed to render error page"))) +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index c94d6a36..43c206bc 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,9 +1,11 @@ +mod config; +mod index; mod redirect; -use actix_web::{web, Scope}; +use actix_web::Scope; use crate::config::AppConfig; pub fn main_scope(config: &AppConfig) -> Scope { - web::scope("").service(redirect::scope(config)) + index::scope(config) } diff --git a/src/routes/redirect.rs b/src/routes/redirect.rs index b0c3e6dc..145f15b8 100644 --- a/src/routes/redirect.rs +++ b/src/routes/redirect.rs @@ -1,165 +1,30 @@ -use std::collections::HashMap; - use actix_web::{ - cookie::Cookie, get, - http::{header::LOCATION, Method, StatusCode}, + http::{header::LOCATION, Method}, web, HttpRequest, Responder, Scope, }; use askama::Template; -use base64::prelude::*; -use chrono::{DateTime, Utc}; -use thiserror::Error; use crate::{ config::AppConfig, crawler::{CrawledService, Crawler}, - errors::impl_api_error, + errors::RedirectError, search::{ find_redirect_service_by_name, find_redirect_service_by_url, get_redirect_instance, get_redirect_instances, SearchError, }, - serde_types::{LoadedData, Regexes, SelectMethod, Service, ServicesData, UserConfig}, + serde_types::{LoadedData, Regexes, SelectMethod, Service}, + utils::user_config::load_settings_cookie, }; pub fn scope(_config: &AppConfig) -> Scope { web::scope("") - .service(index) - .service(configure_page) - .service(configure_save) .service(history_redirect) .service(cached_redirect) .route("/{path:.*}", web::get().to(base_redirect)) .route("/{path:.*}", web::post().to(base_redirect)) } -#[derive(Error, Debug)] -pub enum RedirectError { - #[error("search error: `{0}`")] - Search(#[from] SearchError), - #[error("serialization error: `{0}`")] - Serialization(#[from] serde_json::Error), - #[error("urlencode error: `{0}`")] - Base64Decode(#[from] base64::DecodeError), - #[error("url parse error: `{0}`")] - UrlParse(#[from] url::ParseError), -} - -impl_api_error!(RedirectError, - status => { - RedirectError::Search(_) => StatusCode::INTERNAL_SERVER_ERROR, - RedirectError::Serialization(_) => StatusCode::INTERNAL_SERVER_ERROR, - RedirectError::Base64Decode(_) => StatusCode::INTERNAL_SERVER_ERROR, - RedirectError::UrlParse(_) => StatusCode::INTERNAL_SERVER_ERROR, - }, - data => { - _ => None, - } -); - -#[derive(Template)] -#[template(path = "index.html")] -pub struct IndexTemplate<'a> { - pub crawled_services: &'a HashMap, - pub services: &'a ServicesData, - pub time: &'a DateTime, -} - -mod filters { - use crate::crawler::CrawledInstance; - - pub fn sort_list(l: &[CrawledInstance]) -> ::askama::Result> { - let mut new = l.to_owned(); - new.sort_by(|a, b| a.status.as_isize().cmp(&b.status.as_isize())); - Ok(new) - } -} - -#[get("/")] -async fn index( - crawler: web::Data, - loaded_data: web::Data, -) -> actix_web::Result { - let data = crawler.read().await; - let Some(crawled_services) = data.as_ref() else { - return Err(RedirectError::from(SearchError::CrawlerNotFetchedYet))?; - }; - let template = IndexTemplate { - services: &loaded_data.services, - crawled_services: &crawled_services.services, - time: &crawled_services.time, - }; - - Ok(actix_web::HttpResponse::Ok() - .content_type("text/html; charset=utf-8") - .body(template.render().expect("failed to render error page"))) -} - -#[derive(Template)] -#[template(path = "configure.html")] -pub struct ConfigureTemplate<'a> { - current_config: &'a str, -} - -#[get("/configure")] -async fn configure_page( - req: HttpRequest, - loaded_data: web::Data, -) -> actix_web::Result { - let user_config = load_settings_cookie(&req, &loaded_data.default_settings); - let json: String = serde_json::to_string(&user_config).map_err(RedirectError::Serialization)?; - let data = BASE64_STANDARD.encode(json.as_bytes()); - - let template = ConfigureTemplate { - current_config: &data, - }; - - Ok(actix_web::HttpResponse::Ok() - .content_type("text/html; charset=utf-8") - .body(template.render().expect("failed to render error page"))) -} - -#[get("/configure/save")] -async fn configure_save(req: HttpRequest) -> actix_web::Result { - let query_string = req.query_string(); - let b64_decoded = BASE64_STANDARD - .decode(query_string.as_bytes()) - .map_err(RedirectError::Base64Decode)?; - let user_config: UserConfig = - serde_json::from_slice(&b64_decoded).map_err(RedirectError::Serialization)?; - let json: String = serde_json::to_string(&user_config).map_err(RedirectError::Serialization)?; - let data = BASE64_STANDARD.encode(json.as_bytes()); - let cookie = Cookie::build("config", data).path("/").finish(); - Ok(actix_web::HttpResponse::TemporaryRedirect() - .cookie(cookie) - .insert_header((LOCATION, "/configure?success")) - .finish()) -} - -fn load_settings_cookie(req: &HttpRequest, default: &UserConfig) -> UserConfig { - let cookie = match req.cookie("config") { - Some(cookie) => cookie, - None => { - debug!("Cookie not found"); - return default.clone(); - } - }; - let data = match BASE64_STANDARD.decode(cookie.value().as_bytes()) { - Ok(data) => data, - Err(_) => { - debug!("invalid cookie data"); - return default.clone(); - } - }; - match serde_json::from_slice(&data) { - Ok(user_config) => user_config, - Err(_) => { - debug!("invalid cookie query string"); - default.clone() - } - } -} - #[derive(Template)] #[template(path = "cached_redirect.html", escape = "none")] pub struct CachedRedirectTemplate<'a> { diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 00000000..7ff26f13 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod user_config; diff --git a/src/utils/user_config.rs b/src/utils/user_config.rs new file mode 100644 index 00000000..a14a68ab --- /dev/null +++ b/src/utils/user_config.rs @@ -0,0 +1,28 @@ +use actix_web::HttpRequest; +use base64::prelude::*; + +use crate::serde_types::UserConfig; + +pub fn load_settings_cookie(req: &HttpRequest, default: &UserConfig) -> UserConfig { + let cookie = match req.cookie("config") { + Some(cookie) => cookie, + None => { + debug!("Cookie not found"); + return default.clone(); + } + }; + let data = match BASE64_STANDARD.decode(cookie.value().as_bytes()) { + Ok(data) => data, + Err(_) => { + debug!("invalid cookie data"); + return default.clone(); + } + }; + match serde_json::from_slice(&data) { + Ok(user_config) => user_config, + Err(_) => { + debug!("invalid cookie query string"); + default.clone() + } + } +} diff --git a/templates/index.html b/templates/index.html index 861d8e54..5e59673f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -17,7 +17,7 @@

Last synced {{ time }}

Fallback: {{ services[name].fallback }}
    - {% let instances = crawled_service.instances|sort_list %} + {% let instances = crawled_service.instances|sort_crawled_instances %} {% for instance in instances %}
  • {{ instance.url }} Status: