1+ use crate :: error:: TrustedServerError ;
2+ use crate :: settings:: Settings ;
3+ use fastly:: http:: { header, Method } ;
4+ use fastly:: { Request , Response } ;
5+ use log;
6+
7+ /// Handles Didomi CMP reverse proxy requests
8+ ///
9+ /// This module implements the reverse proxy functionality for Didomi CMP
10+ /// according to their self-hosting documentation:
11+ /// https://developers.didomi.io/api-and-platform/domains/self-hosting
12+ pub struct DidomiProxy ;
13+
14+ impl DidomiProxy {
15+ /// Handle requests to /consent/* paths
16+ ///
17+ /// Routes requests to either SDK or API origins based on path:
18+ /// - /consent/api/* → api.privacy-center.org
19+ /// - /consent/* → sdk.privacy-center.org
20+ pub async fn handle_consent_request (
21+ _settings : & Settings ,
22+ req : Request ,
23+ ) -> Result < Response , error_stack:: Report < TrustedServerError > > {
24+ let path = req. get_path ( ) ;
25+
26+ log:: info!( "Didomi proxy handling request: {}" , path) ;
27+ // Force redeploy to fix intermittent issue
28+
29+ log:: info!( "DEBUG: Starting path extraction" ) ;
30+
31+ // Extract the consent path (remove /consent prefix)
32+ let consent_path = path. strip_prefix ( "/consent" ) . unwrap_or ( path) ;
33+
34+ log:: info!( "DEBUG: consent_path = {}" , consent_path) ;
35+
36+ // Determine which origin to use
37+ let ( backend_name, origin_path) = if consent_path. starts_with ( "/api/" ) {
38+ // API calls go to api.privacy-center.org with no caching
39+ ( "didomi_api" , consent_path)
40+ } else {
41+ // SDK files go to sdk.privacy-center.org with geo-based caching
42+ ( "didomi_sdk" , consent_path)
43+ } ;
44+
45+ log:: info!( "DEBUG: backend_name = {}, origin_path = {}" , backend_name, origin_path) ;
46+
47+ log:: info!( "Routing to backend: {} with path: {}" , backend_name, origin_path) ;
48+
49+ log:: info!( "DEBUG: About to create proxy request" ) ;
50+
51+ // Create the full URL for the request
52+ let backend_host = match backend_name {
53+ "didomi_sdk" => "sdk.privacy-center.org" ,
54+ "didomi_api" => "api.privacy-center.org" ,
55+ _ => return Ok ( Response :: from_status ( fastly:: http:: StatusCode :: INTERNAL_SERVER_ERROR )
56+ . with_header ( header:: CONTENT_TYPE , "text/plain" )
57+ . with_body ( "Unknown backend" ) ) ,
58+ } ;
59+
60+ let full_url = format ! ( "https://{}{}" , backend_host, origin_path) ;
61+ log:: info!( "Full URL constructed: {}" , full_url) ;
62+
63+ // Create the proxy request using Request::new like prebid module
64+ let mut proxy_req = Request :: new ( req. get_method ( ) . clone ( ) , full_url) ;
65+
66+ log:: info!( "Created proxy request with method: {:?}" , req. get_method( ) ) ;
67+
68+ // Copy query string
69+ if let Some ( query) = req. get_query_str ( ) {
70+ proxy_req. set_query_str ( query) ;
71+ }
72+
73+ // Set required headers according to Didomi documentation
74+ Self :: set_proxy_headers ( & mut proxy_req, & req, backend_name) ;
75+
76+ // Send the request
77+ log:: info!( "Sending request to backend: {} with path: {}" , backend_name, origin_path) ;
78+
79+ // Copy request body for POST/PUT requests
80+ if matches ! ( req. get_method( ) , & Method :: POST | & Method :: PUT ) {
81+ proxy_req. set_body ( req. into_body ( ) ) ;
82+ }
83+
84+ match proxy_req. send ( backend_name) {
85+ Ok ( mut response) => {
86+ log:: info!( "Received response from {}: {}" , backend_name, response. get_status( ) ) ;
87+
88+ // Process the response according to Didomi requirements
89+ Self :: process_response ( & mut response, backend_name) ;
90+
91+ Ok ( response)
92+ }
93+ Err ( e) => {
94+ log:: error!( "Error proxying request to {}: {:?}" , backend_name, e) ;
95+ Err ( error_stack:: Report :: new ( TrustedServerError :: FastlyError {
96+ message : format ! ( "Proxy error to {}: {}" , backend_name, e)
97+ } ) )
98+ }
99+ }
100+ }
101+
102+ /// Set proxy headers according to Didomi documentation
103+ fn set_proxy_headers (
104+ proxy_req : & mut Request ,
105+ original_req : & Request ,
106+ backend_name : & str ,
107+ ) {
108+ // Host header is automatically set when using full URLs
109+
110+ // Forward user IP in X-Forwarded-For header
111+ if let Some ( client_ip) = original_req. get_client_ip_addr ( ) {
112+ proxy_req. set_header ( "X-Forwarded-For" , client_ip. to_string ( ) ) ;
113+ }
114+
115+ // Forward geographic information for SDK requests (for geo-based caching)
116+ if backend_name == "didomi_sdk" {
117+ // Copy geographic headers from Fastly
118+ let geo_headers = [
119+ ( "X-Geo-Country" , "FastlyGeo-CountryCode" ) ,
120+ ( "X-Geo-Region" , "FastlyGeo-Region" ) ,
121+ ( "CloudFront-Viewer-Country" , "FastlyGeo-CountryCode" ) ,
122+ ] ;
123+
124+ for ( header_name, fastly_header) in geo_headers {
125+ if let Some ( value) = original_req. get_header ( fastly_header) {
126+ proxy_req. set_header ( header_name, value) ;
127+ }
128+ }
129+ }
130+
131+ // Forward essential headers
132+ let headers_to_forward = [
133+ header:: ACCEPT ,
134+ header:: ACCEPT_LANGUAGE ,
135+ header:: ACCEPT_ENCODING ,
136+ header:: USER_AGENT ,
137+ header:: REFERER ,
138+ header:: ORIGIN ,
139+ header:: AUTHORIZATION ,
140+ ] ;
141+
142+ for header_name in headers_to_forward {
143+ if let Some ( value) = original_req. get_header ( & header_name) {
144+ proxy_req. set_header ( & header_name, value) ;
145+ }
146+ }
147+
148+ // DO NOT forward cookies (as per Didomi documentation)
149+ // proxy_req.remove_header(header::COOKIE);
150+
151+ // Set content type for POST/PUT requests
152+ if matches ! ( original_req. get_method( ) , & Method :: POST | & Method :: PUT ) {
153+ if let Some ( content_type) = original_req. get_header ( header:: CONTENT_TYPE ) {
154+ proxy_req. set_header ( header:: CONTENT_TYPE , content_type) ;
155+ }
156+ }
157+
158+ log:: info!( "Proxy headers set for {}" , backend_name) ;
159+ }
160+
161+ /// Process response according to Didomi requirements
162+ fn process_response ( response : & mut Response , backend_name : & str ) {
163+ // Add CORS headers for SDK requests
164+ if backend_name == "didomi_sdk" {
165+ response. set_header ( header:: ACCESS_CONTROL_ALLOW_ORIGIN , "*" ) ;
166+ response. set_header (
167+ header:: ACCESS_CONTROL_ALLOW_HEADERS ,
168+ "Content-Type, Authorization, X-Requested-With" ,
169+ ) ;
170+ response. set_header (
171+ header:: ACCESS_CONTROL_ALLOW_METHODS ,
172+ "GET, POST, PUT, DELETE, OPTIONS" ,
173+ ) ;
174+ }
175+
176+ // Log cache headers for debugging
177+ if let Some ( cache_control) = response. get_header ( header:: CACHE_CONTROL ) {
178+ log:: info!( "Cache-Control from {}: {:?}" , backend_name, cache_control) ;
179+ }
180+
181+ // Ensure cache headers are preserved (they will be returned to the client)
182+ // This is important for Didomi's caching requirements
183+
184+ log:: info!( "Response processed for {}" , backend_name) ;
185+ }
186+ }
187+
188+ #[ cfg( test) ]
189+ mod tests {
190+ use super :: * ;
191+
192+ #[ test]
193+ fn test_consent_path_extraction ( ) {
194+ let path = "/consent/api/events" ;
195+ let consent_path = path. strip_prefix ( "/consent" ) . unwrap_or ( path) ;
196+ assert_eq ! ( consent_path, "/api/events" ) ;
197+
198+ let path = "/consent/24cd3901-9da4-4643-96a3-9b1c573b5264/loader.js" ;
199+ let consent_path = path. strip_prefix ( "/consent" ) . unwrap_or ( path) ;
200+ assert_eq ! ( consent_path, "/24cd3901-9da4-4643-96a3-9b1c573b5264/loader.js" ) ;
201+ }
202+
203+ #[ test]
204+ fn test_backend_selection ( ) {
205+ // API requests
206+ let api_path = "/api/events" ;
207+ assert ! ( api_path. starts_with( "/api/" ) ) ;
208+
209+ // SDK requests
210+ let sdk_path = "/24cd3901-9da4-4643-96a3-9b1c573b5264/loader.js" ;
211+ assert ! ( !sdk_path. starts_with( "/api/" ) ) ;
212+
213+ let sdk_path2 = "/sdk/version/core.js" ;
214+ assert ! ( !sdk_path2. starts_with( "/api/" ) ) ;
215+ }
216+ }
0 commit comments