Repository navigation
Match SDK URLs by vendor hostname and path #1405
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
21f91f4
ed91166
34a776b
a9fb1b2
4df289f
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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -15,6 +15,7 @@ use validator::Validate; | |||||
|
|
||||||
| use crate::constants::INTERNAL_HEADERS; | ||||||
| use crate::error::TrustedServerError; | ||||||
| use crate::integrations::sdk_url::parse_sdk_network_url; | ||||||
| use crate::integrations::{ | ||||||
| AttributeRewriteAction, INTEGRATION_MAX_BODY_BYTES, IntegrationAttributeContext, | ||||||
| IntegrationAttributeRewriter, IntegrationEndpoint, IntegrationProxy, IntegrationRegistration, | ||||||
|
|
@@ -99,9 +100,17 @@ impl PermutiveIntegration { | |||||
|
|
||||||
| /// Check if a URL is a Permutive SDK URL. | ||||||
| fn is_permutive_sdk_url(&self, url: &str) -> bool { | ||||||
| let lower = url.to_ascii_lowercase(); | ||||||
| (lower.contains(".edge.permutive.app") || lower.contains("cdn.permutive.com")) | ||||||
| && lower.ends_with("-web.js") | ||||||
| // Keep browser/Rust cases in test/fixtures/sdk-url-ownership.json aligned. | ||||||
|
Collaborator
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. ⛏ nitpick — Same as
Suggested change
|
||||||
| let Some(parsed) = parse_sdk_network_url(url) else { | ||||||
| return false; | ||||||
| }; | ||||||
| let host = parsed.host_str().unwrap_or_default(); | ||||||
| let path = parsed.path().to_ascii_lowercase(); | ||||||
| (host == "cdn.permutive.com" | ||||||
| || host | ||||||
| .strip_suffix(".edge.permutive.app") | ||||||
| .is_some_and(|subdomain| !subdomain.is_empty())) | ||||||
| && path.ends_with("-web.js") | ||||||
| } | ||||||
|
|
||||||
| /// Handle SDK serving - fetch from Permutive CDN and serve through first-party domain. | ||||||
|
|
@@ -474,9 +483,37 @@ mod tests { | |||||
| use std::sync::Arc; | ||||||
|
|
||||||
| use super::*; | ||||||
| use crate::integrations::sdk_url_test_support::{assert_sdk_url_rewrite, sdk_url_cases}; | ||||||
| use crate::platform::test_support::{StubHttpClient, build_services_with_http_client}; | ||||||
| use crate::test_support::tests::create_test_settings; | ||||||
|
|
||||||
| #[test] | ||||||
| fn sdk_url_ownership_matches_shared_corpus() { | ||||||
| let integration = PermutiveIntegration::new(PermutiveConfig { | ||||||
| enabled: true, | ||||||
| organization_id: "example".to_string(), | ||||||
| workspace_id: "workspace".to_string(), | ||||||
| project_id: String::new(), | ||||||
| api_endpoint: default_api_endpoint(), | ||||||
| secure_signals_endpoint: default_secure_signals_endpoint(), | ||||||
| cache_ttl_seconds: 3600, | ||||||
| rewrite_sdk: true, | ||||||
| }); | ||||||
|
|
||||||
| for case in sdk_url_cases() { | ||||||
| let matches = case.owner.as_deref() == Some(PERMUTIVE_INTEGRATION_ID); | ||||||
| assert_eq!( | ||||||
| integration.is_permutive_sdk_url(&case.url), | ||||||
| matches, | ||||||
| "should match expected permutive ownership for {}: {}", | ||||||
| case.name, | ||||||
| case.url | ||||||
| ); | ||||||
|
|
||||||
| assert_sdk_url_rewrite(integration.as_ref(), &case, "/integrations/permutive/sdk"); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| #[test] | ||||||
| fn test_permutive_sdk_url_generation() { | ||||||
| let config = PermutiveConfig { | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,22 @@ | ||||||
| //! Shared input policy for SDK URL ownership checks. | ||||||
|
|
||||||
| use url::Url; | ||||||
|
|
||||||
| /// Parse an HTTP(S) SDK URL with an explicit authority, or a protocol-relative URL. | ||||||
| /// | ||||||
| /// Trims ASCII whitespace and rejects relative paths and shorthand schemes. | ||||||
| /// Browser/Rust ownership cases live in test/fixtures/sdk-url-ownership.json. | ||||||
|
Collaborator
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. ⛏ nitpick —
Suggested change
|
||||||
| pub(super) fn parse_sdk_network_url(url: &str) -> Option<Url> { | ||||||
| let value = url.trim_matches([' ', '\t', '\n', '\r', '\x0c', '\x0b']); | ||||||
| let parsed = if value.starts_with("//") { | ||||||
| Url::parse(&format!("https:{value}")) | ||||||
| } else { | ||||||
| if !value.split_once("://").is_some_and(|(scheme, _)| { | ||||||
| scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https") | ||||||
| }) { | ||||||
| return None; | ||||||
| } | ||||||
| Url::parse(value) | ||||||
| }; | ||||||
| parsed.ok() | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,62 @@ | ||||||||||||||||
| //! Shared SDK URL corpus and attribute-rewrite assertions. | ||||||||||||||||
|
|
||||||||||||||||
| use serde::Deserialize; | ||||||||||||||||
|
|
||||||||||||||||
| use super::{AttributeRewriteAction, IntegrationAttributeContext, IntegrationAttributeRewriter}; | ||||||||||||||||
|
|
||||||||||||||||
| /// One raw URL and its expected integration owner from the shared corpus. | ||||||||||||||||
| #[derive(Deserialize)] | ||||||||||||||||
| pub(super) struct SdkUrlCase { | ||||||||||||||||
| /// Descriptive case name for assertion failures. | ||||||||||||||||
| pub(super) name: String, | ||||||||||||||||
| /// Input before browser DOM URL resolution. | ||||||||||||||||
| pub(super) url: String, | ||||||||||||||||
| /// Expected integration ID, or none for an unowned URL. | ||||||||||||||||
| pub(super) owner: Option<String>, | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /// Load the URL ownership cases used by both Rust and browser tests. | ||||||||||||||||
| /// | ||||||||||||||||
| /// # Panics | ||||||||||||||||
| /// | ||||||||||||||||
| /// Panics if the committed fixture is invalid. | ||||||||||||||||
| pub(super) fn sdk_url_cases() -> Vec<SdkUrlCase> { | ||||||||||||||||
| serde_json::from_str(include_str!( | ||||||||||||||||
| "../../../trusted-server-js/lib/test/fixtures/sdk-url-ownership.json" | ||||||||||||||||
| )) | ||||||||||||||||
| .expect("should parse shared SDK URL ownership corpus") | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /// Assert SDK rewriting for both script and link attribute contexts. | ||||||||||||||||
| /// | ||||||||||||||||
| /// # Panics | ||||||||||||||||
| /// | ||||||||||||||||
| /// Panics if the rewrite result disagrees with the corpus owner and SDK route. | ||||||||||||||||
| pub(super) fn assert_sdk_url_rewrite( | ||||||||||||||||
| integration: &dyn IntegrationAttributeRewriter, | ||||||||||||||||
| case: &SdkUrlCase, | ||||||||||||||||
| sdk_path: &str, | ||||||||||||||||
| ) { | ||||||||||||||||
| let expected = if case.owner.as_deref() == Some(integration.integration_id()) { | ||||||||||||||||
| AttributeRewriteAction::Replace(sdk_path.to_string()) | ||||||||||||||||
| } else { | ||||||||||||||||
| AttributeRewriteAction::Keep | ||||||||||||||||
| }; | ||||||||||||||||
| for (attribute, element) in [("src", "script"), ("href", "link")] { | ||||||||||||||||
|
Collaborator
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. 🔧 wrench: The With this assertion, both mutants fail (
Suggested change
|
||||||||||||||||
| let context = IntegrationAttributeContext { | ||||||||||||||||
| attribute_name: attribute, | ||||||||||||||||
| element_name: element, | ||||||||||||||||
| request_host: "edge.example.com", | ||||||||||||||||
| request_scheme: "https", | ||||||||||||||||
| origin_host: "origin.example.com", | ||||||||||||||||
| }; | ||||||||||||||||
| assert_eq!( | ||||||||||||||||
| integration.rewrite(attribute, &case.url, &context), | ||||||||||||||||
| expected, | ||||||||||||||||
| "should apply expected {} {attribute} rewrite for {}: {}", | ||||||||||||||||
| integration.integration_id(), | ||||||||||||||||
| case.name, | ||||||||||||||||
| case.url | ||||||||||||||||
| ); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| /** | ||
| * Parse an HTTP(S) SDK URL with an explicit authority, or a protocol-relative URL. | ||
| * | ||
| * Uses the same ASCII whitespace and prefix policy as Rust's SDK URL parser. | ||
| * Browser/Rust ownership cases live in test/fixtures/sdk-url-ownership.json. | ||
| */ | ||
| export function parseSdkNetworkUrl(url: string): URL | null { | ||
| const value = url.replace(/^[ \t\n\r\f\v]+|[ \t\n\r\f\v]+$/g, ''); | ||
| if (!/^(?:https?:)?\/\//i.test(value)) return null; | ||
|
|
||
| try { | ||
| return new URL(value.startsWith('//') ? `https:${value}` : value); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⛏ nitpick — Same as
sdk_url.rs:8: use the repo-relative fixture path so it resolves from this crate.