NCNetworking.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. //
  2. // NCNetworking.swift
  3. // Nextcloud
  4. //
  5. // Created by Marino Faggiana on 23/10/19.
  6. // Copyright © 2019 Marino Faggiana. All rights reserved.
  7. //
  8. // Author Marino Faggiana <marino.faggiana@nextcloud.com>
  9. //
  10. // This program is free software: you can redistribute it and/or modify
  11. // it under the terms of the GNU General Public License as published by
  12. // the Free Software Foundation, either version 3 of the License, or
  13. // (at your option) any later version.
  14. //
  15. // This program is distributed in the hope that it will be useful,
  16. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. // GNU General Public License for more details.
  19. //
  20. // You should have received a copy of the GNU General Public License
  21. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. //
  23. import UIKit
  24. import OpenSSL
  25. import NextcloudKit
  26. import Alamofire
  27. import Queuer
  28. #if EXTENSION_FILE_PROVIDER_EXTENSION || EXTENSION_WIDGET
  29. @objc protocol uploadE2EEDelegate: AnyObject { }
  30. #endif
  31. @objc protocol NCNetworkingDelegate {
  32. func downloadProgress(_ progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String, session: URLSession, task: URLSessionTask)
  33. func uploadProgress(_ progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String, session: URLSession, task: URLSessionTask)
  34. func downloadComplete(fileName: String, serverUrl: String, etag: String?, date: Date?, dateLastModified: Date?, length: Int64, task: URLSessionTask, error: NKError)
  35. func uploadComplete(fileName: String, serverUrl: String, ocId: String?, etag: String?, date: Date?, size: Int64, task: URLSessionTask, error: NKError)
  36. }
  37. @objc protocol ClientCertificateDelegate {
  38. func onIncorrectPassword()
  39. func didAskForClientCertificate()
  40. }
  41. @objcMembers
  42. class NCNetworking: NSObject, NextcloudKitDelegate {
  43. public static let shared: NCNetworking = {
  44. let instance = NCNetworking()
  45. NotificationCenter.default.addObserver(instance, selector: #selector(applicationDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
  46. return instance
  47. }()
  48. struct FileNameServerUrl: Hashable {
  49. var fileName: String
  50. var serverUrl: String
  51. }
  52. let sessionDownload = NextcloudKit.shared.nkCommonInstance.identifierSessionDownload
  53. let sessionDownloadBackground = NextcloudKit.shared.nkCommonInstance.identifierSessionDownloadBackground
  54. let sessionUpload = NextcloudKit.shared.nkCommonInstance.identifierSessionUpload
  55. let sessionUploadBackground = NextcloudKit.shared.nkCommonInstance.identifierSessionUploadBackground
  56. let sessionUploadBackgroundWWan = NextcloudKit.shared.nkCommonInstance.identifierSessionUploadBackgroundWWan
  57. let sessionUploadBackgroundExt = NextcloudKit.shared.nkCommonInstance.identifierSessionUploadBackgroundExt
  58. let utilityFileSystem = NCUtilityFileSystem()
  59. let utility = NCUtility()
  60. let database = NCManageDatabase.shared
  61. let global = NCGlobal.shared
  62. var requestsUnifiedSearch: [DataRequest] = []
  63. var lastReachability: Bool = true
  64. var networkReachability: NKCommon.TypeReachability?
  65. weak var delegate: NCNetworkingDelegate?
  66. weak var certificateDelegate: ClientCertificateDelegate?
  67. var p12Data: Data?
  68. var p12Password: String?
  69. var tapHudStopDelete = false
  70. var isOffline: Bool {
  71. return networkReachability == NKCommon.TypeReachability.notReachable || networkReachability == NKCommon.TypeReachability.unknown
  72. }
  73. var isOnline: Bool {
  74. return networkReachability == NKCommon.TypeReachability.reachableEthernetOrWiFi || networkReachability == NKCommon.TypeReachability.reachableCellular
  75. }
  76. // OPERATIONQUEUE
  77. let downloadThumbnailQueue = Queuer(name: "downloadThumbnailQueue", maxConcurrentOperationCount: 10, qualityOfService: .default)
  78. let downloadThumbnailActivityQueue = Queuer(name: "downloadThumbnailActivityQueue", maxConcurrentOperationCount: 10, qualityOfService: .default)
  79. let downloadThumbnailTrashQueue = Queuer(name: "downloadThumbnailTrashQueue", maxConcurrentOperationCount: 10, qualityOfService: .default)
  80. let unifiedSearchQueue = Queuer(name: "unifiedSearchQueue", maxConcurrentOperationCount: 1, qualityOfService: .default)
  81. let saveLivePhotoQueue = Queuer(name: "saveLivePhotoQueue", maxConcurrentOperationCount: 1, qualityOfService: .default)
  82. let downloadQueue = Queuer(name: "downloadQueue", maxConcurrentOperationCount: NCBrandOptions.shared.maxConcurrentOperationDownload, qualityOfService: .default)
  83. let downloadAvatarQueue = Queuer(name: "downloadAvatarQueue", maxConcurrentOperationCount: 10, qualityOfService: .default)
  84. let fileExistsQueue = Queuer(name: "fileExistsQueue", maxConcurrentOperationCount: 10, qualityOfService: .default)
  85. let deleteFileOrFolderQueue = Queuer(name: "deleteFileOrFolderQueue", maxConcurrentOperationCount: 10, qualityOfService: .default)
  86. // MARK: - init
  87. override init() {
  88. super.init()
  89. if let account = database.getActiveTableAccount()?.account {
  90. getActiveAccountCertificate(account: account)
  91. }
  92. NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: global.notificationCenterChangeUser), object: nil, queue: nil) { notification in
  93. if let userInfo = notification.userInfo {
  94. if let account = userInfo["account"] as? String {
  95. self.getActiveAccountCertificate(account: account)
  96. }
  97. }
  98. }
  99. }
  100. // MARK: - NotificationCenter
  101. func applicationDidEnterBackground() {
  102. NCTransferProgress.shared.clearAllCountError()
  103. }
  104. // MARK: - Communication Delegate
  105. func networkReachabilityObserver(_ typeReachability: NKCommon.TypeReachability) {
  106. if typeReachability == NKCommon.TypeReachability.reachableCellular || typeReachability == NKCommon.TypeReachability.reachableEthernetOrWiFi {
  107. lastReachability = true
  108. } else {
  109. if lastReachability {
  110. let error = NKError(errorCode: global.errorNetworkNotAvailable, errorDescription: "")
  111. NCContentPresenter().messageNotification("_network_not_available_", error: error, delay: global.dismissAfterSecond, type: NCContentPresenter.messageType.info)
  112. }
  113. lastReachability = false
  114. }
  115. networkReachability = typeReachability
  116. }
  117. func authenticationChallenge(_ session: URLSession,
  118. didReceive challenge: URLAuthenticationChallenge,
  119. completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
  120. if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {
  121. if let p12Data = self.p12Data,
  122. let cert = (p12Data, self.p12Password) as? UserCertificate,
  123. let pkcs12 = try? PKCS12(pkcs12Data: cert.data, password: cert.password, onIncorrectPassword: {
  124. self.certificateDelegate?.onIncorrectPassword()
  125. }) {
  126. let creds = PKCS12.urlCredential(for: pkcs12)
  127. completionHandler(URLSession.AuthChallengeDisposition.useCredential, creds)
  128. } else {
  129. self.certificateDelegate?.didAskForClientCertificate()
  130. completionHandler(URLSession.AuthChallengeDisposition.performDefaultHandling, nil)
  131. }
  132. } else {
  133. self.checkTrustedChallenge(session, didReceive: challenge, completionHandler: completionHandler)
  134. }
  135. }
  136. func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
  137. #if !EXTENSION
  138. if let appDelegate = UIApplication.shared.delegate as? AppDelegate, let completionHandler = appDelegate.backgroundSessionCompletionHandler {
  139. NextcloudKit.shared.nkCommonInstance.writeLog("[INFO] Called urlSessionDidFinishEvents for Background URLSession")
  140. appDelegate.backgroundSessionCompletionHandler = nil
  141. completionHandler()
  142. }
  143. #endif
  144. }
  145. func request<Value>(_ request: Alamofire.DataRequest, didParseResponse response: Alamofire.AFDataResponse<Value>) {
  146. /// GLOBAL RESPONSE ERROR
  147. if let statusCode = response.response?.statusCode {
  148. switch statusCode {
  149. case NCGlobal.shared.errorMaintenance:
  150. if let errorDescription = NKError.getErrorDescription(for: statusCode) {
  151. let error = NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: errorDescription)
  152. NCContentPresenter().showWarning(error: error, priority: .max)
  153. }
  154. default:
  155. break
  156. }
  157. }
  158. }
  159. // MARK: -
  160. func cancelAllQueue() {
  161. downloadQueue.cancelAll()
  162. downloadThumbnailQueue.cancelAll()
  163. downloadThumbnailActivityQueue.cancelAll()
  164. downloadThumbnailTrashQueue.cancelAll()
  165. downloadAvatarQueue.cancelAll()
  166. unifiedSearchQueue.cancelAll()
  167. saveLivePhotoQueue.cancelAll()
  168. fileExistsQueue.cancelAll()
  169. }
  170. // MARK: - Pinning check
  171. public func checkTrustedChallenge(_ session: URLSession,
  172. didReceive challenge: URLAuthenticationChallenge,
  173. completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
  174. let protectionSpace: URLProtectionSpace = challenge.protectionSpace
  175. let directoryCertificate = utilityFileSystem.directoryCertificates
  176. let host = challenge.protectionSpace.host
  177. let certificateSavedPath = directoryCertificate + "/" + host + ".der"
  178. var isTrusted: Bool
  179. if let trust: SecTrust = protectionSpace.serverTrust,
  180. let certificates = (SecTrustCopyCertificateChain(trust) as? [SecCertificate]),
  181. let certificate = certificates.first {
  182. // extarct certificate txt
  183. saveX509Certificate(certificate, host: host, directoryCertificate: directoryCertificate)
  184. let isServerTrusted = SecTrustEvaluateWithError(trust, nil)
  185. let certificateCopyData = SecCertificateCopyData(certificate)
  186. let data = CFDataGetBytePtr(certificateCopyData)
  187. let size = CFDataGetLength(certificateCopyData)
  188. let certificateData = NSData(bytes: data, length: size)
  189. certificateData.write(toFile: directoryCertificate + "/" + host + ".tmp", atomically: true)
  190. if isServerTrusted {
  191. isTrusted = true
  192. } else if let certificateDataSaved = NSData(contentsOfFile: certificateSavedPath), certificateData.isEqual(to: certificateDataSaved as Data) {
  193. isTrusted = true
  194. } else {
  195. isTrusted = false
  196. }
  197. } else {
  198. isTrusted = false
  199. }
  200. if isTrusted {
  201. completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
  202. } else {
  203. #if !EXTENSION
  204. DispatchQueue.main.async { (UIApplication.shared.delegate as? AppDelegate)?.trustCertificateError(host: host) }
  205. #endif
  206. completionHandler(URLSession.AuthChallengeDisposition.performDefaultHandling, nil)
  207. }
  208. }
  209. func writeCertificate(host: String) {
  210. let directoryCertificate = utilityFileSystem.directoryCertificates
  211. let certificateAtPath = directoryCertificate + "/" + host + ".tmp"
  212. let certificateToPath = directoryCertificate + "/" + host + ".der"
  213. if !utilityFileSystem.copyFile(atPath: certificateAtPath, toPath: certificateToPath) {
  214. NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Write certificare error")
  215. }
  216. }
  217. func saveX509Certificate(_ certificate: SecCertificate, host: String, directoryCertificate: String) {
  218. let certNamePathTXT = directoryCertificate + "/" + host + ".txt"
  219. let data: CFData = SecCertificateCopyData(certificate)
  220. let mem = BIO_new_mem_buf(CFDataGetBytePtr(data), Int32(CFDataGetLength(data)))
  221. let x509cert = d2i_X509_bio(mem, nil)
  222. if x509cert == nil {
  223. NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] OpenSSL couldn't parse X509 Certificate")
  224. } else {
  225. // save details
  226. if FileManager.default.fileExists(atPath: certNamePathTXT) {
  227. do {
  228. try FileManager.default.removeItem(atPath: certNamePathTXT)
  229. } catch { }
  230. }
  231. let fileCertInfo = fopen(certNamePathTXT, "w")
  232. if fileCertInfo != nil {
  233. let output = BIO_new_fp(fileCertInfo, BIO_NOCLOSE)
  234. X509_print_ex(output, x509cert, UInt(XN_FLAG_COMPAT), UInt(X509_FLAG_COMPAT))
  235. BIO_free(output)
  236. }
  237. fclose(fileCertInfo)
  238. X509_free(x509cert)
  239. }
  240. BIO_free(mem)
  241. }
  242. func checkPushNotificationServerProxyCertificateUntrusted(viewController: UIViewController?,
  243. completion: @escaping (_ error: NKError) -> Void) {
  244. guard let host = URL(string: NCBrandOptions.shared.pushNotificationServerProxy)?.host else { return }
  245. NextcloudKit.shared.checkServer(serverUrl: NCBrandOptions.shared.pushNotificationServerProxy) { _, error in
  246. guard error == .success else {
  247. completion(.success)
  248. return
  249. }
  250. if error == .success {
  251. self.writeCertificate(host: host)
  252. completion(error)
  253. } else if error.errorCode == NSURLErrorServerCertificateUntrusted {
  254. let alertController = UIAlertController(title: NSLocalizedString("_ssl_certificate_untrusted_", comment: ""), message: NSLocalizedString("_connect_server_anyway_", comment: ""), preferredStyle: .alert)
  255. alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .default, handler: { _ in
  256. self.writeCertificate(host: host)
  257. completion(.success)
  258. }))
  259. alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", comment: ""), style: .default, handler: { _ in
  260. completion(error)
  261. }))
  262. alertController.addAction(UIAlertAction(title: NSLocalizedString("_certificate_details_", comment: ""), style: .default, handler: { _ in
  263. if let navigationController = UIStoryboard(name: "NCViewCertificateDetails", bundle: nil).instantiateInitialViewController() as? UINavigationController,
  264. let vcCertificateDetails = navigationController.topViewController as? NCViewCertificateDetails {
  265. vcCertificateDetails.host = host
  266. viewController?.present(navigationController, animated: true)
  267. }
  268. }))
  269. viewController?.present(alertController, animated: true)
  270. }
  271. }
  272. }
  273. private func getActiveAccountCertificate(account: String) {
  274. (self.p12Data, self.p12Password) = NCKeychain().getClientCertificate(account: account)
  275. }
  276. // MARK: - User Default Data Request
  277. func isResponseDataChanged<T>(account: String, responseData: AFDataResponse<T>?) -> Bool {
  278. guard let responseData,
  279. let request = responseData.request else { return true }
  280. let key = getResponseDataKey(account: account, request: request)
  281. let retrievedData = UserDefaults.standard.data(forKey: key)
  282. switch responseData.result {
  283. case .success(let data):
  284. if let data = data as? Data {
  285. if retrievedData != data, let request = responseData.request {
  286. let key = getResponseDataKey(account: account, request: request)
  287. UserDefaults.standard.set(data, forKey: key)
  288. }
  289. return retrievedData != data
  290. } else {
  291. return true
  292. }
  293. case .failure(let error):
  294. print("Errore: \(error.localizedDescription)")
  295. return true
  296. }
  297. }
  298. func removeAllKeyUserDefaultsData(account: String?) {
  299. let userDefaults = UserDefaults.standard
  300. for key in userDefaults.dictionaryRepresentation().keys {
  301. if let account {
  302. if key.hasPrefix(account) {
  303. userDefaults.removeObject(forKey: key)
  304. }
  305. } else {
  306. userDefaults.removeObject(forKey: key)
  307. }
  308. }
  309. }
  310. private func getResponseDataKey(account: String, request: URLRequest) -> String {
  311. let depth = request.allHTTPHeaderFields?["Depth"] ?? "none"
  312. let key = account + "|" + (request.url?.absoluteString ?? "") + "|Depth=\(depth)|" + (request.httpMethod ?? "")
  313. return key
  314. }
  315. }