diff --git a/src/common/utils/auth.rs b/src/common/utils/auth.rs index 1fa84cd941..ad3590b567 100644 --- a/src/common/utils/auth.rs +++ b/src/common/utils/auth.rs @@ -406,6 +406,7 @@ impl FromRequest for AuthExtractor { || (method.eq("LIST") && path.contains("pipelines")) || path.contains("clusters") || path.contains("query_manager") + || path.contains("/short") { return ready(Ok(AuthExtractor { auth: auth_str.to_owned(), diff --git a/src/common/utils/redirect_response.rs b/src/common/utils/redirect_response.rs index e30c81d3a7..054075fc38 100644 --- a/src/common/utils/redirect_response.rs +++ b/src/common/utils/redirect_response.rs @@ -13,7 +13,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use std::fmt; +use std::{collections::HashMap, fmt}; use actix_web::{HttpResponse, ResponseError}; @@ -21,33 +21,46 @@ const DEFAULT_REDIRECT_RELATIVE_URI: &str = "/web/"; #[derive(Debug)] pub struct RedirectResponse { - pub redirect_relative_uri: String, + redirect_relative_uri: String, + query_params: HashMap, } impl RedirectResponse { - /// Create a new `RedirectResponse` with the given redirection URL. - /// - /// If no URL is provided, a default URL (`/web`) will be used. - pub fn new(redirect_relative_uri: &str) -> Self { - RedirectResponse { - redirect_relative_uri: redirect_relative_uri.to_string(), - } - } - - pub fn redirect(&self) -> HttpResponse { + pub fn redirect_http(&self) -> HttpResponse { self.build_redirect_response() } + fn build_full_redirect_uri(&self) -> String { + let mut redirect_uri = self.redirect_relative_uri.clone(); + + // If there are query parameters, append them to the URI. + if !self.query_params.is_empty() { + let query_string: String = self + .query_params + .iter() + .map(|(key, value)| format!("{}={}", key, value)) + .collect::>() + .join("&"); + + redirect_uri = format!("{}?{}", redirect_uri, query_string); + } + + redirect_uri + } + fn build_redirect_response(&self) -> HttpResponse { + let redirect_uri = self.build_full_redirect_uri(); + HttpResponse::Found() - .append_header(("Location", self.redirect_relative_uri.clone())) + .append_header(("Location", redirect_uri)) .finish() } } impl fmt::Display for RedirectResponse { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Redirecting to {}", self.redirect_relative_uri) + let redirect_uri = self.build_full_redirect_uri(); + write!(f, "Redirecting to {}", redirect_uri) } } @@ -58,10 +71,104 @@ impl ResponseError for RedirectResponse { } } -impl Default for RedirectResponse { - fn default() -> Self { +pub struct RedirectResponseBuilder { + redirect_relative_uri: String, + query_params: HashMap, +} + +impl RedirectResponseBuilder { + /// Create a new `RedirectResponseBuilder` with a given redirection URL. + /// + /// If no URL is provided, a default URL (`/web`) will be used. + pub fn new(redirect_relative_uri: &str) -> Self { + RedirectResponseBuilder { + redirect_relative_uri: redirect_relative_uri.to_string(), + query_params: HashMap::new(), + } + } + + pub fn with_query_param(mut self, key: &str, value: &str) -> Self { + self.query_params.insert(key.to_string(), value.to_string()); + self + } + + pub fn build(self) -> RedirectResponse { RedirectResponse { - redirect_relative_uri: DEFAULT_REDIRECT_RELATIVE_URI.to_string(), + redirect_relative_uri: self.redirect_relative_uri, + query_params: self.query_params, } } } + +impl Default for RedirectResponseBuilder { + fn default() -> Self { + RedirectResponseBuilder { + redirect_relative_uri: DEFAULT_REDIRECT_RELATIVE_URI.to_string(), + query_params: HashMap::new(), + } + } +} + +#[cfg(test)] +mod tests { + use actix_web::{http::header::LOCATION, HttpResponse}; + + use super::*; + + #[test] + fn test_redirect_with_short_url() { + // Build a RedirectResponse with a short_url query parameter + let redirect_response = RedirectResponseBuilder::new("/web") + .with_query_param( + "redirect_url", + "http://localhost:5080/api/default/short/1234", + ) + .build(); + + // Check if the constructed URI is correct + let expected_uri = "/web?redirect_url=http://localhost:5080/api/default/short/1234"; + assert_eq!(redirect_response.build_full_redirect_uri(), expected_uri); + + // Check if the HTTP response contains the correct "Location" header + let http_response: HttpResponse = redirect_response.redirect_http(); + assert_eq!(http_response.status(), actix_web::http::StatusCode::FOUND); + assert_eq!(http_response.headers().get(LOCATION).unwrap(), expected_uri); + } + + #[test] + fn test_redirect_without_query_params() { + // Build a RedirectResponse without any query parameters + let redirect_response = RedirectResponseBuilder::new("/web").build(); + + // Check if the constructed URI is correct (no query params) + let expected_uri = "/web"; + assert_eq!(redirect_response.build_full_redirect_uri(), expected_uri); + + // Check if the HTTP response contains the correct "Location" header + let http_response: HttpResponse = redirect_response.redirect_http(); + assert_eq!(http_response.status(), actix_web::http::StatusCode::FOUND); + assert_eq!(http_response.headers().get(LOCATION).unwrap(), expected_uri); + } + + #[test] + fn test_redirect_with_multiple_query_params() { + // Build a RedirectResponse with multiple query parameters + let redirect_response = RedirectResponseBuilder::new("/web") + .with_query_param( + "redirect_url", + "http://localhost:5080/api/default/short/1234", + ) + .with_query_param("user_id", "42") + .build(); + + // Check if the constructed URI is correct with multiple query parameters + let expected_uri = + "/web?redirect_url=http://localhost:5080/api/default/short/1234&user_id=42"; + assert_eq!(redirect_response.build_full_redirect_uri(), expected_uri); + + // Check if the HTTP response contains the correct "Location" header + let http_response: HttpResponse = redirect_response.redirect_http(); + assert_eq!(http_response.status(), actix_web::http::StatusCode::FOUND); + assert_eq!(http_response.headers().get(LOCATION).unwrap(), expected_uri); + } +} diff --git a/src/handler/http/auth/validator.rs b/src/handler/http/auth/validator.rs index 4f79bd2e30..54c6c4ce08 100644 --- a/src/handler/http/auth/validator.rs +++ b/src/handler/http/auth/validator.rs @@ -33,7 +33,7 @@ use crate::{ }, utils::{ auth::{get_hash, is_root_user, AuthExtractor}, - redirect_response::RedirectResponse, + redirect_response::RedirectResponseBuilder, }, }, service::{db, users}, @@ -643,35 +643,34 @@ fn get_user_details(decoded: String) -> Option<(String, String)> { /// /// If the authentication is invalid, it returns an `ErrorUnauthorized` error. pub async fn oo_validator( - req: ServiceRequest, - auth_info: AuthExtractor, -) -> Result { - let path_prefix = "/api/"; - oo_validator_internal(req, auth_info, path_prefix).await -} - -/// Validates the authentication result and redirects on failure for requests with the `/short/` -/// prefix. -/// -/// This function is a proxy for the `oo_validator_internal` function, setting the `path_prefix` to -/// "/short/". -/// -/// # Errors -/// If authentication fails, the function logs the failure and returns an error that causes a -/// redirect to a predefined URL. -pub async fn validate_short_or_redirect( req: ServiceRequest, auth_result: Result, ) -> Result { - match auth_result { - Ok(auth_info) => { - let path_prefix = "/short/"; - match oo_validator_internal(req, auth_info, path_prefix).await { - Ok(req) => Ok(req), - Err((err, err_req)) => Err(handle_auth_failure_for_redirect(err_req, &err)), + let path_prefix = "/api/"; + let path = extract_relative_path(req.request().path(), path_prefix); + let path_columns = path.split('/').collect::>(); + let is_short_url = is_short_url_path(&path_columns); + + let auth_info = match auth_result { + Ok(info) => info, + Err(e) => { + return if is_short_url { + Err(handle_auth_failure_for_redirect(req, &e)) + } else { + Err((e, req)) + }; + } + }; + + match oo_validator_internal(req, auth_info, path_prefix).await { + Ok(service_req) => Ok(service_req), + Err((err, err_req)) => { + if is_short_url { + Err(handle_auth_failure_for_redirect(err_req, &err)) + } else { + Err((err, err_req)) } } - Err(err) => Err(handle_auth_failure_for_redirect(req, &err)), } } @@ -800,20 +799,55 @@ pub(crate) async fn list_objects_for_user( } } +/// Helper function to extract the relative path after the base URI and path prefix +fn extract_relative_path(full_path: &str, path_prefix: &str) -> String { + let base_uri = config::get_config().common.base_uri.clone(); + let full_prefix = format!("{}{}", base_uri, path_prefix); + full_path + .strip_prefix(&full_prefix) + .unwrap_or(full_path) + .to_string() +} + +/// Helper function to check if the path corresponds to a short URL +fn is_short_url_path(path_columns: &[&str]) -> bool { + path_columns + .get(1) + .map_or(false, |&segment| segment.to_lowercase() == "short") +} + /// Handles authentication failure by logging the error and returning a redirect response. /// /// This function is responsible for logging the authentication failure and returning a redirect /// response. It takes in the request and the error message, and returns a tuple containing the /// redirect response and the service request. fn handle_auth_failure_for_redirect(req: ServiceRequest, error: &Error) -> (Error, ServiceRequest) { - let redirect = RedirectResponse::default(); + let full_url = extract_full_url(&req); + let redirect_http = RedirectResponseBuilder::default() + .with_query_param("short_url", &full_url) + .build(); log::warn!( - "Authentication failed for path: {}, err: {}, redirecting to {}", + "Authentication failed for path: {}, err: {}, {}", req.path(), error, - &redirect.redirect_relative_uri, + &redirect_http, ); - (redirect.into(), req) + (redirect_http.into(), req) +} + +/// Extracts the full URL from the request. +fn extract_full_url(req: &ServiceRequest) -> String { + let connection_info = req.connection_info(); + let scheme = connection_info.scheme(); + let host = connection_info.host(); + let path = req + .request() + .uri() + .path_and_query() + .map(|pq| pq.as_str()) + .unwrap_or(""); + + format!("{}://{}{}", scheme, host, path) } #[cfg(test)] diff --git a/src/handler/http/request/short_url/mod.rs b/src/handler/http/request/short_url/mod.rs index fd7983a58f..ace297049b 100644 --- a/src/handler/http/request/short_url/mod.rs +++ b/src/handler/http/request/short_url/mod.rs @@ -15,10 +15,17 @@ use std::io::Error; +use actix_http::StatusCode; use actix_web::{get, post, web, HttpRequest, HttpResponse}; use config::meta::short_url::ShortenUrlResponse; -use crate::{common::utils::redirect_response::RedirectResponse, service::short_url}; +use crate::{ + common::{ + meta::{self, http::HttpResponse as MetaHttpResponse}, + utils::redirect_response::RedirectResponseBuilder, + }, + service::short_url, +}; /// Shorten a URL #[utoipa::path( @@ -47,9 +54,13 @@ use crate::{common::utils::redirect_response::RedirectResponse, service::short_u tag = "Short Url" )] #[post("/{org_id}/short")] -pub async fn shorten(_org_id: web::Path, body: web::Bytes) -> Result { - let req: config::meta::short_url::ShortenUrlRequest = serde_json::from_slice(&body)?; - match short_url::shorten(&req.original_url).await { +pub async fn shorten(org_id: web::Path, body: web::Bytes) -> Result { + let req: config::meta::short_url::ShortenUrlRequest = match serde_json::from_slice(&body) { + Ok(v) => v, + Err(e) => return Ok(MetaHttpResponse::bad_request(e)), + }; + + match short_url::shorten(&org_id, &req.original_url).await { Ok(short_url) => { let response = ShortenUrlResponse { short_url: short_url.clone(), @@ -59,7 +70,12 @@ pub async fn shorten(_org_id: web::Path, body: web::Bytes) -> Result { log::error!("Failed to shorten URL: {:?}", e); - Ok(HttpResponse::InternalServerError().finish()) + Ok( + HttpResponse::InternalServerError().json(meta::http::HttpResponse::error( + StatusCode::INTERNAL_SERVER_ERROR.into(), + e.to_string(), + )), + ) } } } @@ -79,22 +95,24 @@ pub async fn shorten(_org_id: web::Path, body: web::Bytes) -> Result, + path: web::Path<(String, String)>, ) -> Result { log::info!( "short_url::retrieve handler called for path: {}", req.path() ); - let short_id = short_id.into_inner(); + let (_org_id, short_id) = path.into_inner(); let original_url = short_url::retrieve(&short_id).await; if let Some(url) = original_url { - let redirect_response = RedirectResponse::new(&url); - Ok(redirect_response.redirect()) + let redirect_http = RedirectResponseBuilder::new(&url).build().redirect_http(); + Ok(redirect_http) } else { - Ok(HttpResponse::NotFound().body("Short URL not found")) + let redirect = RedirectResponseBuilder::default().build(); + log::error!("Short URL not found, {}", &redirect); + Ok(redirect.redirect_http()) } } diff --git a/src/handler/http/router/mod.rs b/src/handler/http/router/mod.rs index bc4f513722..34180348c8 100644 --- a/src/handler/http/router/mod.rs +++ b/src/handler/http/router/mod.rs @@ -226,15 +226,6 @@ pub fn get_basic_routes(cfg: &mut web::ServiceConfig) { .service(status::flush_node), ); - cfg.service( - web::scope("/short") - .wrap(HttpAuthentication::with_fn( - super::auth::validator::validate_short_or_redirect, - )) - .wrap(cors.clone()) - .service(short_url::retrieve), - ); - if get_config().common.swagger_enabled { cfg.service( SwaggerUi::new("/swagger/{_:.*}") @@ -497,7 +488,8 @@ pub fn get_service_routes(cfg: &mut web::ServiceConfig) { .service(search::multi_streams::_search_partition_multi) .service(search::multi_streams::around_multi) .service(stream::delete_stream_cache) - .service(short_url::shorten), + .service(short_url::shorten) + .service(short_url::retrieve), ); } diff --git a/src/service/alerts/alert.rs b/src/service/alerts/alert.rs index 99e836d91f..17f413a834 100644 --- a/src/service/alerts/alert.rs +++ b/src/service/alerts/alert.rs @@ -804,7 +804,7 @@ async fn process_dest_template( }; // Shorten the alert url - let alert_url = match short_url::shorten(&alert_url).await { + let alert_url = match short_url::shorten(&alert.org_id, &alert_url).await { Ok(short_url) => short_url, Err(e) => { log::error!("Error shortening alert url: {e}"); diff --git a/src/service/dashboards/reports.rs b/src/service/dashboards/reports.rs index 390d7e0fc1..384ff82873 100644 --- a/src/service/dashboards/reports.rs +++ b/src/service/dashboards/reports.rs @@ -618,7 +618,7 @@ async fn generate_report( log::debug!("done with headless browser"); // convert to short_url - let email_dashb_url = match short_url::shorten(&email_dashb_url).await { + let email_dashb_url = match short_url::shorten(org_id, &email_dashb_url).await { Ok(short_url) => short_url, Err(e) => { log::error!("Error shortening email dashboard url: {e}"); diff --git a/src/service/short_url.rs b/src/service/short_url.rs index d64a5acda7..45541ed4e3 100644 --- a/src/service/short_url.rs +++ b/src/service/short_url.rs @@ -29,15 +29,25 @@ pub fn get_base_url() -> String { format!("{}{}", config.common.web_url, config.common.base_uri) } -async fn store_short_url(short_id: &str, original_url: &str) -> Result { - let entry = ShortUrlRecord::new(short_id, original_url); - db::short_url::set(short_id, entry).await?; - Ok(format!( - "{}{}{}", +fn construct_short_url(org_id: &str, short_id: &str) -> String { + format!( + "{}/{}/{}{}{}", get_base_url(), + "api", + org_id, SHORT_URL_WEB_PATH, short_id - )) + ) +} + +async fn store_short_url( + org_id: &str, + short_id: &str, + original_url: &str, +) -> Result { + let entry = ShortUrlRecord::new(short_id, original_url); + db::short_url::set(short_id, entry).await?; + Ok(construct_short_url(org_id, short_id)) } fn generate_short_id(original_url: &str, timestamp: Option) -> String { @@ -51,21 +61,16 @@ fn generate_short_id(original_url: &str, timestamp: Option) -> String { } /// Shortens the given original URL and stores it in the database -pub async fn shorten(original_url: &str) -> Result { +pub async fn shorten(org_id: &str, original_url: &str) -> Result { let mut short_id = generate_short_id(original_url, None); if let Ok(existing_url) = db::short_url::get(&short_id).await { if existing_url == original_url { - return Ok(format!( - "{}{}{}", - get_base_url(), - SHORT_URL_WEB_PATH, - short_id - )); + return Ok(construct_short_url(org_id, &short_id)); } } - let result = store_short_url(&short_id, original_url).await; + let result = store_short_url(org_id, &short_id, original_url).await; match result { Ok(url) => Ok(url), Err(e) => { @@ -74,7 +79,7 @@ pub async fn shorten(original_url: &str) -> Result { Error::DbError(DbError::UniqueViolation) => { let timestamp = Utc::now().timestamp(); short_id = generate_short_id(original_url, Some(timestamp)); - store_short_url(&short_id, original_url).await + store_short_url(org_id, &short_id, original_url).await } _ => Err(e), } @@ -90,27 +95,27 @@ pub async fn retrieve(short_id: &str) -> Option { db::short_url::get(short_id).await.ok() } -/// Extracts the short ID from the shortened URL -pub fn get_short_id_from_url(short_url: &str) -> Option { - let prefix = format!("{}{}", get_base_url(), SHORT_URL_WEB_PATH); - short_url.strip_prefix(&prefix).map(|s| s.to_string()) -} - #[cfg(test)] mod tests { use super::*; + /// Extracts the short ID from the shortened URL + fn get_short_id_from_url(org_id: &str, short_url: &str) -> Option { + let prefix = format!("{}api/{}{}", get_base_url(), org_id, SHORT_URL_WEB_PATH); + short_url.strip_prefix(&prefix).map(|s| s.to_string()) + } + #[tokio::test] #[ignore] async fn test_shorten_and_retrieve() { let original_url = "https://www.example.com/some/long/url"; - let short_url = shorten(original_url).await.unwrap(); - let short_id = get_short_id_from_url(&short_url).unwrap(); + let short_url = shorten("default", original_url).await.unwrap(); + let short_id = get_short_id_from_url("default", &short_url).unwrap(); let retrieved_url = retrieve(&short_id).await.expect("Failed to retrieve URL"); assert_eq!(retrieved_url, original_url); - let short_id = get_short_id_from_url(&short_url).unwrap(); + let short_id = get_short_id_from_url("default", &short_url).unwrap(); assert_eq!(short_id.len(), 16); } @@ -126,8 +131,8 @@ mod tests { async fn test_unique_original_urls() { let original_url = "https://www.example.com/some/long/url"; - let short_url1 = shorten(original_url).await.unwrap(); - let short_url2 = shorten(original_url).await.unwrap(); + let short_url1 = shorten("default", original_url).await.unwrap(); + let short_url2 = shorten("default", original_url).await.unwrap(); // Should return the same short_id assert_eq!(short_url1, short_url2); diff --git a/web/src/router/index.ts b/web/src/router/index.ts index fba7208db5..93f799fb73 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -71,16 +71,18 @@ export default function (store: any) { to.path != "/web/cb" && sessionUserInfo === null ) { - if (to.path !== "/logout" && + if ( + to.path !== "/logout" && to.path !== "/cb" && - to.path != "/web/cb") { + to.path != "/web/cb" + ) { // if query params contains redirect_url, store that URL in session storage // else store the current URL in session storage // this conditions added specifically for short URL feature where user will be redirected to backend API endpoint // if user is not logged in, then user will be redirected to login page and after successful login, user will be redirected to the short URL if (Object.hasOwn(to.query, "short_url")) { window.sessionStorage.setItem("redirectURI", to.query.short_url); - } + } else { window.sessionStorage.setItem("redirectURI", window.location.href); }