Dec 16

This post is on a very narrow issue, but one that I spent several hours tracking down and finding a fix for. Hopefully this will save some grey hairs for a few of my loyal readers and Google searchers.

An app that we’re working on connects to an IIS server and authenticates using NTLM. For security reasons (for this particular app) NTLM is the only acceptable authentication mechanism.

Therefore, in the didReceiveAuthenticationChallenge callback method we specifically check to ensure that the authentication method is the one we’re accepting.

- (void)connection:(NSURLConnection*)connection
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge {

  if (([[[challenge protectionSpace] authenticationMethod] isEqual:authenticationMethod]) &&
    ([challenge previousFailureCount] <= kAllowedLoginFailures)) {

    [[challenge sender]  useCredential:[NSURLCredential
                    credentialWithUser:user.username
                              password:user.password
                           persistence:NSURLCredentialPersistenceNone]
            forAuthenticationChallenge:challenge];
  } else {
    [[challenge sender] cancelAuthenticationChallenge:challenge];
  }
}

This worked great until the app connected to a server over SSL. Then we experience a very strange behavior. Communication with the server worked fine the first time the app was launched. But after quitting the app and launching it a second time, all server communications failed.

After a bit of debugging and a liberal sprinkling of log statements we discovered that the server sometimes sent the authentication method NSURLAuthenticationMethodServerTrust instead of the expected NSURLAuthenticationMethodNTLM.

We spent some time digging deep into the settings for IIS. The idea of forcing IIS to only use NTLM authentication seemed promising. But in the end we were unable to find the right switch in IIS to get the behavior we desired.

So back to Xcode for a closer look at how authentication is handled. We soon came to the canAuthenticateAgainstProtectionSpace callback method. Which had this naïve implementation:

- (BOOL)connection:(NSURLConnection*)conn
canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace*)protectionSpace {
  return YES;
}

After fixing this and rejecting authentication methods we can't or don't want to handle, the method now looks like this:

- (BOOL)connection:(NSURLConnection*)conn
canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace*)protectionSpace {
  DLog(@"protectionSpace: %@", [protectionSpace authenticationMethod]);

  // We only know how to handle NTLM authentication.
  if([[protectionSpace authenticationMethod] isEqualToString:NSURLAuthenticationMethodNTLM])
    return YES;

  // Explicitly reject ServerTrust. This is occasionally sent by IIS.
  if([[protectionSpace authenticationMethod] isEqualToString:NSURLAuthenticationMethodServerTrust])
    return NO;

  return NO;
}

The IIS server still sends NSURLAuthenticationMethodServerTrust occasionally but when the iOS app rejects it, IIS immediately follows with a request for NSURLAuthenticationMethodNTLM. The didReceiveAuthenticationChallenge method is then called with an authentication method that it can handle. All is well again.

written by Nick \\ tags: , , ,

Nov 04

A long standing “problem” with Cocoa has been Apple’s insistence that HTTP header field names are case-insensitive. This is clearly described in the documentation and also in the relevant RFC.

However not all server systems follow these standards so strictly and often require HTTP header field names to have a specific case. For example if the server expects the name “MyHeaderField” and your app sends “Myheaderfield” then the server may not read the value sent in the header field at all.

Your code may look something like this:

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.server.com"]];
[request setValue:@"value" forHTTPHeaderField:@"MyHeaderField"];

The problem was that when the HTTP request is sent to the server, the case of the name of the header field was “helpfully” changed for you, without giving you any control over the exact change or any way to override it. In the example above the name would become “Myheaderfield” instead of “MyHeaderField”, which is what you specified in the code.

If you have control over the server API you could just ask the server team to look for “Myheaderfield” instead and all would be well. But if the server API was a third party serving multiple clients, you would have a problem.

With iOS 5 this behavior has changed. Now the header field names are not changed at all. Whatever you specify in your code is what is sent to the server. Nice!

If your code looks like the snippet above, and you made a deal with the server team to look for the modified case string, then you may have issues when your app is running on iOS 5. Of course in the perfect world where servers followed the RFC to the letter, there would not be an issue (in iOS 5 or earlier) because the server would not care about the capitalization and treat “MyHeaderField” and “Myheaderfield” as the same.

http://0xced.blogspot.com/2010/06/fixing-nsmutableurlrequest.html

http://www.cocoabuilder.com/archive/cocoa/142670-http-extensions-to-nsmutableurlrequest-erroneously-modify-headers.html

http://stackoverflow.com/questions/2543396/how-to-add-lowercase-field-to-nsurlrequest-header-field

written by Nick \\ tags:

Oct 28

Apple has made several subtle changes related to case (as in upper/lower/mixed-case) in the URL and HTTP communication classes for iOS 5. Here’s the first one, and more blog posts to follow.

With a UIWebView I often implement the shouldStartLoadWithRequest method in the UIWebViewDelegate to look at the link the user tapped and take specific action for certain types of links. Here’s a simple example that handles mailto links:

- (BOOL)webView:(UIWebView *)theWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
	BOOL shouldStartLoadWithRequest = YES;

	NSString *scheme = [[request URL] scheme];
	if ([scheme isEqualToString:@"mailto"]) {
		[[UIApplication sharedApplication] openURL:[request URL]];
		shouldStartLoadWithRequest = NO;
	}

	return shouldStartLoadWithRequest;
}

Another common use is to handle application specific links like “MyApp://goto?id=42″. If you just replaced @”mailto” with @”MyApp” in the example code above, it turns out that this breaks in iOS 5. The reason is that iOS 5 returns “myapp” (all lowercase) for the scheme, whereas iOS 4 and earlier returned the actual string from the HTML link unaltered.

The solution is simple. Just make your string comparison case insensitive. For example:

if ([scheme compare:@"MyApp" options:NSCaseInsensitiveSearch] == NSOrderedSame) {

written by Nick \\ tags: , ,

Oct 19

Yesterday I mentioned that wireless networks have high latency. I thought I’d expand on that and provide some statistics from a real world scenario.

High network latency means that it takes a long time for a packet to arrive at its destination. This is not a big deal when you’re downloading a large file or watching online video, because it just means that the start of the download is slightly delayed.

When communication goes back and forth and the other party has to wait for a packet to arrive before responding, that’s when high latency becomes a problem. This is particularly apparent when you open a new network connection to a server, because of the handshake packets between the client and the server.

Wireless networks are prefect examples of high latency networks. Therefore you need to consciously consider how to optimize data transfers in your iPhone/iPad apps. You should avoid multiple small server requests and instead try to do one large download that contains all the data in one request/response. Of course that may require changing the server API.

Here’s an example of why this may be worth your while:

A project I worked on recently needed to request URLs for image thumbnails for a large number of videos. The first – very naive – implementation looped through the video objects and requested each thumbnail URL. This resulted in 60 requests to the server, which took a total of 46 seconds over a wireless network. (Over a lower latency network the same number of requests took just 26 seconds. This illustrates the difference latency makes when you have many requests.) Of course waiting 46 seconds (or even 26 seconds) before the app is fully usable is not acceptable.

After a slight redesign, the app now creates an array of all the videos URIs and sends that to the server in one request. The response is an array of URLs. There are some other server requests that are necessary, so the total number of requests is now down to 8, a significant reduction from 60. And the total time to retrieve the same data is now reduced from 46 seconds to just 5.

It’s the same amount of payload data before and after the optimization. The performance difference is due to the overhead in creating network connections.

A performance improvement close to a factor of 10 is not bad for a few hours of coding.

written by Nick

Oct 18

You should always avoid downloading data over a wireless network whenever possible. If there is a file on a server that your app needs and it’s infrequently updated, then a good approach would be to cache that file in your app and only download it when an update is detected. (You could also package a version of the file inside the app bundle to provide a quick startup time the first time the app is launched.)

There are several ways to solve this problem. The code below does a HTTP HEAD request, checks the returned "Last-Modified" header, and compares it with updated timestamp of the local file. (A slightly more efficient approach would be to send a "If-Modified-Since" header with your request. But there seems to be some issues with that, so proceed carefully with that approach.)

High-level steps:

  1. Create a HTTP HEAD request.
  2. Read the "Last-Modified" header and convert the string to a NSDate.
  3. Read the last modification timestamp of the local file.
  4. Compare the two timstamps.
  5. Download the file from the server if it has been updated.
  6. Save the downloaded file.
  7. Set the last modification timestamp of the file to match the "Last-Modified" header on the server.

Please note that the code uses synchronous requests to make the code shorter and better illustrate the point. Obviously you should not call this method from the main thread. Depending on your requirements, you could rewrite the code to use asynchronous requests instead.

- (void)downloadFileIfUpdated {
	NSString *urlString = ... your URL ...
	DLog(@"Downloading HTTP header from: %@", urlString);
	NSURL *url = [NSURL URLWithString:urlString];

	NSString *cachedPath = ... path to your cached file ...
	NSFileManager *fileManager = [NSFileManager defaultManager];

	BOOL downloadFromServer = NO;
	NSString *lastModifiedString = nil;
	NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
	[request setHTTPMethod:@"HEAD"];
	NSHTTPURLResponse *response;
	[NSURLConnection sendSynchronousRequest:request returningResponse:&response error: NULL];
	if ([response respondsToSelector:@selector(allHeaderFields)]) {
		lastModifiedString = [[response allHeaderFields] objectForKey:@"Last-Modified"];
	}

	NSDate *lastModifiedServer = nil;
	@try {
		NSDateFormatter *df = [[NSDateFormatter alloc] init];
		df.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
		df.locale = [[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"] autorelease];
		df.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
		lastModifiedServer = [df dateFromString:lastModifiedString];
		[df release];
	}
	@catch (NSException * e) {
		NSLog(@"Error parsing last modified date: %@ - %@", lastModifiedString, [e description]);
	}
	DLog(@"lastModifiedServer: %@", lastModifiedServer);

	NSDate *lastModifiedLocal = nil;
	if ([fileManager fileExistsAtPath:cachedPath]) {
		NSError *error = nil;
		NSDictionary *fileAttributes = [fileManager attributesOfItemAtPath:cachedPath error:&error];
		if (error) {
			NSLog(@"Error reading file attributes for: %@ - %@", cachedPath, [error localizedDescription]);
		}
		lastModifiedLocal = [fileAttributes fileModificationDate];
		DLog(@"lastModifiedLocal : %@", lastModifiedLocal);
	}

	// Download file from server if we don't have a local file
	if (!lastModifiedLocal) {
		downloadFromServer = YES;
	}
	// Download file from server if the server modified timestamp is later than the local modified timestamp
	if ([lastModifiedLocal laterDate:lastModifiedServer] == lastModifiedServer) {
		downloadFromServer = YES;
	}

	if (downloadFromServer) {
		DLog(@"Downloading new file from server");
		NSData *data = [NSData dataWithContentsOfURL:url];
		if (data) {
			// Save the data
			if ([data writeToFile:cachedPath atomically:YES]) {
				DLog(@"Downloaded file saved to: %@", cachedPath);
			}

			// Set the file modification date to the timestamp from the server
			if (lastModifiedServer) {
				NSDictionary *fileAttributes = [NSDictionary dictionaryWithObject:lastModifiedServer forKey:NSFileModificationDate];
				NSError *error = nil;
				if ([fileManager setAttributes:fileAttributes ofItemAtPath:cachedPath error:&error]) {
					DLog(@"File modification date updated");
				}
				if (error) {
					NSLog(@"Error setting file attributes for: %@ - %@", cachedPath, [error localizedDescription]);
				}
			}
		}
	}
}

Line item notes:

03. See this post for the source to the DLog macro.

14. It’s always good practice to check if the returned object responds to a selector before you send a message to it. See this post for more details.

22. The "Last-Modified" timestamp on the server includes the name of the month as text. Set the locale of the date formatter to English (assuming that your server responds in English) otherwise the code will fail when run on devices in non-English countries. See this post for more details.

Keep in mind that wireless networks have very high latency. That means it’s very expensive to create a network connection to a server. If the size of the file you want to download is very small, then you will not gain much with the approach above since it will take almost as long time to get the header information as downloading the entire file.

written by Nick \\ tags: , , , , , , ,