mod support; use support::*; use jwt_simple::prelude::ECDSAP256KeyPairLike; use ecamo::test; fn make_valid_url_token(env: &Environment) -> String { test::encode_url_token( &env.test_config.service_key_1, ecamo::token::UrlToken { ecamo_url: "http://upstream.test.invalid/test".try_into().unwrap(), ecamo_send_token: false, }, "https://service1.test.invalid", ) } fn make_valid_auth_token_cookie(env: &Environment) -> String { let token = test::generate_auth_token( &env.test_config.service_key_1, "https://service1.test.invalid", ); format!("__Host-ecamo_token={}", token) } #[actix_rt::test] async fn test_redirect_invalid_service() { let env = init_and_spawn().await; let http = build_reqwest_client(); let url_token = test::encode_url_token( &env.test_config.service_key_2, ecamo::token::UrlToken { ecamo_url: "http://upstream.test.invalid/test".try_into().unwrap(), ecamo_send_token: false, }, "https://invalid-service.test.invalid", ); let auth_token = format!( "__Host-ecamo_token={}", test::generate_auth_token( &env.test_config.service_key_2, "https://invalid-service.test.invalid", ) ); let resp = http .get( env.url .join(&format!("/.ecamo/v1/r/{}", url_token)) .unwrap(), ) .header("host", "invalid-service.test.invalid") .header("cookie", auth_token) .send() .await .unwrap(); assert_eq!(resp.status(), reqwest::StatusCode::FORBIDDEN); } #[actix_rt::test] async fn test_redirect_invalid_url_token_key() { let env = init_and_spawn().await; let http = build_reqwest_client(); let token = test::encode_url_token( &env.test_config.service_key_invalid, ecamo::token::UrlToken { ecamo_url: "http://upstream.test.invalid/test".try_into().unwrap(), ecamo_send_token: false, }, "https://service1.test.invalid", ); let resp = http .get(env.url.join(&format!("/.ecamo/v1/r/{}", token)).unwrap()) .header("host", "service1.test.invalid") .send() .await .unwrap(); assert_eq!(resp.status(), reqwest::StatusCode::UNAUTHORIZED); } #[actix_rt::test] async fn test_redirect_invalid_url_token_iss() { let env = init_and_spawn().await; let http = build_reqwest_client(); let token = test::encode_url_token( &env.test_config.service_key_1, ecamo::token::UrlToken { ecamo_url: "http://upstream.test.invalid/test".try_into().unwrap(), ecamo_send_token: false, }, "https://service2.test.invalid", ); let resp = http .get(env.url.join(&format!("/.ecamo/v1/r/{}", token)).unwrap()) .header("host", "service1.test.invalid") .send() .await .unwrap(); assert_eq!(resp.status(), reqwest::StatusCode::UNAUTHORIZED); } #[actix_rt::test] async fn test_redirect_invalid_source() { let env = init_and_spawn().await; let http = build_reqwest_client(); let token = test::encode_url_token( &env.test_config.service_key_1, ecamo::token::UrlToken { ecamo_url: "http://unallowed-upstream.test.invalid/test" .try_into() .unwrap(), ecamo_send_token: false, }, "https://service1.test.invalid", ); let resp = http .get(env.url.join(&format!("/.ecamo/v1/r/{}", token)).unwrap()) .header("host", "service1.test.invalid") .send() .await .unwrap(); assert_eq!(resp.status(), reqwest::StatusCode::FORBIDDEN); } #[actix_rt::test] async fn test_redirect_invalid_source_format() { let env = init_and_spawn().await; let http = build_reqwest_client(); let claims = jwt_simple::claims::Claims::with_custom_claims( UrlTokenInString { ecamo_url: "http://in%va~lid.test.invalid".to_owned(), }, std::time::Duration::new(60, 0).into(), ) .with_issuer("https://service1.test.invalid"); let token = env.test_config.service_key_1.sign(claims).unwrap(); let resp = http .get(env.url.join(&format!("/.ecamo/v1/r/{}", token)).unwrap()) .header("host", "service1.test.invalid") .send() .await .unwrap(); assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST); } #[actix_rt::test] async fn test_redirect_source_by_cookie() { let env = init_and_spawn().await; let http = build_reqwest_client(); let token = make_valid_url_token(&env); let resp = http .get(env.url.join(&format!("/.ecamo/v1/r/{}", token)).unwrap()) .header("host", "service1.test.invalid") .send() .await .unwrap(); assert_eq!(resp.status(), reqwest::StatusCode::FOUND); assert_eq!( resp.headers().get("location").unwrap().to_str().unwrap(), "http://upstream.test.invalid/test" ); assert_eq!( resp.headers() .get("cache-control") .unwrap() .to_str() .unwrap(), "no-cache, no-store, max-age=0, must-revalidate" ); } #[actix_rt::test] async fn test_redirect_source_by_fetch_dest() { let env = init_and_spawn().await; let http = build_reqwest_client(); let token = make_valid_url_token(&env); let resp = http .get(env.url.join(&format!("/.ecamo/v1/r/{}", token)).unwrap()) .header("host", "service1.test.invalid") .header("cookie", make_valid_auth_token_cookie(&env)) .header("sec-fetch-dest", "document") .send() .await .unwrap(); assert_eq!(resp.status(), reqwest::StatusCode::FOUND); assert_eq!( resp.headers().get("location").unwrap().to_str().unwrap(), "http://upstream.test.invalid/test" ); assert_eq!( resp.headers() .get("cache-control") .unwrap() .to_str() .unwrap(), "no-cache, no-store, max-age=0, must-revalidate" ); } // TODO: test_redirect_invalid_auth_token_exp #[actix_rt::test] async fn test_redirect_invalid_auth_token_key() { let env = init_and_spawn().await; let http = build_reqwest_client(); let url_token = make_valid_url_token(&env); let auth_token = test::generate_auth_token( &env.test_config.service_key_2, "https://service1.test.invalid", ); let resp = http .get( env.url .join(&format!("/.ecamo/v1/r/{}", url_token)) .unwrap(), ) .header("host", "service1.test.invalid") .header("cookie", format!("__Host-ecamo_token={}", auth_token)) .send() .await .unwrap(); assert_eq!(resp.status(), reqwest::StatusCode::UNAUTHORIZED); } #[actix_rt::test] async fn test_redirect_invalid_auth_token_iss() { let env = init_and_spawn().await; let http = build_reqwest_client(); let url_token = make_valid_url_token(&env); let auth_token = test::generate_auth_token( &env.test_config.service_key_1, "https://service2.test.invalid", ); let resp = http .get( env.url .join(&format!("/.ecamo/v1/r/{}", url_token)) .unwrap(), ) .header("host", "service1.test.invalid") .header("cookie", format!("__Host-ecamo_token={}", auth_token)) .send() .await .unwrap(); assert_eq!(resp.status(), reqwest::StatusCode::UNAUTHORIZED); } #[actix_rt::test] async fn test_redirect_proxy() { let env = init_and_spawn().await; let http = build_reqwest_client(); let url_token = make_valid_url_token(&env); let auth_token = make_valid_auth_token_cookie(&env); let resp = http .get( env.url .join(&format!("/.ecamo/v1/r/{}", url_token)) .unwrap(), ) .header("host", "service1.test.invalid") .header("cookie", auth_token) .header("sec-fetch-dest", "image") .send() .await .unwrap(); assert_eq!(resp.status(), reqwest::StatusCode::FOUND); assert_eq!( resp.headers() .get("cache-control") .unwrap() .to_str() .unwrap(), "no-cache, no-store, max-age=0, must-revalidate" ); let url = url::Url::parse(resp.headers().get("location").unwrap().to_str().unwrap()).unwrap(); assert_eq!(url.scheme(), "https"); assert_eq!(url.host_str().unwrap(), "ecamo.test.invalid"); assert_eq!( url.path(), "/.ecamo/v1/p/4NDjngsnmTsgtuEwT-xuia33YXiYq3j_4zm4XRawQwYAhbdlcbe4HybG-iDuY2pI" ); let (_, proxy_token_str) = url.query_pairs().find(|(k, _)| k == "t").unwrap(); let proxy_token = ecamo::token::ProxyToken::decode( &proxy_token_str, &env.test_config.app_config.signing_decoding_keys(), ) .unwrap(); let exp = chrono::Utc::now() + chrono::Duration::seconds(90); assert!(proxy_token.expires_at.unwrap().as_secs() < exp.timestamp().try_into().unwrap()); assert_eq!(proxy_token.issuer.unwrap(), "ecamo:s"); assert!(proxy_token .audiences .unwrap() .contains(&std::collections::HashSet::from_iter( ["ecamo:p".to_owned()].into_iter() ))); assert_eq!( proxy_token.custom.ecamo_service_origin, "https://service1.test.invalid" ); assert_eq!( proxy_token.custom.ecamo_url.as_str(), "http://upstream.test.invalid/test" ); assert_eq!(proxy_token.custom.ecamo_send_token, false); }