From 4c1e8c4271c4e21000528d3910f20a958d03f308 Mon Sep 17 00:00:00 2001 From: Sai Kanduri Date: Wed, 30 May 2018 14:45:17 +0530 Subject: [PATCH] Implement HTTP Basic Authentication --- Foundation/URLAuthenticationChallenge.swift | 25 +++++- Foundation/URLSession/NativeProtocol.swift | 6 +- Foundation/URLSession/URLSessionTask.swift | 80 ++++++++++++++++++- .../URLSession/http/HTTPURLProtocol.swift | 2 +- TestFoundation/HTTPServer.swift | 51 +++++++++++- TestFoundation/TestURLSession.swift | 15 ++++ 6 files changed, 172 insertions(+), 7 deletions(-) diff --git a/Foundation/URLAuthenticationChallenge.swift b/Foundation/URLAuthenticationChallenge.swift index c9b808be07..33b337ac2a 100644 --- a/Foundation/URLAuthenticationChallenge.swift +++ b/Foundation/URLAuthenticationChallenge.swift @@ -33,7 +33,7 @@ public protocol URLAuthenticationChallengeSender : NSObjectProtocol { */ func performDefaultHandling(for challenge: URLAuthenticationChallenge) - + /*! @method rejectProtectionSpaceAndContinueWithChallenge: */ @@ -192,3 +192,26 @@ open class URLAuthenticationChallenge : NSObject, NSSecureCoding { } } } + +extension _HTTPURLProtocol : URLAuthenticationChallengeSender { + + func cancel(_ challenge: URLAuthenticationChallenge) { + NSUnimplemented() + } + + func continueWithoutCredential(for challenge: URLAuthenticationChallenge) { + NSUnimplemented() + } + + func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) { + NSUnimplemented() + } + + func performDefaultHandling(for challenge: URLAuthenticationChallenge) { + NSUnimplemented() + } + + func rejectProtectionSpaceAndContinue(with challenge: URLAuthenticationChallenge) { + NSUnimplemented() + } +} diff --git a/Foundation/URLSession/NativeProtocol.swift b/Foundation/URLSession/NativeProtocol.swift index 05988d3b3c..d29924dec3 100644 --- a/Foundation/URLSession/NativeProtocol.swift +++ b/Foundation/URLSession/NativeProtocol.swift @@ -329,7 +329,11 @@ internal class _NativeProtocol: URLProtocol, _EasyHandleDelegate { } self.internalState = .transferReady(createTransferState(url: url, workQueue: t.workQueue)) - configureEasyHandle(for: request) + if let authRequest = task?.authRequest { + configureEasyHandle(for: authRequest) + } else { + configureEasyHandle(for: request) + } if (t.suspendCount) < 1 { resume() } diff --git a/Foundation/URLSession/URLSessionTask.swift b/Foundation/URLSession/URLSessionTask.swift index e02aa8a081..5e65fa638b 100644 --- a/Foundation/URLSession/URLSessionTask.swift +++ b/Foundation/URLSession/URLSessionTask.swift @@ -105,7 +105,14 @@ open class URLSessionTask : NSObject, NSCopying { open private(set) var taskIdentifier: Int /// May be nil if this is a stream task + /*@NSCopying*/ open private(set) var originalRequest: URLRequest? + + /// If there's an authentication failure, we'd need to create a new request with the credentials supplied by the user + var authRequest: URLRequest? = nil + + /// Authentication failure count + fileprivate var previousFailureCount = 0 /// May differ from originalRequest due to http server redirection /*@NSCopying*/ open internal(set) var currentRequest: URLRequest? { @@ -539,9 +546,38 @@ extension _ProtocolClient : URLProtocolClient { } } + func createProtectionSpace(_ response: HTTPURLResponse) -> URLProtectionSpace? { + let host = response.url?.host ?? "" + let port = response.url?.port ?? 80 //we're doing http + let _protocol = response.url?.scheme + if response.allHeaderFields["WWW-Authenticate"] != nil { + let wwwAuthHeaderValue = response.allHeaderFields["WWW-Authenticate"] as! String + let authMethod = wwwAuthHeaderValue.components(separatedBy: " ")[0] + let realm = String(String(wwwAuthHeaderValue.components(separatedBy: "realm=")[1].dropFirst()).dropLast()) + return URLProtectionSpace(host: host, port: port, protocol: _protocol, realm: realm, authenticationMethod: authMethod) + } else { + return nil + } + } + func urlProtocolDidFinishLoading(_ protocol: URLProtocol) { guard let task = `protocol`.task else { fatalError() } guard let session = task.session as? URLSession else { fatalError() } + guard let response = task.response as? HTTPURLResponse else { fatalError("No response") } + if response.statusCode == 401 { + if let protectionSpace = createProtectionSpace(response) { + //TODO: Fetch and set proposed credentials if they exist + let authenticationChallenge = URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, + previousFailureCount: task.previousFailureCount, failureResponse: response, error: nil, + sender: `protocol` as! _HTTPURLProtocol) + task.previousFailureCount += 1 + urlProtocol(`protocol`, didReceive: authenticationChallenge) + return + } else { + let urlError = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: NSURLErrorUserAuthenticationRequired, userInfo: nil)) + urlProtocol(`protocol`, didFailWithError: urlError) + } + } switch session.behaviour(for: task) { case .taskDelegate(let delegate): if let downloadDelegate = delegate as? URLSessionDownloadDelegate, let downloadTask = task as? URLSessionDownloadTask { @@ -586,7 +622,24 @@ extension _ProtocolClient : URLProtocolClient { } func urlProtocol(_ protocol: URLProtocol, didReceive challenge: URLAuthenticationChallenge) { - NSUnimplemented() + guard let task = `protocol`.task else { fatalError("Received response, but there's no task.") } + guard let session = task.session as? URLSession else { fatalError("Task not associated with URLSession.") } + switch session.behaviour(for: task) { + case .taskDelegate(let delegate): + session.delegateQueue.addOperation { + let authScheme = challenge.protectionSpace.authenticationMethod + delegate.urlSession(session, task: task, didReceive: challenge) { disposition, credential in + task.suspend() + guard let handler = URLSessionTask.authHandler(for: authScheme) else { + fatalError("\(authScheme) is not supported") + } + handler(task, disposition, credential) + task._protocol = _HTTPURLProtocol(task: task, cachedResponse: nil, client: nil) + task.resume() + } + } + default: return + } } func urlProtocol(_ protocol: URLProtocol, didLoad data: Data) { @@ -653,6 +706,31 @@ extension _ProtocolClient : URLProtocolClient { NSUnimplemented() } } +extension URLSessionTask { + typealias _AuthHandler = ((URLSessionTask, URLSession.AuthChallengeDisposition, URLCredential?) -> ()) + + static func authHandler(for authScheme: String) -> _AuthHandler? { + let handlers: [String : _AuthHandler] = [ + "Basic" : basicAuth, + "Digest": digestAuth + ] + return handlers[authScheme] + } + + //Authentication handlers + static func basicAuth(_ task: URLSessionTask, _ disposition: URLSession.AuthChallengeDisposition, _ credential: URLCredential?) { + //TODO: Handle disposition. For now, we default to .useCredential + let user = credential?.user ?? "" + let password = credential?.password ?? "" + let encodedString = "\(user):\(password)".data(using: .utf8)?.base64EncodedString() + task.authRequest = task.originalRequest + task.authRequest?.setValue("Basic \(encodedString!)", forHTTPHeaderField: "Authorization") + } + + static func digestAuth(_ task: URLSessionTask, _ disposition: URLSession.AuthChallengeDisposition, _ credential: URLCredential?) { + NSUnimplemented() + } +} extension URLProtocol { enum _PropertyKey: String { diff --git a/Foundation/URLSession/http/HTTPURLProtocol.swift b/Foundation/URLSession/http/HTTPURLProtocol.swift index 1083da5682..33f3366beb 100644 --- a/Foundation/URLSession/http/HTTPURLProtocol.swift +++ b/Foundation/URLSession/http/HTTPURLProtocol.swift @@ -120,7 +120,7 @@ internal class _HTTPURLProtocol: _NativeProtocol { httpHeaders = hh } - if let hh = self.task?.originalRequest?.allHTTPHeaderFields { + if let hh = request.allHTTPHeaderFields { if httpHeaders == nil { httpHeaders = hh } else { diff --git a/TestFoundation/HTTPServer.swift b/TestFoundation/HTTPServer.swift index bc87cdd09f..7837364f8c 100644 --- a/TestFoundation/HTTPServer.swift +++ b/TestFoundation/HTTPServer.swift @@ -183,6 +183,7 @@ class _TCPSocket { class _HTTPServer { let socket: _TCPSocket + var willReadAgain = false var port: UInt16 { get { return self.socket.port @@ -202,8 +203,10 @@ class _HTTPServer { } public func stop() { - socket.closeClient() - socket.shutdownListener() + if !willReadAgain { + socket.closeClient() + socket.shutdownListener() + } } public func request() throws -> _HTTPRequest { @@ -282,6 +285,34 @@ class _HTTPServer { try self.socket.writeRawData(responseData) } + func respondWithAuthResponse(uri: String, firstRead: Bool) throws { + let responseData: Data + if firstRead { + responseData = ("HTTP/1.1 401 UNAUTHORIZED \r\n" + + "Content-Length: 0\r\n" + + "WWW-Authenticate: Basic realm=\"Fake Relam\"\r\n" + + "Access-Control-Allow-Origin: *\r\n" + + "Access-Control-Allow-Credentials: true\r\n" + + "Via: 1.1 vegur\r\n" + + "Cache-Control: proxy-revalidate\r\n" + + "Connection: keep-Alive\r\n" + + "\r\n").data(using: .utf8)! + } else { + responseData = ("HTTP/1.1 200 OK \r\n" + + "Content-Length: 37\r\n" + + "Content-Type: application/json\r\n" + + "Access-Control-Allow-Origin: *\r\n" + + "Access-Control-Allow-Credentials: true\r\n" + + "Via: 1.1 vegur\r\n" + + "Cache-Control: proxy-revalidate\r\n" + + "Connection: keep-Alive\r\n" + + "\r\n" + + "{\"authenticated\":true,\"user\":\"user\"}\n").data(using: .utf8)! + } + try self.socket.writeRawData(responseData) + } + + } struct _HTTPRequest { @@ -395,11 +426,22 @@ public class TestURLSessionServer { } else { try httpServer.respond(with: _HTTPResponse(response: .NOTFOUND, body: "Not Found")) } + } else if req.uri.hasPrefix("/auth") { + httpServer.willReadAgain = true + try httpServer.respondWithAuthResponse(uri: req.uri, firstRead: true) } else { try httpServer.respond(with: process(request: req), startDelay: self.startDelay, sendDelay: self.sendDelay, bodyChunks: self.bodyChunks) } } + public func readAndRespondAgain() throws { + let req = try httpServer.request() + if req.uri.hasPrefix("/auth/") { + try httpServer.respondWithAuthResponse(uri: req.uri, firstRead: false) + } + httpServer.willReadAgain = false + } + func process(request: _HTTPRequest) -> _HTTPResponse { if request.method == .GET || request.method == .POST || request.method == .PUT { return getResponse(request: request) @@ -559,12 +601,15 @@ class LoopbackServerTest : XCTestCase { do { try server.httpServer.listen(notify: condition) try server.readAndRespond() + if server.httpServer.willReadAgain { + try server.httpServer.listen(notify: condition) + try server.readAndRespondAgain() + } server.httpServer.socket.closeClient() } catch { } } serverPort = -2 - } globalDispatchQueue.async { diff --git a/TestFoundation/TestURLSession.swift b/TestFoundation/TestURLSession.swift index 8a531b7085..5e23f2f6e2 100644 --- a/TestFoundation/TestURLSession.swift +++ b/TestFoundation/TestURLSession.swift @@ -42,6 +42,7 @@ class TestURLSession : LoopbackServerTest { ("test_setCookies", test_setCookies), ("test_dontSetCookies", test_dontSetCookies), ("test_initURLSessionConfiguration", test_initURLSessionConfiguration), + ("test_basicAuthRequest", test_basicAuthRequest), ] } @@ -642,6 +643,14 @@ class TestURLSession : LoopbackServerTest { XCTAssertEqual(config.urlCredentialStorage, nil) XCTAssertEqual(config.urlCache, nil) XCTAssertEqual(config.shouldUseExtendedBackgroundIdleMode, true) + } + + func test_basicAuthRequest() { + let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/auth/basic" + let url = URL(string: urlString)! + let d = DataTask(with: expectation(description: "GET \(urlString): with a delegate")) + d.run(with: url) + waitForExpectations(timeout: 60) } } @@ -791,6 +800,12 @@ extension DataTask : URLSessionTaskDelegate { } self.error = true } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: + URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, + URLCredential?) -> Void) { + completionHandler(.useCredential, URLCredential(user: "user", password: "passwd", persistence: .none)) + } } class DownloadTask : NSObject {