1
1
/*
2
2
This source file is part of the Swift.org open source project
3
+
3
4
Copyright (c) 2021 Apple Inc. and the Swift project authors
4
5
Licensed under Apache License v2.0 with Runtime Library Exception
6
+
5
7
See http://swift.org/LICENSE.txt for license information
6
8
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
7
9
*/
8
10
9
- import Foundation
11
+ import struct Foundation. Data
12
+ import struct Foundation. URL
13
+ #if canImport(Security)
14
+ import Security
15
+ #endif
16
+
17
+ import TSCBasic
18
+ import TSCUtility
10
19
11
20
public protocol AuthorizationProvider {
12
- func authentication( for url: URL ) -> ( user: String , password: String ) ?
21
+ func authentication( for url: Foundation . URL ) -> ( user: String , password: String ) ?
13
22
}
14
23
15
- extension AuthorizationProvider {
16
- public func httpAuthorizationHeader( for url: URL ) -> String ? {
24
+ public enum AuthorizationProviderError : Error {
25
+ case invalidURLHost
26
+ case notFound
27
+ case cannotEncodePassword
28
+ case other( String )
29
+ }
30
+
31
+ public extension AuthorizationProvider {
32
+ func httpAuthorizationHeader( for url: Foundation . URL ) -> String ? {
17
33
guard let ( user, password) = self . authentication ( for: url) else {
18
34
return nil
19
35
}
@@ -24,3 +40,251 @@ extension AuthorizationProvider {
24
40
return " Basic \( authData. base64EncodedString ( ) ) "
25
41
}
26
42
}
43
+
44
+ extension Foundation . URL {
45
+ var authenticationID : String ? {
46
+ guard let host = host? . lowercased ( ) else {
47
+ return nil
48
+ }
49
+ return host. isEmpty ? nil : host
50
+ }
51
+ }
52
+
53
+ // MARK: - netrc
54
+
55
+ public struct NetrcAuthorizationProvider : AuthorizationProvider {
56
+ let path : AbsolutePath
57
+ private let fileSystem : FileSystem
58
+
59
+ private var underlying : TSCUtility . Netrc ?
60
+
61
+ var machines : [ TSCUtility . Netrc . Machine ] {
62
+ self . underlying? . machines ?? [ ]
63
+ }
64
+
65
+ public init ( path: AbsolutePath , fileSystem: FileSystem ) throws {
66
+ self . path = path
67
+ self . fileSystem = fileSystem
68
+ self . underlying = try Self . load ( from: path)
69
+ }
70
+
71
+ public mutating func addOrUpdate( for url: Foundation . URL , user: String , password: String , callback: @escaping ( Result < Void , Error > ) -> Void ) {
72
+ guard let machine = url. authenticationID else {
73
+ return callback ( . failure( AuthorizationProviderError . invalidURLHost) )
74
+ }
75
+
76
+ // Same entry already exists, no need to add or update
77
+ guard self . machines. first ( where: { $0. name. lowercased ( ) == machine && $0. login == user && $0. password == password } ) == nil else {
78
+ return
79
+ }
80
+
81
+ do {
82
+ // Append to end of file
83
+ try self . fileSystem. withLock ( on: self . path, type: . exclusive) {
84
+ let contents = try ? self . fileSystem. readFileContents ( self . path) . contents
85
+ try self . fileSystem. writeFileContents ( self . path) { stream in
86
+ // File does not exist yet
87
+ if let contents = contents {
88
+ stream. write ( contents)
89
+ stream. write ( " \n " )
90
+ }
91
+ stream. write ( " machine \( machine) login \( user) password \( password) " )
92
+ stream. write ( " \n " )
93
+ }
94
+ }
95
+
96
+ // At this point the netrc file should exist and non-empty
97
+ guard let netrc = try Self . load ( from: self . path) else {
98
+ throw AuthorizationProviderError . other ( " Failed to update netrc file at \( self . path) " )
99
+ }
100
+ self . underlying = netrc
101
+
102
+ callback ( . success( ( ) ) )
103
+ } catch {
104
+ callback ( . failure( AuthorizationProviderError . other ( " Failed to update netrc file at \( self . path) : \( error) " ) ) )
105
+ }
106
+ }
107
+
108
+ public func authentication( for url: Foundation . URL ) -> ( user: String , password: String ) ? {
109
+ self . machine ( for: url) . map { ( user: $0. login, password: $0. password) }
110
+ }
111
+
112
+ private func machine( for url: Foundation . URL ) -> TSCUtility . Netrc . Machine ? {
113
+ if let machine = url. authenticationID, let existing = self . machines. first ( where: { $0. name. lowercased ( ) == machine } ) {
114
+ return existing
115
+ }
116
+ if let existing = self . machines. first ( where: { $0. isDefault } ) {
117
+ return existing
118
+ }
119
+ return . none
120
+ }
121
+
122
+ private static func load( from path: AbsolutePath ) throws -> TSCUtility . Netrc ? {
123
+ do {
124
+ return try TSCUtility . Netrc. load ( fromFileAtPath: path) . get ( )
125
+ } catch {
126
+ switch error {
127
+ case Netrc . Error. fileNotFound, Netrc . Error. machineNotFound:
128
+ // These are recoverable errors. We will just create the file and append entry to it.
129
+ return nil
130
+ default :
131
+ throw error
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ // MARK: - Keychain
138
+
139
+ #if canImport(Security)
140
+ public struct KeychainAuthorizationProvider : AuthorizationProvider {
141
+ private let observabilityScope : ObservabilityScope
142
+
143
+ public init ( observabilityScope: ObservabilityScope ) {
144
+ self . observabilityScope = observabilityScope
145
+ }
146
+
147
+ public func addOrUpdate( for url: Foundation . URL , user: String , password: String , callback: @escaping ( Result < Void , Error > ) -> Void ) {
148
+ guard let server = url. authenticationID else {
149
+ return callback ( . failure( AuthorizationProviderError . invalidURLHost) )
150
+ }
151
+ guard let passwordData = password. data ( using: . utf8) else {
152
+ return callback ( . failure( AuthorizationProviderError . cannotEncodePassword) )
153
+ }
154
+ let `protocol` = self . protocol ( for: url)
155
+
156
+ do {
157
+ if !( try self . update ( server: server, protocol: `protocol`, account: user, password: passwordData) ) {
158
+ try self . create ( server: server, protocol: `protocol`, account: user, password: passwordData)
159
+ }
160
+ callback ( . success( ( ) ) )
161
+ } catch {
162
+ callback ( . failure( error) )
163
+ }
164
+ }
165
+
166
+ public func authentication( for url: Foundation . URL ) -> ( user: String , password: String ) ? {
167
+ guard let server = url. authenticationID else {
168
+ return nil
169
+ }
170
+
171
+ do {
172
+ guard let existingItem = try self . search ( server: server, protocol: self . protocol ( for: url) ) as? [ String : Any ] ,
173
+ let passwordData = existingItem [ kSecValueData as String ] as? Data ,
174
+ let password = String ( data: passwordData, encoding: String . Encoding. utf8) ,
175
+ let account = existingItem [ kSecAttrAccount as String ] as? String
176
+ else {
177
+ throw AuthorizationProviderError . other ( " Failed to extract credentials for server \( server) from keychain " )
178
+ }
179
+ return ( user: account, password: password)
180
+ } catch {
181
+ switch error {
182
+ case AuthorizationProviderError . notFound:
183
+ self . observabilityScope. emit ( info: " No credentials found for server \( server) in keychain " )
184
+ case AuthorizationProviderError . other( let detail) :
185
+ self . observabilityScope. emit ( error: detail)
186
+ default :
187
+ self . observabilityScope. emit ( error: " Failed to find credentials for server \( server) in keychain: \( error) " )
188
+ }
189
+ return nil
190
+ }
191
+ }
192
+
193
+ private func create( server: String , protocol: CFString , account: String , password: Data ) throws {
194
+ let query : [ String : Any ] = [ kSecClass as String : kSecClassInternetPassword,
195
+ kSecAttrServer as String : server,
196
+ kSecAttrProtocol as String : `protocol`,
197
+ kSecAttrAccount as String : account,
198
+ kSecValueData as String : password]
199
+
200
+ let status = SecItemAdd ( query as CFDictionary , nil )
201
+ guard status == errSecSuccess else {
202
+ throw AuthorizationProviderError . other ( " Failed to save credentials for server \( server) to keychain: status \( status) " )
203
+ }
204
+ }
205
+
206
+ private func update( server: String , protocol: CFString , account: String , password: Data ) throws -> Bool {
207
+ let query : [ String : Any ] = [ kSecClass as String : kSecClassInternetPassword,
208
+ kSecAttrServer as String : server,
209
+ kSecAttrProtocol as String : `protocol`]
210
+ let attributes : [ String : Any ] = [ kSecAttrAccount as String : account,
211
+ kSecValueData as String : password]
212
+
213
+ let status = SecItemUpdate ( query as CFDictionary , attributes as CFDictionary )
214
+ guard status != errSecItemNotFound else {
215
+ return false
216
+ }
217
+ guard status == errSecSuccess else {
218
+ throw AuthorizationProviderError . other ( " Failed to update credentials for server \( server) in keychain: status \( status) " )
219
+ }
220
+ return true
221
+ }
222
+
223
+ private func search( server: String , protocol: CFString ) throws -> CFTypeRef ? {
224
+ let query : [ String : Any ] = [ kSecClass as String : kSecClassInternetPassword,
225
+ kSecAttrServer as String : server,
226
+ kSecAttrProtocol as String : `protocol`,
227
+ kSecMatchLimit as String : kSecMatchLimitOne,
228
+ kSecReturnAttributes as String : true ,
229
+ kSecReturnData as String : true ]
230
+
231
+ var item : CFTypeRef ?
232
+ // Search keychain for server's username and password, if any.
233
+ let status = SecItemCopyMatching ( query as CFDictionary , & item)
234
+ guard status != errSecItemNotFound else {
235
+ throw AuthorizationProviderError . notFound
236
+ }
237
+ guard status == errSecSuccess else {
238
+ throw AuthorizationProviderError . other ( " Failed to find credentials for server \( server) in keychain: status \( status) " )
239
+ }
240
+
241
+ return item
242
+ }
243
+
244
+ private func `protocol`( for url: Foundation . URL ) -> CFString {
245
+ // See https://developer.apple.com/documentation/security/keychain_services/keychain_items/item_attribute_keys_and_values?language=swift
246
+ // for a list of possible values for the `kSecAttrProtocol` attribute.
247
+ switch url. scheme? . lowercased ( ) {
248
+ case " https " :
249
+ return kSecAttrProtocolHTTPS
250
+ default :
251
+ return kSecAttrProtocolHTTPS
252
+ }
253
+ }
254
+ }
255
+ #endif
256
+
257
+ // MARK: - Composite
258
+
259
+ public struct CompositeAuthorizationProvider : AuthorizationProvider {
260
+ private let providers : [ AuthorizationProvider ]
261
+ private let observabilityScope : ObservabilityScope
262
+
263
+ public init ( _ providers: AuthorizationProvider ... , observabilityScope: ObservabilityScope ) {
264
+ self . init ( providers, observabilityScope: observabilityScope)
265
+ }
266
+
267
+ public init ( _ providers: [ AuthorizationProvider ] , observabilityScope: ObservabilityScope ) {
268
+ self . providers = providers
269
+ self . observabilityScope = observabilityScope
270
+ }
271
+
272
+ public func authentication( for url: Foundation . URL ) -> ( user: String , password: String ) ? {
273
+ for provider in self . providers {
274
+ if let authentication = provider. authentication ( for: url) {
275
+ switch provider {
276
+ case let provider as NetrcAuthorizationProvider :
277
+ self . observabilityScope. emit ( info: " Credentials for \( url) found in netrc file at \( provider. path) " )
278
+ #if canImport(Security)
279
+ case is KeychainAuthorizationProvider :
280
+ self . observabilityScope. emit ( info: " Credentials for \( url) found in keychain " )
281
+ #endif
282
+ default :
283
+ self . observabilityScope. emit ( info: " Credentials for \( url) found in \( provider) " )
284
+ }
285
+ return authentication
286
+ }
287
+ }
288
+ return nil
289
+ }
290
+ }
0 commit comments