From 04cfb7fa2fae7db996f63dd391a02fd355a4d4a2 Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:40:07 -0500 Subject: [PATCH] handle network switching --- .../app/alextran/immich/core/Network.g.kt | 5 ++- .../alextran/immich/core/NetworkApiPlugin.kt | 2 +- mobile/ios/Runner/Core/Network.g.swift | 5 ++- mobile/ios/Runner/Core/NetworkApiImpl.swift | 44 ++++++++++++++++--- .../ios/Runner/Core/URLSessionManager.swift | 8 ++-- .../pages/common/headers_settings.page.dart | 2 +- mobile/lib/platform/network_api.g.dart | 4 +- mobile/lib/providers/auth.provider.dart | 3 +- mobile/lib/services/api.service.dart | 25 +++++++++++ mobile/pigeon/network_api.dart | 2 +- 10 files changed, 80 insertions(+), 20 deletions(-) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt index 6458d71261..5e48d7fef5 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt @@ -184,7 +184,7 @@ interface NetworkApi { fun removeCertificate(callback: (Result) -> Unit) fun hasCertificate(): Boolean fun getClientPointer(): Long - fun setRequestHeaders(headers: Map) + fun setRequestHeaders(headers: Map, serverUrls: List) companion object { /** The codec used by NetworkApi. */ @@ -286,8 +286,9 @@ interface NetworkApi { channel.setMessageHandler { message, reply -> val args = message as List val headersArg = args[0] as Map + val serverUrlsArg = args[1] as List val wrapped: List = try { - api.setRequestHeaders(headersArg) + api.setRequestHeaders(headersArg, serverUrlsArg) listOf(null) } catch (exception: Throwable) { NetworkPigeonUtils.wrapError(exception) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt index 05c9ce8bac..853df6bfba 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt @@ -79,7 +79,7 @@ private class NetworkApiImpl() : NetworkApi { return HttpClientManager.getClientPointer() } - override fun setRequestHeaders(headers: Map) { + override fun setRequestHeaders(headers: Map, serverUrls: List) { HttpClientManager.setRequestHeaders(headers) } } diff --git a/mobile/ios/Runner/Core/Network.g.swift b/mobile/ios/Runner/Core/Network.g.swift index 38d3eb7a5b..96294c1cd4 100644 --- a/mobile/ios/Runner/Core/Network.g.swift +++ b/mobile/ios/Runner/Core/Network.g.swift @@ -225,7 +225,7 @@ protocol NetworkApi { func removeCertificate(completion: @escaping (Result) -> Void) func hasCertificate() throws -> Bool func getClientPointer() throws -> Int64 - func setRequestHeaders(headers: [String: String]) throws + func setRequestHeaders(headers: [String: String], serverUrls: [String]) throws } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -314,8 +314,9 @@ class NetworkApiSetup { setRequestHeadersChannel.setMessageHandler { message, reply in let args = message as! [Any?] let headersArg = args[0] as! [String: String] + let serverUrlsArg = args[1] as! [String] do { - try api.setRequestHeaders(headers: headersArg) + try api.setRequestHeaders(headers: headersArg, serverUrls: serverUrlsArg) reply(wrapResult(nil)) } catch { reply(wrapError(error)) diff --git a/mobile/ios/Runner/Core/NetworkApiImpl.swift b/mobile/ios/Runner/Core/NetworkApiImpl.swift index e4978e8058..75572587fe 100644 --- a/mobile/ios/Runner/Core/NetworkApiImpl.swift +++ b/mobile/ios/Runner/Core/NetworkApiImpl.swift @@ -59,13 +59,43 @@ class NetworkApiImpl: NetworkApi { return Int64(Int(bitPattern: pointer)) } - func setRequestHeaders(headers: [String : String]) throws { - var filtered = headers - filtered.removeValue(forKey: "x-immich-user-token") // the session uses cookie auth - var current = URLSessionManager.shared.session.configuration.httpAdditionalHeaders as? [String: String] ?? [:] - current.removeValue(forKey: "User-Agent") - if filtered != current { - UserDefaults.standard.set(filtered, forKey: HEADERS_KEY) + func setRequestHeaders(headers: [String : String], serverUrls: [String]) throws { + var headers = headers + if let token = headers.removeValue(forKey: "x-immich-user-token") { + for serverUrl in serverUrls { + guard let url = URL(string: serverUrl), let domain = url.host else { continue } + let isSecure = serverUrl.hasPrefix("https") + let cookies: [(String, String, Bool)] = [ + ("immich_access_token", token, true), + ("immich_is_authenticated", "true", false), + ("immich_auth_type", "password", true), + ] + let expiry = Date().addingTimeInterval(400 * 24 * 60 * 60) + for (name, value, httpOnly) in cookies { + var properties: [HTTPCookiePropertyKey: Any] = [ + .name: name, + .value: value, + .domain: domain, + .path: "/", + .expires: expiry, + ] + if isSecure { properties[.secure] = "TRUE" } + if httpOnly { properties[.init("HttpOnly")] = "TRUE" } + if let cookie = HTTPCookie(properties: properties) { + URLSessionManager.cookieStorage.setCookie(cookie) + } + } + } + } else { + URLSessionManager.cookieStorage.removeCookies(since: .distantPast) + } + + guard let groupDefaults = UserDefaults(suiteName: APP_GROUP) else { return } + if headers != groupDefaults.dictionary(forKey: HEADERS_KEY) as? [String: String] { + groupDefaults.set(headers, forKey: HEADERS_KEY) + } + if serverUrls.first != groupDefaults.string(forKey: SERVER_URL_KEY) { + groupDefaults.set(serverUrls.first, forKey: SERVER_URL_KEY) } } } diff --git a/mobile/ios/Runner/Core/URLSessionManager.swift b/mobile/ios/Runner/Core/URLSessionManager.swift index 0f3b7cb6f1..a87e954c98 100644 --- a/mobile/ios/Runner/Core/URLSessionManager.swift +++ b/mobile/ios/Runner/Core/URLSessionManager.swift @@ -3,7 +3,8 @@ import native_video_player let CLIENT_CERT_LABEL = "app.alextran.immich.client_identity" let HEADERS_KEY = "immich.request_headers" -private let APP_GROUP = "group.app.immich.share" +let SERVER_URL_KEY = "immich.server_url" +let APP_GROUP = "group.app.immich.share" /// Manages a shared URLSession with SSL configuration support. class URLSessionManager: NSObject { @@ -11,6 +12,7 @@ class URLSessionManager: NSObject { let session: URLSession let delegate: URLSessionManagerDelegate + static let cookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: APP_GROUP) private let configuration = { let config = URLSessionConfiguration.default @@ -25,14 +27,14 @@ class URLSessionManager: NSObject { directory: cacheDir ) - config.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: APP_GROUP) + config.httpCookieStorage = cookieStorage config.httpMaximumConnectionsPerHost = 64 config.timeoutIntervalForRequest = 60 config.timeoutIntervalForResource = 300 let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown" var headers: [String: String] = ["User-Agent": "Immich_iOS_\(version)"] - if let saved = UserDefaults.standard.dictionary(forKey: HEADERS_KEY) as? [String: String] { + if let saved = UserDefaults(suiteName: APP_GROUP)?.dictionary(forKey: HEADERS_KEY) as? [String: String] { headers.merge(saved) { _, new in new } } config.httpAdditionalHeaders = headers diff --git a/mobile/lib/pages/common/headers_settings.page.dart b/mobile/lib/pages/common/headers_settings.page.dart index 7bd5ad3fdd..22faebd3e8 100644 --- a/mobile/lib/pages/common/headers_settings.page.dart +++ b/mobile/lib/pages/common/headers_settings.page.dart @@ -100,7 +100,7 @@ class HeaderSettingsPage extends HookConsumerWidget { var encoded = jsonEncode(headersMap); await Store.put(StoreKey.customHeaders, encoded); - await networkApi.setRequestHeaders(ApiService.getRequestHeaders()); + await networkApi.setRequestHeaders(ApiService.getRequestHeaders(), ApiService.getServerUrls()); } } diff --git a/mobile/lib/platform/network_api.g.dart b/mobile/lib/platform/network_api.g.dart index 12d00363d1..314a943f7d 100644 --- a/mobile/lib/platform/network_api.g.dart +++ b/mobile/lib/platform/network_api.g.dart @@ -281,7 +281,7 @@ class NetworkApi { } } - Future setRequestHeaders(Map headers) async { + Future setRequestHeaders(Map headers, List serverUrls) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NetworkApi.setRequestHeaders$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( @@ -289,7 +289,7 @@ class NetworkApi { pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([headers]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([headers, serverUrls]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index a09700bb96..20f23dd543 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -91,6 +91,7 @@ class AuthNotifier extends StateNotifier { await _widgetService.clearCredentials(); await _authService.logout(); + await networkApi.setRequestHeaders(const {}, const []); await _ref.read(backgroundUploadServiceProvider).cancel(); _ref.read(foregroundUploadServiceProvider).cancel(); } finally { @@ -125,7 +126,7 @@ class AuthNotifier extends StateNotifier { Future saveAuthInfo({required String accessToken}) async { await _apiService.setAccessToken(accessToken); - await networkApi.setRequestHeaders(ApiService.getRequestHeaders()); + await networkApi.setRequestHeaders(ApiService.getRequestHeaders(), ApiService.getServerUrls()); final serverEndpoint = Store.get(StoreKey.serverEndpoint); final customHeaders = Store.tryGet(StoreKey.customHeaders); diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index 06125bd48d..67db47d4c4 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -174,6 +174,31 @@ class ApiService implements Authentication { } } + static List getServerUrls() { + final urls = []; + final serverEndpoint = Store.tryGet(StoreKey.serverEndpoint); + if (serverEndpoint != null && serverEndpoint.isNotEmpty) { + urls.add(serverEndpoint); + } + final serverUrl = Store.tryGet(StoreKey.serverUrl); + if (serverUrl != null && serverUrl.isNotEmpty) { + urls.add(serverUrl); + } + final localEndpoint = Store.tryGet(StoreKey.localEndpoint); + if (localEndpoint != null && localEndpoint.isNotEmpty) { + urls.add(localEndpoint); + } + final externalJson = Store.tryGet(StoreKey.externalEndpointList); + if (externalJson != null) { + final List list = jsonDecode(externalJson); + for (final entry in list) { + final url = entry['url'] as String?; + if (url != null && url.isNotEmpty) urls.add(url); + } + } + return urls; + } + static Map getRequestHeaders() { var accessToken = Store.get(StoreKey.accessToken, ""); var customHeadersStr = Store.get(StoreKey.customHeaders, ""); diff --git a/mobile/pigeon/network_api.dart b/mobile/pigeon/network_api.dart index 53bc5f23fc..3ea29052d9 100644 --- a/mobile/pigeon/network_api.dart +++ b/mobile/pigeon/network_api.dart @@ -43,5 +43,5 @@ abstract class NetworkApi { int getClientPointer(); - void setRequestHeaders(Map headers); + void setRequestHeaders(Map headers, List serverUrls); }