-
Notifications
You must be signed in to change notification settings - Fork 8
Add didomi integration #131
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
Merged
Merged
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
2481099
Proposal to standardize integrations
aram356 8da8a3c
Fixed formatting
aram356 ab9ca6a
Improviing integration so that we can move prebid and nextjs as integ…
aram356 f38a7a3
Use integration standards for Prebid
aram356 063f1ca
Fixed clippy
aram356 44fa1ba
Merge branch 'feature/standardize-integrations' into feature/refactor…
aram356 42b48a8
Improve routing
aram356 3c69966
Merge branch 'feature/standardize-integrations' into feature/refactor…
aram356 e19be32
Removed unused file
aram356 f1247dd
Unified JS bundling
ChristianPavilonis 787c642
Fixed formatting
aram356 4b9aaab
Added a way to remove scripts
aram356 d1deffb
Merge branch 'main' into feature/refactor-prebid-integration
aram356 97d0d1c
Merge branch 'feature/standardize-integrations' into feature/refactor…
aram356 20ddc20
Refactor Next.js integration
aram356 0f3663d
Merge branch 'main' into feature/refactor-prebid-integration
aram356 c9f9ab6
Merge branch 'feature/refactor-prebid-integration' into feature/refac…
aram356 2d40b79
Merge branch 'feature/refactor-nextjs-integration' into feature/integ…
aram356 ef78d75
Allow integration to proxy requests
aram356 012fb4c
Merge branch 'main' into feature/integration-proxy
aram356 5f78374
Add Didomi integration
aram356 4efe60b
Merge branch 'main' into feature/didomi-integration
aram356 4c2a864
Override config for Didomi integration
aram356 1f80c4e
Reuse backends correctly
aram356 1147156
Set additional parameters for SSL backends
aram356 74547e2
Merge branch 'main' into feature/didomi-integration
aram356 d711e13
Fixed typescript integration routes for Didomi integration.
aram356 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,280 @@ | ||
| use std::sync::Arc; | ||
|
|
||
| use async_trait::async_trait; | ||
| use error_stack::{Report, ResultExt}; | ||
| use fastly::http::{header, Method}; | ||
| use fastly::{Request, Response}; | ||
| use serde::{Deserialize, Serialize}; | ||
| use url::Url; | ||
| use validator::Validate; | ||
|
|
||
| use crate::backend::ensure_backend_from_url; | ||
| use crate::error::TrustedServerError; | ||
| use crate::integrations::{IntegrationEndpoint, IntegrationProxy, IntegrationRegistration}; | ||
| use crate::settings::{IntegrationConfig, Settings}; | ||
|
|
||
| const DIDOMI_INTEGRATION_ID: &str = "didomi"; | ||
| const DIDOMI_PREFIX: &str = "/integrations/didomi/consent"; | ||
|
|
||
| /// Configuration for the Didomi consent notice reverse proxy. | ||
| #[derive(Debug, Clone, Deserialize, Serialize, Validate)] | ||
| pub struct DidomiIntegrationConfig { | ||
| /// Whether the integration is enabled. | ||
| #[serde(default = "default_enabled")] | ||
| pub enabled: bool, | ||
| /// Base URL for the Didomi SDK origin. | ||
| #[serde(default = "default_sdk_origin")] | ||
| #[validate(url)] | ||
| pub sdk_origin: String, | ||
| /// Base URL for the Didomi API origin. | ||
| #[serde(default = "default_api_origin")] | ||
| #[validate(url)] | ||
| pub api_origin: String, | ||
| } | ||
|
|
||
| impl IntegrationConfig for DidomiIntegrationConfig { | ||
| fn is_enabled(&self) -> bool { | ||
| self.enabled | ||
| } | ||
| } | ||
|
|
||
| fn default_enabled() -> bool { | ||
| true | ||
| } | ||
|
|
||
| fn default_sdk_origin() -> String { | ||
| "https://sdk.privacy-center.org".to_string() | ||
| } | ||
|
|
||
| fn default_api_origin() -> String { | ||
| "https://api.privacy-center.org".to_string() | ||
| } | ||
|
|
||
| enum DidomiBackend { | ||
| Sdk, | ||
| Api, | ||
| } | ||
|
|
||
| struct DidomiIntegration { | ||
| config: Arc<DidomiIntegrationConfig>, | ||
| } | ||
|
|
||
| impl DidomiIntegration { | ||
| fn new(config: Arc<DidomiIntegrationConfig>) -> Arc<Self> { | ||
| Arc::new(Self { config }) | ||
| } | ||
|
|
||
| fn error(message: impl Into<String>) -> TrustedServerError { | ||
| TrustedServerError::Integration { | ||
| integration: DIDOMI_INTEGRATION_ID.to_string(), | ||
| message: message.into(), | ||
| } | ||
| } | ||
|
|
||
| fn backend_for_path(&self, consent_path: &str) -> DidomiBackend { | ||
| if consent_path.starts_with("/api/") { | ||
| DidomiBackend::Api | ||
| } else { | ||
| DidomiBackend::Sdk | ||
| } | ||
| } | ||
|
|
||
| fn build_target_url( | ||
| &self, | ||
| base: &str, | ||
| consent_path: &str, | ||
| query: Option<&str>, | ||
| ) -> Result<String, Report<TrustedServerError>> { | ||
| let mut target = | ||
| Url::parse(base).change_context(Self::error("Invalid Didomi origin URL"))?; | ||
| let path = if consent_path.is_empty() { | ||
| "/" | ||
| } else { | ||
| consent_path | ||
| }; | ||
| target.set_path(path); | ||
| target.set_query(query); | ||
| Ok(target.to_string()) | ||
| } | ||
|
|
||
| fn copy_headers( | ||
| &self, | ||
| backend: &DidomiBackend, | ||
| original_req: &Request, | ||
| proxy_req: &mut Request, | ||
| ) { | ||
| if let Some(client_ip) = original_req.get_client_ip_addr() { | ||
| proxy_req.set_header("X-Forwarded-For", client_ip.to_string()); | ||
| } | ||
|
|
||
| for header_name in [ | ||
| header::ACCEPT, | ||
| header::ACCEPT_LANGUAGE, | ||
| header::ACCEPT_ENCODING, | ||
| header::USER_AGENT, | ||
| header::REFERER, | ||
| header::ORIGIN, | ||
| header::AUTHORIZATION, | ||
| ] { | ||
| if let Some(value) = original_req.get_header(&header_name) { | ||
| proxy_req.set_header(&header_name, value); | ||
| } | ||
| } | ||
|
|
||
| if matches!(backend, DidomiBackend::Sdk) { | ||
| Self::copy_geo_headers(original_req, proxy_req); | ||
| } | ||
| } | ||
|
|
||
| fn copy_geo_headers(original_req: &Request, proxy_req: &mut Request) { | ||
| let geo_headers = [ | ||
| ("X-Geo-Country", "FastlyGeo-CountryCode"), | ||
| ("X-Geo-Region", "FastlyGeo-Region"), | ||
| ("CloudFront-Viewer-Country", "FastlyGeo-CountryCode"), | ||
| ]; | ||
|
|
||
| for (target, source) in geo_headers { | ||
| if let Some(value) = original_req.get_header(source) { | ||
| proxy_req.set_header(target, value); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn add_cors_headers(response: &mut Response) { | ||
| 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", | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| fn build(settings: &Settings) -> Option<Arc<DidomiIntegration>> { | ||
| let config = match settings.integration_config::<DidomiIntegrationConfig>(DIDOMI_INTEGRATION_ID) | ||
| { | ||
| Ok(Some(config)) => Arc::new(config), | ||
| Ok(None) => return None, | ||
| Err(err) => { | ||
| log::error!("Failed to load Didomi integration config: {err:?}"); | ||
| return None; | ||
| } | ||
| }; | ||
| Some(DidomiIntegration::new(config)) | ||
| } | ||
|
|
||
| /// Register the Didomi consent notice integration when enabled. | ||
| pub fn register(settings: &Settings) -> Option<IntegrationRegistration> { | ||
| let integration = build(settings)?; | ||
| Some( | ||
| IntegrationRegistration::builder(DIDOMI_INTEGRATION_ID) | ||
| .with_proxy(integration) | ||
| .build(), | ||
| ) | ||
| } | ||
|
|
||
| #[async_trait(?Send)] | ||
| impl IntegrationProxy for DidomiIntegration { | ||
| fn integration_name(&self) -> &'static str { | ||
| DIDOMI_INTEGRATION_ID | ||
| } | ||
|
|
||
| fn routes(&self) -> Vec<IntegrationEndpoint> { | ||
| vec![self.get("/consent/*"), self.post("/consent/*")] | ||
| } | ||
|
|
||
| async fn handle( | ||
| &self, | ||
| _settings: &Settings, | ||
| req: Request, | ||
| ) -> Result<Response, Report<TrustedServerError>> { | ||
| let path = req.get_path(); | ||
| let consent_path = path.strip_prefix(DIDOMI_PREFIX).unwrap_or(path); | ||
| let backend = self.backend_for_path(consent_path); | ||
| let base_origin = match backend { | ||
| DidomiBackend::Sdk => self.config.sdk_origin.as_str(), | ||
| DidomiBackend::Api => self.config.api_origin.as_str(), | ||
| }; | ||
|
|
||
| let target_url = self | ||
| .build_target_url(base_origin, consent_path, req.get_query_str()) | ||
| .change_context(Self::error("Failed to build Didomi target URL"))?; | ||
| let backend_name = ensure_backend_from_url(base_origin) | ||
| .change_context(Self::error("Failed to configure Didomi backend"))?; | ||
|
|
||
| let mut proxy_req = Request::new(req.get_method().clone(), &target_url); | ||
| self.copy_headers(&backend, &req, &mut proxy_req); | ||
|
|
||
| if matches!(req.get_method(), &Method::POST | &Method::PUT) { | ||
| if let Some(content_type) = req.get_header(header::CONTENT_TYPE) { | ||
| proxy_req.set_header(header::CONTENT_TYPE, content_type); | ||
| } | ||
| proxy_req.set_body(req.into_body()); | ||
| } | ||
|
|
||
| let mut response = proxy_req | ||
| .send(&backend_name) | ||
| .change_context(Self::error("Didomi upstream request failed"))?; | ||
|
|
||
| if matches!(backend, DidomiBackend::Sdk) { | ||
| Self::add_cors_headers(&mut response); | ||
| } | ||
|
|
||
| Ok(response) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use crate::integrations::IntegrationRegistry; | ||
| use crate::test_support::tests::create_test_settings; | ||
| use fastly::http::Method; | ||
|
|
||
| fn config(enabled: bool) -> DidomiIntegrationConfig { | ||
| DidomiIntegrationConfig { | ||
| enabled, | ||
| sdk_origin: default_sdk_origin(), | ||
| api_origin: default_api_origin(), | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn selects_api_backend_for_api_paths() { | ||
| let integration = DidomiIntegration::new(Arc::new(config(true))); | ||
| assert!(matches!( | ||
| integration.backend_for_path("/api/events"), | ||
| DidomiBackend::Api | ||
| )); | ||
| assert!(matches!( | ||
| integration.backend_for_path("/24cd/loader.js"), | ||
| DidomiBackend::Sdk | ||
| )); | ||
| } | ||
|
|
||
| #[test] | ||
| fn builds_target_url_with_query() { | ||
| let integration = DidomiIntegration::new(Arc::new(config(true))); | ||
| let url = integration | ||
| .build_target_url("https://sdk.privacy-center.org", "/loader.js", Some("v=1")) | ||
| .unwrap(); | ||
| assert_eq!(url, "https://sdk.privacy-center.org/loader.js?v=1"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn registers_prefix_routes() { | ||
| let mut settings = create_test_settings(); | ||
| settings | ||
| .integrations | ||
| .insert_config(DIDOMI_INTEGRATION_ID, &config(true)) | ||
| .expect("should insert config"); | ||
|
|
||
| let registry = IntegrationRegistry::new(&settings); | ||
| assert!(registry.has_route(&Method::GET, "/integrations/didomi/consent/loader.js")); | ||
| assert!(registry.has_route(&Method::POST, "/integrations/didomi/consent/api/events")); | ||
| assert!(!registry.has_route(&Method::GET, "/other")); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.