fix(mobile): use shared auth for background_downloader (#26911)

shared client for background_downloader on ios
This commit is contained in:
Mert
2026-03-13 22:23:07 -05:00
committed by GitHub
parent ff936f901d
commit b66c97b785
3 changed files with 73 additions and 6 deletions

View File

@@ -27,7 +27,11 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.File import java.io.File
import java.net.Authenticator
import java.net.CookieHandler
import java.net.PasswordAuthentication
import java.net.Socket import java.net.Socket
import java.net.URI
import java.security.KeyStore import java.security.KeyStore
import java.security.Principal import java.security.Principal
import java.security.PrivateKey import java.security.PrivateKey
@@ -104,6 +108,25 @@ object HttpClientManager {
keyChainAlias = prefs.getString(PREFS_CERT_ALIAS, null) keyChainAlias = prefs.getString(PREFS_CERT_ALIAS, null)
cookieJar.init(prefs) cookieJar.init(prefs)
System.setProperty("http.agent", USER_AGENT)
Authenticator.setDefault(object : Authenticator() {
override fun getPasswordAuthentication(): PasswordAuthentication? {
val url = requestingURL ?: return null
if (url.userInfo.isNullOrEmpty()) return null
val parts = url.userInfo.split(":", limit = 2)
return PasswordAuthentication(parts[0], parts.getOrElse(1) { "" }.toCharArray())
}
})
CookieHandler.setDefault(object : CookieHandler() {
override fun get(uri: URI, requestHeaders: Map<String, List<String>>): Map<String, List<String>> {
val httpUrl = uri.toString().toHttpUrlOrNull() ?: return emptyMap()
val cookies = cookieJar.loadForRequest(httpUrl)
if (cookies.isEmpty()) return emptyMap()
return mapOf("Cookie" to listOf(cookies.joinToString("; ") { "${it.name}=${it.value}" }))
}
override fun put(uri: URI, responseHeaders: Map<String, List<String>>) {}
})
val savedHeaders = prefs.getString(PREFS_HEADERS, null) val savedHeaders = prefs.getString(PREFS_HEADERS, null)
if (savedHeaders != null) { if (savedHeaders != null) {

View File

@@ -20,6 +20,7 @@ import UIKit
} }
SwiftNativeVideoPlayerPlugin.cookieStorage = URLSessionManager.cookieStorage SwiftNativeVideoPlayerPlugin.cookieStorage = URLSessionManager.cookieStorage
URLSessionManager.patchBackgroundDownloader()
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
AppDelegate.registerPlugins(with: controller.engine, controller: controller) AppDelegate.registerPlugins(with: controller.engine, controller: controller)

View File

@@ -51,7 +51,7 @@ class URLSessionManager: NSObject {
diskCapacity: 1024 * 1024 * 1024, diskCapacity: 1024 * 1024 * 1024,
directory: cacheDir directory: cacheDir
) )
private static let userAgent: String = { static let userAgent: String = {
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown" let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown"
return "Immich_iOS_\(version)" return "Immich_iOS_\(version)"
}() }()
@@ -158,6 +158,49 @@ class URLSessionManager: NSObject {
return URLSession(configuration: config, delegate: delegate, delegateQueue: nil) return URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
} }
/// Patches background_downloader's URLSession to use shared auth configuration.
/// Must be called before background_downloader creates its session (i.e. early in app startup).
static func patchBackgroundDownloader() {
// Swizzle URLSessionConfiguration.background(withIdentifier:) to inject shared config
let originalSel = NSSelectorFromString("backgroundSessionConfigurationWithIdentifier:")
let swizzledSel = #selector(URLSessionConfiguration.immich_background(withIdentifier:))
if let original = class_getClassMethod(URLSessionConfiguration.self, originalSel),
let swizzled = class_getClassMethod(URLSessionConfiguration.self, swizzledSel) {
method_exchangeImplementations(original, swizzled)
}
// Add auth challenge handling to background_downloader's UrlSessionDelegate
guard let targetClass = NSClassFromString("background_downloader.UrlSessionDelegate") else { return }
let sessionBlock: @convention(block) (AnyObject, URLSession, URLAuthenticationChallenge,
@escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void
= { _, session, challenge, completion in
URLSessionManager.shared.delegate.handleChallenge(session, challenge, completion)
}
class_replaceMethod(targetClass,
NSSelectorFromString("URLSession:didReceiveChallenge:completionHandler:"),
imp_implementationWithBlock(sessionBlock), "v@:@@@?")
let taskBlock: @convention(block) (AnyObject, URLSession, URLSessionTask, URLAuthenticationChallenge,
@escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void
= { _, session, task, challenge, completion in
URLSessionManager.shared.delegate.handleChallenge(session, challenge, completion, task: task)
}
class_replaceMethod(targetClass,
NSSelectorFromString("URLSession:task:didReceiveChallenge:completionHandler:"),
imp_implementationWithBlock(taskBlock), "v@:@@@@?")
}
}
private extension URLSessionConfiguration {
@objc dynamic class func immich_background(withIdentifier id: String) -> URLSessionConfiguration {
// After swizzle, this calls the original implementation
let config = immich_background(withIdentifier: id)
config.httpCookieStorage = URLSessionManager.cookieStorage
config.httpAdditionalHeaders = ["User-Agent": URLSessionManager.userAgent]
return config
}
} }
class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate, URLSessionWebSocketDelegate { class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate, URLSessionWebSocketDelegate {
@@ -168,7 +211,7 @@ class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate, URLSessionWeb
) { ) {
handleChallenge(session, challenge, completionHandler) handleChallenge(session, challenge, completionHandler)
} }
func urlSession( func urlSession(
_ session: URLSession, _ session: URLSession,
task: URLSessionTask, task: URLSessionTask,
@@ -177,7 +220,7 @@ class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate, URLSessionWeb
) { ) {
handleChallenge(session, challenge, completionHandler, task: task) handleChallenge(session, challenge, completionHandler, task: task)
} }
func handleChallenge( func handleChallenge(
_ session: URLSession, _ session: URLSession,
_ challenge: URLAuthenticationChallenge, _ challenge: URLAuthenticationChallenge,
@@ -190,7 +233,7 @@ class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate, URLSessionWeb
default: completionHandler(.performDefaultHandling, nil) default: completionHandler(.performDefaultHandling, nil)
} }
} }
private func handleClientCertificate( private func handleClientCertificate(
_ session: URLSession, _ session: URLSession,
completion: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void completion: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
@@ -200,7 +243,7 @@ class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate, URLSessionWeb
kSecAttrLabel as String: CLIENT_CERT_LABEL, kSecAttrLabel as String: CLIENT_CERT_LABEL,
kSecReturnRef as String: true, kSecReturnRef as String: true,
] ]
var item: CFTypeRef? var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item) let status = SecItemCopyMatching(query as CFDictionary, &item)
if status == errSecSuccess, let identity = item { if status == errSecSuccess, let identity = item {
@@ -214,7 +257,7 @@ class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate, URLSessionWeb
} }
completion(.performDefaultHandling, nil) completion(.performDefaultHandling, nil)
} }
private func handleBasicAuth( private func handleBasicAuth(
_ session: URLSession, _ session: URLSession,
task: URLSessionTask?, task: URLSessionTask?,