6

I can't consistently read response from a server if I use code below.

Header:

#import <Foundation/Foundation.h>
@interface TestHttpClient : NSObject<NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDownloadDelegate>

-(void)POST:(NSString*) relativePath payLoad:(NSData*)payLoad;

@end

Implementation:

#import "TestHttpClient.h"

@implementation TestHttpClient

-(void)POST:(NSString*)relativePath payLoad:(NSData*)payLoad
{
    NSURL* url = [NSURL URLWithString:@"http://apps01.ditat.net/mobile/batch"];

    // Set URL credentials and save to storage
    NSURLCredential *credential = [NSURLCredential credentialWithUser:@"BadUser" password:@"BadPassword" persistence: NSURLCredentialPersistencePermanent];
    NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:[url host] port:443 protocol:[url scheme] realm:@"Ditat mobile services endpoint" authenticationMethod:NSURLAuthenticationMethodHTTPBasic];
    [[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential forProtectionSpace:protectionSpace];

    // Configure session
    NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration ephemeralSessionConfiguration];
    sessionConfig.timeoutIntervalForRequest = 30.0;
    sessionConfig.timeoutIntervalForResource = 60.0;
    sessionConfig.HTTPMaximumConnectionsPerHost = 1;
    sessionConfig.URLCredentialStorage = [NSURLCredentialStorage sharedCredentialStorage]; // Should this line be here??

    NSURLSession *session =     [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:[NSOperationQueue mainQueue]];

    // Create request object with parameters
    NSMutableURLRequest *request =
    [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:60.0];

    // Set header data
    [request setHTTPMethod:@"POST"];
    [request setValue:@"application/x-protobuf" forHTTPHeaderField:@"Content-Type"];
    [request setValue:@"Version 1.0" forHTTPHeaderField:@"User-Agent"];
    [request setValue:@"Demo" forHTTPHeaderField:@"AccountId"];
    [request setValue:@"1234-5678" forHTTPHeaderField:@"DeviceSerialNumber"];
    [request setValue:@"iOS 7.1" forHTTPHeaderField:@"OSVersion"];
    [request setHTTPBody:payLoad];

    // Call session to post data to server??
    NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];
    [downloadTask resume];
}

-(void)invokeDelegateWithResponse:(NSHTTPURLResponse *)response fileLocation:(NSURL*)location
{
    NSLog(@"HttpClient.invokeDelegateWithResponse - code %ld", (long)[response statusCode]);
}

#pragma mark - NSURLSessionDownloadDelegate
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
    NSLog(@"NSURLSessionDownloadDelegate.didFinishDownloadingToURL");
    [self invokeDelegateWithResponse:(NSHTTPURLResponse*)[downloadTask response] fileLocation:location];
    [session invalidateAndCancel];
}

// Implemented as blank to avoid compiler warning
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
     didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
}

// Implemented as blank to avoid compiler warning
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes
{
}

Can be called from any VC (place code under button action for example)

-(IBAction)buttonTouchUp:(UIButton *)sender
{
    TestHttpClient *client = [[TestHttpClient alloc] init];
    [client POST:@"" payLoad:nil];
    return;
}

If you start program and call this code - it will show in NSLog completion with 401. Second try - will not work. Or might work if wait a little. But it won't send server requests as you push button.

NSURLSession somehow "remembers" failed attempts and won't return anything? Why is this behavior? I want to see 2 NSLog messages every time I push button.

katit
  • 17,375
  • 35
  • 128
  • 256
  • 2
    Do you need to implement your own wrapper, or could you use an existing framework? [AFNetworking](https://github.com/AFNetworking/AFNetworking) is a good choice. Even if you can't use it, you may find the answer in it's implementation. – i_am_jorf Jul 21 '14 at 17:21
  • AFNetworking is a wrapper too, doubt it will solve my problem, just add another level of code.. I prefer to figure it out myself and not use 3rd party library, especially that all I do is download files.. My guess it that somehow response being cached and I just need to code some kind of forced cleanup.. – katit Jul 21 '14 at 17:27
  • In terms of always seeing two requests (rather than just the first time), that may be because you're creating a new `NSURLSession` for every request. You really want to create a single `NSURLSession` and have subsequent requests use that same session. – Rob Jul 21 '14 at 17:46
  • Probably unrelated to your problem at hand, but your `didReceiveChallenge` should probably have an `else` condition that calls the `completionHandler` with `NSURLSessionAuthChallengeCancelAuthenticationChallenge`. If that `if` statement fails, your current implementation will stall if you don't call that `completionHandler` at all. – Rob Jul 21 '14 at 17:48
  • Rob, I see doubles on a single request, it's just the way built in clients work. I've seen it in .NET behaving the same – katit Jul 21 '14 at 17:48
  • @katit Yes, I'd expect to see doubles on the first one, but I wasn't sure about the second request with different userid/password. But because every request is the first request for the session (because you're creating a new one for every request), you may lose some efficiency, if nothing else. – Rob Jul 21 '14 at 17:51
  • Rob, I understand what you mean about not reusing session, my goal for now is to get it working, it's still prototyping. And my application doesn't chat with server, one request/response exchange all data needed.. – katit Jul 21 '14 at 17:54
  • On my second point, `didReceiveChallenge`, did you confirm that that `if` statement is always succeeding? In the absence of the `else` clause I suggested above, if the `if` test failed, the `NSURLSession` would stall the request because the completion handler was never called. – Rob Jul 21 '14 at 18:04
  • I think I checked, but will double-check. Per Apples documentation didReceive challenge on session and task level different. Task level were getting basic auth challenge and session were getting ServerTrust challenge. If I remove session delegate - task delegate will get both server trust and basic auth challenge – katit Jul 21 '14 at 18:16

1 Answers1

26

TL;DR; You are not handling authentication correctly in your example.

This is what happens when an iOS or MacOS client encounters a URL that requires authentication:

  1. The client requests a resource from the server GET www.example.com/protected

  2. The server response for that request has a status code of 401 and includes the WWW-Authenticate header. This tells the client this is a protected resource, and specifies what authentication method to use to access the resource. In iOS and MacOS, this is the "authentication challenge" that a delegate responds to. The WWW-Authenticate header is specifically mentioned in the documentation to highlight this.

    • Normally on iOS and MacOS, if a delegate is not provided or does not handle authentication challenges, the URL loading system will try to find an appropriate credential for this resource and authentication type by looking in NSURLCredentialStorage. It looks for a matching credential that has been saved as the default.

    • If a delegate implementing authentication IS provided, it's up to that delegate to provide a credential for that resource.

  3. When the system has a credential for the authentication challenge the request is attempted again with the credential.

    GET www.example.com/protected Authorization: Basic blablahaala

This explains the behavior you see in Charles, and is the correct behavior according to the various HTTP specifications.

Obviously, if you do not want to implement a delegate for your connection you have the option of putting a credential for the resource you are accessing in NSURLCredentialStorage. The system will use this, and will not require you to implement a delegate for the credential.

Create an NSURLCredential:

credential = [NSURLCredential credentialWithUser:@"some user" password:@"clever password" persistence: NSURLCredentialPersistencePermanent];

NSURLCredentialPersistencePermanent will tell NSURLCredentialStorage to store this permanently in the keychain. There are other possible values you can use, such as NSURLCredentialPersistenceForSession. These are covered in the documentation. . You should avoid using NSURLCredentialPersistencePermanent with credentials that have not been validated, use session or none until the credential has been validated. You have probably seen projects using 'KeychainWrapper' or directly accessing the Keychain API to save internet usernames and passwords - this is not the preferred way to do this, NSURLCredentialStorage is.

Create an NSURLProtectionSpace with the correct host, port, protocol, realm, and authentication method:

protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:[url host] port:443 protocol:[url scheme] realm:@"Protected Area" authenticationMethod:NSURLAuthenticationMethodHTTPBasic];

Note that [[url port] integerValue] will not provide defaults for HTTP (80) or HTTPS (443). You must provide those. The realm MUST match what the server provides.

Finally, put it into NSURLCredentialStorage:

[[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential forProtectionSpace:protectionSpace];

This will allow the URL loading system to use this credential from this point forward. Essentially the same process can also be used for a SSL/TLS server trust reference as well.

In your question you are handling server trust, but not the NSURLAuthenticationMethodHTTPBasic . When your application receives an authentication challenge for the HTTP Basic Auth, you are not responding to it, and things go downhill from there. In your case, you probably do not need to implement URLSession:didReceiveChallenge:completionHandler: at all if you do the steps above to set the default basic authentication credential for this protection space. The system will handle the NSURLAuthenticationMethodServerTrust by performing the default trust evaluation. The system will then find the default credential you set for this protection space for basic authentication and use that.

UPDATE

Based on new information in the comments, and running a modified version of the code, OP is actually getting this error in response to his request: NSURLConnection/CFURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813) This error can be found in SecureTransport.h. The root certificate on the server credentials doesn't exist or is not trusted by the system. This is very rare, but can happen. Technote 2232 explains how to customize the server trust evaluation in the client to allow this certificate.

quellish
  • 21,123
  • 4
  • 76
  • 83
  • You should remove the authentication related methods of your delegate as well. Between those and setting your own auth headers, there's your problem. When it can't authenticate you are either preventing it from returning an error, or you're not seeing/handling the error. – quellish Jul 27 '14 at 22:48
  • Again, in didReceiveChallenge you are handing the server trust challenger, but NOT the basic authentication challenge. It's getting into a loop because the server is telling the client to authenticate, but you are not handling the challenge. You do not need didReceiveChallenge at all for this to work. – quellish Jul 28 '14 at 16:40
  • 1. You are not performing any server trust evaluation that is different from the system default. 2. You are not performing any basic auth at all in your delegate, and implementing didReceiveChallenge prevents the system from doing it. You can remove this delegate method, and the system will do both challenges. – quellish Jul 28 '14 at 17:06
  • Server trust does not need to be done separately unless you need to customize the server trust evaluation as per Technote 2232. You are not doing *any* trust evaluation in your implementation, so I assume you have no need to customize trust evaluation. If you are using a self signed certificate, etc. you would. Your server trust evaluation in your implementation just looks for an existing credential. It doesn't evaluation the server trust. – quellish Jul 28 '14 at 17:13
  • What *error* are you getting back from the session when that happens? Are you handling the error at all? – quellish Jul 28 '14 at 17:20
  • No, you are not evaluating the server trust. You are not checking wether it's trustworthy, you are just creating a credential for a potentially untrusted server. The code you have provided *doesnt* run without many changes, and when you do run it it's obvious the problem is the server, not the client (once didReceiveChallenge is removed) . Your server's credentials are bad: They have no root certificate (this is what kCFStreamErrorDomainSSL, -9813 means as well). That needs to be fixed, OR you need to follow TN2232 and change your trust evaluation to allow it. – quellish Jul 28 '14 at 17:36
  • Again, I modified your sample to run, and I was able to avoid the looping behavior by removing that delegate method. And again, your problem is the server credentials. You don't have a self signed cert, you have a certificate chain that has no verifiable root certificate. If you can't fix the server, you must properly implement your server trust evaluation to handle this. This is explained in tech note 2232. If you read that tech note, it explains that "Basic Trust Customization" - which you are not doing - is not enough for your needs. – quellish Jul 28 '14 at 17:48
  • The root certificate of the server credential isn't trusted by iOS. You would have to re-create the server trust with that certificate added as an anchor and perform trust evaluation on that. If you do not do this you may be introducing a big security problem in your application. As it is now, you're *attempting* to blindly trust every server the client encounters. This is not good. When you provide unverifiable server trust credentials over and over - which you are doing - that causes the loop you were seeing. – quellish Jul 28 '14 at 18:47
  • It requires you to have a thorough understanding of server trust evaluation and the security APIs. Read the tech note and the portions of the documentation it references. Again, it is very easy to implement this in a way that creates a security vulnerability. – quellish Jul 28 '14 at 19:32
  • When you run this with Charles and SSL proxying, the system will see the man in the middle attack you are performing and give you a different error (the trust will evaluate to kSecTrustResultRecoverableTrustFailure). When you are not using Charles, you will see a different error and a different trust evaluation. – quellish Jul 28 '14 at 19:57
  • "I want to focus on basic auth and not deal with server trust at all". Then don't use SSL. If I fix the SSL issues in your sample, I can get it to behave normally and report the basic auth failure. And btw, you need to set your Content-Length on the POST. – quellish Jul 28 '14 at 20:05
  • SSL/TLS is itself a trust model. Trust evaluation *is* the security of SSL. If you are trusting every server, SSL is useless. Don't use SSL if that is the case. – quellish Jul 28 '14 at 20:20
  • Then you are putting your customers at risk. If you want to customize the security of SSL in the client, read the tech note very thoroughly and make sure you understand the consequences of your actions. – quellish Jul 28 '14 at 20:42
  • Ok. I cleaned up some of the comment history as it doesn't matter. Please see latest edit to original question. SSL is not a problem. That particular server supports HTTP. If I call it time after time - it won't send request to server on each button press. Why is that? This was my main point of frustration. I can't run same code and get same result every time. – katit Jul 28 '14 at 21:10
  • I can, with the changes I had to put in to make it run at all. Re-read my answer if you are still having difficulty. I am not triggering the provided code in the way that you are, perhaps that is where your problems are now. – quellish Jul 29 '14 at 01:53
  • I'll bet you are putting a bad credential into storage using NSURLCredentialPersistencePermanent. Re-read the answer. – quellish Jul 29 '14 at 01:55
  • Yes, it is bad credentials. I do it on purpose. I want to get 401 code every time I call this code as I do. If I don't call it correctly I need to know where is a problem. Because this is my main question. Calling same code again and again does not produce exact same response (401) I only get didFinishDownloading... callback once or twice and then calling code again does not produce anything. Try this code just like I have it in original question. You only need to have one VC with a button to duplicate 1:1 – katit Jul 29 '14 at 02:29
  • Again, the authentication process never completes the way you have it written now, which is why you see the behavior you are seeing. Change NSURLCredentialPersistencePermanent to NSURLCredentialPersistenceNone. As I said in my answer, avoid using *permanent* with bad credentials. – quellish Jul 29 '14 at 02:45
  • Didn't make any difference. I click button which runs my code (in original post) and first time it goes through (I see NSLogs) but second try - nothing happens. After about 40-60 seconds it will work again (once).. – katit Jul 29 '14 at 13:07
  • Hi Katt/Quellish Please let us know, how you guys are solved this problem and what is the main issue here. It is helpful for us – purnachandra.tech Nov 21 '14 at 08:43
  • Well. They way WE solved it - we changed how SERVER responds. So we always get 200 from server but we return code in other header value. This way NSURL.. API always get' "OK" back and we handle actual codes without relying on API's "magic" – katit Dec 26 '14 at 21:40