Skip to content

Commit 2e67353

Browse files
committed
HTTP Basic Auth implemenatation
1 parent 1da6fc8 commit 2e67353

File tree

6 files changed

+162
-5
lines changed

6 files changed

+162
-5
lines changed

Foundation/URLAuthenticationChallenge.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public protocol URLAuthenticationChallengeSender : NSObjectProtocol {
3333
*/
3434
func performDefaultHandling(for challenge: URLAuthenticationChallenge)
3535

36-
36+
3737
/*!
3838
@method rejectProtectionSpaceAndContinueWithChallenge:
3939
*/
@@ -192,3 +192,26 @@ open class URLAuthenticationChallenge : NSObject, NSSecureCoding {
192192
}
193193
}
194194
}
195+
196+
extension _HTTPURLProtocol : URLAuthenticationChallengeSender {
197+
198+
func cancel(_ challenge: URLAuthenticationChallenge) {
199+
NSUnimplemented()
200+
}
201+
202+
func continueWithoutCredential(for challenge: URLAuthenticationChallenge) {
203+
NSUnimplemented()
204+
}
205+
206+
func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {
207+
NSUnimplemented()
208+
}
209+
210+
func performDefaultHandling(for challenge: URLAuthenticationChallenge) {
211+
NSUnimplemented()
212+
}
213+
214+
func rejectProtectionSpaceAndContinue(with challenge: URLAuthenticationChallenge) {
215+
NSUnimplemented()
216+
}
217+
}

Foundation/URLSession/NativeProtocol.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,11 @@ internal class _NativeProtocol: URLProtocol, _EasyHandleDelegate {
329329
}
330330

331331
self.internalState = .transferReady(createTransferState(url: url, workQueue: t.workQueue))
332-
configureEasyHandle(for: request)
332+
if let authRequest = task?.authRequest {
333+
configureEasyHandle(for: authRequest)
334+
} else {
335+
configureEasyHandle(for: request)
336+
}
333337
if (t.suspendCount) < 1 {
334338
resume()
335339
}

Foundation/URLSession/URLSessionTask.swift

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ open class URLSessionTask : NSObject, NSCopying {
106106

107107
/// May be nil if this is a stream task
108108
/*@NSCopying*/ open let originalRequest: URLRequest?
109+
110+
/// If there's an authentication failure, we'd need to create a new request with the credentials supplied by the user
111+
var authRequest: URLRequest? = nil
112+
113+
/// Authentication failure count
114+
fileprivate var previousFailureCount = 0
109115

110116
/// May differ from originalRequest due to http server redirection
111117
/*@NSCopying*/ open internal(set) var currentRequest: URLRequest? {
@@ -528,9 +534,30 @@ extension _ProtocolClient : URLProtocolClient {
528534
}
529535
}
530536

537+
func createProtectionSpace(_ response: HTTPURLResponse) -> URLProtectionSpace {
538+
let host = response.url?.host ?? ""
539+
let port = response.url?.port ?? 80 //we're doing http
540+
let _protocol = response.url?.scheme
541+
let wwwAuthHeader = response.allHeaderFields["WWW-Authenticate"] as! String
542+
let authMethod = wwwAuthHeader.components(separatedBy: " ")[0]
543+
let realm = String(String(wwwAuthHeader.components(separatedBy: "realm=")[1].dropFirst()).dropLast())
544+
return URLProtectionSpace(host: host, port: port, protocol: _protocol, realm: realm, authenticationMethod: authMethod)
545+
}
546+
531547
func urlProtocolDidFinishLoading(_ protocol: URLProtocol) {
532548
guard let task = `protocol`.task else { fatalError() }
533549
guard let session = task.session as? URLSession else { fatalError() }
550+
guard let response = task.response as? HTTPURLResponse else { fatalError("No response") }
551+
if response.statusCode == 401 {
552+
let protectionSpace = createProtectionSpace(response)
553+
//TODO: Fetch and set proposed credentials if they exist
554+
let authenticationChallenge = URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil,
555+
previousFailureCount: task.previousFailureCount, failureResponse: response, error: nil,
556+
sender: `protocol` as! _HTTPURLProtocol)
557+
task.previousFailureCount += 1
558+
urlProtocol(`protocol`, didReceive: authenticationChallenge)
559+
return
560+
}
534561
switch session.behaviour(for: task) {
535562
case .taskDelegate(let delegate):
536563
if let downloadDelegate = delegate as? URLSessionDownloadDelegate, let downloadTask = task as? URLSessionDownloadTask {
@@ -569,7 +596,24 @@ extension _ProtocolClient : URLProtocolClient {
569596
}
570597

571598
func urlProtocol(_ protocol: URLProtocol, didReceive challenge: URLAuthenticationChallenge) {
572-
NSUnimplemented()
599+
guard let task = `protocol`.task else { fatalError() }
600+
guard let session = task.session as? URLSession else { fatalError() }
601+
switch session.behaviour(for: task) {
602+
case .taskDelegate(let delegate):
603+
session.delegateQueue.addOperation {
604+
let authScheme = challenge.protectionSpace.authenticationMethod
605+
delegate.urlSession(session, task: task, didReceive: challenge) { disposition, credential in
606+
task.suspend()
607+
guard let handler = URLSessionTask.authHandler(for: authScheme) else {
608+
fatalError("\(authScheme) is not supported")
609+
}
610+
handler(task, disposition, credential)
611+
task._protocol = _HTTPURLProtocol(task: task, cachedResponse: nil, client: nil)
612+
task.resume()
613+
}
614+
}
615+
default: return
616+
}
573617
}
574618

575619
func urlProtocol(_ protocol: URLProtocol, didLoad data: Data) {
@@ -632,6 +676,31 @@ extension _ProtocolClient : URLProtocolClient {
632676
NSUnimplemented()
633677
}
634678
}
679+
extension URLSessionTask {
680+
typealias _AuthHandler = ((URLSessionTask, URLSession.AuthChallengeDisposition, URLCredential?) -> ())
681+
682+
static func authHandler(for authScheme: String) -> _AuthHandler? {
683+
let handlers: [String : _AuthHandler] = [
684+
"Basic" : basicAuth,
685+
"Digest": digestAuth
686+
]
687+
return handlers[authScheme]
688+
}
689+
690+
//Authentication handlers
691+
static func basicAuth(_ task: URLSessionTask, _ disposition: URLSession.AuthChallengeDisposition, _ credential: URLCredential?) {
692+
//TODO: Handle disposition. For now, we default to .useCredential
693+
let user = credential?.user ?? ""
694+
let password = credential?.password ?? ""
695+
let encodedString = "\(user):\(password)".data(using: .utf8)?.base64EncodedString()
696+
task.authRequest = task.originalRequest
697+
task.authRequest?.setValue("Basic \(encodedString!)", forHTTPHeaderField: "Authorization")
698+
}
699+
700+
static func digestAuth(_ task: URLSessionTask, _ disposition: URLSession.AuthChallengeDisposition, _ credential: URLCredential?) {
701+
NSUnimplemented()
702+
}
703+
}
635704

636705
extension URLProtocol {
637706
enum _PropertyKey: String {

Foundation/URLSession/http/HTTPURLProtocol.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ internal class _HTTPURLProtocol: _NativeProtocol {
120120
httpHeaders = hh
121121
}
122122

123-
if let hh = self.task?.originalRequest?.allHTTPHeaderFields {
123+
if let hh = request.allHTTPHeaderFields {
124124
if httpHeaders == nil {
125125
httpHeaders = hh
126126
} else {

TestFoundation/HTTPServer.swift

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ class _TCPSocket {
163163
class _HTTPServer {
164164

165165
let socket: _TCPSocket
166+
var willReadAgain = false
166167
var port: UInt16 {
167168
get {
168169
return self.socket.port
@@ -182,7 +183,9 @@ class _HTTPServer {
182183
}
183184

184185
public func stop() {
185-
socket.shutdown()
186+
if !willReadAgain {
187+
socket.shutdown()
188+
}
186189
}
187190

188191
public func request() throws -> _HTTPRequest {
@@ -271,6 +274,34 @@ class _HTTPServer {
271274
try self.socket.writeRawData(responseData)
272275
}
273276

277+
func respondWithAuthResponse(uri: String, firstRead: Bool) throws {
278+
let responseData: Data
279+
if firstRead {
280+
responseData = ("HTTP/1.1 401 UNAUTHORIZED \r\n" +
281+
"Content-Length: 0\r\n" +
282+
"WWW-Authenticate: Basic realm=\"Fake Relam\"\r\n" +
283+
"Access-Control-Allow-Origin: *\r\n" +
284+
"Access-Control-Allow-Credentials: true\r\n" +
285+
"Via: 1.1 vegur\r\n" +
286+
"Cache-Control: proxy-revalidate\r\n" +
287+
"Connection: keep-Alive\r\n" +
288+
"\r\n").data(using: .utf8)!
289+
} else {
290+
responseData = ("HTTP/1.1 200 OK \r\n" +
291+
"Content-Length: 37\r\n" +
292+
"Content-Type: application/json\r\n" +
293+
"Access-Control-Allow-Origin: *\r\n" +
294+
"Access-Control-Allow-Credentials: true\r\n" +
295+
"Via: 1.1 vegur\r\n" +
296+
"Cache-Control: proxy-revalidate\r\n" +
297+
"Connection: keep-Alive\r\n" +
298+
"\r\n" +
299+
"{\"authenticated\":true,\"user\":\"user\"}\n").data(using: .utf8)!
300+
}
301+
try self.socket.writeRawData(responseData)
302+
}
303+
304+
274305
}
275306

276307
struct _HTTPRequest {
@@ -369,11 +400,22 @@ public class TestURLSessionServer {
369400
} else {
370401
try httpServer.respond(with: _HTTPResponse(response: .NOTFOUND, body: "Not Found"))
371402
}
403+
} else if req.uri.hasPrefix("/auth") {
404+
httpServer.willReadAgain = true
405+
try httpServer.respondWithAuthResponse(uri: req.uri, firstRead: true)
372406
} else {
373407
try httpServer.respond(with: process(request: req), startDelay: self.startDelay, sendDelay: self.sendDelay, bodyChunks: self.bodyChunks)
374408
}
375409
}
376410

411+
public func readAndRespondAgain() throws {
412+
let req = try httpServer.request()
413+
if req.uri.hasPrefix("/auth/") {
414+
try httpServer.respondWithAuthResponse(uri: req.uri, firstRead: false)
415+
}
416+
httpServer.willReadAgain = false
417+
}
418+
377419
func process(request: _HTTPRequest) -> _HTTPResponse {
378420
if request.method == .GET || request.method == .POST || request.method == .PUT {
379421
return getResponse(request: request)
@@ -512,6 +554,10 @@ class LoopbackServerTest : XCTestCase {
512554
serverPort = Int(test.port)
513555
try test.start(started: condition)
514556
try test.readAndRespond()
557+
if test.httpServer.willReadAgain {
558+
try test.start(started: condition)
559+
try test.readAndRespondAgain()
560+
}
515561
serverPort = -2
516562
test.stop()
517563
}

TestFoundation/TestURLSession.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class TestURLSession : LoopbackServerTest {
4141
("test_cookiesStorage", test_cookiesStorage),
4242
("test_setCookies", test_setCookies),
4343
("test_dontSetCookies", test_dontSetCookies),
44+
("test_basicAuthRequest", test_basicAuthRequest),
4445
]
4546
}
4647

@@ -597,6 +598,14 @@ class TestURLSession : LoopbackServerTest {
597598
task.resume()
598599
waitForExpectations(timeout: 30)
599600
}
601+
602+
func test_basicAuthRequest() {
603+
let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/auth/basic"
604+
let url = URL(string: urlString)!
605+
let d = DataTask(with: expectation(description: "GET \(urlString): with a delegate"))
606+
d.run(with: url)
607+
waitForExpectations(timeout: 60)
608+
}
600609
}
601610

602611
class SharedDelegate: NSObject {
@@ -745,6 +754,12 @@ extension DataTask : URLSessionTaskDelegate {
745754
}
746755
self.error = true
747756
}
757+
758+
public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge:
759+
URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition,
760+
URLCredential?) -> Void) {
761+
completionHandler(.useCredential, URLCredential(user: "user", password: "passwd", persistence: .none))
762+
}
748763
}
749764

750765
class DownloadTask : NSObject {

0 commit comments

Comments
 (0)