@@ -3,6 +3,46 @@ import Foundation
33import SweetCookieKit
44#endif
55
6+ /// Cookie headers narrowed to ZoomMate's fixed request hosts. Keeping the destination in the
7+ /// credential value makes it impossible for host failover to reuse a leaf-host cookie on its
8+ /// sibling host.
9+ public struct ZoomMateCookieHeaders : Codable , Equatable , Sendable {
10+ static let allowedHosts = [ " ai.zoom.us " , " zoommate.zoom.us " ]
11+
12+ private let headersByHost : [ String : String ]
13+
14+ public init ( headersByHost: [ String : String ] ) {
15+ self . headersByHost = Dictionary ( uniqueKeysWithValues: Self . allowedHosts. compactMap { host in
16+ guard let header = headersByHost [ host] ? . trimmingCharacters ( in: . whitespacesAndNewlines) ,
17+ !header. isEmpty
18+ else {
19+ return nil
20+ }
21+ return ( host, header)
22+ } )
23+ }
24+
25+ public func header( forHost host: String ) -> String ? {
26+ self . headersByHost [ host. lowercased ( ) ]
27+ }
28+
29+ public var isEmpty : Bool {
30+ self . headersByHost. isEmpty
31+ }
32+
33+ func encodedForStorage( ) -> String ? {
34+ let encoder = JSONEncoder ( )
35+ encoder. outputFormatting = [ . sortedKeys]
36+ guard let data = try ? encoder. encode ( self ) else { return nil }
37+ return String ( data: data, encoding: . utf8)
38+ }
39+
40+ static func decodeFromStorage( _ value: String ) -> Self ? {
41+ guard let data = value. data ( using: . utf8) else { return nil }
42+ return try ? JSONDecoder ( ) . decode ( Self . self, from: data)
43+ }
44+ }
45+
646#if os(macOS)
747private let zoomMateCookieImportOrder : BrowserCookieImportOrder =
848 ProviderDefaults . metadata [ . zoommate] ? . browserCookieOrder ?? Browser . defaultImportOrder
@@ -19,17 +59,12 @@ public enum ZoomMateCookieImporter {
1959 /// then narrowed at send time by `isSendable(toSessionHosts:)`.
2060 private static let cookieDomains = [ " zoommate.zoom.us " , " ai.zoom.us " , " zoom.us " ]
2161
22- /// Hosts whose cookies the fetchers actually transmit (the login-bootstrap and credits calls
23- /// hit `ai.zoom.us`; the browser session lives on both). Used to drop cookies that a browser
24- /// would never attach to these requests — see `isSendable(cookieDomain:)`.
25- private static let sessionHosts = [ " ai.zoom.us " , " zoommate.zoom.us " ]
26-
2762 public struct SessionInfo : Sendable {
28- public let cookieHeader : String
63+ public let cookieHeaders : ZoomMateCookieHeaders
2964 public let sourceLabel : String
3065
31- public init ( cookieHeader : String , sourceLabel: String ) {
32- self . cookieHeader = cookieHeader
66+ public init ( cookieHeaders : ZoomMateCookieHeaders , sourceLabel: String ) {
67+ self . cookieHeaders = cookieHeaders
3368 self . sourceLabel = sourceLabel
3469 }
3570 }
@@ -58,11 +93,10 @@ public enum ZoomMateCookieImporter {
5893 logger: log)
5994 for source in sources where !source. records. isEmpty {
6095 let cookies = BrowserCookieClient . makeHTTPCookies ( source. records, origin: query. origin)
61- . filter { Self . isSendable ( cookieDomain: $0. domain) }
62- guard !cookies. isEmpty else { continue }
63- log ( " \( source. label) : found \( cookies. count) matching cookies " )
64- let header = cookies. map { " \( $0. name) = \( $0. value) " } . joined ( separator: " ; " )
65- sessions. append ( SessionInfo ( cookieHeader: header, sourceLabel: source. label) )
96+ let cookieHeaders = Self . cookieHeaders ( from: cookies)
97+ guard !cookieHeaders. isEmpty else { continue }
98+ log ( " \( source. label) : found host-scoped cookie headers " )
99+ sessions. append ( SessionInfo ( cookieHeaders: cookieHeaders, sourceLabel: source. label) )
66100 }
67101 } catch {
68102 BrowserCookieAccessGate . recordIfNeeded ( error)
@@ -74,19 +108,31 @@ public enum ZoomMateCookieImporter {
74108 return sessions
75109 }
76110
77- /// Whether a browser would attach a cookie scoped to `cookieDomain` to a request to one of
78- /// `sessionHosts`, per RFC 6265 domain-matching: a host-only cookie matches its exact host; a
111+ /// Whether a browser would attach a cookie scoped to `cookieDomain` to a request to `host`, per
112+ /// RFC 6265 domain-matching: a host-only cookie matches its exact host; a
79113 /// domain cookie (stored with a leading dot) matches that host and all of its subdomains. This
80- /// keeps the parent `.zoom.us` SSO cookies the endpoints need while dropping cookies host-scoped
81- /// to unrelated `*.zoom.us` siblings (marketing/support/web) swept in by the coarse `.contains`
82- /// domain read above — cookies those endpoints would never receive.
83- static func isSendable( cookieDomain: String ) -> Bool {
84- let bare = cookieDomain. hasPrefix ( " . " ) ? String ( cookieDomain. dropFirst ( ) ) : cookieDomain
85- let normalized = bare. lowercased ( )
86- guard !normalized. isEmpty else { return false }
87- return self . sessionHosts. contains { host in
88- host == normalized || host. hasSuffix ( " . " + normalized)
114+ /// keeps parent `.zoom.us` SSO cookies while preventing an `ai.zoom.us` host-only cookie from
115+ /// reaching `zoommate.zoom.us` (and vice versa).
116+ static func isSendable( cookieDomain: String , toHost host: String ) -> Bool {
117+ let normalizedDomain = cookieDomain. lowercased ( )
118+ let normalizedHost = host. lowercased ( )
119+ guard ZoomMateCookieHeaders . allowedHosts. contains ( normalizedHost) , !normalizedDomain. isEmpty else {
120+ return false
121+ }
122+ guard normalizedDomain. hasPrefix ( " . " ) else { return normalizedHost == normalizedDomain }
123+ let bareDomain = String ( normalizedDomain. dropFirst ( ) )
124+ guard !bareDomain. isEmpty else { return false }
125+ return normalizedHost == bareDomain || normalizedHost. hasSuffix ( " . " + bareDomain)
126+ }
127+
128+ static func cookieHeaders( from cookies: [ HTTPCookie ] ) -> ZoomMateCookieHeaders {
129+ let pairs : [ ( String , String ) ] = ZoomMateCookieHeaders . allowedHosts. compactMap { host in
130+ let sendable = cookies. filter { Self . isSendable ( cookieDomain: $0. domain, toHost: host) }
131+ guard !sendable. isEmpty else { return nil }
132+ let header = sendable. map { " \( $0. name) = \( $0. value) " } . joined ( separator: " ; " )
133+ return ( host, header)
89134 }
135+ return ZoomMateCookieHeaders ( headersByHost: Dictionary ( uniqueKeysWithValues: pairs) )
90136 }
91137}
92138#endif
0 commit comments