apply custom headers immediately

This commit is contained in:
mertalev
2026-02-21 15:38:58 -05:00
parent a0009d1772
commit 8b7b36a27a
6 changed files with 74 additions and 47 deletions

View File

@@ -90,12 +90,13 @@ class NetworkApiImpl: NetworkApi {
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 != UserDefaults.group.string(forKey: SERVER_URL_KEY) {
UserDefaults.group.set(serverUrls.first, forKey: SERVER_URL_KEY)
}
if serverUrls.first != groupDefaults.string(forKey: SERVER_URL_KEY) {
groupDefaults.set(serverUrls.first, forKey: SERVER_URL_KEY)
if headers != UserDefaults.group.dictionary(forKey: HEADERS_KEY) as? [String: String] {
UserDefaults.group.set(headers, forKey: HEADERS_KEY)
URLSessionManager.shared.recreateSession() // Recreate session to apply custom headers without app restart
}
}
}

View File

@@ -6,41 +6,34 @@ let HEADERS_KEY = "immich.request_headers"
let SERVER_URL_KEY = "immich.server_url"
let APP_GROUP = "group.app.immich.share"
extension UserDefaults {
static let group = UserDefaults(suiteName: APP_GROUP)!
}
/// Manages a shared URLSession with SSL configuration support.
/// Old sessions are kept alive by Dart's FFI retain until all isolates release them.
class URLSessionManager: NSObject {
static let shared = URLSessionManager()
let session: URLSession
private(set) var session: URLSession
let delegate: URLSessionManagerDelegate
static let cookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: APP_GROUP)
private let configuration = {
let config = URLSessionConfiguration.default
let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
private static let cacheDir: URL = {
let dir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
.first!
.appendingPathComponent("api", isDirectory: true)
try! FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true)
config.urlCache = URLCache(
memoryCapacity: 0,
diskCapacity: 1024 * 1024 * 1024,
directory: cacheDir
)
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(suiteName: APP_GROUP)?.dictionary(forKey: HEADERS_KEY) as? [String: String] {
headers.merge(saved) { _, new in new }
}
config.httpAdditionalHeaders = headers
return config
try! FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir
}()
private static let urlCache = URLCache(
memoryCapacity: 0,
diskCapacity: 1024 * 1024 * 1024,
directory: cacheDir
)
private static let userAgent: String = {
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown"
return "Immich_iOS_\(version)"
}()
static let cookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: APP_GROUP)
var sessionPointer: UnsafeMutableRawPointer {
Unmanaged.passUnretained(session).toOpaque()
@@ -48,9 +41,28 @@ class URLSessionManager: NSObject {
private override init() {
delegate = URLSessionManagerDelegate()
session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)
session = Self.buildSession(delegate: delegate)
super.init()
}
func recreateSession() {
session = Self.buildSession(delegate: delegate)
}
private static func buildSession(delegate: URLSessionManagerDelegate) -> URLSession {
let config = URLSessionConfiguration.default
config.urlCache = urlCache
config.httpCookieStorage = cookieStorage
config.httpMaximumConnectionsPerHost = 64
config.timeoutIntervalForRequest = 60
config.timeoutIntervalForResource = 300
var headers = UserDefaults.group.dictionary(forKey: HEADERS_KEY) as? [String: String] ?? [:]
headers["User-Agent"] = headers["User-Agent"] ?? userAgent
config.httpAdditionalHeaders = headers
return URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
}
}
class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate, URLSessionWebSocketDelegate {

View File

@@ -9,26 +9,37 @@ import 'package:web_socket/web_socket.dart';
class NetworkRepository {
static http.Client? _client;
static late Pointer<Void> _clientPointer;
static Pointer<Void>? _clientPointer;
static Future<void> init() async {
_clientPointer = Pointer<Void>.fromAddress(await networkApi.getClientPointer());
final clientPointer = Pointer<Void>.fromAddress(await networkApi.getClientPointer());
if (clientPointer == _clientPointer) {
return;
}
_clientPointer = clientPointer;
_client?.close();
if (Platform.isIOS) {
final session = URLSession.fromRawPointer(_clientPointer.cast());
final session = URLSession.fromRawPointer(clientPointer.cast());
_client = CupertinoClient.fromSharedSession(session);
} else {
_client = OkHttpClient.fromJniGlobalRef(_clientPointer);
_client = OkHttpClient.fromJniGlobalRef(clientPointer);
}
}
static Future<void> setHeaders(Map<String, String> headers, List<String> serverUrls) async {
await networkApi.setRequestHeaders(headers, serverUrls);
if (Platform.isIOS) {
await init();
}
}
// ignore: avoid-unused-parameters
static Future<WebSocket> createWebSocket(Uri uri, {Map<String, String>? headers, Iterable<String>? protocols}) {
if (Platform.isIOS) {
final session = URLSession.fromRawPointer(_clientPointer.cast());
final session = URLSession.fromRawPointer(_clientPointer!.cast());
return CupertinoWebSocket.connectWithSession(session, uri, protocols: protocols);
} else {
return OkHttpWebSocket.connectFromJniGlobalRef(_clientPointer, uri, protocols: protocols);
return OkHttpWebSocket.connectFromJniGlobalRef(_clientPointer!, uri, protocols: protocols);
}
}

View File

@@ -8,8 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/providers/api.provider.dart';
class SettingsHeader {
String key = "";
@@ -76,7 +75,7 @@ class HeaderSettingsPage extends HookConsumerWidget {
],
),
body: PopScope(
onPopInvokedWithResult: (didPop, _) => saveHeaders(headers.value),
onPopInvokedWithResult: (didPop, _) => saveHeaders(ref, headers.value),
child: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
itemCount: list.length,
@@ -88,7 +87,7 @@ class HeaderSettingsPage extends HookConsumerWidget {
);
}
saveHeaders(List<SettingsHeader> headers) async {
saveHeaders(WidgetRef ref, List<SettingsHeader> headers) async {
final headersMap = {};
for (var header in headers) {
final key = header.key.trim();
@@ -100,7 +99,7 @@ class HeaderSettingsPage extends HookConsumerWidget {
var encoded = jsonEncode(headersMap);
await Store.put(StoreKey.customHeaders, encoded);
await networkApi.setRequestHeaders(ApiService.getRequestHeaders(), ApiService.getServerUrls());
await ref.read(apiServiceProvider).updateHeaders();
}
}

View File

@@ -8,7 +8,6 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/models/auth/auth_state.model.dart';
import 'package:immich_mobile/models/auth/login_response.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/auth.service.dart';
@@ -91,7 +90,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
await _widgetService.clearCredentials();
await _authService.logout();
await networkApi.setRequestHeaders(const {}, const []);
await _apiService.updateHeaders();
await _ref.read(backgroundUploadServiceProvider).cancel();
_ref.read(foregroundUploadServiceProvider).cancel();
} finally {
@@ -126,7 +125,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
Future<bool> saveAuthInfo({required String accessToken}) async {
await _apiService.setAccessToken(accessToken);
await networkApi.setRequestHeaders(ApiService.getRequestHeaders(), ApiService.getServerUrls());
await _apiService.updateHeaders();
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final customHeaders = Store.tryGet(StoreKey.customHeaders);

View File

@@ -48,6 +48,11 @@ class ApiService implements Authentication {
String? _accessToken;
final _log = Logger("ApiService");
Future<void> updateHeaders() async {
await NetworkRepository.setHeaders(getRequestHeaders(), getServerUrls());
_apiClient.client = NetworkRepository.client;
}
setEndpoint(String endpoint) {
_apiClient = ApiClient(basePath: endpoint, authentication: this);
_apiClient.client = NetworkRepository.client;