Apple’s CFNetwork documentation leaves a lot to be desired. At one point while figuring out how to use it, I believe I went on a rant where I described its contents as “vicious lies”. Now that I’ve actually figured it out, I’ve revised my opinion. The documentation doesn’t lie, exactly. It omits. And what’s worse is that I couldn’t find anything on the Internet which told the entire story. So, I’m using this blog post to try to fill that gap. With a little bit of luck, Google will find me. And with a lot of luck I’m not posting incredibly inaccurate information here. So. Caveat: I’m still learning OS X programming. Don’t take this as gospel; take it as a good place to start. Also, I’ve removed all error checking for brevity. You’ll need to add it back. Good luck. (We’re all counting on you.)
I’m going to talk about sending a very basic HTTP post request to a server which authenticates with NTLM and then reading the response data. My example makes the request synchronously (because my application currently makes the request synchronously). You could modify this to use threads, or you could use polling or run loops. I’m not doing any of these, but I believe the ideas are basically the same. Refer to the documentation for more details.
The basic strategy is:
In higher level frameworks (like Microsoft’s .Net or even Apple’s NSURLConnection), a lot of these steps are done for you. Indeed, if you don’t need a feature of the CFNetwork stack, I highly recommend using NSURLConnection. Unfortunately, NSURLConnection cannot do NTLM authentication. So if you’re talking to a Windows web-server that’s expecting domain-level credentials, you’re stuck. It’s not so bad, though.
This is easy enough that I’m just going to point you at the code:
-(CFHTTPMessageRef)buildMessage
{
NSURL *myURL = [NSURL URLWithString:@"http://myurl.com"];
NSData *dataToPost = [[NSString stringWithString:@"POST Data It Doesn't Matter What It Is"] dataUsingEncoding:NSUTF8StringEncoding];
//Create with the default allocator (NULL), a post request,
//the URL, and pick either
//kCFHTTPVersion1_0 or kCFHTTPVersion1_1
CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CSTR("POST"), (CFURLRef)myURL, kCFHTTPVersion1_1);
CFHTTPMessageSetBody(request, (CFDataRef)dataToPost);
//Unfortunately, this isn't smart enough to set reasonable headers for you
CFHTTPMessageSetHeaderFieldValue(request, CFSTR("HOST"), (CFStringRef)[myURL host]);
CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Content-Length"), (CFStringRef)[NSString stringWithFormat:"%d", [dataToPost length]);
CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Content-Type"), CFSTR("charset=utf-8"));
return [NSMakeCollectable(request) autorelease];
}
This one might require a little more explanation. We’re going to
-(CFHTTPMessageRef)performHTTPRequest:(CFHTTPMessageRef)request
{
CFReadStreamRef requestStream = CFReadStreamCreateForHTTPRequest(NULL, request);
CFReadStreamOpen(requestStream);
NSMutableData *responseBytes = [NSMutableData data];
CFIndex numBytesRead = 0 ;
do
{
UInt8 buf[1024];
numBytesRead = CFReadStreamRead(requestStream, buf, sizeof(buf));
if(numBytesRead > 0)
[responseBytes appendBytes:buf length:numBytesRead];
} while(numBytesRead > 0);
CFHTTPMessageRef response = (CFHTTPMessageRef)CFReadStreamCopyProperty(requestStream, kCFStreamPropertyHTTPResponseHeader);
CFHTTPMessageSetBody(response, (CFDataRef)responseBytes);
CFReadStreamClose(requestStream);
CFRelease(requestStream);
return [NSMakeCollectable(response) autorelease];
}
I’m going to skip to step 6 for a moment, just so I can use this method in the next step (which combines steps 5 – 9 in a single loop). This is actually pretty simple (the only wrinkle is that you HAVE to use CFNetwork code and can’t use NSURLConnection) so I’ll skip to the code.
-(void)addAuthenticationToRequest:(CFHTTPMessageRef)request withResponse:(CFHTTPMessageRef)response
{
CFHTTPAuthenticationRef authentication = CFHTTPAuthenticationCreateFromResponse(NULL, response);
[NSMakeCollectable(authentication) autorelease];
CFStreamError err;
Boolean success = CFHTTPMessageApplyCredentials(request, authentication, CFSTR("username"), CFSTR("password"), &err);
}
Now, we’re going to actually make the HTTP request in a loop. The loop will let us see if we need to authenticate. (Since good HTTP authentication uses a challenge-response mechanism, you have to make multiple requests. It’s a shame the libraries don’t wrap this up for us…) Once we have a response back, we’ll get the body and use NSLog to print it to the console.
-(void)magicHappens
{
CFHTTPMessageRef request = [self buildMessage];
CFHTTPMessageRef response = [self performHTTPRequest: request];
UInt32 statusCode;
statusCode = CFHTTPMessageGetResponseStatusCode(response);
//An HTTP status code of 401 or 407 indicates that authentication is //required I use an auth count to make sure we don't get stuck in an //infinite loop if our credentials are bad. Sometimes, making the //request more than once lets it go through.
//I admit I don't know why.
int authCount = 0;
while((statusCode == 401 || statusCode == 407) && authCount < 3)
{
request = [self buildMessage];
[self addAuthenticationToRequest:request withResponse:response];
response = [self performHTTPRequest: request];
statusCode = CFHTTPMessageGetResponseStatusCode;
authCount++;
}
NSData *responseBodyData = [(NSData*)CFHTTPMessageCopyBody(response) autorelease];
NSString *responseBody = [[[NSString alloc] initWithData:responseBodyData encoding:NSUTF8StringEncoding] autorelease];
NSLog(responseBody);
}
I hope that was both clear and helpful. I tried to keep my commentary to a minimum because I’m a better coder than I am a writer (though, I’m not a particularly good coder when it comes to ObjC and Cocoa/Carbon). I wrote this inside a WordPress edit window so I could keep formatting mistakes to a minimum; but that means I haven’t actually compiled it. It may not work as presented; but it should be enough to get you the jist of what to do.
References:
Comments are currently closed for this post.