Skip to content

Commit b66c97b

Browse files
authored
fix(mobile): use shared auth for background_downloader (immich-app#26911)
shared client for background_downloader on ios
1 parent ff936f9 commit b66c97b

3 files changed

Lines changed: 73 additions & 6 deletions

File tree

mobile/android/app/src/main/kotlin/app/alextran/immich/core/HttpClientManager.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ import kotlinx.serialization.Serializable
2727
import kotlinx.serialization.json.Json
2828
import java.io.ByteArrayInputStream
2929
import java.io.File
30+
import java.net.Authenticator
31+
import java.net.CookieHandler
32+
import java.net.PasswordAuthentication
3033
import java.net.Socket
34+
import java.net.URI
3135
import java.security.KeyStore
3236
import java.security.Principal
3337
import java.security.PrivateKey
@@ -104,6 +108,25 @@ object HttpClientManager {
104108
keyChainAlias = prefs.getString(PREFS_CERT_ALIAS, null)
105109

106110
cookieJar.init(prefs)
111+
System.setProperty("http.agent", USER_AGENT)
112+
Authenticator.setDefault(object : Authenticator() {
113+
override fun getPasswordAuthentication(): PasswordAuthentication? {
114+
val url = requestingURL ?: return null
115+
if (url.userInfo.isNullOrEmpty()) return null
116+
val parts = url.userInfo.split(":", limit = 2)
117+
return PasswordAuthentication(parts[0], parts.getOrElse(1) { "" }.toCharArray())
118+
}
119+
})
120+
CookieHandler.setDefault(object : CookieHandler() {
121+
override fun get(uri: URI, requestHeaders: Map<String, List<String>>): Map<String, List<String>> {
122+
val httpUrl = uri.toString().toHttpUrlOrNull() ?: return emptyMap()
123+
val cookies = cookieJar.loadForRequest(httpUrl)
124+
if (cookies.isEmpty()) return emptyMap()
125+
return mapOf("Cookie" to listOf(cookies.joinToString("; ") { "${it.name}=${it.value}" }))
126+
}
127+
128+
override fun put(uri: URI, responseHeaders: Map<String, List<String>>) {}
129+
})
107130

108131
val savedHeaders = prefs.getString(PREFS_HEADERS, null)
109132
if (savedHeaders != null) {

mobile/ios/Runner/AppDelegate.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import UIKit
2020
}
2121

2222
SwiftNativeVideoPlayerPlugin.cookieStorage = URLSessionManager.cookieStorage
23+
URLSessionManager.patchBackgroundDownloader()
2324
GeneratedPluginRegistrant.register(with: self)
2425
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
2526
AppDelegate.registerPlugins(with: controller.engine, controller: controller)

mobile/ios/Runner/Core/URLSessionManager.swift

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class URLSessionManager: NSObject {
5151
diskCapacity: 1024 * 1024 * 1024,
5252
directory: cacheDir
5353
)
54-
private static let userAgent: String = {
54+
static let userAgent: String = {
5555
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown"
5656
return "Immich_iOS_\(version)"
5757
}()
@@ -158,6 +158,49 @@ class URLSessionManager: NSObject {
158158

159159
return URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
160160
}
161+
162+
/// Patches background_downloader's URLSession to use shared auth configuration.
163+
/// Must be called before background_downloader creates its session (i.e. early in app startup).
164+
static func patchBackgroundDownloader() {
165+
// Swizzle URLSessionConfiguration.background(withIdentifier:) to inject shared config
166+
let originalSel = NSSelectorFromString("backgroundSessionConfigurationWithIdentifier:")
167+
let swizzledSel = #selector(URLSessionConfiguration.immich_background(withIdentifier:))
168+
if let original = class_getClassMethod(URLSessionConfiguration.self, originalSel),
169+
let swizzled = class_getClassMethod(URLSessionConfiguration.self, swizzledSel) {
170+
method_exchangeImplementations(original, swizzled)
171+
}
172+
173+
// Add auth challenge handling to background_downloader's UrlSessionDelegate
174+
guard let targetClass = NSClassFromString("background_downloader.UrlSessionDelegate") else { return }
175+
176+
let sessionBlock: @convention(block) (AnyObject, URLSession, URLAuthenticationChallenge,
177+
@escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void
178+
= { _, session, challenge, completion in
179+
URLSessionManager.shared.delegate.handleChallenge(session, challenge, completion)
180+
}
181+
class_replaceMethod(targetClass,
182+
NSSelectorFromString("URLSession:didReceiveChallenge:completionHandler:"),
183+
imp_implementationWithBlock(sessionBlock), "v@:@@@?")
184+
185+
let taskBlock: @convention(block) (AnyObject, URLSession, URLSessionTask, URLAuthenticationChallenge,
186+
@escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void
187+
= { _, session, task, challenge, completion in
188+
URLSessionManager.shared.delegate.handleChallenge(session, challenge, completion, task: task)
189+
}
190+
class_replaceMethod(targetClass,
191+
NSSelectorFromString("URLSession:task:didReceiveChallenge:completionHandler:"),
192+
imp_implementationWithBlock(taskBlock), "v@:@@@@?")
193+
}
194+
}
195+
196+
private extension URLSessionConfiguration {
197+
@objc dynamic class func immich_background(withIdentifier id: String) -> URLSessionConfiguration {
198+
// After swizzle, this calls the original implementation
199+
let config = immich_background(withIdentifier: id)
200+
config.httpCookieStorage = URLSessionManager.cookieStorage
201+
config.httpAdditionalHeaders = ["User-Agent": URLSessionManager.userAgent]
202+
return config
203+
}
161204
}
162205

163206
class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate, URLSessionWebSocketDelegate {
@@ -168,7 +211,7 @@ class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate, URLSessionWeb
168211
) {
169212
handleChallenge(session, challenge, completionHandler)
170213
}
171-
214+
172215
func urlSession(
173216
_ session: URLSession,
174217
task: URLSessionTask,
@@ -177,7 +220,7 @@ class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate, URLSessionWeb
177220
) {
178221
handleChallenge(session, challenge, completionHandler, task: task)
179222
}
180-
223+
181224
func handleChallenge(
182225
_ session: URLSession,
183226
_ challenge: URLAuthenticationChallenge,
@@ -190,7 +233,7 @@ class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate, URLSessionWeb
190233
default: completionHandler(.performDefaultHandling, nil)
191234
}
192235
}
193-
236+
194237
private func handleClientCertificate(
195238
_ session: URLSession,
196239
completion: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
@@ -200,7 +243,7 @@ class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate, URLSessionWeb
200243
kSecAttrLabel as String: CLIENT_CERT_LABEL,
201244
kSecReturnRef as String: true,
202245
]
203-
246+
204247
var item: CFTypeRef?
205248
let status = SecItemCopyMatching(query as CFDictionary, &item)
206249
if status == errSecSuccess, let identity = item {
@@ -214,7 +257,7 @@ class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate, URLSessionWeb
214257
}
215258
completion(.performDefaultHandling, nil)
216259
}
217-
260+
218261
private func handleBasicAuth(
219262
_ session: URLSession,
220263
task: URLSessionTask?,

0 commit comments

Comments
 (0)