From 0720ac84c6247b841c7e8b6c2dcaeb23d24295ab Mon Sep 17 00:00:00 2001 From: Michael-128 Date: Sun, 15 Jun 2025 21:07:24 +0200 Subject: [PATCH] Add 'Advanced' page to 'Add Server' view & Add basic authentication --- .../Localizations/Localizable.xcstrings | 3 ++ qBitControl.xcodeproj/project.pbxproj | 4 ++ qBitControl/Classes/AuthClass.swift | 9 +++- qBitControl/Classes/ServersHelper.swift | 4 +- qBitControl/Classes/qBitRequestClass.swift | 52 +++++++++++++------ qBitControl/Models/Server.swift | 18 ++++++- .../ServersView/ServerAddViewModel.swift | 4 +- .../Views/ServersViews/ServerAddView.swift | 8 +++ .../ServersViews/ServerAdvancedView.swift | 51 ++++++++++++++++++ 9 files changed, 132 insertions(+), 21 deletions(-) create mode 100644 qBitControl/Views/ServersViews/ServerAdvancedView.swift diff --git a/Localization/Localizations/Localizable.xcstrings b/Localization/Localizations/Localizable.xcstrings index 2e4409d..d2231c7 100644 --- a/Localization/Localizations/Localizable.xcstrings +++ b/Localization/Localizations/Localizable.xcstrings @@ -1368,6 +1368,9 @@ } } } + }, + "Basic Authentication" : { + }, "Books" : { "extractionState" : "manual", diff --git a/qBitControl.xcodeproj/project.pbxproj b/qBitControl.xcodeproj/project.pbxproj index f8e3fcf..cb1fc62 100644 --- a/qBitControl.xcodeproj/project.pbxproj +++ b/qBitControl.xcodeproj/project.pbxproj @@ -86,6 +86,7 @@ 90BCFE8D2DB4262400A54003 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 90BCFE8C2DB4262400A54003 /* Localizable.xcstrings */; }; 90DCA7E52B51C393008A9C1B /* ServersHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90DCA7E42B51C393008A9C1B /* ServersHelper.swift */; }; 90E1D0542D30438F00B81F12 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90E1D0532D30438C00B81F12 /* Version.swift */; }; + 90E6DD6F2DFF407000BC1D76 /* ServerAdvancedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90E6DD6E2DFF406A00BC1D76 /* ServerAdvancedView.swift */; }; 90E86FFE2C81C89A00F4EA01 /* qBitDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90E86FFD2C81C89A00F4EA01 /* qBitDataClass.swift */; }; 90EF6A492909267A001E9E7F /* AuthClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90EF6A482909267A001E9E7F /* AuthClass.swift */; }; 90EF6A4D29093142001E9E7F /* qBitRequestClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90EF6A4C29093142001E9E7F /* qBitRequestClass.swift */; }; @@ -194,6 +195,7 @@ 90BCFE8C2DB4262400A54003 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 90DCA7E42B51C393008A9C1B /* ServersHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersHelper.swift; sourceTree = ""; }; 90E1D0532D30438C00B81F12 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; + 90E6DD6E2DFF406A00BC1D76 /* ServerAdvancedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerAdvancedView.swift; sourceTree = ""; }; 90E86FFD2C81C89A00F4EA01 /* qBitDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = qBitDataClass.swift; sourceTree = ""; }; 90EF6A482909267A001E9E7F /* AuthClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthClass.swift; sourceTree = ""; }; 90EF6A4C29093142001E9E7F /* qBitRequestClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = qBitRequestClass.swift; sourceTree = ""; }; @@ -315,6 +317,7 @@ 90726B382CDA22190032993E /* ServersViews */ = { isa = PBXGroup; children = ( + 90E6DD6E2DFF406A00BC1D76 /* ServerAdvancedView.swift */, 90726B352CDA22190032993E /* ServerAddView.swift */, 90726B362CDA22190032993E /* ServerRowView.swift */, 90726B372CDA22190032993E /* ServersView.swift */, @@ -649,6 +652,7 @@ 907C632A2DAEDACA004B1F41 /* SearchStartResult.swift in Sources */, 90726B652CDA22190032993E /* PeersRowView.swift in Sources */, 90726B662CDA22190032993E /* PeersView.swift in Sources */, + 90E6DD6F2DFF407000BC1D76 /* ServerAdvancedView.swift in Sources */, 90726B672CDA22190032993E /* TrackerRow.swift in Sources */, 907023322CDA62FD007B2199 /* TorrentListView.swift in Sources */, 90726B682CDA22190032993E /* TrackersView.swift in Sources */, diff --git a/qBitControl/Classes/AuthClass.swift b/qBitControl/Classes/AuthClass.swift index 31e19b4..cb34ef0 100644 --- a/qBitControl/Classes/AuthClass.swift +++ b/qBitControl/Classes/AuthClass.swift @@ -16,7 +16,7 @@ class Auth { cookies[id] = cookie } - static func getCookie(url: String, username: String, password: String, isSuccess: @escaping (Bool) -> Void, setCookie: Bool = true) async { + static func getCookie(url: String, username: String, password: String, basicAuth: Server.BasicAuth? = nil, isSuccess: @escaping (Bool) -> Void, setCookie: Bool = true) async { let urlString = url guard let url = URL(string: "\(url)/api/v2/auth/login") else { return } @@ -37,6 +37,12 @@ class Auth { let sessionConfiguration = URLSessionConfiguration.default sessionConfiguration.timeoutIntervalForRequest = 10 + if let basicAuth = basicAuth { + sessionConfiguration.httpAdditionalHeaders = [ + "Authorization": "Basic \(basicAuth.getAuthString())" + ] + } + let session = URLSession(configuration: sessionConfiguration) await session.reset() @@ -48,6 +54,7 @@ class Auth { if setCookie { qBittorrent.setURL(url: urlString) qBittorrent.setCookie(cookie: cookie) + qBitRequest.setBasicAuth(auth: basicAuth) } isSuccess(true) } else { diff --git a/qBitControl/Classes/ServersHelper.swift b/qBitControl/Classes/ServersHelper.swift index 81b499a..8be8bf9 100644 --- a/qBitControl/Classes/ServersHelper.swift +++ b/qBitControl/Classes/ServersHelper.swift @@ -96,7 +96,7 @@ class ServersHelper: ObservableObject { func checkConnection(server: Server, result: @escaping (Bool) -> Void) { Task { - await Auth.getCookie(url: server.url, username: server.username, password: server.password, isSuccess: { + await Auth.getCookie(url: server.url, username: server.username, password: server.password, basicAuth: server.basicAuth, isSuccess: { success in result(success); }, setCookie: false) @@ -107,7 +107,7 @@ class ServersHelper: ObservableObject { connectingServerId = server.id Task { - await Auth.getCookie(url: server.url, username: server.username, password: server.password, isSuccess: { + await Auth.getCookie(url: server.url, username: server.username, password: server.password, basicAuth: server.basicAuth, isSuccess: { success in DispatchQueue.main.async { if let result = result { diff --git a/qBitControl/Classes/qBitRequestClass.swift b/qBitControl/Classes/qBitRequestClass.swift index 87349d2..d60b30d 100644 --- a/qBitControl/Classes/qBitRequestClass.swift +++ b/qBitControl/Classes/qBitRequestClass.swift @@ -7,6 +7,26 @@ import Foundation class qBitRequest { + static private var basicAuth: Server.BasicAuth? + + static func setBasicAuth(auth: Server.BasicAuth?) { + self.basicAuth = auth + } + + private static func getSession() -> URLSession { + let configuration = URLSessionConfiguration.default + + if let basicAuth = qBitRequest.basicAuth { + // Set the auth header + configuration.httpAdditionalHeaders = [ + "Authorization": "Basic \(basicAuth.getAuthString())" + ] + } + + // Return a new session with this configuration + return URLSession(configuration: configuration) + } + static func prepareURLRequest(path: String, queryItems: [URLQueryItem]) -> URLRequest { let cookie = qBittorrent.getCookie() let url = qBittorrent.getURL() @@ -51,7 +71,7 @@ class qBitRequest { } static func requestTorrentListJSON(request: URLRequest, completionHandler: @escaping ([Torrent]) -> Void) { - URLSession.shared.dataTask(with: request) { + self.getSession().dataTask(with: request) { data, response, error in if let data = data { do { @@ -65,13 +85,13 @@ class qBitRequest { } static func requestUniversal(request: URLRequest) { - URLSession.shared.dataTask(with: request) { + self.getSession().dataTask(with: request) { data, response, error in }.resume() } static func requestTorrentManagement(request: URLRequest, statusCode: @escaping (Int?) -> Void) { - URLSession.shared.dataTask(with: request) { + self.getSession().dataTask(with: request) { data, response, error in if let response = response as? HTTPURLResponse { statusCode(response.statusCode) @@ -82,7 +102,7 @@ class qBitRequest { } static func requestPreferencesJSON(request: URLRequest, completionHandler: @escaping (qBitPreferences) -> Void) { - URLSession.shared.dataTask(with: request) { + self.getSession().dataTask(with: request) { data, response, error in if let data = data { do { @@ -96,7 +116,7 @@ class qBitRequest { } static func requestSearchStart(request: URLRequest, completionHandler: @escaping (SearchStartResult) -> Void) { - URLSession.shared.dataTask(with: request) { + self.getSession().dataTask(with: request) { data, response, error in if let data = data { do { @@ -110,7 +130,7 @@ class qBitRequest { } static func requestSearchResults(request: URLRequest, completionHandler: @escaping (SearchResponse) -> Void) { - URLSession.shared.dataTask(with: request) { + self.getSession().dataTask(with: request) { data, response, error in if let data = data { do { @@ -124,7 +144,7 @@ class qBitRequest { } static func requestSearchPlugins(request: URLRequest, completionHandler: @escaping ([SearchPlugin]) -> Void) { - URLSession.shared.dataTask(with: request) { + self.getSession().dataTask(with: request) { data, response, error in if let data = data { do { @@ -138,7 +158,7 @@ class qBitRequest { } static func requestGlobalTransferInfo(request: URLRequest, completionHandler: @escaping (GlobalTransferInfo) -> Void) { - URLSession.shared.dataTask(with: request) { + self.getSession().dataTask(with: request) { data, response, error in if let data = data { do { @@ -152,7 +172,7 @@ class qBitRequest { } static func requestMainData(request: URLRequest, completionHandler: @escaping (MainData) -> Void) { - URLSession.shared.dataTask(with: request) { + self.getSession().dataTask(with: request) { data, response, error in if let data = data { do { @@ -167,7 +187,7 @@ class qBitRequest { static func requestPeersJSON(request: URLRequest, completionHandler: @escaping (Peers) -> Void) { - URLSession.shared.dataTask(with: request) { + self.getSession().dataTask(with: request) { data, response, error in if let data = data { do { @@ -181,7 +201,7 @@ class qBitRequest { } static func requestTrackersJSON(request: URLRequest, completionHandler: @escaping ([Tracker]) -> Void) { - URLSession.shared.dataTask(with: request) { + self.getSession().dataTask(with: request) { data, response, error in if let data = data { do { @@ -195,7 +215,7 @@ class qBitRequest { } static func requestFilesJSON(request: URLRequest, completionHandler: @escaping ([File]) -> Void) { - URLSession.shared.dataTask(with: request) { + self.getSession().dataTask(with: request) { data, response, error in if let data = data { do { @@ -209,7 +229,7 @@ class qBitRequest { } static func requestCategoriesJSON(request: URLRequest, completionHandler: @escaping ([String: Category]) -> Void) { - URLSession.shared.dataTask(with: request) { + self.getSession().dataTask(with: request) { data, response, error in if let data = data { do { @@ -223,7 +243,7 @@ class qBitRequest { } static func requestVersion(request: URLRequest, completionHandler: @escaping (Version) -> Void) { - URLSession.shared.dataTask(with: request) { + self.getSession().dataTask(with: request) { data, response, error in if let data = data, let versionData = String(data: data, encoding: .utf8) { let versionString = versionData.filter { "0123456789.".contains($0) } @@ -237,7 +257,7 @@ class qBitRequest { } static func requestTagsJSON(request: URLRequest, completionHandler: @escaping ([String]) -> Void) { - URLSession.shared.dataTask(with: request) { + self.getSession().dataTask(with: request) { data, response, error in if let data = data { do { @@ -251,7 +271,7 @@ class qBitRequest { } static func requestRSSFeedJSON(request: URLRequest, completion: @escaping (RSSNode) -> Void) { - URLSession.shared.dataTask(with: request) { + self.getSession().dataTask(with: request) { data, response, error in if let data = data { diff --git a/qBitControl/Models/Server.swift b/qBitControl/Models/Server.swift index 3bde497..2484fd7 100644 --- a/qBitControl/Models/Server.swift +++ b/qBitControl/Models/Server.swift @@ -9,4 +9,20 @@ struct Server: Codable, Identifiable { let url: String let username: String let password: String -} \ No newline at end of file + let basicAuth: BasicAuth? + + struct BasicAuth: Codable { + let username: String + let password: String + + init(_ username: String, _ password: String) { + self.username = username + self.password = password + } + + func getAuthString() -> String { + let authString = "\(username):\(password)".data(using: .utf8)! + return authString.base64EncodedString() + } + } +} diff --git a/qBitControl/ViewModels/ServersView/ServerAddViewModel.swift b/qBitControl/ViewModels/ServersView/ServerAddViewModel.swift index ea6ddc0..e9052ab 100644 --- a/qBitControl/ViewModels/ServersView/ServerAddViewModel.swift +++ b/qBitControl/ViewModels/ServersView/ServerAddViewModel.swift @@ -14,6 +14,7 @@ class ServerAddViewModel: ObservableObject { @Published var url = "" @Published var username = "" @Published var password = "" + @Published var basicAuth: Server.BasicAuth? @Published var isCheckConnection = true; @Published var isInvalidAlert = false; @@ -34,6 +35,7 @@ class ServerAddViewModel: ObservableObject { url = server.url username = server.username password = server.password + basicAuth = server.basicAuth } } @@ -83,7 +85,7 @@ class ServerAddViewModel: ObservableObject { if(!validateInputs()) { return; } if(!validateIsConnecting()) { return; } - let server = Server(name: friendlyName, url: url, username: username, password: password) + let server = Server(name: friendlyName, url: url, username: username, password: password, basicAuth: basicAuth) if(!isCheckConnection) { if let editServerId = self.editServerId { serversHelper.removeServer(id: editServerId) } diff --git a/qBitControl/Views/ServersViews/ServerAddView.swift b/qBitControl/Views/ServersViews/ServerAddView.swift index e2debe1..e78406a 100644 --- a/qBitControl/Views/ServersViews/ServerAddView.swift +++ b/qBitControl/Views/ServersViews/ServerAddView.swift @@ -36,6 +36,14 @@ struct ServerAddView: View { .autocorrectionDisabled() } + Section { + NavigationLink { + ServerAdvancedView(basicAuth: $viewModel.basicAuth) + } label: { + Text("Advanced") + } + } + Section { Toggle(isOn: $viewModel.isCheckConnection, label: { Text("Check Connection") diff --git a/qBitControl/Views/ServersViews/ServerAdvancedView.swift b/qBitControl/Views/ServersViews/ServerAdvancedView.swift new file mode 100644 index 0000000..862bd32 --- /dev/null +++ b/qBitControl/Views/ServersViews/ServerAdvancedView.swift @@ -0,0 +1,51 @@ +import SwiftUI + +struct ServerAdvancedView: View { + @Binding var basicAuth: Server.BasicAuth? + + @State var isBasicAuthEnabled: Bool = false + @State var username = "" + @State var password = "" + + var body: some View { + List { + Section(header: Text("Basic Authentication")) { + self.basicAuthView() + } + }.onAppear { self.restoreValues() } + } + + @ViewBuilder + private func basicAuthView() -> some View { + Toggle("Basic Authentication", isOn: self.$isBasicAuthEnabled) + .onChange(of: isBasicAuthEnabled) { _ in self.onChangeHandler() } + + if(self.isBasicAuthEnabled) { + TextField("Username", text: self.$username) + .disableAutocorrection(true) + .autocapitalization(.none) + .onChange(of: username) { _ in self.onChangeHandler() } + SecureField("Password", text: self.$password) + .onChange(of: password) { _ in self.onChangeHandler() } + } + } + + private func restoreValues() { + if let basicAuth = self.basicAuth { + self.isBasicAuthEnabled = true + self.username = basicAuth.username + self.password = basicAuth.password + } + } + + private func onChangeHandler() { + if(self.isBasicAuthEnabled == false) { + self.basicAuth = nil + return + } + + if(!username.isEmpty && !password.isEmpty) { + self.basicAuth = Server.BasicAuth(username, password) + } + } +}