1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
|
import Foundation
import TSCBasic
/// Supplies `Authorization` header, typically to be appended to `URLRequest`
// deprecated 9/2021
@available(*, deprecated)
public protocol AuthorizationProviding {
/// Optional `Authorization` header, likely added to `URLRequest`
func authorization(for url: Foundation.URL) -> String?
}
// deprecated 9/2021
@available(*, deprecated)
extension AuthorizationProviding {
public func authorization(for url: Foundation.URL) -> String? {
return nil
}
}
/*
Netrc feature depends upon `NSTextCheckingResult.range(withName name: String) -> NSRange`,
which is only available in macOS 10.13+, iOS 11+, etc at this time.
*/
/// Container of parsed netrc connection settings
// FIXME: deprecate 2/2022, remove once clients transitioned
@available(*, deprecated, message: "moved to SwiftPM")
@available (macOS 10.13, iOS 11, tvOS 11, watchOS 4, *)
public struct Netrc {
/// 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) throws -> Result<Netrc, Netrc.Error> {
let filePath = try filePath ?? AbsolutePath(validating: "\(NSHomeDirectory())/.netrc")
guard FileManager.default.fileExists(atPath: filePath.pathString) else { return .failure(.fileNotFound(filePath)) }
guard FileManager.default.isReadableFile(atPath: filePath.pathString) else { return .failure(.unreadableFile(filePath)) }
let fileContents = try String(contentsOf: filePath.asURL, encoding: .utf8)
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<Netrc, Netrc.Error> {
let content = trimComments(from: content)
let regex = try! NSRegularExpression(pattern: RegexUtil.netrcPattern, options: [])
let matches = regex.matches(in: content, options: [], range: NSRange(content.startIndex..<content.endIndex, in: content))
let machines: [Machine] = matches.compactMap {
return Machine(for: $0, string: content, variant: "lp") ??
Machine(for: $0, string: content, variant: "pl")
}
if let defIndex = machines.firstIndex(where: { $0.isDefault }) {
guard defIndex == machines.index(before: machines.endIndex) else { return .failure(.invalidDefaultMachinePosition) }
}
guard machines.count > 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
}
}
// deprecated 9/2021
@available(*, deprecated)
@available (macOS 10.13, iOS 11, tvOS 11, watchOS 4, *)
extension Netrc: AuthorizationProviding {}
// FIXME: deprecate 2/2022, remove once clients transitioned
@available(*, deprecated, message: "moved to SwiftPM")
@available (macOS 10.13, iOS 11, tvOS 11, watchOS 4, *)
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)
}
}
}
// FIXME: deprecate 2/2022, remove once clients transitioned
@available(*, deprecated, message: "moved to SwiftPM")
@available (macOS 10.13, iOS 11, tvOS 11, watchOS 4, *)
extension Netrc.Error: CustomNSError {
public var errorUserInfo: [String : Any] {
return [NSLocalizedDescriptionKey: "\(self)"]
}
}
// FIXME: deprecate 2/2022, remove once clients transitioned
@available(*, deprecated, message: "moved to SwiftPM")
@available (macOS 10.13, iOS 11, tvOS 11, watchOS 4, *)
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>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++)"#
}
}
|