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 os(macOS)
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
+ mutating func addOrUpdate( for url: Foundation . URL , user: String , password: String , callback: @escaping ( Result < Void , Error > ) -> Void )
22
+
23
+ func authentication( for url: Foundation . URL ) -> ( user: String , password: String ) ?
24
+ }
25
+
26
+ public enum AuthorizationProviderError : Error {
27
+ case noURLHost
28
+ case notFound
29
+ case unexpectedPasswordData
30
+ case unexpectedError( String )
13
31
}
14
32
15
33
extension AuthorizationProvider {
16
- public func httpAuthorizationHeader( for url: URL ) -> String ? {
34
+ public func httpAuthorizationHeader( for url: Foundation . URL ) -> String ? {
17
35
guard let ( user, password) = self . authentication ( for: url) else {
18
36
return nil
19
37
}
@@ -24,3 +42,224 @@ extension AuthorizationProvider {
24
42
return " Basic \( authData. base64EncodedString ( ) ) "
25
43
}
26
44
}
45
+
46
+ // MARK: - netrc
47
+
48
+ public struct NetrcAuthorizationProvider : AuthorizationProvider {
49
+ private let path : AbsolutePath
50
+ private let fileSystem : FileSystem
51
+
52
+ private var underlying : TSCUtility . Netrc ?
53
+
54
+ private var machines : [ TSCUtility . Netrc . Machine ] {
55
+ self . underlying? . machines ?? [ ]
56
+ }
57
+
58
+ public init ( path: AbsolutePath , fileSystem: FileSystem ) throws {
59
+ self . path = path
60
+ self . fileSystem = fileSystem
61
+ self . underlying = try Self . loadFromDisk ( path: path)
62
+ }
63
+
64
+ public mutating func addOrUpdate( for url: Foundation . URL , user: String , password: String , callback: @escaping ( Result < Void , Error > ) -> Void ) {
65
+ guard let machineName = self . machineName ( for: url) else {
66
+ return callback ( . failure( AuthorizationProviderError . noURLHost) )
67
+ }
68
+ let machine = TSCUtility . Netrc. Machine ( name: machineName, login: user, password: password)
69
+
70
+ var machines = [ TSCUtility . Netrc. Machine] ( )
71
+ var hasExisting = false
72
+
73
+ self . machines. forEach {
74
+ if $0. name. lowercased ( ) != machineName {
75
+ machines. append ( $0)
76
+ } else if !hasExisting {
77
+ // Update existing entry and retain one copy only
78
+ machines. append ( machine)
79
+ hasExisting = true
80
+ }
81
+ }
82
+
83
+ // New entry
84
+ if !hasExisting {
85
+ machines. append ( machine)
86
+ }
87
+
88
+ do {
89
+ try self . saveToDisk ( machines: machines)
90
+ // At this point the netrc file should exist and non-empty
91
+ guard let netrc = try Self . loadFromDisk ( path: self . path) else {
92
+ throw AuthorizationProviderError . unexpectedError ( " Failed to update netrc file at \( self . path) " )
93
+ }
94
+ self . underlying = netrc
95
+ callback ( . success( ( ) ) )
96
+ } catch {
97
+ callback ( . failure( AuthorizationProviderError . unexpectedError ( " Failed to update netrc file at \( self . path) : \( error) " ) ) )
98
+ }
99
+ }
100
+
101
+ public func authentication( for url: Foundation . URL ) -> ( user: String , password: String ) ? {
102
+ self . machine ( for: url) . map { ( user: $0. login, password: $0. password) }
103
+ }
104
+
105
+ private func machineName( for url: Foundation . URL ) -> String ? {
106
+ url. host? . lowercased ( )
107
+ }
108
+
109
+ private func machine( for url: Foundation . URL ) -> TSCUtility . Netrc . Machine ? {
110
+ if let machineName = self . machineName ( for: url) , let machine = self . machines. first ( where: { $0. name. lowercased ( ) == machineName } ) {
111
+ return machine
112
+ }
113
+ if let machine = self . machines. first ( where: { $0. isDefault } ) {
114
+ return machine
115
+ }
116
+ return . none
117
+ }
118
+
119
+ private func saveToDisk( machines: [ TSCUtility . Netrc . Machine ] ) throws {
120
+ try self . fileSystem. withLock ( on: self . path, type: . exclusive) {
121
+ try self . fileSystem. writeFileContents ( self . path) { stream in
122
+ machines. forEach {
123
+ stream. write ( " machine \( $0. name) login \( $0. login) password \( $0. password) \n " )
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ private static func loadFromDisk( path: AbsolutePath ) throws -> TSCUtility . Netrc ? {
130
+ do {
131
+ return try TSCUtility . Netrc. load ( fromFileAtPath: path) . get ( )
132
+ } catch {
133
+ switch error {
134
+ case Netrc . Error. fileNotFound, Netrc . Error. machineNotFound:
135
+ // These are recoverable errors. We will just create the file and append entry to it.
136
+ return nil
137
+ default :
138
+ throw error
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ // MARK: - Keychain
145
+
146
+ #if os(macOS)
147
+ public struct KeychainAuthorizationProvider : AuthorizationProvider {
148
+ public init ( ) { }
149
+
150
+ public func addOrUpdate( for url: Foundation . URL , user: String , password: String , callback: @escaping ( Result < Void , Error > ) -> Void ) {
151
+ guard let server = self . server ( for: url) else {
152
+ return callback ( . failure( AuthorizationProviderError . noURLHost) )
153
+ }
154
+ guard let passwordData = password. data ( using: . utf8) else {
155
+ return callback ( . failure( AuthorizationProviderError . unexpectedPasswordData) )
156
+ }
157
+ let `protocol` = self . `protocol` ( for: url)
158
+
159
+ do {
160
+ try self . update ( server: server, protocol: `protocol`, account: user, password: passwordData)
161
+ callback ( . success( ( ) ) )
162
+ } catch AuthorizationProviderError . notFound {
163
+ do {
164
+ try self . create ( server: server, protocol: `protocol`, account: user, password: passwordData)
165
+ callback ( . success( ( ) ) )
166
+ } catch {
167
+ callback ( . failure( error) )
168
+ }
169
+ } catch {
170
+ callback ( . failure( error) )
171
+ }
172
+ }
173
+
174
+ public func authentication( for url: Foundation . URL ) -> ( user: String , password: String ) ? {
175
+ guard let server = self . server ( for: url) else {
176
+ return nil
177
+ }
178
+
179
+ do {
180
+ guard let existingItem = try self . search ( server: server, protocol: self . `protocol` ( for: url) ) as? [ String : Any ] ,
181
+ let passwordData = existingItem [ kSecValueData as String ] as? Data ,
182
+ let password = String ( data: passwordData, encoding: String . Encoding. utf8) ,
183
+ let account = existingItem [ kSecAttrAccount as String ] as? String else {
184
+ throw AuthorizationProviderError . unexpectedError ( " Failed to extract credentials for server \( server) from keychain " )
185
+ }
186
+ return ( user: account, password: password)
187
+ } catch {
188
+ switch error {
189
+ case AuthorizationProviderError . notFound:
190
+ ObservabilitySystem . topScope. emit ( info: " No credentials found for server \( server) in keychain " )
191
+ case AuthorizationProviderError . unexpectedError( let detail) :
192
+ ObservabilitySystem . topScope. emit ( error: detail)
193
+ default :
194
+ ObservabilitySystem . topScope. emit ( error: " Failed to find credentials for server \( server) in keychain: \( error) " )
195
+ }
196
+ return nil
197
+ }
198
+ }
199
+
200
+ private func create( server: String , `protocol`: CFString , account: String , password: Data ) throws {
201
+ let query : [ String : Any ] = [ kSecClass as String : kSecClassInternetPassword,
202
+ kSecAttrServer as String : server,
203
+ kSecAttrProtocol as String : `protocol`,
204
+ kSecAttrAccount as String : account,
205
+ kSecValueData as String : password]
206
+
207
+ let status = SecItemAdd ( query as CFDictionary , nil )
208
+ guard status == errSecSuccess else {
209
+ throw AuthorizationProviderError . unexpectedError ( " Failed to save credentials for server \( server) to keychain: status \( status) " )
210
+ }
211
+ }
212
+
213
+ private func update( server: String , `protocol`: CFString , account: String , password: Data ) throws {
214
+ let query : [ String : Any ] = [ kSecClass as String : kSecClassInternetPassword,
215
+ kSecAttrServer as String : server,
216
+ kSecAttrProtocol as String : `protocol`]
217
+ let attributes : [ String : Any ] = [ kSecAttrAccount as String : account,
218
+ kSecValueData as String : password]
219
+
220
+ let status = SecItemUpdate ( query as CFDictionary , attributes as CFDictionary )
221
+ guard status != errSecItemNotFound else {
222
+ throw AuthorizationProviderError . notFound
223
+ }
224
+ guard status == errSecSuccess else {
225
+ throw AuthorizationProviderError . unexpectedError ( " Failed to update credentials for server \( server) in keychain: status \( status) " )
226
+ }
227
+ }
228
+
229
+ private func search( server: String , `protocol`: CFString ) throws -> CFTypeRef ? {
230
+ let query : [ String : Any ] = [ kSecClass as String : kSecClassInternetPassword,
231
+ kSecAttrServer as String : server,
232
+ kSecAttrProtocol as String : `protocol`,
233
+ kSecMatchLimit as String : kSecMatchLimitOne,
234
+ kSecReturnAttributes as String : true ,
235
+ kSecReturnData as String : true ]
236
+
237
+ var item : CFTypeRef ?
238
+ // Search keychain for server's username and password, if any.
239
+ let status = SecItemCopyMatching ( query as CFDictionary , & item)
240
+ guard status != errSecItemNotFound else {
241
+ throw AuthorizationProviderError . notFound
242
+ }
243
+ guard status == errSecSuccess else {
244
+ throw AuthorizationProviderError . unexpectedError ( " Failed to find credentials for server \( server) in keychain: status \( status) " )
245
+ }
246
+
247
+ return item
248
+ }
249
+
250
+ private func server( for url: Foundation . URL ) -> String ? {
251
+ url. host? . lowercased ( )
252
+ }
253
+
254
+ private func `protocol`( for url: Foundation . URL ) -> CFString {
255
+ // See https://developer.apple.com/documentation/security/keychain_services/keychain_items/item_attribute_keys_and_values?language=swift
256
+ // for a list of possible values for the `kSecAttrProtocol` attribute.
257
+ switch url. scheme? . lowercased ( ) {
258
+ case " https " :
259
+ return kSecAttrProtocolHTTPS
260
+ default :
261
+ return kSecAttrProtocolHTTPS
262
+ }
263
+ }
264
+ }
265
+ #endif
0 commit comments