From 42be61c9aaa28d6ba9e9d39c8b15ffb9dfd17ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boris=20B=C3=BCgling?= Date: Tue, 18 Aug 2020 10:30:51 -0700 Subject: [PATCH 1/2] Merge pull request #88 from sstadelman/master Add support for Netrc for Downloader --- Sources/TSCUtility/CMakeLists.txt | 1 + Sources/TSCUtility/Downloader.swift | 11 +- Sources/TSCUtility/Netrc.swift | 164 +++++++ Tests/TSCUtilityTests/DownloaderTests.swift | 149 +++++++ Tests/TSCUtilityTests/NetrcTests.swift | 448 ++++++++++++++++++++ 5 files changed, 772 insertions(+), 1 deletion(-) create mode 100644 Sources/TSCUtility/Netrc.swift create mode 100644 Tests/TSCUtilityTests/NetrcTests.swift diff --git a/Sources/TSCUtility/CMakeLists.txt b/Sources/TSCUtility/CMakeLists.txt index d7dff6f7..dd4fd6b2 100644 --- a/Sources/TSCUtility/CMakeLists.txt +++ b/Sources/TSCUtility/CMakeLists.txt @@ -22,6 +22,7 @@ add_library(TSCUtility InterruptHandler.swift JSONMessageStreamingParser.swift misc.swift + Netrc.swift OSLog.swift PkgConfig.swift Platform.swift diff --git a/Sources/TSCUtility/Downloader.swift b/Sources/TSCUtility/Downloader.swift index ad412458..7f76a4ce 100644 --- a/Sources/TSCUtility/Downloader.swift +++ b/Sources/TSCUtility/Downloader.swift @@ -43,11 +43,13 @@ public protocol Downloader { /// - Parameters: /// - url: The `URL` to the file to download. /// - destination: The `AbsolutePath` to download the file to. + /// - authorizationProvider: Optional provider supplying `Authorization` header to be added to `URLRequest`. /// - progress: A closure to receive the download's progress as number of bytes. /// - completion: A closure to be notifed of the completion of the download. func downloadFile( at url: Foundation.URL, to destination: AbsolutePath, + withAuthorizationProvider authorizationProvider: AuthorizationProviding?, progress: @escaping Progress, completion: @escaping Completion ) @@ -109,11 +111,18 @@ public final class FoundationDownloader: NSObject, Downloader { public func downloadFile( at url: Foundation.URL, to destination: AbsolutePath, + withAuthorizationProvider authorizationProvider: AuthorizationProviding? = nil, progress: @escaping Downloader.Progress, completion: @escaping Downloader.Completion ) { queue.addOperation { - let task = self.session.downloadTask(with: url) + var request = URLRequest(url: url) + + if let authorization = authorizationProvider?.authorization(for: url) { + request.addValue(authorization, forHTTPHeaderField: "Authorization") + } + + let task = self.session.downloadTask(with: request) let download = Download( task: task, destination: destination, diff --git a/Sources/TSCUtility/Netrc.swift b/Sources/TSCUtility/Netrc.swift new file mode 100644 index 00000000..6c979910 --- /dev/null +++ b/Sources/TSCUtility/Netrc.swift @@ -0,0 +1,164 @@ +import Foundation +import TSCBasic + +/// Supplies `Authorization` header, typically to be appended to `URLRequest` +public protocol AuthorizationProviding { + /// Optional `Authorization` header, likely added to `URLRequest` + func authorization(for url: Foundation.URL) -> String? +} + +extension AuthorizationProviding { + public func authorization(for url: Foundation.URL) -> String? { + return nil + } +} + +#if os(Windows) +// FIXME: - add support for Windows when regex function available +#endif + +#if os(Linux) +// FIXME: - add support for Linux when regex function available +#endif + +#if os(macOS) +/* + Netrc feature depends upon `NSTextCheckingResult.range(withName name: String) -> NSRange`, + which is only available in macOS 10.13+ at this time. + */ +@available (OSX 10.13, *) +/// Container of parsed netrc connection settings +public struct Netrc: AuthorizationProviding { + /// Representation of `machine` connection settings & `default` connection settings. + /// If `default` connection settings present, they will be last element. + public let machines: [Machine] + + private init(machines: [Machine]) { + self.machines = machines + } + + /// Basic authorization header string + /// - Parameter url: URI of network resource to be accessed + /// - Returns: (optional) Basic Authorization header string to be added to the request + public func authorization(for url: Foundation.URL) -> String? { + guard let index = machines.firstIndex(where: { $0.name == url.host }) ?? machines.firstIndex(where: { $0.isDefault }) else { return nil } + let machine = machines[index] + let authString = "\(machine.login):\(machine.password)" + guard let authData = authString.data(using: .utf8) else { return nil } + return "Basic \(authData.base64EncodedString())" + } + + /// Reads file at path or default location, and returns parsed Netrc representation + /// - Parameter fileURL: Location of netrc file, defaults to `~/.netrc` + /// - Returns: `Netrc` container with parsed connection settings, or error + public static func load(fromFileAtPath filePath: AbsolutePath? = nil) -> Result { + let filePath = filePath ?? AbsolutePath("\(NSHomeDirectory())/.netrc") + + guard FileManager.default.fileExists(atPath: filePath.pathString) else { return .failure(.fileNotFound(filePath)) } + guard FileManager.default.isReadableFile(atPath: filePath.pathString), + let fileContents = try? String(contentsOf: filePath.asURL, encoding: .utf8) else { return .failure(.unreadableFile(filePath)) } + + return Netrc.from(fileContents) + } + + /// Regex matching logic for deriving `Netrc` container from string content + /// - Parameter content: String text of netrc file + /// - Returns: `Netrc` container with parsed connection settings, or error + public static func from(_ content: String) -> Result { + let content = trimComments(from: content) + let regex = try! NSRegularExpression(pattern: RegexUtil.netrcPattern, options: []) + let matches = regex.matches(in: content, options: [], range: NSRange(content.startIndex.. 0 else { return .failure(.machineNotFound) } + return .success(Netrc(machines: machines)) + } + /// Utility method to trim comments from netrc content + /// - Parameter text: String text of netrc file + /// - Returns: String text of netrc file *sans* comments + private static func trimComments(from text: String) -> String { + let regex = try! NSRegularExpression(pattern: RegexUtil.comments, options: .anchorsMatchLines) + let nsString = text as NSString + let range = NSRange(location: 0, length: nsString.length) + let matches = regex.matches(in: text, range: range) + var trimmedCommentsText = text + matches.forEach { + trimmedCommentsText = trimmedCommentsText + .replacingOccurrences(of: nsString.substring(with: $0.range), with: "") + } + return trimmedCommentsText + } +} + +@available (OSX 10.13, *) +public extension Netrc { + enum Error: Swift.Error { + case invalidFilePath + case fileNotFound(AbsolutePath) + case unreadableFile(AbsolutePath) + case machineNotFound + case invalidDefaultMachinePosition + } + + /// Representation of connection settings + /// - important: Default connection settings are stored in machine named `default` + struct Machine: Equatable { + public let name: String + public let login: String + public let password: String + + public var isDefault: Bool { + return name == "default" + } + + public init(name: String, login: String, password: String) { + self.name = name + self.login = login + self.password = password + } + + init?(for match: NSTextCheckingResult, string: String, variant: String = "") { + guard let name = RegexUtil.Token.machine.capture(in: match, string: string) ?? RegexUtil.Token.default.capture(in: match, string: string), + let login = RegexUtil.Token.login.capture(prefix: variant, in: match, string: string), + let password = RegexUtil.Token.password.capture(prefix: variant, in: match, string: string) else { + return nil + } + self = Machine(name: name, login: login, password: password) + } + } +} + +@available (OSX 10.13, *) +fileprivate enum RegexUtil { + @frozen fileprivate enum Token: String, CaseIterable { + case machine, login, password, account, macdef, `default` + + func capture(prefix: String = "", in match: NSTextCheckingResult, string: String) -> String? { + guard let range = Range(match.range(withName: prefix + rawValue), in: string) else { return nil } + return String(string[range]) + } + } + + static let comments: String = "\\#[\\s\\S]*?.*$" + static let `default`: String = #"(?:\s*(?default))"# + static let accountOptional: String = #"(?:\s*account\s+\S++)?"# + static let loginPassword: String = #"\#(namedTrailingCapture("login", prefix: "lp"))\#(accountOptional)\#(namedTrailingCapture("password", prefix: "lp"))"# + static let passwordLogin: String = #"\#(namedTrailingCapture("password", prefix: "pl"))\#(accountOptional)\#(namedTrailingCapture("login", prefix: "pl"))"# + static let netrcPattern = #"(?:(?:(\#(namedTrailingCapture("machine"))|\#(namedMatch("default"))))(?:\#(loginPassword)|\#(passwordLogin)))"# + + static func namedMatch(_ string: String) -> String { + return #"(?:\s*(?<\#(string)>\#(string)))"# + } + + static func namedTrailingCapture(_ string: String, prefix: String = "") -> String { + return #"\s*\#(string)\s+(?<\#(prefix + string)>\S++)"# + } +} +#endif diff --git a/Tests/TSCUtilityTests/DownloaderTests.swift b/Tests/TSCUtilityTests/DownloaderTests.swift index a71ad014..6f926f2a 100644 --- a/Tests/TSCUtilityTests/DownloaderTests.swift +++ b/Tests/TSCUtilityTests/DownloaderTests.swift @@ -18,6 +18,7 @@ import FoundationNetworking #endif class DownloaderTests: XCTestCase { + func testSuccess() { // FIXME: Remove once https://github.com/apple/swift-corelibs-foundation/pull/2593 gets inside a toolchain. #if os(macOS) @@ -72,6 +73,140 @@ class DownloaderTests: XCTestCase { } #endif } + + #if os(macOS) + @available(OSX 10.13, *) + /// Netrc feature depends upon `NSTextCheckingResult.range(withName name: String) -> NSRange`, + /// which is only available in macOS 10.13+ at this time. + func testAuthenticatedSuccess() { + let netrcContent = "machine protected.downloader-tests.com login anonymous password qwerty" + guard case .success(let netrc) = Netrc.from(netrcContent) else { + return XCTFail("Cannot load netrc content") + } + let authData = "anonymous:qwerty".data(using: .utf8)! + let testAuthHeader = "Basic \(authData.base64EncodedString())" + + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [MockAuthenticatingURLProtocol.self] + let downloader = FoundationDownloader(configuration: configuration) + + mktmpdir { tmpdir in + let url = URL(string: "https://protected.downloader-tests.com/testBasics.zip")! + let destination = tmpdir.appending(component: "download") + + let didStartLoadingExpectation = XCTestExpectation(description: "didStartLoading") + let progress50Expectation = XCTestExpectation(description: "progress50") + let progress100Expectation = XCTestExpectation(description: "progress100") + let successExpectation = XCTestExpectation(description: "success") + MockAuthenticatingURLProtocol.notifyDidStartLoading(for: url, completion: { didStartLoadingExpectation.fulfill() }) + + downloader.downloadFile(at: url, to: destination, withAuthorizationProvider: netrc, progress: { bytesDownloaded, totalBytesToDownload in + + XCTAssertEqual(MockAuthenticatingURLProtocol.authenticationHeader(for: url), testAuthHeader) + + switch (bytesDownloaded, totalBytesToDownload) { + case (512, 1024): + progress50Expectation.fulfill() + case (1024, 1024): + progress100Expectation.fulfill() + default: + XCTFail("unexpected progress") + } + }, completion: { result in + switch result { + case .success: + XCTAssert(localFileSystem.exists(destination)) + let bytes = ByteString(Array(repeating: 0xbe, count: 512) + Array(repeating: 0xef, count: 512)) + XCTAssertEqual(try! localFileSystem.readFileContents(destination), bytes) + successExpectation.fulfill() + case .failure(let error): + XCTFail("\(error)") + } + }) + + wait(for: [didStartLoadingExpectation], timeout: 1.0) + + let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "1.1", headerFields: [ + "Content-Length": "1024" + ])! + + MockAuthenticatingURLProtocol.sendResponse(response, for: url) + MockAuthenticatingURLProtocol.sendData(Data(repeating: 0xbe, count: 512), for: url) + wait(for: [progress50Expectation], timeout: 1.0) + MockAuthenticatingURLProtocol.sendData(Data(repeating: 0xef, count: 512), for: url) + wait(for: [progress100Expectation], timeout: 1.0) + MockAuthenticatingURLProtocol.sendCompletion(for: url) + wait(for: [successExpectation], timeout: 1.0) + } + } + #endif + + #if os(macOS) + @available(OSX 10.13, *) + /// Netrc feature depends upon `NSTextCheckingResult.range(withName name: String) -> NSRange`, + /// which is only available in macOS 10.13+ at this time. + func testDefaultAuthenticationSuccess() { + let netrcContent = "default login default password default" + guard case .success(let netrc) = Netrc.from(netrcContent) else { + return XCTFail("Cannot load netrc content") + } + let authData = "default:default".data(using: .utf8)! + let testAuthHeader = "Basic \(authData.base64EncodedString())" + + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [MockAuthenticatingURLProtocol.self] + let downloader = FoundationDownloader(configuration: configuration) + + mktmpdir { tmpdir in + let url = URL(string: "https://restricted.downloader-tests.com/testBasics.zip")! + let destination = tmpdir.appending(component: "download") + + let didStartLoadingExpectation = XCTestExpectation(description: "didStartLoading") + let progress50Expectation = XCTestExpectation(description: "progress50") + let progress100Expectation = XCTestExpectation(description: "progress100") + let successExpectation = XCTestExpectation(description: "success") + MockAuthenticatingURLProtocol.notifyDidStartLoading(for: url, completion: { didStartLoadingExpectation.fulfill() }) + + downloader.downloadFile(at: url, to: destination, withAuthorizationProvider: netrc, progress: { bytesDownloaded, totalBytesToDownload in + + XCTAssertEqual(MockAuthenticatingURLProtocol.authenticationHeader(for: url), testAuthHeader) + + switch (bytesDownloaded, totalBytesToDownload) { + case (512, 1024): + progress50Expectation.fulfill() + case (1024, 1024): + progress100Expectation.fulfill() + default: + XCTFail("unexpected progress") + } + }, completion: { result in + switch result { + case .success: + XCTAssert(localFileSystem.exists(destination)) + let bytes = ByteString(Array(repeating: 0xbe, count: 512) + Array(repeating: 0xef, count: 512)) + XCTAssertEqual(try! localFileSystem.readFileContents(destination), bytes) + successExpectation.fulfill() + case .failure(let error): + XCTFail("\(error)") + } + }) + + wait(for: [didStartLoadingExpectation], timeout: 1.0) + + let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "1.1", headerFields: [ + "Content-Length": "1024" + ])! + + MockAuthenticatingURLProtocol.sendResponse(response, for: url) + MockAuthenticatingURLProtocol.sendData(Data(repeating: 0xbe, count: 512), for: url) + wait(for: [progress50Expectation], timeout: 1.0) + MockAuthenticatingURLProtocol.sendData(Data(repeating: 0xef, count: 512), for: url) + wait(for: [progress100Expectation], timeout: 1.0) + MockAuthenticatingURLProtocol.sendCompletion(for: url) + wait(for: [successExpectation], timeout: 1.0) + } + } + #endif func testClientError() { // FIXME: Remove once https://github.com/apple/swift-corelibs-foundation/pull/2593 gets inside a toolchain. @@ -208,6 +343,16 @@ private struct DummyError: Error { private typealias Action = () -> Void +private class MockAuthenticatingURLProtocol: MockURLProtocol { + + fileprivate static func authenticationHeader(for url: Foundation.URL) -> String? { + guard let instance = instance(for: url) else { + fatalError("url did not start loading") + } + return instance.request.allHTTPHeaderFields?["Authorization"] + } +} + private class MockURLProtocol: URLProtocol { private static var queue = DispatchQueue(label: "org.swift.swiftpm.basic-tests.mock-url-protocol") private static var observers: [Foundation.URL: Action] = [:] @@ -309,6 +454,10 @@ private class MockURLProtocol: URLProtocol { Self.instances[url] = nil } } + + fileprivate static func instance(for url: Foundation.URL) -> URLProtocol? { + return Self.instances[url] + } } class FailingFileSystem: FileSystem { diff --git a/Tests/TSCUtilityTests/NetrcTests.swift b/Tests/TSCUtilityTests/NetrcTests.swift new file mode 100644 index 00000000..aa9d89a4 --- /dev/null +++ b/Tests/TSCUtilityTests/NetrcTests.swift @@ -0,0 +1,448 @@ +import XCTest +import TSCUtility + +#if os(macOS) +@available(macOS 10.13, *) +/// Netrc feature depends upon `NSTextCheckingResult.range(withName name: String) -> NSRange`, +/// which is only available in macOS 10.13+ at this time. +class NetrcTests: XCTestCase { + /// should load machines for a given inline format + func testLoadMachinesInline() { + let content = "machine example.com login anonymous password qwerty" + + guard case .success(let netrc) = Netrc.from(content) else { return XCTFail() } + XCTAssertEqual(netrc.machines.count, 1) + + let machine = netrc.machines.first + XCTAssertEqual(machine?.name, "example.com") + XCTAssertEqual(machine?.login, "anonymous") + XCTAssertEqual(machine?.password, "qwerty") + + let authorization = netrc.authorization(for: URL(string: "http://example.com/resource.zip")!) + XCTAssertNotNil(authorization) + + let authData = "anonymous:qwerty".data(using: .utf8)! + XCTAssertEqual(authorization, "Basic \(authData.base64EncodedString())") + + XCTAssertNil(netrc.authorization(for: URL(string: "http://example2.com/resource.zip")!)) + XCTAssertNil(netrc.authorization(for: URL(string: "http://www.example2.com/resource.zip")!)) + } + + /// should load machines for a given multi-line format + func testLoadMachinesMultiLine() { + let content = """ + machine example.com + login anonymous + password qwerty + """ + + guard case .success(let netrc) = Netrc.from(content) else { return XCTFail() } + XCTAssertEqual(netrc.machines.count, 1) + + let machine = netrc.machines.first + XCTAssertEqual(machine?.name, "example.com") + XCTAssertEqual(machine?.login, "anonymous") + XCTAssertEqual(machine?.password, "qwerty") + + let authorization = netrc.authorization(for: URL(string: "http://example.com/resource.zip")!) + XCTAssertNotNil(authorization) + + let authData = "anonymous:qwerty".data(using: .utf8)! + XCTAssertEqual(authorization, "Basic \(authData.base64EncodedString())") + + XCTAssertNil(netrc.authorization(for: URL(string: "http://example2.com/resource.zip")!)) + XCTAssertNil(netrc.authorization(for: URL(string: "http://www.example2.com/resource.zip")!)) + } + + /// Should fall back to default machine when not matching host + func testLoadDefaultMachine() { + let content = """ + machine example.com + login anonymous + password qwerty + + default + login id + password secret + """ + + guard case .success(let netrc) = Netrc.from(content) else { return XCTFail() } + XCTAssertEqual(netrc.machines.count, 2) + + let machine = netrc.machines.first + XCTAssertEqual(machine?.name, "example.com") + XCTAssertEqual(machine?.login, "anonymous") + XCTAssertEqual(machine?.password, "qwerty") + + let machine2 = netrc.machines.last + XCTAssertEqual(machine2?.name, "default") + XCTAssertEqual(machine2?.login, "id") + XCTAssertEqual(machine2?.password, "secret") + + let authorization = netrc.authorization(for: URL(string: "http://example2.com/resource.zip")!) + XCTAssertNotNil(authorization) + + let authData = "id:secret".data(using: .utf8)! + XCTAssertEqual(authorization, "Basic \(authData.base64EncodedString())") + } + + func testRegexParsing() { + let content = """ + machine machine + login login + password password + + machine login + password machine + login password + + default machine + login id + password secret + + machinemachine machine + loginlogin id + passwordpassword secret + + default + login id + password secret + """ + + guard case .success(let netrc) = Netrc.from(content) else { return XCTFail() } + XCTAssertEqual(netrc.machines.count, 3) + + XCTAssertEqual(netrc.machines[0].name, "machine") + XCTAssertEqual(netrc.machines[0].login, "login") + XCTAssertEqual(netrc.machines[0].password, "password") + + XCTAssertEqual(netrc.machines[1].name, "login") + XCTAssertEqual(netrc.machines[1].login, "password") + XCTAssertEqual(netrc.machines[1].password, "machine") + + XCTAssertEqual(netrc.machines[2].name, "default") + XCTAssertEqual(netrc.machines[2].login, "id") + XCTAssertEqual(netrc.machines[2].password, "secret") + + let authorization = netrc.authorization(for: URL(string: "http://example2.com/resource.zip")!) + XCTAssertNotNil(authorization) + + let authData = "id:secret".data(using: .utf8)! + XCTAssertEqual(authorization, "Basic \(authData.base64EncodedString())") + } + + func testOutOfOrderDefault() { + let content = """ + machine machine + login login + password password + + machine login + password machine + login password + + default + login id + password secret + + machine machine + login id + password secret + """ + + guard case .failure(.invalidDefaultMachinePosition) = Netrc.from(content) else { return XCTFail() } + } + + func testErrorOnMultipleDefault() { + let content = """ + machine machine + login login + password password + + machine login + password machine + login password + + default + login id + password secret + + machine machine + login id + password secret + + default + login di + password terces + """ + + guard case .failure(.invalidDefaultMachinePosition) = Netrc.from(content) else { return XCTFail() } + } + + /// should load machines for a given multi-line format with comments + func testLoadMachinesMultilineComments() { + let content = """ + ## This is a comment + # This is another comment + machine example.com # This is an inline comment + login anonymous + password qwerty # and # another #one + """ + + let machines = try? Netrc.from(content).get().machines + XCTAssertEqual(machines?.count, 1) + + let machine = machines?.first + XCTAssertEqual(machine?.name, "example.com") + XCTAssertEqual(machine?.login, "anonymous") + XCTAssertEqual(machine?.password, "qwerty") + } + + /// should load machines for a given multi-line + whitespaces format + func testLoadMachinesMultilineWhitespaces() { + let content = """ + machine example.com login anonymous + password qwerty + """ + + let machines = try? Netrc.from(content).get().machines + XCTAssertEqual(machines?.count, 1) + + let machine = machines?.first + XCTAssertEqual(machine?.name, "example.com") + XCTAssertEqual(machine?.login, "anonymous") + XCTAssertEqual(machine?.password, "qwerty") + } + + /// should load multiple machines for a given inline format + func testLoadMultipleMachinesInline() { + let content = "machine example.com login anonymous password qwerty machine example2.com login anonymous2 password qwerty2" + + guard case .success(let netrc) = Netrc.from(content) else { return XCTFail() } + XCTAssertEqual(netrc.machines.count, 2) + + XCTAssertEqual(netrc.machines[0].name, "example.com") + XCTAssertEqual(netrc.machines[0].login, "anonymous") + XCTAssertEqual(netrc.machines[0].password, "qwerty") + + XCTAssertEqual(netrc.machines[1].name, "example2.com") + XCTAssertEqual(netrc.machines[1].login, "anonymous2") + XCTAssertEqual(netrc.machines[1].password, "qwerty2") + } + + /// should load multiple machines for a given multi-line format + func testLoadMultipleMachinesMultiline() { + let content = """ + machine example.com login anonymous + password qwerty + machine example2.com + login anonymous2 + password qwerty2 + """ + + let machines = try? Netrc.from(content).get().machines + XCTAssertEqual(machines?.count, 2) + + var machine = machines?[0] + XCTAssertEqual(machine?.name, "example.com") + XCTAssertEqual(machine?.login, "anonymous") + XCTAssertEqual(machine?.password, "qwerty") + + machine = machines?[1] + XCTAssertEqual(machine?.name, "example2.com") + XCTAssertEqual(machine?.login, "anonymous2") + XCTAssertEqual(machine?.password, "qwerty2") + } + + /// should throw error when machine parameter is missing + func testErrorMachineParameterMissing() { + let content = "login anonymous password qwerty" + + guard case .failure(.machineNotFound) = Netrc.from(content) else { + return XCTFail("Expected machineNotFound error") + } + } + + /// should throw error for an empty machine values + func testErrorEmptyMachineValue() { + let content = "machine" + + guard case .failure(.machineNotFound) = Netrc.from(content) else { + return XCTFail("Expected machineNotFound error") + } + } + + /// should throw error for an empty machine values + func testEmptyMachineValueFollowedByDefaultNoError() { + let content = "machine default login id password secret" + guard case .success(let netrc) = Netrc.from(content) else { return XCTFail() } + let authorization = netrc.authorization(for: URL(string: "http://example.com/resource.zip")!) + let authData = "id:secret".data(using: .utf8)! + XCTAssertNotNil(authorization) + XCTAssertEqual(authorization, "Basic \(authData.base64EncodedString())") + } + + /// should return authorization when config contains a given machine + func testReturnAuthorizationForMachineMatch() { + let content = "machine example.com login anonymous password qwerty" + + guard case .success(let netrc) = Netrc.from(content) else { return XCTFail() } + + let authorization = netrc.authorization(for: URL(string: "http://example.com/resource.zip")!) + let authData = "anonymous:qwerty".data(using: .utf8)! + XCTAssertNotNil(authorization) + XCTAssertEqual(authorization, "Basic \(authData.base64EncodedString())") + } + + func testReturnNoAuthorizationForUnmatched() { + let content = "machine example.com login anonymous password qwerty" + guard case .success(let netrc) = Netrc.from(content) else { return XCTFail() } + XCTAssertNil(netrc.authorization(for: URL(string: "http://www.example.com/resource.zip")!)) + XCTAssertNil(netrc.authorization(for: URL(string: "ftp.example.com/resource.zip")!)) + XCTAssertNil(netrc.authorization(for: URL(string: "http://example2.com/resource.zip")!)) + XCTAssertNil(netrc.authorization(for: URL(string: "http://www.example2.com/resource.zip")!)) + } + + /// should not return authorization when config does not contain a given machine + func testNoReturnAuthorizationForNoMachineMatch() { + let content = "machine example.com login anonymous password qwerty" + + guard case .success(let netrc) = Netrc.from(content) else { return XCTFail() } + XCTAssertNil(netrc.authorization(for: URL(string: "https://example99.com")!)) + XCTAssertNil(netrc.authorization(for: URL(string: "http://www.example.com/resource.zip")!)) + XCTAssertNil(netrc.authorization(for: URL(string: "ftp.example.com/resource.zip")!)) + XCTAssertNil(netrc.authorization(for: URL(string: "http://example2.com/resource.zip")!)) + XCTAssertNil(netrc.authorization(for: URL(string: "http://www.example2.com/resource.zip")!)) + } + + /// Test case: https://www.ibm.com/support/knowledgecenter/en/ssw_aix_72/filesreference/netrc.html + func testIBMDocumentation() { + let content = "machine host1.austin.century.com login fred password bluebonnet" + + guard let netrc = try? Netrc.from(content).get() else { + return XCTFail() + } + + let machine = netrc.machines.first + XCTAssertEqual(machine?.name, "host1.austin.century.com") + XCTAssertEqual(machine?.login, "fred") + XCTAssertEqual(machine?.password, "bluebonnet") + } + + /// Should not fail on presence of `account`, `macdef`, `default` + /// test case: https://gist.github.com/tpope/4247721 + func testNoErrorTrailingAccountMacdefDefault() { + let content = """ + machine api.heroku.com + login my@email.com + password 01230123012301230123012301230123 + + machine api.github.com password something login somebody + + machine ftp.server login abc password def account ghi macdef somemacro + cd somehwhere + continues until end of paragraph + + default login anonymous password my@email.com + """ + + guard let netrc = try? Netrc.from(content).get() else { + return XCTFail() + } + + XCTAssertEqual(netrc.machines.count, 4) + + XCTAssertEqual(netrc.machines[0].name, "api.heroku.com") + XCTAssertEqual(netrc.machines[0].login, "my@email.com") + XCTAssertEqual(netrc.machines[0].password, "01230123012301230123012301230123") + + XCTAssertEqual(netrc.machines[1].name, "api.github.com") + XCTAssertEqual(netrc.machines[1].login, "somebody") + XCTAssertEqual(netrc.machines[1].password, "something") + + XCTAssertEqual(netrc.machines[2].name, "ftp.server") + XCTAssertEqual(netrc.machines[2].login, "abc") + XCTAssertEqual(netrc.machines[2].password, "def") + + XCTAssertEqual(netrc.machines[3].name, "default") + XCTAssertEqual(netrc.machines[3].login, "anonymous") + XCTAssertEqual(netrc.machines[3].password, "my@email.com") + } + + /// Should not fail on presence of `account`, `macdef`, `default` + /// test case: https://gist.github.com/tpope/4247721 + func testNoErrorMixedAccount() { + let content = """ + machine api.heroku.com + login my@email.com + password 01230123012301230123012301230123 + + machine api.github.com password something account ghi login somebody + + machine ftp.server login abc account ghi password def macdef somemacro + cd somehwhere + continues until end of paragraph + + default login anonymous password my@email.com + """ + + guard let netrc = try? Netrc.from(content).get() else { + return XCTFail() + } + + XCTAssertEqual(netrc.machines.count, 4) + + XCTAssertEqual(netrc.machines[0].name, "api.heroku.com") + XCTAssertEqual(netrc.machines[0].login, "my@email.com") + XCTAssertEqual(netrc.machines[0].password, "01230123012301230123012301230123") + + XCTAssertEqual(netrc.machines[1].name, "api.github.com") + XCTAssertEqual(netrc.machines[1].login, "somebody") + XCTAssertEqual(netrc.machines[1].password, "something") + + XCTAssertEqual(netrc.machines[2].name, "ftp.server") + XCTAssertEqual(netrc.machines[2].login, "abc") + XCTAssertEqual(netrc.machines[2].password, "def") + + XCTAssertEqual(netrc.machines[3].name, "default") + XCTAssertEqual(netrc.machines[3].login, "anonymous") + XCTAssertEqual(netrc.machines[3].password, "my@email.com") + } + + /// Should not fail on presence of `account`, `macdef`, `default` + /// test case: https://renenyffenegger.ch/notes/Linux/fhs/home/username/_netrc + func testNoErrorMultipleMacdefAndComments() { + let content = """ + machine ftp.foobar.baz + login john + password 5ecr3t + + macdef getmyfile # define a macro (here named 'getmyfile') + cd /abc/defghi/jklm # The macro can be executed in ftp client + get myFile.txt # by prepending macro name with $ sign + quit + + macdef init # macro init is searched for when + binary # ftp connects to server. + + machine other.server.org + login fred + password sunshine4ever + """ + + guard let netrc = try? Netrc.from(content).get() else { + return XCTFail() + } + + XCTAssertEqual(netrc.machines.count, 2) + + XCTAssertEqual(netrc.machines[0].name, "ftp.foobar.baz") + XCTAssertEqual(netrc.machines[0].login, "john") + XCTAssertEqual(netrc.machines[0].password, "5ecr3t") + + XCTAssertEqual(netrc.machines[1].name, "other.server.org") + XCTAssertEqual(netrc.machines[1].login, "fred") + XCTAssertEqual(netrc.machines[1].password, "sunshine4ever") + } +} +#endif From 48ca59ab01936c754d87c02b7d447ec990f0ff2e Mon Sep 17 00:00:00 2001 From: Stan Stadelman Date: Fri, 30 Oct 2020 15:28:49 -0700 Subject: [PATCH 2/2] netrc should not look to HOME by default --- Sources/TSCUtility/Netrc.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/TSCUtility/Netrc.swift b/Sources/TSCUtility/Netrc.swift index 6c979910..26aaa4e7 100644 --- a/Sources/TSCUtility/Netrc.swift +++ b/Sources/TSCUtility/Netrc.swift @@ -52,8 +52,7 @@ public struct Netrc: AuthorizationProviding { /// - Parameter fileURL: Location of netrc file, defaults to `~/.netrc` /// - Returns: `Netrc` container with parsed connection settings, or error public static func load(fromFileAtPath filePath: AbsolutePath? = nil) -> Result { - let filePath = filePath ?? AbsolutePath("\(NSHomeDirectory())/.netrc") - + guard let filePath = filePath else { return .failure(.invalidFilePath)} guard FileManager.default.fileExists(atPath: filePath.pathString) else { return .failure(.fileNotFound(filePath)) } guard FileManager.default.isReadableFile(atPath: filePath.pathString), let fileContents = try? String(contentsOf: filePath.asURL, encoding: .utf8) else { return .failure(.unreadableFile(filePath)) }