NSURLRequest doesn't send cookies - objective-c

I'm developing a newsstand application and use NSURLRequest to download issue assets.
NSArray *contents = [issue.tableOfContents objectForKey:kSNTableOfContentsContents];
NSHTTPCookie *cookie;
NSHTTPCookieStorage *cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
NSLog(#"HERE GO MY COOKIES");
for (cookie in [cookieJar cookies]) {
NSLog(#"%#", cookie);
}
for (NSDictionary *contentItem in contents) {
NSString *contentURL_string = [contentItem objectForKey:kSNTableOfContentsRemoteURL];
NSURL *contentURL = [NSURL URLWithString:contentURL_string];
NSString *fileName = [contentItem objectForKey:kSNTableOfContentsContentsURL];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:contentURL];
NKAssetDownload *asset = [newsstandIssue addAssetWithRequest:request];
[request release];
....
[asset downloadWithDelegate:self];
....
}
When the first for loop is executed my cookies appear to be in NSHTTPCookieStorage, but when actual requests are sent, there are no cookie information in headers. I use CharlesProxy to look that up. Could anyone please give some advice what might be causing this issue?

From this thread, the magic incantation appears to be:
NSDictionary * headers = [NSHTTPCookie requestHeaderFieldsWithCookies:
[cookieJar cookies]];
[request setAllHTTPHeaderFields:headers];
(Warning: untested code.)
This will convert your cookie jar into an array of cookies, then to an NSDictionary of headers, and finally, staple those headers to your request. This is comparable to doing it manually, as Adam Shiemke linked in the question errata, but much cleaner in my opinion.
As per the documentation, you may also want to check HTTPShouldHandleCookies to see if your default cookie policy is being used properly.

On iOS projects I found the ASIHTTPRequest very useful for this kind of problems. It does things like authentication and cookies a lot better that the build-in functions:
http://allseeing-i.com/ASIHTTPRequest/

Related

Server not properly receiving POST request from ASIFormDataRequest in Objective-C

I'm trying to use an ASIFormDataRequest in my iPhone application to send a video I have just recorded to a server, along with a string that I can use to ID who the video belongs to. The code for doing so is here:
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
NSURL *urlvideo = [info objectForKey:UIImagePickerControllerMediaURL];
NSString *urlString=[urlvideo path];
NSLog(#"urlString=%#",urlString);
NSString *str = [NSString stringWithFormat:#"http://www.mysite.com/videodata.php"];
NSURL *url = [NSURL URLWithString:[str stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
NSData *patientData = [_patientCode dataUsingEncoding:NSUTF8StringEncoding];
[request setFile:urlString forKey:#"video"];
[request setData:patientData forKey:#"patientcode"];
[request setRequestMethod:#"POST"];
[request setDelegate:self];
[request startSynchronous];
NSLog(#"responseStatusCode: %i",[request responseStatusCode]);
NSLog(#"responseString: %#",[request responseString]);
[picker dismissViewControllerAnimated:YES completion:nil];
}
Everything seems to work correctly, I get back a status code of 200 and the method finishes as expected. However, nothing seems to be received by the php file on the server. I added this line to my server-side php code:
echo(count($_POST));
This returns 0, so it seems as though nothing is actually getting posted by the ASIFormDataRequest. I feel like there might be some simple step I am missing as I have never used the ASIFormDataRequest before, but any help would be greatly appreciated.
EDIT: I changed the setData:patientData to setPostValue:_patientCode and now that part of the post is getting sent correctly, so it seems as though setPostValue works but setData and setFile do not.
You should use -addData:forKey: and -addFile:forKey: instead of -setData:forKey: and -setFile:forKey:.
Other than that, check the debug output when you compile with DEBUG_FORM_DATA_REQUEST.
You should be sending a multipart/form-data request when using -addFile:forKey: and a application/x-www-form-urlencoded request when using -addPostValue:forKey:.
Does your server handle multipart/form-data requests?
I ended up finding the answer while reading something else and noticing that they did something interesting: The file I uploaded was simply going into the $_FILES array, not the $_POST array. I also changed [request setData:patientData forKey:#"patientcode"]; to [request setPostValue:_patientCode forKey:#"patientCode"]; and then that appeared in the $_POST array as I wanted so I was able to get both the string I was sending and the file I was sending in my php script.

AFNetworking Overload a Post Parameter

I am migrating from ASIHTTPRequest to AFNetworking and have run into an issue.
I am trying to hit an API with a request that overloads the post parameter. I was previously using ASIFormDataRequest for this and used this code to update 3 ids at the same time.
// ASIHTTPRequestCode
[request addPostValue:#"value1" forKey:#"id"];
[request addPostValue:#"value2" forKey:#"id"];
[request addPostValue:#"value3" forKey:#"id"];
Since AFNetworking uses an NSDictionary to store key value pairs, it doesn't seem straight forward how to do this. Any ideas?
I can't immediately see a direct way to do this with AFNetworking, but it is possible to do.
If you look at the code for AFHTTPClient requestWithMethod, you'll see this line, which is the one that sets up the request body to contain the parameters:
[request setHTTPBody:[AFQueryStringFromParametersWithEncoding(parameters, self.stringEncoding) dataUsingEncoding:self.stringEncoding]];
Basically you could pass an empty dictionary to requestWithMethod for the parameters, then it returns, call request setHTTPBody yourself, making up the query string yourself in a similar way to the way AFQueryStringFromParametersWithEncoding does it.
You can build the request this way:
NSURL *url = [NSURL URLWithString:#"http://www.mydomain.com/"];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
NSDictionary *postValues = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:#"%#,%#,%#",#"value1",#"value2",#"value3"] forKey:#"id"];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"POST" path:#"/path/to/your/page.php" postValues];
I was facing a similar issue but solved it by updating the url. I added the parameters i need to send with the url and set the "parameters to nil"
so the url became something like
server\url.htm?data=param1&data=param2&data=param3
and sent nil as paramDictionary
[request setHTTPBody:[AFQueryStringFromParametersWithEncoding(nil, self.stringEncoding) dataUsingEncoding:self.stringEncoding]];

Why does NSURLConnection timeout when sending many requests?

I am writing a network class for an iOS app. This class will take care of all logging and network traffic. I have a problem where I have to send possibly thousands of requests at one time, but NSURLConnections are timing out because the delegate methods will not be called until all the NSURLConnections are started, by which time the timeout period has expired. I am using a rest API for Drupal and, unfortunately, I do not know of a way to create multiple instances with one request. How can I receive responses while simultaneously sending them? If I use GCD to pass off the creation of the NSURLConnections, will that solve the problem? I think I would have to pass the entire operation of iterating over the objects to send and sending to GCD to free up the main thread to answer to responses.
-(BOOL)sendOperation:(NetworkOperation)op
NetworkDataType:(NetworkDataType)dataType
JsonToSend:(NSArray *)json
BackupData:(NSArray *)data
{
if(loggingMode)
{
return YES;
}
NSURLConnection *networkConnection;
NSData *send;
NSString *uuid = [self generateUUID];
NSMutableArray *connections = [[NSMutableArray alloc] init];
NSMutableURLRequest *networkRequest;
for (int i=0; i<[json count] && (data ? i<[data count] : YES); i++)
{
if(op == Login)
{
/*Grab all cookies from the server domain and delete them, this prevents login failure
because user was already logged in. Probably find a better solution like recovering
from the error*/
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:
[[NSURL alloc] initWithString:networkServerAddress]];
for (NSHTTPCookie *cookie in cookies)
{
[[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:cookie];
}
networkRequest = [[NSMutableURLRequest alloc] initWithURL:
[NSURL URLWithString:[networkServerAddress stringByAppendingString:#"/user/login"]]];
}
else if(op == StartExperiment)
{
networkRequest = [[NSMutableURLRequest alloc] initWithURL:
[NSURL URLWithString:[networkServerAddress stringByAppendingString:#"/node"]]];
}
else if(op == Event || op == EndExperiment || op == SendAll)
{
networkRequest = [[NSMutableURLRequest alloc] initWithURL:
[NSURL URLWithString:[networkServerAddress stringByAppendingString:#"/node"]]];
}
else if(op == Logout)
{
networkRequest = [[NSMutableURLRequest alloc] initWithURL:
[NSURL URLWithString:[networkServerAddress stringByAppendingString:#"/user/logout"]]];
}
send = [[json objectAtIndex:i] dataUsingEncoding:NSUTF8StringEncoding];
//Set the headers appropriately
[networkRequest setHTTPMethod:#"POST"];
[networkRequest setValue:#"application/json"
forHTTPHeaderField: #"Content-type"];
[networkRequest setValue:[NSString stringWithFormat:#"%d", [send length]]
forHTTPHeaderField:#"Content-length"];
[networkRequest setValue:#"application/json"
forHTTPHeaderField:#"Accept"];
//Set the body to the json encoded string
[networkRequest setHTTPBody:send];
//Starts async request
networkConnection = [[NSURLConnection alloc] initWithRequest:networkRequest delegate:self];
//Successfully created, we are off
if(networkConnection)
{
[networkConnectionsAndData setValue:[[NSMutableArray alloc] initWithObjects:uuid,
[[NSNumber alloc] initWithInt:op], [[NSNumber alloc] initWithInt:dataType], [[NSMutableData alloc] init], (data ? [data objectAtIndex:i] : [NSNull null]), nil]
forKey:[networkConnection description]];
}
else //Failed to conn ect
{
NSLog(#"Failed to create NSURLConnection");
return NO;
}
}
[[self networkOperationAndConnections] setObject:[[NSMutableDictionary alloc] initWithObjectsAndKeys:[[NSMutableArray alloc] initWithObjects:connections, nil], #"connections", [[NSMutableArray alloc] init], #"errors", nil]
forKey:uuid];
return YES;
}
The dictionaries are used to keep track of the correlating data with each NSURLConnection and also to group the NSURLConnections together into one group to determine ultimate success or failure of an entire operation.
Update
AFNetworking was key in finishing this project. It not only cleaned up the code substantially, but dealt with all the threading issues inherit in sending so many requests. Not to mention with AFNetworking I could batch all the requests together into a single operation. Using blocks, like AFNetworking uses, was a much cleaner and better solution than the standard delegates for NSURLConnections.
You definitely need to allow the NSURLRequest / Connection to be operating on another thread. (Not the main thread!)
Edited for clarity**:
I noticed your comment of "//Starts async request" and I wanted to be sure you realized that your call there is not what you would expect out of a typical "asynch" function. Really its just firing off the request synchronously, but since its a web request it inherently behaves asynchronously. You want to actually place these requests on a another thread for full asynch behavior.
Everything else aside, I really suggest digging into Apple's networking example project here: MVCNetworking
As for specifics on your question, there's a couple ways to do this.
One is to keep your connection from starting immediately using initWithRequest:<blah> delegate:<blah> startImmediately:FALSE and then schedule your NSURLConnection instances on another thread's run-loop using: scheduleInRunLoop:forMode:
(Note: You then have to kick off the connection by calling start-- it's best to do this via an NSOperation + NSOperationQueue.)
Or use this static method on NSURLConnection to create/launch the connection instead of doing an alloc/init: sendAsynchronousRequest:queue:completionHandler:
(Note: this approach accomplishes pretty much same as above but obfuscates the details and takes some of the control out of your hands.)
To be honest my quick answers above won't be sufficient to finish this kind of project, and you'll need to do a bit of research to fill in the blanks, especially for the NSOperationQueue, and that's where the MVCNetworking project will help you.
Network connections are a fickle beast -- You can time-out and kill your connections even if they're running on a background thread simply by trying to perform too much work simultaneously! I would seriously reconsider opening up several thousand NSURLConnections at once, and using an NSOperationQueue would help work around this.
ADDITIONAL RESOURCES:
Here's a 3rd party library that may make your networking adventures less painful:
https://github.com/AFNetworking/AFNetworking
http://engineering.gowalla.com/2011/10/24/afnetworking/

How do I authenticate for Twitter friends_timeline?

This is my first web API project, so I hope the solution to this isn't blindingly obvious.
// Contstruct the http request
NSString *urlString = [NSString stringWithFormat:#"http://%#:%##twitter.com/statuses/user_timeline/%#.xml?count=5", username, password, friend];
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
// Recive the data from the synchronous request
NSData *urlData;
urlData = [NSURLConnection sendSynchronousRequest:urlRequest returningResponse:nil error:nil];
The results returned from this query look fine except they don't reflect if I've favorited them or not. All tweets return "false", even if the authenticating user has starred them. I'm fairly confident that I'm authenticating correctly as code further down that does require authentication behaves correctly.
Add a Basic Authentication header field where you specify the username/password. Most libraries have a setCredentials() method though.
As dirkgently pointed out in the previous post, the XML was out of sync with reality. With no changes to the code at all, things that weren't working this morning were working tonight. Thanks Dirk!

Google App Engine with ClientLogin Interface for Objective-C

I'm experiencing the same problem in this previous stackoverflow.com post.
Specifically, I seem to be able to get the "Auth" token correctly, but attempts to use it in the header when I access later pages still just return me the login page's HTML.
Following links related to this post, I've determined that you need to make a subsequent call to this URL.
A call to the URL will then give you an ACSID cookie which then needs to be passed in subsequent calls in order to maintain an authenticated state.
When requesting this cookie, I've read various posts saying you need to specify your original auth token by appending it to the query string such that:
?auth=this_is_my_token
I've also read that you should set it in the http header as described in google's documentation such that a http header name/value is:
Authorization: GoogleLogin auth=yourAuthToken
I've tried both approaches and am not seeing any cookies returned. I've used Wireshark, LiveHttpHeaders for Firefox, and simple NSLog statements trying to see if anything like this is returned.
Below is the code snippet I've been using.
NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:#"http://yourapp.appspot.com/_ah/login?auth=%#", [token objectForKey:#"Auth"]]];
NSHTTPURLResponse* response;
NSError* error;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setValue:[NSString stringWithFormat:#"GoogleLogin auth=%#", [token objectForKey:#"Auth"]] forHTTPHeaderField:#"Authorization"];
NSData * data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
//show me all header fields
NSLog([[response allHeaderFields] description]);
//show me the response
NSLog(#"%#", [[[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding] autorelease]);
NSArray * all = [NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:[NSURL URLWithString:#"http://yourapp.appspot.com/_ah/login"]];
//show me all cookies
for (NSHTTPCookie *cookie in all)
{
NSLog(#"Name: %# : Value: %#", cookie.name, cookie.value);
}
I hope you can use ClientLogin for Google App Engine code.
Adding sample code to this question because someone contacted me directly about my solution. Note that you must set the "service" parameter equal to "ah" on the initial token request.
Initial Request of Token [done synchronously] NOTE: the "service" parameter is set to "ah" and the "source" is just set to "myapp", you should use your app name.
//create request
NSString* content = [NSString stringWithFormat:#"accountType=HOSTED_OR_GOOGLE&Email=%#&Passwd=%#&service=ah&source=myapp", [loginView username].text, [loginView password].text];
NSURL* authUrl = [NSURL URLWithString:#"https://www.google.com/accounts/ClientLogin"];
NSMutableURLRequest* authRequest = [[NSMutableURLRequest alloc] initWithURL:authUrl];
[authRequest setHTTPMethod:#"POST"];
[authRequest setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content-type"];
[authRequest setHTTPBody:[content dataUsingEncoding:NSASCIIStringEncoding]];
NSHTTPURLResponse* authResponse;
NSError* authError;
NSData * authData = [NSURLConnection sendSynchronousRequest:authRequest returningResponse:&authResponse error:&authError];
NSString *authResponseBody = [[NSString alloc] initWithData:authData encoding:NSASCIIStringEncoding];
//loop through response body which is key=value pairs, seperated by \n. The code below is not optimal and certainly error prone.
NSArray *lines = [authResponseBody componentsSeparatedByString:#"\n"];
NSMutableDictionary* token = [NSMutableDictionary dictionary];
for (NSString* s in lines) {
NSArray* kvpair = [s componentsSeparatedByString:#"="];
if ([kvpair count]>1)
[token setObject:[kvpair objectAtIndex:1] forKey:[kvpair objectAtIndex:0]];
}
//if google returned an error in the body [google returns Error=Bad Authentication in the body. which is weird, not sure if they use status codes]
if ([token objectForKey:#"Error"]) {
//handle error
};
The next step is to get your app running on google app engine to give you the ASCID cookie. I'm not sure why there is this extra step, it seems to be an issue on google's end and probably why GAE is not currently in their listed obj-c google data api library. My tests show I have to request the cookie in order sync with GAE. Also, notice I don't do anything with the cookie. It seems just by requesting it and getting cookied, future requests will automatically contain the cookie. I'm not sure if this is an iphone thing bc my app is an iphone app but I don't fully understand what is happening with this cookie. NOTE: the use of "myapp.appspot.com".
NSURL* cookieUrl = [NSURL URLWithString:[NSString stringWithFormat:#"http://myapp.appspot.com/_ah/login?continue=http://myapp.appspot.com/&auth=%#", [token objectForKey:#"Auth"]]];
NSLog([cookieUrl description]);
NSHTTPURLResponse* cookieResponse;
NSError* cookieError;
NSMutableURLRequest *cookieRequest = [[NSMutableURLRequest alloc] initWithURL:cookieUrl];
[cookieRequest setHTTPMethod:#"GET"];
NSData* cookieData = [NSURLConnection sendSynchronousRequest:cookieRequest returningResponse:&cookieResponse error:&cookieError];
Finally, I can post json to my gae app. NOTE: the snippet below is an async request. We can handle responses by implementing didReceiveResponse, didReceiveData, didFailWIthError.
NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:#"http://myapp.appspot.com/addRun?auth=%#", mytoken]];
NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:#"POST"];
[request setHTTPBody:#"my http body";
NSURLConnection *connectionResponse = [[NSURLConnection alloc] initWithRequest:request delegate:self];
if (!connectionResponse) {
NSLog(#"Failed to submit request");
} else {
NSLog(#"Request submitted");
}
Check out the code that does this in the official SDK. The latest SDK release even has it split into its own file.
1st - thanks for the great post it really got me started.
2nd - I have been slugging it out with my app, trying to POST to the GAE while authenticated.
This is the request is built when POSTing, once you have acquired the authtoken:
NSMutableURLRequest *request = [[[NSMutableURLRequest alloc] init] autorelease];
[request setURL:[NSURL URLWithString:url]];
[request setHTTPMethod:#"POST"];
[request setValue:postLength forHTTPHeaderField:#"Content-Length"];
[request setValue:#"image/png" forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:postData];
[request setValue:authtoken forHTTPHeaderField:#"auth"]; // <-- the magic
mattb
I created a few obj-c classes for implementing ClientLogin, including support for Google App Engine:
http://github.com/cameronr/GoogleAppEngineAuth
Note that Google has recently changed the way authorization failure is indicated. They used to place an Error token in the response. Now they just return a 403 (Forbidden) status. This broke my code!
Thank for this post and especially the answer from Keith but it does not works for me.
Even if it seems ok for me ... very strange.
I check this post (How do you access an authenticated Google App Engine service from a (non-web) python client?) which talk about doing the same thing in python. I test it and it works.
And the objective C code proposed by Keith is really similar to the python code.
But when I try to get the "Auth" token authData contains Error=BadAuthentication.
Some one got an idea about possibles problems ?
Using HOSTED_OR_GOOGLE is wrong, and I will explain why.
There are two kinds of accounts in the Google world. The ones you create for GMail, etc are "Google" accounts. The ones you create for Apps for Domains are "Hosted" accounts. You can use a Hosted Account email to make a Google Account, thus creating an email address that is associated with both kinds of accounts.
Your Google App Engine app can be configured to work with (1) Google Accounts or (2) Hosted Accounts for a particular domain.
Assume that we are developing an app for Google Accounts. A user enters in an email address that is associated with a Google Account and a Hosted Account. Google will use their Google Account for the login. This all works fine.
Now, if we use ClientLogin with this same email address and use HOSTED_OR_GOOGLE for the account type, login will be successful, but it will use the Hosted Account, since the Hosted Account takes precedence. As I mentioned above, you cannot use a Hosted Account for an app that expects a Google Account. So the authentication will not work.
So, when using ClientLogin to authenticate with a Google App Engine app, you need to use GOOGLE for the account type if the app is for Google Accounts, or HOSTED for the account type if the app is for a domain.