use http::HeaderMap; use rust_proxy::{ acl::{routing_path, Acl, Action, Rule as AclRule}, config::{compile, demo_config, ValidationPolicy}, headers::{apply as apply_headers, HeaderRules, Operation, Rule as HeaderRule}, vhost::{LocationResponse, PathRoute}, }; fn policy() -> ValidationPolicy { ValidationPolicy { tls_addr: None, cert_dir: None, data_dir: None, proxy_addr: "127.0.0.1:8080".parse().unwrap(), admin_addr: "127.0.0.1:9090".parse().unwrap(), } } fn local_route() -> PathRoute { PathRoute { id: "local-status".into(), path_prefix: "/status".into(), response: Some(LocationResponse { status: 200, content_type: "text/plain; charset=utf-8".into(), body: "ok".into(), }), ..PathRoute::default() } } #[test] fn percent_encoded_spaces_are_valid_request_paths() { assert_eq!( routing_path("/Ubuntu%2014.04%20LTS/").unwrap(), "/Ubuntu 14.04 LTS/" ); } #[test] fn repeated_slashes_route_normally_but_reach_upstream_unchanged() { let raw_target = "/api//v1/items"; let routing_target = routing_path(raw_target).unwrap(); assert_eq!(routing_target, "/api/v1/items"); let mut config = demo_config("127.0.0.1:9000".parse().unwrap()); config.vhosts[0].routes = vec![PathRoute { id: "api".into(), path_prefix: "/api".into(), ..PathRoute::default() }]; let snapshot = compile(config, 1, &policy()).unwrap(); let selected = snapshot.select("localhost", &routing_target).unwrap(); assert_eq!(selected.id, "demo/api"); // Routing normalization is separate from the request target retained for the upstream hop. assert_eq!(raw_target, "/api//v1/items"); } #[test] fn local_location_response_uses_response_rules_without_opening_backend() { let mut config = demo_config("127.0.0.1:9000".parse().unwrap()); let mut route = local_route(); route.headers = HeaderRules { request: vec![], response: vec![HeaderRule { op: Operation::Set, name: "x-local-response".into(), value: Some("yes".into()), }], }; config.vhosts[0].routes = vec![route]; let snapshot = compile(config, 1, &policy()).unwrap(); let selected = snapshot.select("localhost", "/status").unwrap(); let response = selected.response.as_ref().expect("route must remain local"); assert_eq!(response.status, 200); assert_eq!(response.body, "ok"); let mut headers = HeaderMap::new(); apply_headers(&mut headers, &selected.headers.response); assert_eq!(headers.get("x-local-response").unwrap(), "yes"); // The proxy checks route.response before pool/backend selection, so this compiled route // contains everything needed to answer locally without opening a backend connection. assert!(selected.response.is_some()); } #[test] fn local_location_response_still_respects_route_acl() { let mut config = demo_config("127.0.0.1:9000".parse().unwrap()); let mut route = local_route(); route.acl = Some(Acl { default: Action::Deny, rules: vec![AclRule { network: "10.0.0.0/8".into(), action: Action::Allow, }], }); config.vhosts[0].routes = vec![route]; let snapshot = compile(config, 1, &policy()).unwrap(); let selected = snapshot.select("localhost", "/status").unwrap(); let acl = selected.acl.as_ref().expect("route ACL must be compiled"); assert!(acl.allows("10.1.2.3".parse().unwrap())); assert!(!acl.allows("192.0.2.1".parse().unwrap())); assert!(selected.response.is_some()); }