ASIHTTPRequest vs. NSURLRequest

Support for one of my favorite libraries, ASIHTTPRequest, has been discontinued. I have been slowly but surely making a transition from ASIHTTPRequest (back) to NSURLRequest in some of my projects. I picked ASIHTTPRequest over NSURLRequest because NSURLRequest is verbose. Extremely verbose. ASI also has queueing control, throttling control and much, much more. These are are just some of the things that made ASIHTTPRequest such a pleasure to work with. Alas, I am stuck with NSURLRequest again…

Anyway, one of the more interesting aspects of connection handling is session control. I rely on the backend server to set a session cookie when I log in, and to keep that cookie alive while I do other stuff in the program. But in particular, when I transitioned the login screen to NSURLRequest, I noticed that, despite logging in successfully, my program would think that I’m NOT logged in, in the parts where ASIHTTPRequest is still used. It looks like ASIHTTPRequest is blind to the session cookies that are set using NSURLRequest.

I should also mention that I’m using CodeIgniter on the back end for session control, but in theory it shouldn’t matter (spoiler alert: but it does).

Let’s line up the usual suspects and try to figure out why this happens.

The Usual Suspects

Suspect #1: ASIHTTPRequest?

ASIHTTPRequest class is the main class through which all requests (and cookies) are handled. In that file, there is a method called applyCookieHeader. From this method, I can infer that once a request was made through ASIHTTPRequest, the cookies are saved either in the persistent store, or in memory — but ASIHTTPRequest automatically picks up persistent store cookies. In fact, doing a breakpoint in ASIHTTPRequest.m on the applyCookieHeader method, we can examine the cookies and see that, yes, in fact the session cookies are there, in the persistent store.

Btw, to view the cookies in the headers, you can add this code:


- (void)applyCookieHeader
{
	// Add cookies from the persistent (mac os global) store
	if ([self useCookiePersistence]) {
		NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:[[self url] absoluteURL]];
		if (cookies) {
            for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage]cookies]) {
                NSLog(@"\n==========================\nname: %@\nvalue: %@\ndomain: %@\npath: %@",
                [cookie name],
                [cookie value],
                [cookie domain],
                [cookie path]);

            }
		[[self requestCookies] addObjectsFromArray:cookies];
	}
}

Suspect #2: CodeIgniter?

I’m using CodeIgniter as a backend, and their convenient Session Class to set the session cookie and store it in the database. I know the code itself works when I’m using NSURLRequest only, OR ASIHTTPRequest only, but not both.

So, let’s remove CodeIngiter from the equation and do this “the old fashioned way” without help of a framework (forgive me for not scrubbing the input, this is just a test — so if you use this anywhere, make sure to scrub it):


//when logging in
$username = $_REQUEST['username'];
$password = $_REQUEST['password'];
//check password in database with $username and $password -- make sure to scrub first!
if (user_pass_correct($username, $password)) {
    session_start();
    $_SESSION['login'] = "1";
}

//...
//when checking the login
if ($_SESSION['login'] == "1") {
    $data['login'] = 1;
}
echo json_encode($data);

However, the above code seems to work from the phone (simulator). So that means that something in the request is confusing CodeIgniter. On further examination, the session cookie IS present on subsequent calls, BUT the ci_session cookie is different. That’s strange…

Back to the iPhone Client as a Suspect

What could be different in the request headers that causes CodeIgniter to reset the cookie? The only real way to find out is by intercepting the requests coming out of the simulator, and doing a diff. But first I have to have an easy way to switch between different kinds of requests, so I created a quick-and-dirty app that connects to my server with my choice of connection method: ASIHTTPRequest or NSURLRequest.

My test application running in the simulator

My test application running in the simulator

The test app only confirms that when I login using NSURLRequest, and then check the session variable on the server (using the “check login” button), it returns success. But when I switch over to ASIHTTPRequest, the “logged in” variable returns that I’m not logged in.

So, let’s examine the traffic using Charles Proxy (CP is an awesome tool for intercepting connections, even over https).

Outbound connections: top is produced by ASIHTTPRequest, bottom by NSURLRequest

Outbound connections: top is produced by ASIHTTPRequest, bottom by NSURLRequest

There are quite a few differences between the calls. However, my hunch tells me that I should start with User-Agent, which turned out to be the problem. Setting the User-Agent string identical to the one in NSURLRequest results in a successfully recognized session.

Problem Solved – But Why Was It a Problem?

There are two unanswered questions: why does ASIHTTPRequest send its own User-Agent string in the header, and why does CodeIgniter invalidate the session when it sees a different User-Agent string?

There isn’t much in the CodeIgniter docs – only a flag called “sess_match_useragent” with an explanation of “Whether to match the User Agent when reading the session data.” (Knowing what I know now, that seems obvious what it is…). And here it is in the code, in /system/libraries/Session.php:


// Does the User Agent Match?
if ($this->sess_match_useragent == TRUE AND trim($session['user_agent']) != trim(substr($this->CI->input->user_agent(), 0, 120)))
{
	$this->sess_destroy();
	return FALSE;
}

So, they are actively destroying the session if the user-agent string doesn’t match and the config item is set to do that. OK.

What about ASIHTTPRequest docs? Not much better: “If you do not set a user agent, ASIHTTPRequest will create one for you.” Looking at the source code in ASIHTTPRequest.m at method +(NSString *)defaultUserAgentString, it looks like the user agent is chosen based on appName + appVersion + deviceName + OSName + OSVersion + locale.

Finally, how does NSURLRequest create it? I couldn’t find the specifics in the documentation, but this blog post indicates it is computed by “AppName/Version CFNetwork/485.13.9 Darwin/10.7.0” (with varying versions).

Lessons

I’ve obviously learned to read the docs more closely. More importantly, I found out that a changing user-agent string can be an indication of a security problem (i.e. someone trying to spoof the user’s session). It’s easy enough to circumvent, but it is just an additional security measure – I mean, it prevented me from “spoofing” a session 🙂

Leave a Reply