Skip to content

Commit ba812f3

Browse files
authored
Merge pull request #1569 from saiHemak/http-server
2 parents 2433837 + 4c1e8c4 commit ba812f3

File tree

6 files changed

+172
-7
lines changed

6 files changed

+172
-7
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: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,14 @@ open class URLSessionTask : NSObject, NSCopying {
105105
open private(set) var taskIdentifier: Int
106106

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

110117
/// May differ from originalRequest due to http server redirection
111118
/*@NSCopying*/ open internal(set) var currentRequest: URLRequest? {
@@ -539,9 +546,38 @@ extension _ProtocolClient : URLProtocolClient {
539546
}
540547
}
541548

549+
func createProtectionSpace(_ response: HTTPURLResponse) -> URLProtectionSpace? {
550+
let host = response.url?.host ?? ""
551+
let port = response.url?.port ?? 80 //we're doing http
552+
let _protocol = response.url?.scheme
553+
if response.allHeaderFields["WWW-Authenticate"] != nil {
554+
let wwwAuthHeaderValue = response.allHeaderFields["WWW-Authenticate"] as! String
555+
let authMethod = wwwAuthHeaderValue.components(separatedBy: " ")[0]
556+
let realm = String(String(wwwAuthHeaderValue.components(separatedBy: "realm=")[1].dropFirst()).dropLast())
557+
return URLProtectionSpace(host: host, port: port, protocol: _protocol, realm: realm, authenticationMethod: authMethod)
558+
} else {
559+
return nil
560+
}
561+
}
562+
542563
func urlProtocolDidFinishLoading(_ protocol: URLProtocol) {
543564
guard let task = `protocol`.task else { fatalError() }
544565
guard let session = task.session as? URLSession else { fatalError() }
566+
guard let response = task.response as? HTTPURLResponse else { fatalError("No response") }
567+
if response.statusCode == 401 {
568+
if let protectionSpace = createProtectionSpace(response) {
569+
//TODO: Fetch and set proposed credentials if they exist
570+
let authenticationChallenge = URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil,
571+
previousFailureCount: task.previousFailureCount, failureResponse: response, error: nil,
572+
sender: `protocol` as! _HTTPURLProtocol)
573+
task.previousFailureCount += 1
574+
urlProtocol(`protocol`, didReceive: authenticationChallenge)
575+
return
576+
} else {
577+
let urlError = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: NSURLErrorUserAuthenticationRequired, userInfo: nil))
578+
urlProtocol(`protocol`, didFailWithError: urlError)
579+
}
580+
}
545581
switch session.behaviour(for: task) {
546582
case .taskDelegate(let delegate):
547583
if let downloadDelegate = delegate as? URLSessionDownloadDelegate, let downloadTask = task as? URLSessionDownloadTask {
@@ -586,7 +622,24 @@ extension _ProtocolClient : URLProtocolClient {
586622
}
587623

588624
func urlProtocol(_ protocol: URLProtocol, didReceive challenge: URLAuthenticationChallenge) {
589-
NSUnimplemented()
625+
guard let task = `protocol`.task else { fatalError("Received response, but there's no task.") }
626+
guard let session = task.session as? URLSession else { fatalError("Task not associated with URLSession.") }
627+
switch session.behaviour(for: task) {
628+
case .taskDelegate(let delegate):
629+
session.delegateQueue.addOperation {
630+
let authScheme = challenge.protectionSpace.authenticationMethod
631+
delegate.urlSession(session, task: task, didReceive: challenge) { disposition, credential in
632+
task.suspend()
633+
guard let handler = URLSessionTask.authHandler(for: authScheme) else {
634+
fatalError("\(authScheme) is not supported")
635+
}
636+
handler(task, disposition, credential)
637+
task._protocol = _HTTPURLProtocol(task: task, cachedResponse: nil, client: nil)
638+
task.resume()
639+
}
640+
}
641+
default: return
642+
}
590643
}
591644

592645
func urlProtocol(_ protocol: URLProtocol, didLoad data: Data) {
@@ -653,6 +706,31 @@ extension _ProtocolClient : URLProtocolClient {
653706
NSUnimplemented()
654707
}
655708
}
709+
extension URLSessionTask {
710+
typealias _AuthHandler = ((URLSessionTask, URLSession.AuthChallengeDisposition, URLCredential?) -> ())
711+
712+
static func authHandler(for authScheme: String) -> _AuthHandler? {
713+
let handlers: [String : _AuthHandler] = [
714+
"Basic" : basicAuth,
715+
"Digest": digestAuth
716+
]
717+
return handlers[authScheme]
718+
}
719+
720+
//Authentication handlers
721+
static func basicAuth(_ task: URLSessionTask, _ disposition: URLSession.AuthChallengeDisposition, _ credential: URLCredential?) {
722+
//TODO: Handle disposition. For now, we default to .useCredential
723+
let user = credential?.user ?? ""
724+
let password = credential?.password ?? ""
725+
let encodedString = "\(user):\(password)".data(using: .utf8)?.base64EncodedString()
726+
task.authRequest = task.originalRequest
727+
task.authRequest?.setValue("Basic \(encodedString!)", forHTTPHeaderField: "Authorization")
728+
}
729+
730+
static func digestAuth(_ task: URLSessionTask, _ disposition: URLSession.AuthChallengeDisposition, _ credential: URLCredential?) {
731+
NSUnimplemented()
732+
}
733+
}
656734

657735
extension URLProtocol {
658736
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: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ class _TCPSocket {
183183
class _HTTPServer {
184184

185185
let socket: _TCPSocket
186+
var willReadAgain = false
186187
var port: UInt16 {
187188
get {
188189
return self.socket.port
@@ -202,8 +203,10 @@ class _HTTPServer {
202203
}
203204

204205
public func stop() {
205-
socket.closeClient()
206-
socket.shutdownListener()
206+
if !willReadAgain {
207+
socket.closeClient()
208+
socket.shutdownListener()
209+
}
207210
}
208211

209212
public func request() throws -> _HTTPRequest {
@@ -282,6 +285,34 @@ class _HTTPServer {
282285
try self.socket.writeRawData(responseData)
283286
}
284287

288+
func respondWithAuthResponse(uri: String, firstRead: Bool) throws {
289+
let responseData: Data
290+
if firstRead {
291+
responseData = ("HTTP/1.1 401 UNAUTHORIZED \r\n" +
292+
"Content-Length: 0\r\n" +
293+
"WWW-Authenticate: Basic realm=\"Fake Relam\"\r\n" +
294+
"Access-Control-Allow-Origin: *\r\n" +
295+
"Access-Control-Allow-Credentials: true\r\n" +
296+
"Via: 1.1 vegur\r\n" +
297+
"Cache-Control: proxy-revalidate\r\n" +
298+
"Connection: keep-Alive\r\n" +
299+
"\r\n").data(using: .utf8)!
300+
} else {
301+
responseData = ("HTTP/1.1 200 OK \r\n" +
302+
"Content-Length: 37\r\n" +
303+
"Content-Type: application/json\r\n" +
304+
"Access-Control-Allow-Origin: *\r\n" +
305+
"Access-Control-Allow-Credentials: true\r\n" +
306+
"Via: 1.1 vegur\r\n" +
307+
"Cache-Control: proxy-revalidate\r\n" +
308+
"Connection: keep-Alive\r\n" +
309+
"\r\n" +
310+
"{\"authenticated\":true,\"user\":\"user\"}\n").data(using: .utf8)!
311+
}
312+
try self.socket.writeRawData(responseData)
313+
}
314+
315+
285316
}
286317

287318
struct _HTTPRequest {
@@ -395,11 +426,22 @@ public class TestURLSessionServer {
395426
} else {
396427
try httpServer.respond(with: _HTTPResponse(response: .NOTFOUND, body: "Not Found"))
397428
}
429+
} else if req.uri.hasPrefix("/auth") {
430+
httpServer.willReadAgain = true
431+
try httpServer.respondWithAuthResponse(uri: req.uri, firstRead: true)
398432
} else {
399433
try httpServer.respond(with: process(request: req), startDelay: self.startDelay, sendDelay: self.sendDelay, bodyChunks: self.bodyChunks)
400434
}
401435
}
402436

437+
public func readAndRespondAgain() throws {
438+
let req = try httpServer.request()
439+
if req.uri.hasPrefix("/auth/") {
440+
try httpServer.respondWithAuthResponse(uri: req.uri, firstRead: false)
441+
}
442+
httpServer.willReadAgain = false
443+
}
444+
403445
func process(request: _HTTPRequest) -> _HTTPResponse {
404446
if request.method == .GET || request.method == .POST || request.method == .PUT {
405447
return getResponse(request: request)
@@ -559,12 +601,15 @@ class LoopbackServerTest : XCTestCase {
559601
do {
560602
try server.httpServer.listen(notify: condition)
561603
try server.readAndRespond()
604+
if server.httpServer.willReadAgain {
605+
try server.httpServer.listen(notify: condition)
606+
try server.readAndRespondAgain()
607+
}
562608
server.httpServer.socket.closeClient()
563609
} catch {
564610
}
565611
}
566612
serverPort = -2
567-
568613
}
569614

570615
globalDispatchQueue.async {

TestFoundation/TestURLSession.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class TestURLSession : LoopbackServerTest {
4242
("test_setCookies", test_setCookies),
4343
("test_dontSetCookies", test_dontSetCookies),
4444
("test_initURLSessionConfiguration", test_initURLSessionConfiguration),
45+
("test_basicAuthRequest", test_basicAuthRequest),
4546
]
4647
}
4748

@@ -642,6 +643,14 @@ class TestURLSession : LoopbackServerTest {
642643
XCTAssertEqual(config.urlCredentialStorage, nil)
643644
XCTAssertEqual(config.urlCache, nil)
644645
XCTAssertEqual(config.shouldUseExtendedBackgroundIdleMode, true)
646+
}
647+
648+
func test_basicAuthRequest() {
649+
let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/auth/basic"
650+
let url = URL(string: urlString)!
651+
let d = DataTask(with: expectation(description: "GET \(urlString): with a delegate"))
652+
d.run(with: url)
653+
waitForExpectations(timeout: 60)
645654
}
646655
}
647656

@@ -791,6 +800,12 @@ extension DataTask : URLSessionTaskDelegate {
791800
}
792801
self.error = true
793802
}
803+
804+
public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge:
805+
URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition,
806+
URLCredential?) -> Void) {
807+
completionHandler(.useCredential, URLCredential(user: "user", password: "passwd", persistence: .none))
808+
}
794809
}
795810

796811
class DownloadTask : NSObject {

0 commit comments

Comments
 (0)