From d4b692650325e4f2a7689f10e87864971a8a531a Mon Sep 17 00:00:00 2001 From: Viktor Gubriienko <814362+vGubriienko@users.noreply.github.com> Date: Mon, 20 Dec 2021 03:25:40 +0200 Subject: [PATCH] Refactored file storing. Other minor refactoring --- netfox.xcodeproj/project.pbxproj | 4 +- .../xcschemes/netfox_ios.xcscheme | 6 +- .../xcschemes/netfox_osx.xcscheme | 6 +- netfox/Core/NFX.swift | 33 ++--- netfox/Core/NFXHTTPModel.swift | 125 +++++++---------- netfox/Core/NFXHTTPModelManager.swift | 19 +-- netfox/Core/NFXHelper.swift | 130 +++++++++++++++--- netfox/iOS/NFXDetailsController_iOS.swift | 11 +- netfox/iOS/NFXListController_iOS.swift | 11 +- netfox/iOS/NFXSettingsController_iOS.swift | 4 +- 10 files changed, 195 insertions(+), 154 deletions(-) diff --git a/netfox.xcodeproj/project.pbxproj b/netfox.xcodeproj/project.pbxproj index db93ff3e..4dd6492f 100644 --- a/netfox.xcodeproj/project.pbxproj +++ b/netfox.xcodeproj/project.pbxproj @@ -350,7 +350,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0720; - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1310; ORGANIZATIONNAME = kasketis; TargetAttributes = { 8229AD611F8FB34300A9D613 = { @@ -606,6 +606,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -665,6 +666,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; diff --git a/netfox.xcodeproj/xcshareddata/xcschemes/netfox_ios.xcscheme b/netfox.xcodeproj/xcshareddata/xcschemes/netfox_ios.xcscheme index 210f7126..5b1cd797 100644 --- a/netfox.xcodeproj/xcshareddata/xcschemes/netfox_ios.xcscheme +++ b/netfox.xcodeproj/xcshareddata/xcschemes/netfox_ios.xcscheme @@ -1,6 +1,6 @@ - - - - - - - - Data? { - var data: Data? = nil - if let sessionLogData = try? Data(contentsOf: URL(fileURLWithPath: NFXPath.SessionLog)) { - data = sessionLogData - } - - return data + return try? Data(contentsOf: NFXPath.sessionLogURL) } @objc open func ignoreURLs(_ urls: [String]) { @@ -227,17 +223,8 @@ open class NFX: NSObject { internal func clearOldData() { NFXHTTPModelManager.sharedInstance.clear() - do { - let documentsPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.allDomainsMask, true).first! - let filePathsArray = try FileManager.default.subpathsOfDirectory(atPath: documentsPath) - for filePath in filePathsArray { - if filePath.hasPrefix(Constants.prefixForCheck.rawValue) { - try FileManager.default.removeItem(atPath: (documentsPath as NSString).appendingPathComponent(filePath)) - } - } - - try FileManager.default.removeItem(atPath: NFXPath.SessionLog) - } catch {} + + NFXPath.deleteNFXDir() } func getIgnoredURLs() -> [String] { @@ -258,18 +245,24 @@ open class NFX: NSObject { func getCachedFilters() -> [Bool] { if filters.isEmpty { - filters = [Bool](repeating: true, count: HTTPModelShortType.allValues.count) + filters = [Bool](repeating: true, count: HTTPModelShortType.allCases.count) } return filters } + func getCachedFilterTypes() -> [HTTPModelShortType] { + return getCachedFilters() + .enumerated() + .compactMap { $1 ? HTTPModelShortType.allCases[$0] : nil } + } + } #if os(iOS) extension NFX { fileprivate var presentingViewController: UIViewController? { - var rootViewController = UIApplication.shared.keyWindow?.rootViewController + var rootViewController = UIWindow.keyWindow?.rootViewController while let controller = rootViewController?.presentedViewController { rootViewController = controller } diff --git a/netfox/Core/NFXHTTPModel.swift b/netfox/Core/NFXHTTPModel.swift index 6b8461a2..bddf6172 100755 --- a/netfox/Core/NFXHTTPModel.swift +++ b/netfox/Core/NFXHTTPModel.swift @@ -41,11 +41,11 @@ fileprivate func < (lhs: T?, rhs: T?) -> Bool { public var timeInterval: Float? - @objc public var randomHash: NSString? - @objc public var shortType: NSString = HTTPModelShortType.OTHER.rawValue as NSString - @objc public var noResponse: Bool = true - + @objc public lazy var randomHash = UUID().uuidString + public var shortType = HTTPModelShortType.OTHER + @objc public var shortTypeString: String { return shortType.rawValue } + @objc public var noResponse = true func saveRequest(_ request: URLRequest) { requestDate = Date() @@ -66,7 +66,7 @@ fileprivate func < (lhs: T?, rhs: T?) -> Bool { } func logRequest(_ request: URLRequest) { - formattedRequestLogEntry().appendToFile(filePath: NFXPath.SessionLog) + formattedRequestLogEntry().appendToFileURL(NFXPath.sessionLogURL) } func saveErrorResponse() { @@ -83,108 +83,100 @@ fileprivate func < (lhs: T?, rhs: T?) -> Bool { let headers = response.getNFXHeaders() if let contentType = headers["Content-Type"] as? String { - responseType = contentType.components(separatedBy: ";")[0] - shortType = getShortTypeFrom(responseType!).rawValue as NSString + let responseType = contentType.components(separatedBy: ";")[0] + shortType = HTTPModelShortType(contentType: responseType) + self.responseType = responseType } timeInterval = Float(responseDate!.timeIntervalSince(requestDate!)) saveResponseBodyData(data) - formattedResponseLogEntry().appendToFile(filePath: NFXPath.SessionLog) + formattedResponseLogEntry().appendToFileURL(NFXPath.sessionLogURL) } - func saveRequestBodyData(_ data: Data) { - let tempBodyString = NSString.init(data: data, encoding: String.Encoding.utf8.rawValue) + let tempBodyString = String.init(data: data, encoding: String.Encoding.utf8) self.requestBodyLength = data.count if (tempBodyString != nil) { - saveData(tempBodyString!, toFile: getRequestBodyFilepath()) + saveData(tempBodyString!, to: getRequestBodyFileURL()) } } func saveResponseBodyData(_ data: Data) { - var bodyString: NSString? + var bodyString: String? - if shortType as String == HTTPModelShortType.IMAGE.rawValue { - bodyString = data.base64EncodedString(options: .endLineWithLineFeed) as NSString? + if shortType == .IMAGE { + bodyString = data.base64EncodedString(options: .endLineWithLineFeed) } else { - if let tempBodyString = NSString.init(data: data, encoding: String.Encoding.utf8.rawValue) { + if let tempBodyString = String(data: data, encoding: String.Encoding.utf8) { bodyString = tempBodyString } } - if bodyString != nil { + if let bodyString = bodyString { responseBodyLength = data.count - saveData(bodyString!, toFile: getResponseBodyFilepath()) + saveData(bodyString, to: getResponseBodyFileURL()) } } - fileprivate func prettyOutput(_ rawData: Data, contentType: String? = nil) -> NSString { - if let contentType = contentType { - let shortType = getShortTypeFrom(contentType) - if let output = prettyPrint(rawData, type: shortType) { - return output as NSString - } + fileprivate func prettyOutput(_ rawData: Data, contentType: String? = nil) -> String { + guard let contentType = contentType, + let output = prettyPrint(rawData, type: .init(contentType: contentType)) + else { + return String(data: rawData, encoding: String.Encoding.utf8) ?? "" } - return NSString(data: rawData, encoding: String.Encoding.utf8.rawValue) ?? "" + + return output } - @objc public func getRequestBody() -> NSString { - guard let data = readRawData(getRequestBodyFilepath()) else { + @objc public func getRequestBody() -> String { + guard let data = readRawData(from: getRequestBodyFileURL()) else { return "" } return prettyOutput(data, contentType: requestType) } - @objc public func getResponseBody() -> NSString { - guard let data = readRawData(getResponseBodyFilepath()) else { + @objc public func getResponseBody() -> String { + guard let data = readRawData(from: getResponseBodyFileURL()) else { return "" } return prettyOutput(data, contentType: responseType) } - @objc public func getRandomHash() -> NSString { - if !(randomHash != nil) { - randomHash = UUID().uuidString as NSString? - } - return randomHash! - } - - @objc public func getRequestBodyFilepath() -> String { - let dir = getDocumentsPath() as NSString - return dir.appendingPathComponent(getRequestBodyFilename()) + @objc public func getRequestBodyFileURL() -> URL { + return NFXPath.pathURLToFile(getRequestBodyFilename()) } @objc public func getRequestBodyFilename() -> String { - return String("nfx_request_body_") + "\(requestTime!)_\(getRandomHash() as String)" + return "request_body_\(requestTime!)_\(randomHash)" } - @objc public func getResponseBodyFilepath() -> String { - let dir = getDocumentsPath() as NSString - return dir.appendingPathComponent(getResponseBodyFilename()) + @objc public func getResponseBodyFileURL() -> URL { + return NFXPath.pathURLToFile(getResponseBodyFilename()) } @objc public func getResponseBodyFilename() -> String { - return String("nfx_response_body_") + "\(requestTime!)_\(getRandomHash() as String)" - } - - @objc public func getDocumentsPath() -> String { - return NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.allDomainsMask, true).first! + return "response_body_\(requestTime!)_\(randomHash)" } - @objc public func saveData(_ dataString: NSString, toFile: String) { + @objc public func saveData(_ dataString: String, to fileURL: URL) { do { - try dataString.write(toFile: toFile, atomically: false, encoding: String.Encoding.utf8.rawValue) - } catch { - print("catch !!!") + try dataString.write(to: fileURL, atomically: true, encoding: .utf8) + } catch let error { + print("[NFX]: Failed to save data to [\(fileURL)] - \(error.localizedDescription)") } } - @objc public func readRawData(_ fromFile: String) -> Data? { - return (try? Data(contentsOf: URL(fileURLWithPath: fromFile))) + @objc public func readRawData(from fileURL: URL) -> Data? { + do { + return try Data(contentsOf: fileURL) + } catch let error { + print("[NFX]: Failed to load data from [\(fileURL)] - \(error.localizedDescription)") + return nil + } } @objc public func getTimeFromDate(_ date: Date) -> String? { @@ -200,41 +192,18 @@ fileprivate func < (lhs: T?, rhs: T?) -> Bool { } } - public func getShortTypeFrom(_ contentType: String) -> HTTPModelShortType { - if NSPredicate(format: "SELF MATCHES %@", - "^application/(vnd\\.(.*)\\+)?json$").evaluate(with: contentType) { - return .JSON - } - - if (contentType == "application/xml") || (contentType == "text/xml") { - return .XML - } - - if contentType == "text/html" { - return .HTML - } - - if contentType.hasPrefix("image/") { - return .IMAGE - } - - return .OTHER - } - - public func prettyPrint(_ rawData: Data, type: HTTPModelShortType) -> NSString? { + public func prettyPrint(_ rawData: Data, type: HTTPModelShortType) -> String? { switch type { case .JSON: do { let rawJsonData = try JSONSerialization.jsonObject(with: rawData, options: []) let prettyPrintedString = try JSONSerialization.data(withJSONObject: rawJsonData, options: [.prettyPrinted]) - return NSString(data: prettyPrintedString, encoding: String.Encoding.utf8.rawValue) + return String(data: prettyPrintedString, encoding: String.Encoding.utf8) } catch { return nil } - default: return nil - } } diff --git a/netfox/Core/NFXHTTPModelManager.swift b/netfox/Core/NFXHTTPModelManager.swift index b489640f..c6da878d 100755 --- a/netfox/Core/NFXHTTPModelManager.swift +++ b/netfox/Core/NFXHTTPModelManager.swift @@ -29,21 +29,8 @@ final class NFXHTTPModelManager: NSObject { } func getModels() -> [NFXHTTPModel] { - var predicates = [NSPredicate]() - - let filterValues = NFX.sharedInstance().getCachedFilters() - let filterNames = HTTPModelShortType.allValues - - for (index, filterValue) in filterValues.enumerated() { - if filterValue { - let filterName = filterNames[index].rawValue - let predicate = NSPredicate(format: "shortType == '\(filterName)'") - predicates.append(predicate) - } - } - - let searchPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: predicates) - let array = (models as NSArray).filtered(using: searchPredicate) - return array as! [NFXHTTPModel] + let filteredTypes = NFX.sharedInstance().getCachedFilterTypes() + return models.filter { filteredTypes.contains($0.shortType) } } + } diff --git a/netfox/Core/NFXHelper.swift b/netfox/Core/NFXHelper.swift index 1ec2eb3e..0c397252 100644 --- a/netfox/Core/NFXHelper.swift +++ b/netfox/Core/NFXHelper.swift @@ -12,16 +12,33 @@ import Cocoa import UIKit #endif -public enum HTTPModelShortType: String { +public enum HTTPModelShortType: String, CaseIterable { case JSON = "JSON" case XML = "XML" case HTML = "HTML" case IMAGE = "Image" case OTHER = "Other" - - static let allValues = [JSON, XML, HTML, IMAGE, OTHER] } + +public extension HTTPModelShortType { + + init(contentType: String) { + if NSPredicate(format: "SELF MATCHES %@", "^application/(vnd\\.(.*)\\+)?json$").evaluate(with: contentType) { + self = .JSON + } else if (contentType == "application/xml") || (contentType == "text/xml") { + self = .XML + } else if contentType == "text/html" { + self = .HTML + } else if contentType.hasPrefix("image/") { + self = .IMAGE + } else { + self = .OTHER + } + } +} + + extension NFXColor { convenience init(red: Int, green: Int, blue: Int) { assert(red >= 0 && red <= 255, "Invalid red component") @@ -317,29 +334,81 @@ class NFXDebugInfo { struct NFXPath { - static let Documents = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.allDomainsMask, true).first! as NSString - static let SessionLog = NFXPath.Documents.appendingPathComponent("session.log"); + static let tmpDirURL = URL(fileURLWithPath: NSTemporaryDirectory()) + static let nfxDirURL = tmpDirURL.appendingPathComponent("NFX", isDirectory: true) + static let sessionLogURL = tmpDirURL.appendingPathComponent("session.log") + + static func createNFXDirIfNotExist() { + do { + try FileManager.default.createDirectory(at: nfxDirURL, withIntermediateDirectories: true, attributes: nil) + } catch let error { + print("[NFX]: failed to create working dir - \(error.localizedDescription)") + } + } + + static func deleteNFXDir() { + guard FileManager.default.fileExists(atPath: nfxDirURL.path, isDirectory: nil) else { return } + + do { + try FileManager.default.removeItem(at: nfxDirURL) + } catch let error { + print("[NFX]: failed to delete working dir - \(error.localizedDescription)") + } + } + + static func deleteOldNFXLogs() { + let fileManager = FileManager.default + guard let documentsDir = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first, + let fileEnumarator = fileManager.enumerator(at: documentsDir, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants], errorHandler: nil) else { return } + + for case let fileURL as URL in fileEnumarator { + if fileURL.lastPathComponent == "session.log" || fileURL.lastPathComponent.hasPrefix("nfx_re") { + try? fileManager.removeItem(at: fileURL) + } + } + } + + static func pathURLToFile(_ fileName: String) -> URL { + return nfxDirURL.appendingPathComponent(fileName) + } + } extension String { - func appendToFile(filePath: String) { - let contentToAppend = self + + func appendToFileURL(_ fileURL: URL) { + guard let fileHandle = try? FileHandle(forWritingTo: fileURL) else { + write(to: fileURL) + return + } + + let data = data(using: .utf8)! - if let fileHandle = FileHandle(forWritingAtPath: filePath) { - /* Append to file */ - fileHandle.seekToEndOfFile() - fileHandle.write(contentToAppend.data(using: String.Encoding.utf8)!) - } else { - /* Create new file */ + if #available(iOS 13.4, *) { do { - try contentToAppend.write(toFile: filePath, atomically: true, encoding: String.Encoding.utf8) - } catch { - print("Error creating \(filePath)") + try fileHandle.seekToEnd() + try fileHandle.write(contentsOf: data) + } catch let error { + print("[NFX]: Failed to append [\(self.prefix(128))] to \(fileURL), trying to create new file - \(error.localizedDescription)") + write(to: fileURL) } + } else { + // TODO: replace FileHandle with more safe way, possible crash on iOS <13.4 https://github.com/kasketis/netfox/issues/221 + fileHandle.seekToEndOfFile() + fileHandle.write(data) + } + } + + private func write(to fileURL: URL) { + do { + try write(to: fileURL, atomically: true, encoding: .utf8) + } catch let error { + print("[NFX]: Failed to save [\(self.prefix(128))] to \(fileURL) - \(error.localizedDescription)") } } + } @objc extension URLSessionConfiguration { @@ -447,3 +516,32 @@ public extension NSNotification.Name { static let NFXAddedModel = Notification.Name("NFXAddedModel") static let NFXClearedModels = Notification.Name("NFXClearedModels") } + +#if os(iOS) +extension UIWindow { + static var keyWindow: UIWindow? { + if #available(iOS 13.0, *) { + return UIApplication.shared.connectedScenes + .sorted { $0.activationState.sortPriority < $1.activationState.sortPriority } + .compactMap { $0 as? UIWindowScene } + .compactMap { $0.windows.first { $0.isKeyWindow } } + .first + } else { + return UIApplication.shared.keyWindow + } + } +} + +@available(iOS 13.0, *) +private extension UIScene.ActivationState { + var sortPriority: Int { + switch self { + case .foregroundActive: return 1 + case .foregroundInactive: return 2 + case .background: return 3 + case .unattached: return 4 + @unknown default: return 5 + } + } +} +#endif diff --git a/netfox/iOS/NFXDetailsController_iOS.swift b/netfox/iOS/NFXDetailsController_iOS.swift index 2670bee5..2eb8a4f6 100755 --- a/netfox/iOS/NFXDetailsController_iOS.swift +++ b/netfox/iOS/NFXDetailsController_iOS.swift @@ -263,7 +263,7 @@ class NFXDetailsController_iOS: NFXDetailsController, MFMailComposeViewControlle var bodyDetailsController: NFXGenericBodyDetailsController - if selectedModel.shortType as String == HTTPModelShortType.IMAGE.rawValue { + if selectedModel.shortType == .IMAGE { bodyDetailsController = NFXImageBodyDetailsController() } else { bodyDetailsController = NFXRawBodyDetailsController() @@ -288,16 +288,17 @@ class NFXDetailsController_iOS: NFXDetailsController, MFMailComposeViewControlle tempString += "logged via netfox - [https://github.com/kasketis/netfox]\n" if full { - let requestFilePath = selectedModel.getRequestBodyFilepath() - if let requestFileData = try? String(contentsOf: URL(fileURLWithPath: requestFilePath as String), encoding: .utf8) { + let requestFileURL = selectedModel.getRequestBodyFileURL() + if let requestFileData = try? String(contentsOf: requestFileURL, encoding: .utf8) { tempString += requestFileData } - let responseFilePath = selectedModel.getResponseBodyFilepath() - if let responseFileData = try? String(contentsOf: URL(fileURLWithPath: responseFilePath as String), encoding: .utf8) { + let responseFileURL = selectedModel.getResponseBodyFileURL() + if let responseFileData = try? String(contentsOf: responseFileURL, encoding: .utf8) { tempString += responseFileData } } + displayShareSheet(shareContent: tempString, sender: sender) } diff --git a/netfox/iOS/NFXListController_iOS.swift b/netfox/iOS/NFXListController_iOS.swift index 101273fd..853486bd 100755 --- a/netfox/iOS/NFXListController_iOS.swift +++ b/netfox/iOS/NFXListController_iOS.swift @@ -137,12 +137,12 @@ class NFXListController_iOS: NFXListController, UITableViewDelegate, UITableView if searchController.isActive { if !filteredTableData.isEmpty { - let obj = filteredTableData[(indexPath as NSIndexPath).row] + let obj = filteredTableData[indexPath.row] cell.configForObject(obj) } } else { if NFXHTTPModelManager.sharedInstance.getModels().count > 0 { - let obj = NFXHTTPModelManager.sharedInstance.getModels()[(indexPath as NSIndexPath).row] + let obj = NFXHTTPModelManager.sharedInstance.getModels()[indexPath.row] cell.configForObject(obj) } } @@ -151,7 +151,7 @@ class NFXListController_iOS: NFXListController, UITableViewDelegate, UITableView } func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - return UIView.init(frame: CGRect.zero) + return UIView(frame: CGRect.zero) } func numberOfSections(in tableView: UITableView) -> Int { @@ -163,13 +163,12 @@ class NFXListController_iOS: NFXListController, UITableViewDelegate, UITableView detailsController = NFXDetailsController_iOS() var model: NFXHTTPModel if searchController.isActive { - model = filteredTableData[(indexPath as NSIndexPath).row] + model = filteredTableData[indexPath.row] } else { - model = NFXHTTPModelManager.sharedInstance.getModels()[(indexPath as NSIndexPath).row] + model = NFXHTTPModelManager.sharedInstance.getModels()[indexPath.row] } detailsController.selectedModel(model) navigationController?.pushViewController(detailsController, animated: true) - } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { diff --git a/netfox/iOS/NFXSettingsController_iOS.swift b/netfox/iOS/NFXSettingsController_iOS.swift index 9037eb13..c9e4de09 100644 --- a/netfox/iOS/NFXSettingsController_iOS.swift +++ b/netfox/iOS/NFXSettingsController_iOS.swift @@ -23,7 +23,7 @@ class NFXSettingsController_iOS: NFXSettingsController, UITableViewDelegate, UIT title = "Settings" - tableData = HTTPModelShortType.allValues + tableData = HTTPModelShortType.allCases filters = NFX.sharedInstance().getCachedFilters() edgesForExtendedLayout = UIRectEdge() @@ -261,7 +261,7 @@ class NFXSettingsController_iOS: NFXSettingsController, UITableViewDelegate, UIT mailComposer.mailComposeDelegate = self mailComposer.setSubject("netfox log - Session Log \(NSDate())") - if let sessionLogData = NSData(contentsOfFile: NFXPath.SessionLog as String) { + if let sessionLogData = try? Data(contentsOf: NFXPath.sessionLogURL) { mailComposer.addAttachmentData(sessionLogData as Data, mimeType: "text/plain", fileName: "session.log") }