22//!
33//! This module provides transparent proxying for Lockr's SDK and API,
44//! enabling first-party identity resolution while maintaining privacy controls.
5+ //!
6+ //! ## Host Rewriting
7+ //!
8+ //! The integration can rewrite the Lockr SDK JavaScript to replace the hardcoded
9+ //! API host with a relative URL pointing to the first-party proxy. This ensures
10+ //! all API calls from the SDK go through the trusted server instead of directly
11+ //! to Lockr's servers, improving privacy and enabling additional controls.
12+ //!
13+ //! The rewriting finds the obfuscated host assignment pattern in the SDK and
14+ //! replaces it with: `'host': '/integrations/lockr/api'`
515
616use std:: sync:: Arc ;
717
@@ -19,6 +29,7 @@ use crate::integrations::{
1929 IntegrationEndpoint , IntegrationProxy , IntegrationRegistration ,
2030} ;
2131use crate :: settings:: { IntegrationConfig as IntegrationConfigTrait , Settings } ;
32+ use crate :: streaming_replacer:: StreamingReplacer ;
2233
2334const LOCKR_INTEGRATION_ID : & str = "lockr" ;
2435
@@ -51,6 +62,10 @@ pub struct LockrConfig {
5162 /// Whether to rewrite Lockr SDK URLs in HTML
5263 #[ serde( default = "default_rewrite_sdk" ) ]
5364 pub rewrite_sdk : bool ,
65+
66+ /// Whether to rewrite the host variable in the Lockr SDK JavaScript
67+ #[ serde( default = "default_rewrite_sdk_host" ) ]
68+ pub rewrite_sdk_host : bool ,
5469}
5570
5671impl IntegrationConfigTrait for LockrConfig {
@@ -85,6 +100,26 @@ impl LockrIntegration {
85100 && lower. ends_with ( ".js" )
86101 }
87102
103+ /// Rewrite the host variable in the Lockr SDK JavaScript.
104+ ///
105+ /// Replaces the obfuscated host assignment with a direct assignment to the
106+ /// first-party API proxy endpoint.
107+ fn rewrite_sdk_host ( & self , sdk_body : Vec < u8 > ) -> Result < Vec < u8 > , Report < TrustedServerError > > {
108+ // Pattern to find: 'host': _0x3a740e(0x3d1) + _0x3a740e(0x367) + _0x3a740e(0x14e)
109+ // This is the obfuscated way Lockr sets the API host
110+ // We'll replace it with a relative URL to use the first-party proxy
111+
112+ let mut replacer = StreamingReplacer :: new_single (
113+ "'host': _0x3a740e(0x3d1) + _0x3a740e(0x367) + _0x3a740e(0x14e)" ,
114+ "'host': '/integrations/lockr/api'" ,
115+ ) ;
116+
117+ // Process the entire SDK in one chunk
118+ let processed = replacer. process_chunk ( & sdk_body, true ) ;
119+
120+ Ok ( processed)
121+ }
122+
88123 /// Handle SDK serving - fetch from Lockr CDN and serve through first-party domain.
89124 async fn handle_sdk_serving (
90125 & self ,
@@ -125,9 +160,15 @@ impl LockrIntegration {
125160 ) ) ) ) ;
126161 }
127162
128- let sdk_body = lockr_response. take_body_bytes ( ) ;
163+ let mut sdk_body = lockr_response. take_body_bytes ( ) ;
129164 log:: info!( "Successfully fetched Lockr SDK: {} bytes" , sdk_body. len( ) ) ;
130165
166+ // Rewrite the host variable in the SDK if enabled
167+ if self . config . rewrite_sdk_host {
168+ sdk_body = self . rewrite_sdk_host ( sdk_body) ?;
169+ log:: info!( "Rewrote SDK host variable: {} bytes" , sdk_body. len( ) ) ;
170+ }
171+
131172 // TODO: Cache in KV store (future enhancement)
132173
133174 Ok ( Response :: from_status ( StatusCode :: OK )
@@ -144,6 +185,10 @@ impl LockrIntegration {
144185 )
145186 . with_header ( "X-Lockr-SDK-Proxy" , "true" )
146187 . with_header ( "X-SDK-Source" , sdk_url)
188+ . with_header (
189+ "X-Lockr-Host-Rewritten" ,
190+ self . config . rewrite_sdk_host . to_string ( ) ,
191+ )
147192 . with_body ( sdk_body) )
148193 }
149194
@@ -190,10 +235,15 @@ impl LockrIntegration {
190235 let backend_name = ensure_backend_from_url ( & self . config . api_endpoint )
191236 . change_context ( Self :: error ( "Failed to determine backend for API proxy" ) ) ?;
192237
193- let response = match target_req . send ( backend_name) {
238+ let response = match target_req. send ( backend_name) {
194239 Ok ( res) => res,
195240 Err ( e) => {
196- return Err ( Self :: error ( format ! ( "failed to forward request to {}, {}" , target_url, e. root_cause( ) ) ) . into ( ) ) ;
241+ return Err ( Self :: error ( format ! (
242+ "failed to forward request to {}, {}" ,
243+ target_url,
244+ e. root_cause( )
245+ ) )
246+ . into ( ) ) ;
197247 }
198248 } ;
199249
@@ -340,6 +390,10 @@ fn default_rewrite_sdk() -> bool {
340390 true
341391}
342392
393+ fn default_rewrite_sdk_host ( ) -> bool {
394+ true
395+ }
396+
343397#[ cfg( test) ]
344398mod tests {
345399 use super :: * ;
@@ -353,6 +407,7 @@ mod tests {
353407 sdk_url : default_sdk_url ( ) ,
354408 cache_ttl_seconds : 3600 ,
355409 rewrite_sdk : true ,
410+ rewrite_sdk_host : true ,
356411 } ;
357412 let integration = LockrIntegration :: new ( config) ;
358413
@@ -373,6 +428,7 @@ mod tests {
373428 sdk_url : default_sdk_url ( ) ,
374429 cache_ttl_seconds : 3600 ,
375430 rewrite_sdk : true ,
431+ rewrite_sdk_host : true ,
376432 } ;
377433 let integration = LockrIntegration :: new ( config) ;
378434
@@ -403,6 +459,7 @@ mod tests {
403459 sdk_url : default_sdk_url ( ) ,
404460 cache_ttl_seconds : 3600 ,
405461 rewrite_sdk : false , // Disabled
462+ rewrite_sdk_host : true ,
406463 } ;
407464 let integration = LockrIntegration :: new ( config) ;
408465
@@ -460,6 +517,7 @@ mod tests {
460517 sdk_url : default_sdk_url ( ) ,
461518 cache_ttl_seconds : 3600 ,
462519 rewrite_sdk : true ,
520+ rewrite_sdk_host : true ,
463521 } ;
464522 let integration = LockrIntegration :: new ( config) ;
465523
@@ -479,4 +537,91 @@ mod tests {
479537 . iter( )
480538 . any( |r| r. path == "/integrations/lockr/api/*" && r. method == Method :: GET ) ) ;
481539 }
540+
541+ #[ test]
542+ fn test_sdk_host_rewriting ( ) {
543+ let config = LockrConfig {
544+ enabled : true ,
545+ app_id : "test-app-id" . to_string ( ) ,
546+ api_endpoint : default_api_endpoint ( ) ,
547+ sdk_url : default_sdk_url ( ) ,
548+ cache_ttl_seconds : 3600 ,
549+ rewrite_sdk : true ,
550+ rewrite_sdk_host : true ,
551+ } ;
552+ let integration = LockrIntegration :: new ( config) ;
553+
554+ // Mock obfuscated SDK JavaScript with the host pattern
555+ let mock_sdk = r#"
556+ const identityLockr = {
557+ 'host': _0x3a740e(0x3d1) + _0x3a740e(0x367) + _0x3a740e(0x14e),
558+ 'app_id': null,
559+ 'expiryDateKeys': localStorage['getItem']('identityLockr_expiryDateKeys') ? JSON['parse'](localStorage['getItem']('identityLockr_expiryDateKeys')) : [],
560+ 'firstPartyCookies': [],
561+ 'canRefreshToken': !![]
562+ };
563+ "# ;
564+
565+ let result = integration. rewrite_sdk_host ( mock_sdk. as_bytes ( ) . to_vec ( ) ) ;
566+ assert ! ( result. is_ok( ) ) ;
567+
568+ let rewritten = String :: from_utf8 ( result. unwrap ( ) ) . unwrap ( ) ;
569+
570+ // Verify the host was rewritten to the proxy endpoint
571+ assert ! ( rewritten. contains( "'host': '/integrations/lockr/api'" ) ) ;
572+
573+ // Verify the obfuscated pattern was removed
574+ assert ! ( !rewritten. contains( "_0x3a740e(0x3d1) + _0x3a740e(0x367) + _0x3a740e(0x14e)" ) ) ;
575+
576+ // Verify other parts of the code remain intact
577+ assert ! ( rewritten. contains( "'app_id': null" ) ) ;
578+ assert ! ( rewritten. contains( "'firstPartyCookies': []" ) ) ;
579+ }
580+
581+ #[ test]
582+ fn test_sdk_host_rewriting_disabled ( ) {
583+ let config = LockrConfig {
584+ enabled : true ,
585+ app_id : "test-app-id" . to_string ( ) ,
586+ api_endpoint : default_api_endpoint ( ) ,
587+ sdk_url : default_sdk_url ( ) ,
588+ cache_ttl_seconds : 3600 ,
589+ rewrite_sdk : true ,
590+ rewrite_sdk_host : false , // Disabled
591+ } ;
592+
593+ // When rewrite_sdk_host is false, the handle_sdk_serving function
594+ // won't call rewrite_sdk_host at all, so the SDK is served as-is
595+ assert ! ( !config. rewrite_sdk_host) ;
596+ }
597+
598+ #[ test]
599+ fn test_sdk_host_rewriting_no_match ( ) {
600+ let config = LockrConfig {
601+ enabled : true ,
602+ app_id : "test-app-id" . to_string ( ) ,
603+ api_endpoint : default_api_endpoint ( ) ,
604+ sdk_url : default_sdk_url ( ) ,
605+ cache_ttl_seconds : 3600 ,
606+ rewrite_sdk : true ,
607+ rewrite_sdk_host : true ,
608+ } ;
609+ let integration = LockrIntegration :: new ( config) ;
610+
611+ // Test with SDK that doesn't have the expected pattern
612+ let mock_sdk = r#"
613+ const identityLockr = {
614+ 'host': 'https://example.com',
615+ 'app_id': null
616+ };
617+ "# ;
618+
619+ let result = integration. rewrite_sdk_host ( mock_sdk. as_bytes ( ) . to_vec ( ) ) ;
620+ assert ! ( result. is_ok( ) ) ;
621+
622+ let rewritten = String :: from_utf8 ( result. unwrap ( ) ) . unwrap ( ) ;
623+
624+ // When pattern doesn't match, content should be unchanged
625+ assert ! ( rewritten. contains( "'host': 'https://example.com'" ) ) ;
626+ }
482627}
0 commit comments