-
Notifications
You must be signed in to change notification settings - Fork 3
feat: implement Didomi CMP reverse proxy #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,235 @@ | ||
use crate::error::TrustedServerError; | ||
use crate::settings::Settings; | ||
use fastly::http::{header, Method}; | ||
use fastly::{Request, Response}; | ||
use log; | ||
|
||
/// Handles Didomi CMP reverse proxy requests | ||
/// | ||
/// This module implements the reverse proxy functionality for Didomi CMP | ||
/// according to their self-hosting documentation: | ||
/// https://developers.didomi.io/api-and-platform/domains/self-hosting | ||
pub struct DidomiProxy; | ||
|
||
impl DidomiProxy { | ||
/// Handle requests to /consent/* paths | ||
/// | ||
/// Routes requests to either SDK or API origins based on path: | ||
/// - /consent/api/* → api.privacy-center.org | ||
/// - /consent/* → sdk.privacy-center.org | ||
pub async fn handle_consent_request( | ||
_settings: &Settings, | ||
req: Request, | ||
) -> Result<Response, error_stack::Report<TrustedServerError>> { | ||
let path = req.get_path(); | ||
|
||
log::info!("Didomi proxy handling request: {}", path); | ||
// Force redeploy to fix intermittent issue | ||
|
||
log::info!("DEBUG: Starting path extraction"); | ||
|
||
// Extract the consent path (remove /consent prefix) | ||
let consent_path = path.strip_prefix("/consent").unwrap_or(path); | ||
|
||
log::info!("DEBUG: consent_path = {}", consent_path); | ||
|
||
// Determine which origin to use | ||
let (backend_name, origin_path) = if consent_path.starts_with("/api/") { | ||
// API calls go to api.privacy-center.org with no caching | ||
("didomi_api", consent_path) | ||
} else { | ||
// SDK files go to sdk.privacy-center.org with geo-based caching | ||
("didomi_sdk", consent_path) | ||
}; | ||
|
||
log::info!( | ||
"DEBUG: backend_name = {}, origin_path = {}", | ||
backend_name, | ||
origin_path | ||
); | ||
|
||
log::info!( | ||
"Routing to backend: {} with path: {}", | ||
backend_name, | ||
origin_path | ||
); | ||
|
||
log::info!("DEBUG: About to create proxy request"); | ||
|
||
// Create the full URL for the request | ||
let backend_host = match backend_name { | ||
"didomi_sdk" => "sdk.privacy-center.org", | ||
"didomi_api" => "api.privacy-center.org", | ||
_ => { | ||
return Ok( | ||
Response::from_status(fastly::http::StatusCode::INTERNAL_SERVER_ERROR) | ||
.with_header(header::CONTENT_TYPE, "text/plain") | ||
.with_body("Unknown backend"), | ||
) | ||
} | ||
}; | ||
|
||
let full_url = format!("https://{}{}", backend_host, origin_path); | ||
log::info!("Full URL constructed: {}", full_url); | ||
|
||
// Create the proxy request using Request::new like prebid module | ||
let mut proxy_req = Request::new(req.get_method().clone(), full_url); | ||
|
||
log::info!("Created proxy request with method: {:?}", req.get_method()); | ||
|
||
// Copy query string | ||
if let Some(query) = req.get_query_str() { | ||
proxy_req.set_query_str(query); | ||
} | ||
|
||
// Set required headers according to Didomi documentation | ||
Self::set_proxy_headers(&mut proxy_req, &req, backend_name); | ||
|
||
// Send the request | ||
log::info!( | ||
"Sending request to backend: {} with path: {}", | ||
backend_name, | ||
origin_path | ||
); | ||
|
||
// Copy request body for POST/PUT requests | ||
if matches!(req.get_method(), &Method::POST | &Method::PUT) { | ||
proxy_req.set_body(req.into_body()); | ||
} | ||
|
||
match proxy_req.send(backend_name) { | ||
Ok(mut response) => { | ||
log::info!( | ||
"Received response from {}: {}", | ||
backend_name, | ||
response.get_status() | ||
); | ||
|
||
// Process the response according to Didomi requirements | ||
Self::process_response(&mut response, backend_name); | ||
|
||
Ok(response) | ||
} | ||
Err(e) => { | ||
log::error!("Error proxying request to {}: {:?}", backend_name, e); | ||
Err(error_stack::Report::new(TrustedServerError::FastlyError { | ||
message: format!("Proxy error to {}: {}", backend_name, e), | ||
})) | ||
} | ||
} | ||
} | ||
|
||
/// Set proxy headers according to Didomi documentation | ||
fn set_proxy_headers(proxy_req: &mut Request, original_req: &Request, backend_name: &str) { | ||
// Host header is automatically set when using full URLs | ||
|
||
// Forward user IP in X-Forwarded-For header | ||
if let Some(client_ip) = original_req.get_client_ip_addr() { | ||
proxy_req.set_header("X-Forwarded-For", client_ip.to_string()); | ||
} | ||
|
||
// Forward geographic information for SDK requests (for geo-based caching) | ||
if backend_name == "didomi_sdk" { | ||
// Copy geographic headers from Fastly | ||
let geo_headers = [ | ||
("X-Geo-Country", "FastlyGeo-CountryCode"), | ||
("X-Geo-Region", "FastlyGeo-Region"), | ||
("CloudFront-Viewer-Country", "FastlyGeo-CountryCode"), | ||
]; | ||
|
||
for (header_name, fastly_header) in geo_headers { | ||
if let Some(value) = original_req.get_header(fastly_header) { | ||
proxy_req.set_header(header_name, value); | ||
} | ||
} | ||
} | ||
|
||
// Forward essential headers | ||
let headers_to_forward = [ | ||
header::ACCEPT, | ||
header::ACCEPT_LANGUAGE, | ||
header::ACCEPT_ENCODING, | ||
header::USER_AGENT, | ||
header::REFERER, | ||
header::ORIGIN, | ||
header::AUTHORIZATION, | ||
]; | ||
|
||
for header_name in headers_to_forward { | ||
if let Some(value) = original_req.get_header(&header_name) { | ||
proxy_req.set_header(&header_name, value); | ||
} | ||
} | ||
|
||
// DO NOT forward cookies (as per Didomi documentation) | ||
// proxy_req.remove_header(header::COOKIE); | ||
|
||
// Set content type for POST/PUT requests | ||
if matches!(original_req.get_method(), &Method::POST | &Method::PUT) { | ||
if let Some(content_type) = original_req.get_header(header::CONTENT_TYPE) { | ||
proxy_req.set_header(header::CONTENT_TYPE, content_type); | ||
} | ||
} | ||
|
||
log::info!("Proxy headers set for {}", backend_name); | ||
} | ||
|
||
/// Process response according to Didomi requirements | ||
fn process_response(response: &mut Response, backend_name: &str) { | ||
// Add CORS headers for SDK requests | ||
if backend_name == "didomi_sdk" { | ||
response.set_header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*"); | ||
response.set_header( | ||
header::ACCESS_CONTROL_ALLOW_HEADERS, | ||
"Content-Type, Authorization, X-Requested-With", | ||
); | ||
response.set_header( | ||
header::ACCESS_CONTROL_ALLOW_METHODS, | ||
"GET, POST, PUT, DELETE, OPTIONS", | ||
); | ||
} | ||
|
||
// Log cache headers for debugging | ||
if let Some(cache_control) = response.get_header(header::CACHE_CONTROL) { | ||
log::info!("Cache-Control from {}: {:?}", backend_name, cache_control); | ||
} | ||
|
||
// Ensure cache headers are preserved (they will be returned to the client) | ||
// This is important for Didomi's caching requirements | ||
|
||
log::info!("Response processed for {}", backend_name); | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn test_consent_path_extraction() { | ||
let path = "/consent/api/events"; | ||
let consent_path = path.strip_prefix("/consent").unwrap_or(path); | ||
assert_eq!(consent_path, "/api/events"); | ||
|
||
let path = "/consent/24cd3901-9da4-4643-96a3-9b1c573b5264/loader.js"; | ||
let consent_path = path.strip_prefix("/consent").unwrap_or(path); | ||
assert_eq!( | ||
consent_path, | ||
"/24cd3901-9da4-4643-96a3-9b1c573b5264/loader.js" | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_backend_selection() { | ||
// API requests | ||
let api_path = "/api/events"; | ||
assert!(api_path.starts_with("/api/")); | ||
|
||
// SDK requests | ||
let sdk_path = "/24cd3901-9da4-4643-96a3-9b1c573b5264/loader.js"; | ||
assert!(!sdk_path.starts_with("/api/")); | ||
|
||
let sdk_path2 = "/sdk/version/core.js"; | ||
assert!(!sdk_path2.starts_with("/api/")); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -249,6 +249,7 @@ pub const HTML_TEMPLATE: &str = r#"<!DOCTYPE html> | |
}); | ||
}); | ||
</script> | ||
<script type="text/javascript">(function(){function i(e){if(!window.frames[e]){if(document.body&&document.body.firstChild){var t=document.body;var n=document.createElement("iframe");n.style.display="none";n.name=e;n.title=e;t.insertBefore(n,t.firstChild)}else{setTimeout(function(){i(e)},5)}}}function e(n,o,r,f,s){function e(e,t,n,i){if(typeof n!=="function"){return}if(!window[o]){window[o]=[]}var a=false;if(s){a=s(e,i,n)}if(!a){window[o].push({command:e,version:t,callback:n,parameter:i})}}e.stub=true;e.stubVersion=2;function t(i){if(!window[n]||window[n].stub!==true){return}if(!i.data){return}var a=typeof i.data==="string";var e;try{e=a?JSON.parse(i.data):i.data}catch(t){return}if(e[r]){var o=e[r];window[n](o.command,o.version,function(e,t){var n={};n[f]={returnValue:e,success:t,callId:o.callId};if(i.source){i.source.postMessage(a?JSON.stringify(n):n,"*")}},o.parameter)}}if(typeof window[n]!=="function"){window[n]=e;if(window.addEventListener){window.addEventListener("message",t,false)}else{window.attachEvent("onmessage",t)}}}e("__tcfapi","__tcfapiBuffer","__tcfapiCall","__tcfapiReturn");i("__tcfapiLocator")})();</script><script type="text/javascript">(function(){window.__gpp_addFrame=function(e){if(!window.frames[e]){if(document.body){var t=document.createElement("iframe");t.style.cssText="display:none";t.name=e;document.body.appendChild(t)}else{window.setTimeout(window.__gpp_addFrame,10,e)}}};window.__gpp_stub=function(){var e=arguments;__gpp.queue=__gpp.queue||[];__gpp.events=__gpp.events||[];if(!e.length||e.length==1&&e[0]=="queue"){return __gpp.queue}if(e.length==1&&e[0]=="events"){return __gpp.events}var t=e[0];var p=e.length>1?e[1]:null;var s=e.length>2?e[2]:null;if(t==="ping"){p({gppVersion:"1.1",cmpStatus:"stub",cmpDisplayStatus:"hidden",signalStatus:"not ready",supportedAPIs:["2:tcfeuv2","5:tcfcav1","6:uspv1","7:usnat","8:usca","9:usva","10:usco","11:usut","12:usct"],cmpId:"7",sectionList:[],applicableSections:[],gppString:"",parsedSections:{}},true)}else if(t==="addEventListener"){if(!("lastId"in __gpp)){__gpp.lastId=0}__gpp.lastId++;var n=__gpp.lastId;__gpp.events.push({id:n,callback:p,parameter:s});p({eventName:"listenerRegistered",listenerId:n,data:true,pingData:{gppVersion:"1.1",cmpStatus:"stub",cmpDisplayStatus:"hidden",signalStatus:"not ready",supportedAPIs:["2:tcfeuv2","5:tcfcav1","6:uspv1","7:usnat","8:usca","9:usva","10:usco","11:usut","12:usct"],cmpId:"7",sectionList:[],applicableSections:[],gppString:"",parsedSections:{}}},true)}else if(t==="removeEventListener"){var a=false;for(var i=0;i<__gpp.events.length;i++){if(__gpp.events[i].id==s){__gpp.events.splice(i,1);a=true;break}}p({eventName:"listenerRemoved",listenerId:s,data:a,pingData:{gppVersion:"1.1",cmpStatus:"stub",cmpDisplayStatus:"hidden",signalStatus:"not ready",supportedAPIs:["2:tcfeuv2","5:tcfcav1","6:uspv1","7:usnat","8:usca","9:usva","10:usco","11:usut","12:usct"],cmpId:"7",sectionList:[],applicableSections:[],gppString:"",parsedSections:{}}},true)}else if(t==="hasSection"){p(false,true)}else if(t==="getSection"||t==="getField"){p(null,true)}else{__gpp.queue.push([].slice.apply(e))}};window.__gpp_msghandler=function(s){var n=typeof s.data==="string";try{var e=n?JSON.parse(s.data):s.data}catch(t){var e=null}if(typeof e==="object"&&e!==null&&"__gppCall"in e){var a=e.__gppCall;window.__gpp(a.command,function(e,t){var p={__gppReturn:{returnValue:e,success:t,callId:a.callId}};s.source.postMessage(n?JSON.stringify(p):p,"*")},"parameter"in a?a.parameter:null,"version"in a?a.version:"1.1")}};if(!("__gpp"in window)||typeof window.__gpp!=="function"){window.__gpp=window.__gpp_stub;window.addEventListener("message",window.__gpp_msghandler,false);window.__gpp_addFrame("__gppLocator")}})();</script><script type="text/javascript">(function(){(function(e,i,o){var n=document.createElement("link");n.rel="preconnect";n.as="script";var t=document.createElement("link");t.rel="dns-prefetch";t.as="script";var r=document.createElement("script");r.id="spcloader";r.type="text/javascript";r["async"]=true;r.charset="utf-8";window.didomiConfig=window.didomiConfig||{};window.didomiConfig.sdkPath=window.didomiConfig.sdkPath||o||"https://sdk.privacy-center.org/";const d=window.didomiConfig.sdkPath;var a=d+e+"/loader.js?target_type=notice&target="+i;if(window.didomiConfig&&window.didomiConfig.user){var c=window.didomiConfig.user;var s=c.country;var f=c.region;if(s){a=a+"&country="+s;if(f){a=a+"®ion="+f}}}n.href=d;t.href=d;r.src=a;var m=document.getElementsByTagName("script")[0];m.parentNode.insertBefore(n,m);m.parentNode.insertBefore(t,m);m.parentNode.insertBefore(r,m)})("24cd3901-9da4-4643-96a3-9b1c573b5264","J3nR2TTU","http://127.0.0.1:7676/consent/")})();</script> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 Is it possible for Didomi to host the content of this file? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not really as this is the snippet code Didomi customers must add to their frontend to actually load our SDK. A setting as recommended below sounds a good idea. The setting could enable further options if necessary / in the future. |
||
</head> | ||
<body> | ||
<!-- GDPR Consent Banner --> | ||
|
@@ -1113,6 +1114,7 @@ pub const GAM_TEST_TEMPLATE: &str = r#" | |
color: #856404; | ||
} | ||
</style> | ||
<script type="text/javascript">(function(){function i(e){if(!window.frames[e]){if(document.body&&document.body.firstChild){var t=document.body;var n=document.createElement("iframe");n.style.display="none";n.name=e;n.title=e;t.insertBefore(n,t.firstChild)}else{setTimeout(function(){i(e)},5)}}}function e(n,o,r,f,s){function e(e,t,n,i){if(typeof n!=="function"){return}if(!window[o]){window[o]=[]}var a=false;if(s){a=s(e,i,n)}if(!a){window[o].push({command:e,version:t,callback:n,parameter:i})}}e.stub=true;e.stubVersion=2;function t(i){if(!window[n]||window[n].stub!==true){return}if(!i.data){return}var a=typeof i.data==="string";var e;try{e=a?JSON.parse(i.data):i.data}catch(t){return}if(e[r]){var o=e[r];window[n](o.command,o.version,function(e,t){var n={};n[f]={returnValue:e,success:t,callId:o.callId};if(i.source){i.source.postMessage(a?JSON.stringify(n):n,"*")}},o.parameter)}}if(typeof window[n]!=="function"){window[n]=e;if(window.addEventListener){window.addEventListener("message",t,false)}else{window.attachEvent("onmessage",t)}}}e("__tcfapi","__tcfapiBuffer","__tcfapiCall","__tcfapiReturn");i("__tcfapiLocator")})();</script><script type="text/javascript">(function(){window.__gpp_addFrame=function(e){if(!window.frames[e]){if(document.body){var t=document.createElement("iframe");t.style.cssText="display:none";t.name=e;document.body.appendChild(t)}else{window.setTimeout(window.__gpp_addFrame,10,e)}}};window.__gpp_stub=function(){var e=arguments;__gpp.queue=__gpp.queue||[];__gpp.events=__gpp.events||[];if(!e.length||e.length==1&&e[0]=="queue"){return __gpp.queue}if(e.length==1&&e[0]=="events"){return __gpp.events}var t=e[0];var p=e.length>1?e[1]:null;var s=e.length>2?e[2]:null;if(t==="ping"){p({gppVersion:"1.1",cmpStatus:"stub",cmpDisplayStatus:"hidden",signalStatus:"not ready",supportedAPIs:["2:tcfeuv2","5:tcfcav1","6:uspv1","7:usnat","8:usca","9:usva","10:usco","11:usut","12:usct"],cmpId:"7",sectionList:[],applicableSections:[],gppString:"",parsedSections:{}},true)}else if(t==="addEventListener"){if(!("lastId"in __gpp)){__gpp.lastId=0}__gpp.lastId++;var n=__gpp.lastId;__gpp.events.push({id:n,callback:p,parameter:s});p({eventName:"listenerRegistered",listenerId:n,data:true,pingData:{gppVersion:"1.1",cmpStatus:"stub",cmpDisplayStatus:"hidden",signalStatus:"not ready",supportedAPIs:["2:tcfeuv2","5:tcfcav1","6:uspv1","7:usnat","8:usca","9:usva","10:usco","11:usut","12:usct"],cmpId:"7",sectionList:[],applicableSections:[],gppString:"",parsedSections:{}}},true)}else if(t==="removeEventListener"){var a=false;for(var i=0;i<__gpp.events.length;i++){if(__gpp.events[i].id==s){__gpp.events.splice(i,1);a=true;break}}p({eventName:"listenerRemoved",listenerId:s,data:a,pingData:{gppVersion:"1.1",cmpStatus:"stub",cmpDisplayStatus:"hidden",signalStatus:"not ready",supportedAPIs:["2:tcfeuv2","5:tcfcav1","6:uspv1","7:usnat","8:usca","9:usva","10:usco","11:usut","12:usct"],cmpId:"7",sectionList:[],applicableSections:[],gppString:"",parsedSections:{}}},true)}else if(t==="hasSection"){p(false,true)}else if(t==="getSection"||t==="getField"){p(null,true)}else{__gpp.queue.push([].slice.apply(e))}};window.__gpp_msghandler=function(s){var n=typeof s.data==="string";try{var e=n?JSON.parse(s.data):s.data}catch(t){var e=null}if(typeof e==="object"&&e!==null&&"__gppCall"in e){var a=e.__gppCall;window.__gpp(a.command,function(e,t){var p={__gppReturn:{returnValue:e,success:t,callId:a.callId}};s.source.postMessage(n?JSON.stringify(p):p,"*")},"parameter"in a?a.parameter:null,"version"in a?a.version:"1.1")}};if(!("__gpp"in window)||typeof window.__gpp!=="function"){window.__gpp=window.__gpp_stub;window.addEventListener("message",window.__gpp_msghandler,false);window.__gpp_addFrame("__gppLocator")}})();</script><script type="text/javascript">(function(){(function(e,i,o){var n=document.createElement("link");n.rel="preconnect";n.as="script";var t=document.createElement("link");t.rel="dns-prefetch";t.as="script";var r=document.createElement("script");r.id="spcloader";r.type="text/javascript";r["async"]=true;r.charset="utf-8";window.didomiConfig=window.didomiConfig||{};window.didomiConfig.sdkPath=window.didomiConfig.sdkPath||o||"https://sdk.privacy-center.org/";const d=window.didomiConfig.sdkPath;var a=d+e+"/loader.js?target_type=notice&target="+i;if(window.didomiConfig&&window.didomiConfig.user){var c=window.didomiConfig.user;var s=c.country;var f=c.region;if(s){a=a+"&country="+s;if(f){a=a+"®ion="+f}}}n.href=d;t.href=d;r.src=a;var m=document.getElementsByTagName("script")[0];m.parentNode.insertBefore(n,m);m.parentNode.insertBefore(t,m);m.parentNode.insertBefore(r,m)})("24cd3901-9da4-4643-96a3-9b1c573b5264","J3nR2TTU","http://127.0.0.1:7676/consent/")})();</script> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❓ Should this script be included only if Didomi is used by publisher? I would recommend we have a setting to enable Didomi. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The script must be included only if Didomi is used by the publisher, indeed. What would be your recommendation in regard of creating such setting? Note that the script is differs from one publisher to another and must be added by the publisher himself. Any help on how to create such setting would be appreciated as this will define the way those settings are set in the project. |
||
</head> | ||
<body> | ||
<div class="container"> | ||
|
Uh oh!
There was an error while loading. Please reload this page.