@@ -47,6 +47,18 @@ const SNI_REWRITE_SUFFIXES: &[&str] = &[
4747 "youtu.be" ,
4848 "youtube-nocookie.com" ,
4949 "ytimg.com" ,
50+ // YouTube video CDN. Issue #275: a user reported that with
51+ // `youtube_via_relay = true`, every video chunk was traversing the
52+ // Apps Script relay and a single chunk timeout aborted playback —
53+ // because `googlevideo.com` was not on this list, all chunks
54+ // fell through to the relay path. Adding it here keeps video
55+ // bytes on the direct GFE tunnel even when the relay flag is on
56+ // (the YOUTUBE_RELAY_HOSTS carve-out below excludes only the API
57+ // / HTML surfaces, not the CDN). The chunks are unauthenticated
58+ // bytes anyway — there's no Restricted Mode logic on the CDN, so
59+ // routing them through Apps Script gains nothing and costs the
60+ // 6-minute Apps Script execution cap on long videos.
61+ "googlevideo.com" ,
5062 // Google Video Transport CDN — YouTube video chunks, Chrome
5163 // auto-updates, Google Play Store downloads. The single biggest
5264 // gap vs the upstream Python port: without these in the list
@@ -72,27 +84,62 @@ const SNI_REWRITE_SUFFIXES: &[&str] = &[
7284 "blogger.com" ,
7385] ;
7486
75- /// YouTube-family suffixes. Extracted so `youtube_via_relay` config can
76- /// pull them out of the SNI-rewrite dispatch at runtime.
77- const YOUTUBE_SNI_SUFFIXES : & [ & str ] = & [
87+ /// YouTube hosts that should be routed through the Apps Script relay
88+ /// when `youtube_via_relay` is enabled — the API + HTML surfaces where
89+ /// Restricted Mode is actually enforced (via the SNI=www.google.com
90+ /// edge looking at the request). Issue #102 / #275.
91+ ///
92+ /// Deliberately narrower than the YouTube section of
93+ /// `SNI_REWRITE_SUFFIXES`:
94+ /// - `youtube.com` / `youtu.be` / `youtube-nocookie.com`: HTML pages
95+ /// and player frames. These trigger Restricted Mode if served via
96+ /// the SNI rewrite, so when the flag is on we relay them.
97+ /// - `youtubei.googleapis.com`: the YouTube data API the player
98+ /// queries for video metadata + manifest. Restricted Mode also
99+ /// gates video availability here. Without this entry, the JSON
100+ /// RPC layer would still hit the SNI-rewrite tunnel via the
101+ /// broader `googleapis.com` suffix — the user-visible symptom of
102+ /// that miss is "youtube_via_relay flips on but Restricted Mode
103+ /// stays sticky on some videos."
104+ ///
105+ /// **NOT** in this list (intentional, was a regression in #275):
106+ /// - `ytimg.com`: thumbnails. No Restricted Mode logic on a static
107+ /// image CDN; routing through Apps Script makes thumbnails slow
108+ /// for zero gain.
109+ /// - `googlevideo.com`: video chunk CDN. Routing through Apps Script
110+ /// means every chunk eats Apps Script quota *and* risks the 6-min
111+ /// execution cap aborting long videos mid-playback.
112+ /// - `ggpht.com`: channel/profile images, same reasoning as ytimg.
113+ const YOUTUBE_RELAY_HOSTS : & [ & str ] = & [
78114 "youtube.com" ,
79115 "youtu.be" ,
80116 "youtube-nocookie.com" ,
81- "ytimg .com" ,
117+ "youtubei.googleapis .com" ,
82118] ;
83119
84120fn matches_sni_rewrite ( host : & str , youtube_via_relay : bool ) -> bool {
85121 let h = host. to_ascii_lowercase ( ) ;
86122 let h = h. trim_end_matches ( '.' ) ;
123+
124+ // YouTube relay carve-out runs FIRST so it wins over the broad
125+ // `googleapis.com` suffix that would otherwise pull
126+ // `youtubei.googleapis.com` into the SNI-rewrite path. The earlier
127+ // implementation iterated SNI_REWRITE_SUFFIXES with a filter, which
128+ // works for sibling entries (e.g. `youtube.com` in both lists) but
129+ // not for nested ones (`youtubei.googleapis.com` matches the broad
130+ // `googleapis.com` even when its specific entry is filtered out).
131+ // The short-circuit here is unconditional — we don't need to check
132+ // SNI rewrite once we've decided this host goes to the relay.
133+ if youtube_via_relay {
134+ for s in YOUTUBE_RELAY_HOSTS {
135+ if h == * s || h. ends_with ( & format ! ( ".{}" , s) ) {
136+ return false ;
137+ }
138+ }
139+ }
140+
87141 SNI_REWRITE_SUFFIXES
88142 . iter ( )
89- . filter ( |s| {
90- // If the user opted into youtube_via_relay, skip YouTube
91- // suffixes so they fall through to the Apps Script relay
92- // path. See config.rs `youtube_via_relay` docs for the
93- // trade-off. Issue #102.
94- !( youtube_via_relay && YOUTUBE_SNI_SUFFIXES . contains ( s) )
95- } )
96143 . any ( |s| h == * s || h. ends_with ( & format ! ( ".{}" , s) ) )
97144}
98145
@@ -2587,36 +2634,75 @@ mod tests {
25872634
25882635 #[ test]
25892636 fn youtube_via_relay_routes_youtube_through_relay_path ( ) {
2590- // Issue #102. When youtube_via_relay=true, YouTube suffixes
2591- // must NOT match the SNI-rewrite path, so traffic falls
2592- // through to Apps Script relay. Other Google suffixes are
2593- // unaffected.
2637+ // Issue #102 + #275. When youtube_via_relay=true:
2638+ // - YouTube API + HTML hosts (where Restricted Mode lives)
2639+ // opt out of SNI rewrite so they go through the relay.
2640+ // - YouTube image / video / channel-asset CDNs STAY on SNI
2641+ // rewrite — Restricted Mode isn't enforced on those, and
2642+ // routing video chunks through Apps Script burns quota
2643+ // and risks the 6-min execution cap. Pre-#275 ytimg.com
2644+ // was incorrectly carved out alongside the API surfaces.
2645+ // - Non-YouTube Google suffixes are unaffected by the flag.
25942646 let hosts = std:: collections:: HashMap :: new ( ) ;
25952647
2596- // Default behaviour: everything in the pool rewrites.
2648+ // Default behaviour (flag off): everything in the SNI pool
2649+ // rewrites including all YouTube assets.
2650+ assert ! ( should_use_sni_rewrite( & hosts, "www.youtube.com" , 443 , false ) ) ;
2651+ assert ! ( should_use_sni_rewrite( & hosts, "i.ytimg.com" , 443 , false ) ) ;
2652+ assert ! ( should_use_sni_rewrite( & hosts, "youtu.be" , 443 , false ) ) ;
2653+ assert ! ( should_use_sni_rewrite( & hosts, "www.google.com" , 443 , false ) ) ;
25972654 assert ! ( should_use_sni_rewrite(
25982655 & hosts,
2599- "www.youtube.com" ,
2656+ "rr1---sn-abc.googlevideo.com" ,
2657+ 443 ,
2658+ false
2659+ ) ) ;
2660+ assert ! ( should_use_sni_rewrite(
2661+ & hosts,
2662+ "youtubei.googleapis.com" ,
26002663 443 ,
26012664 false
26022665 ) ) ;
2603- assert ! ( should_use_sni_rewrite( & hosts, "i.ytimg.com" , 443 , false ) ) ;
2604- assert ! ( should_use_sni_rewrite( & hosts, "youtu.be" , 443 , false ) ) ;
2605- assert ! ( should_use_sni_rewrite( & hosts, "www.google.com" , 443 , false ) ) ;
26062666
2607- // With the toggle on: YouTube opts out, Google stays.
2667+ // Flag on: only the API + HTML hosts opt out.
2668+ assert ! ( !should_use_sni_rewrite( & hosts, "www.youtube.com" , 443 , true ) ) ;
2669+ assert ! ( !should_use_sni_rewrite( & hosts, "youtu.be" , 443 , true ) ) ;
26082670 assert ! ( !should_use_sni_rewrite(
26092671 & hosts,
2610- "www.youtube.com" ,
2672+ "www.youtube-nocookie .com" ,
26112673 443 ,
26122674 true
26132675 ) ) ;
2614- assert ! ( !should_use_sni_rewrite( & hosts, "i.ytimg.com" , 443 , true ) ) ;
2615- assert ! ( !should_use_sni_rewrite( & hosts, "youtu.be" , 443 , true ) ) ;
2676+ assert ! ( !should_use_sni_rewrite(
2677+ & hosts,
2678+ "youtubei.googleapis.com" ,
2679+ 443 ,
2680+ true
2681+ ) ) ;
2682+
2683+ // Flag on: video / image / channel-asset CDNs STAY on SNI
2684+ // rewrite. The pre-#275 implementation broke playback by
2685+ // routing googlevideo.com through Apps Script (it wasn't even
2686+ // in the SNI list before #275, so it always went via relay)
2687+ // and routed ytimg.com through the relay too.
2688+ assert ! ( should_use_sni_rewrite( & hosts, "i.ytimg.com" , 443 , true ) ) ;
2689+ assert ! ( should_use_sni_rewrite(
2690+ & hosts,
2691+ "rr1---sn-abc.googlevideo.com" ,
2692+ 443 ,
2693+ true
2694+ ) ) ;
2695+ assert ! ( should_use_sni_rewrite( & hosts, "yt3.ggpht.com" , 443 , true ) ) ;
2696+
2697+ // Flag on: non-YouTube Google suffixes are unaffected. Note
2698+ // youtubei.googleapis.com (above) is the *carve-out* — the
2699+ // broader googleapis.com suffix is NOT carved out, so e.g.
2700+ // Drive / Calendar / etc. continue to SNI-rewrite.
26162701 assert ! ( should_use_sni_rewrite( & hosts, "www.google.com" , 443 , true ) ) ;
2702+ assert ! ( should_use_sni_rewrite( & hosts, "fonts.gstatic.com" , 443 , true ) ) ;
26172703 assert ! ( should_use_sni_rewrite(
26182704 & hosts,
2619- "fonts.gstatic .com" ,
2705+ "drive.googleapis .com" ,
26202706 443 ,
26212707 true
26222708 ) ) ;
0 commit comments