From 605ac2eedf224d6fcf3363992ca1665e05691d97 Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Tue, 10 Sep 2024 16:47:34 -0700 Subject: [PATCH] Finally getting the hand of how documentation comments work --- code/cli/munki/shared/network/fetch.swift | 175 +++++++++++----------- code/cli/munki/shared/network/gurl.swift | 42 +++--- 2 files changed, 107 insertions(+), 110 deletions(-) diff --git a/code/cli/munki/shared/network/fetch.swift b/code/cli/munki/shared/network/fetch.swift index e129f1e0..341a772d 100644 --- a/code/cli/munki/shared/network/fetch.swift +++ b/code/cli/munki/shared/network/fetch.swift @@ -57,29 +57,29 @@ func storeCachedChecksum(toPath path: String, hash: String? = nil) -> String? { return nil } +/// Verifies the integrity of the given software package. +/// +/// The feature is controlled through the PackageVerificationMode key in +/// Munki's preferences. Following modes currently exist: +/// none: No integrity check is performed. +/// hash: Integrity check is performed by calculating a SHA-256 hash of +/// the given file and comparing it against the reference value in +/// catalog. Only applies for package plists that contain the +/// item_key; for packages without the item_key, verification always +/// returns true. +/// hash_strict: Same as hash, but returns false for package plists that +/// do not contain the item_key. +/// +/// Args: +/// path: The file to check integrity on. +/// expectedHash: the sha256 hash expected. +/// alwaysHash: Boolean. Always check and return the hash even if not +/// necessary for this function. +/// +/// Returns: +/// (true/false, sha256-hash) +/// true if the package integrity could be validated. Otherwise, false. func verifySoftwarePackageIntegrity(_ path: String, expectedHash: String, alwaysHash: Bool = false) -> (Bool, String) { - /// Verifies the integrity of the given software package. - - /// The feature is controlled through the PackageVerificationMode key in - /// Munki's preferences. Following modes currently exist: - /// none: No integrity check is performed. - /// hash: Integrity check is performed by calculating a SHA-256 hash of - /// the given file and comparing it against the reference value in - /// catalog. Only applies for package plists that contain the - /// item_key; for packages without the item_key, verification always - /// returns true. - /// hash_strict: Same as hash, but returns false for package plists that - /// do not contain the item_key. - /// - /// Args: - /// path: The file to check integrity on. - /// expectedHash: the sha256 hash expected. - /// alwaysHash: Boolean. Always check and return the hash even if not - /// necessary for this function. - /// - /// Returns: - /// (true/false, sha256-hash) - /// true if the package integrity could be validated. Otherwise, false. let mode = pref("PackageVerificationMode") as? String ?? "hash" let itemName = (path as NSString).lastPathComponent var calculatedHash = "" @@ -119,10 +119,10 @@ func verifySoftwarePackageIntegrity(_ path: String, expectedHash: String, always return (false, calculatedHash) } +/// Given a list of strings in http header format, return a dict. +/// A User-Agent header is added if none is present in the list. +/// If strList is nil, returns a dict with only the User-Agent header. func headerDictFromList(_ strList: [String]?) -> [String: String] { - /// Given a list of strings in http header format, return a dict. - /// A User-Agent header is added if none is present in the list. - /// If strList is nil, returns a dict with only the User-Agent header. var headerDict = [String: String]() headerDict["User-Agent"] = DEFAULT_USER_AGENT @@ -144,6 +144,17 @@ func runMiddleware(options: GurlOptions, pkginfo _: PlistDict?) -> GurlOptions { return options } +/// Gets an HTTP or HTTPS URL and stores it in +/// destination path. Returns a dictionary of headers, which includes +/// http_result_code and http_result_description. +/// Will throw FetchError.connection if Gurl has a connection error. +/// Will throw FetchError.http if HTTP Result code is not 2xx or 304. +/// Will throw FetchError.fileSystem if Gurl has a filesystem error. +/// If destinationpath already exists, you can set 'onlyifnewer' to true to +/// indicate you only want to download the file only if it's newer on the +/// server. +/// If you set resume to true, Gurl will attempt to resume an +/// interrupted download. func getURL( _ url: String, destinationPath: String, @@ -154,17 +165,6 @@ func getURL( followRedirects: String = "none", pkginfo: PlistDict? = nil ) throws -> [String: String] { - /// Gets an HTTP or HTTPS URL and stores it in - /// destination path. Returns a dictionary of headers, which includes - /// http_result_code and http_result_description. - /// Will throw FetchError.connection if Gurl has a connection error. - /// Will throw FetchError.http if HTTP Result code is not 2xx or 304. - /// Will throw FetchError.fileSystem if Gurl has a filesystem error. - /// If destinationpath already exists, you can set 'onlyifnewer' to true to - /// indicate you only want to download the file only if it's newer on the - /// server. - /// If you set resume to true, Gurl will attempt to resume an - /// interrupted download. let tempDownloadPath = destinationPath + ".download" if pathExists(tempDownloadPath), !resume { try? FileManager.default.removeItem(atPath: tempDownloadPath) @@ -286,6 +286,13 @@ func getURL( throw FetchError.http(errorCode: session.status, description: statusDescription) } +/// Gets file from HTTP URL, checking first to see if it has changed on the +/// server. +/// +/// Returns True if a new download was required; False if the +/// item is already in the local cache. +/// +/// Throws a FetchError if there is an error (.connection or .download) func getHTTPfileIfChangedAtomically( _ url: String, destinationPath: String, @@ -295,13 +302,6 @@ func getHTTPfileIfChangedAtomically( followRedirects: String = "none", pkginfo: PlistDict? = nil ) throws -> Bool { - /// Gets file from HTTP URL, checking first to see if it has changed on the - /// server. - /// - /// Returns True if a new download was required; False if the - /// item is already in the local cache. - /// - /// Throws a FetchError if there is an error (.connection or .download) // TODO: etag support // var eTag = "" var getOnlyIfNewer = false @@ -381,14 +381,14 @@ func getHTTPfileIfChangedAtomically( return true } +/// Gets file from path, checking first to see if it has changed on the +/// source. +/// +/// Returns true if a new copy was required; false if the +/// item is already in the local cache. +/// +/// Throws FetchError.fileSystem if there is an error. func getFileIfChangedAtomically(_ path: String, destinationPath: String) throws -> Bool { - /// Gets file from path, checking first to see if it has changed on the - /// source. - /// - /// Returns true if a new copy was required; false if the - /// item is already in the local cache. - /// - /// Throws FetchError.fileSystem if there is an error. let filemanager = FileManager.default if !pathExists(path) { throw FetchError.fileSystem("Source does not exist: \(path)") @@ -445,6 +445,19 @@ func getFileIfChangedAtomically(_ path: String, destinationPath: String) throws return true } +/// Gets file from a URL. +/// Checks first if there is already a file with the necessary checksum. +/// Then checks if the file has changed on the server, resuming or +/// re-downloading as necessary. +/// +/// If the file has changed verify the pkg hash if so configured. +/// +/// Supported schemes are http, https, file. +/// +/// Returns true if a new download was required; False if the +/// item is already in the local cache. +/// +/// Throws a FetchError if there is an error. func getResourceIfChangedAtomically( _ url: String, destinationPath: String, @@ -456,20 +469,6 @@ func getResourceIfChangedAtomically( followRedirects: String? = nil, pkginfo: PlistDict? = nil ) throws -> Bool { - /// Gets file from a URL. - /// Checks first if there is already a file with the necessary checksum. - /// Then checks if the file has changed on the server, resuming or - /// re-downloading as necessary. - /// - /// If the file has changed verify the pkg hash if so configured. - /// - /// Supported schemes are http, https, file. - /// - /// Returns true if a new download was required; False if the - /// item is already in the local cache. - /// - /// Throws a FetchError if there is an error. - guard let resolvedURL = URL(string: url) else { throw FetchError.connection(errorCode: -1, description: "Invalid URL: \(url)") } @@ -549,6 +548,19 @@ func getResourceIfChangedAtomically( return changed } +/// A high-level function for getting resources from the Munki repo. +/// Gets a given URL from the Munki server. +/// Adds any additional headers to the request if present +/// Throws a FetchError if there's an error +/// +/// Add any additional headers specified in ManagedInstalls.plist. +/// AdditionalHttpHeaders must be an array of strings with valid HTTP +/// header format. For example: +/// AdditionalHttpHeaders +/// +/// Key-With-Optional-Dashes: Foo Value +/// another-custom-header: bar value +/// func fetchMunkiResourceByURL( _ url: String, destinationPath: String, @@ -558,19 +570,6 @@ func fetchMunkiResourceByURL( verify: Bool = false, pkginfo: PlistDict? = nil ) throws -> Bool { - /// A high-level function for getting resources from the Munki repo. - /// Gets a given URL from the Munki server. - /// Adds any additional headers to the request if present - /// Throws a FetchError if there's an error - /// - /// Add any additional headers specified in ManagedInstalls.plist. - /// AdditionalHttpHeaders must be an array of strings with valid HTTP - /// header format. For example: - /// AdditionalHttpHeaders - /// - /// Key-With-Optional-Dashes: Foo Value - /// another-custom-header: bar value - /// let customHeaders = pref(ADDITIONAL_HTTP_HEADERS_KEY) as? [String] return try getResourceIfChangedAtomically( @@ -593,6 +592,7 @@ enum MunkiResourceType: String { case package = "pkgs" } +/// An even higher-level function for getting resources from the Munki repo. func fetchMunkiResource( kind: MunkiResourceType, name: String, @@ -603,7 +603,6 @@ func fetchMunkiResource( verify: Bool = false, pkginfo: PlistDict? = nil ) throws -> Bool { - /// An even higher-level function for getting resources from the Munki repo. guard let url = munkiRepoURL(kind.rawValue, resource: name) else { throw FetchError.connection( errorCode: -1, @@ -621,13 +620,12 @@ func fetchMunkiResource( ) } +/// Returns data from URL. +/// We use the existing fetchMunkiResource function so any custom +/// authentication/authorization headers are used +/// (including, eventually, middleware-generated headers) +/// May throw a FetchError func getDataFromURL(_ url: String) throws -> Data? { - /// Returns data from URL. - /// We use the existing fetchMunkiResource function so any custom - /// authentication/authorization headers are used - /// (including, eventually, middleware-generated headers) - /// May throw a FetchError - guard let tmpDir = TempDir.shared.makeTempDir() else { displayError("Could not create temporary directory") return nil @@ -640,12 +638,11 @@ func getDataFromURL(_ url: String) throws -> Data? { return FileManager.default.contents(atPath: tempDataPath) } +/// A function we can call to check to see if the server is +/// available before we kick off a full run. This can be fooled by +/// ISPs that return results for non-existent web servers... +/// Returns a tuple (exitCode, exitDescription) func checkServer(_ urlString: String = "") -> (Int, String) { - /// A function we can call to check to see if the server is - /// available before we kick off a full run. This can be fooled by - /// ISPs that return results for non-existent web servers... - /// Returns a tuple (exitCode, exitDescription) - let serverURL: String = if !urlString.isEmpty { urlString } else { diff --git a/code/cli/munki/shared/network/gurl.swift b/code/cli/munki/shared/network/gurl.swift index adcc8578..47619ebe 100644 --- a/code/cli/munki/shared/network/gurl.swift +++ b/code/cli/munki/shared/network/gurl.swift @@ -25,6 +25,7 @@ func defaultLogger(_ message: String) { print(message) } +/// Some options used by Gurl struct GurlOptions { var url: String var destinationPath: String @@ -41,10 +42,9 @@ struct GurlOptions { var log: (String) -> Void = defaultLogger // logging function } +/// A class for getting content from a URL +/// using NSURLSession and friends class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate { - /// A class for getting content from a URL - /// using NSURLSession and friends - let GURL_XATTR = "com.googlecode.munki.downloadData" var options: GurlOptions @@ -67,8 +67,8 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData super.init() } + /// Start the connection func start() { - /// Start the connection guard !options.destinationPath.isEmpty else { options.log("No output file specified") done = true @@ -139,17 +139,17 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData } } + /// Cancel the session func cancel() { - /// Cancel the session if let session { session.invalidateAndCancel() } done = true } + /// Check if the connection request is complete. As a side effect, + /// allow the delegates to work by letting the run loop run for a bit func isDone() -> Bool { - /// Check if the connection request is complete. As a side effect, - /// allow the delegates to work by letting the run loop run for a bit if done { return true } @@ -158,8 +158,8 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData return done } + /// Returns any stored headers for destinationPath func getStoredHeaders() -> [String: String]? { - /// Returns any stored headers for destinationPath if options.destinationPath.isEmpty { displayDebug1("destination path is not defined") return nil @@ -182,8 +182,8 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData } } + /// Store headers dictionary as an xattr for options.destinationPath func storeHeaders(_ headers: [String: String]) { - /// Store headers dictionary as an xattr for options.destinationPath guard let data = try? plistToData(headers) else { options.log("header convert to plist data failure") return @@ -199,10 +199,10 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData } } + /// Since HTTP header names are not case-sensitive, we normalize a + /// dictionary of HTTP headers by converting all the key names to + /// lower case func normalizeHeaderDict(_ headers: [String: String]) -> [String: String] { - /// Since HTTP header names are not case-sensitive, we normalize a - /// dictionary of HTTP headers by converting all the key names to - /// lower case var normalizedHeaders = [String: String]() for (key, value) in headers { normalizedHeaders[key.lowercased()] = value @@ -210,8 +210,8 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData return normalizedHeaders } + /// Record any error info from completed session func recordError(_ error: NSError) { - /// Record any error info from completed session self.error = error // if this was an SSL error, try to extract the SSL error code if let underlyingError = error.userInfo["NSUnderlyingError"] as? NSError, @@ -224,8 +224,8 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData } func removeExpectedSizeFromStoredHeaders() { - /// If a successful transfer, clear the expected size so we - /// don't attempt to resume the download next time + // If a successful transfer, clear the expected size so we + // don't attempt to resume the download next time if String(status).hasPrefix("2"), var headers = getStoredHeaders(), headers.keys.contains("expected-length") @@ -235,8 +235,8 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData } } + /// URLSessionTaskDelegate method @objc func urlSession(_: URLSession, task _: URLSessionTask, didCompleteWithError error: (any Error)?) { - /// URLSessionTaskDelegate method if task != task { return } @@ -261,7 +261,7 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData } func okToResume(downloadData: [String: String]) -> Bool { - /// returns a boolean + // returns a boolean guard let storedData = getStoredHeaders() else { options.log("No stored headers") return false @@ -297,13 +297,13 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData return true } + /// URLSessionDataDelegate method @objc func urlSession( _: URLSession, dataTask _: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping @Sendable (URLSession.ResponseDisposition) -> Void ) { - /// URLSessionDataDelegate method // self.response = response // doesn't appear to be actually used bytesReceived = 0 percentComplete = -1 @@ -372,6 +372,7 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData completionHandler(.allow) } + /// URLSessionTaskDelegate method @objc func urlSession( _: URLSession, task _: URLSessionTask, @@ -379,7 +380,6 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData newRequest request: URLRequest, completionHandler: @escaping @Sendable (URLRequest?) -> Void ) { - /// URLSessionTaskDelegate method guard let newURL = request.url else { // deny the redirect completionHandler(nil) @@ -403,13 +403,13 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData completionHandler(nil) } + /// URLSessionTaskDelegate method @objc func urlSession( _: URLSession, task _: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping @Sendable (URLSession.AuthChallengeDisposition, URLCredential?) -> Void ) { - /// URLSessionTaskDelegate method // Handle an authentication challenge let supportedAuthMethods = [ NSURLAuthenticationMethodDefault, @@ -453,8 +453,8 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData } } + /// URLSessionDataDelegate method @objc func urlSession(_: URLSession, dataTask _: URLSessionDataTask, didReceive data: Data) { - /// URLSessionDataDelegate method // Handle received data if let destination { destination.write(data)