hi in one of my application. i have to send a request to the server (json server) many times continously.my url will be like this as mention below
#"http://185.185.116.51/servername/serverjspfilesname.jsp?filterID=21&ticket=65675656565656567"
actually i have many filter id's (filter id you can find at top).in order to chnage the filterid continously i used for loop like this as mention below
for(int i=0;i<[appdelegate.listOfFiltersArray count];i++)
{
filtersDataModelObject=[[ListOfFiltersDataModel alloc]init];
filtersDataModelObject=[appdelegate.listOfFiltersArray objectAtIndex:i];
homescreenstring=[NSString stringWithFormat:#"http://%#/servername/serverjspfilesname.jsp?filterID=%#&ticket=%#",Ip,filtersDataModelObject.filterID,[storeData stringForKey:#"securityTicket"]];
NSLog(#"url is %#",homescreenstring);
NSURLRequest *request=[NSURLRequest requestWithURL:[NSURL URLWithString:homescreenstring]];
connection=[[NSURLConnection alloc]initWithRequest:request delegate:self];
if(connection)
{
homeScreenResponseData=[[NSMutableData alloc]init];
}
else
{
NSLog(#"connection failed");
}
}
actually after each condition is satisfied in for loop i have to connect with the server for getting the data from the server using nsurlconnection delegate methods. but here after complete execution of for loop only nsurlconnection delegate methods are executing with last filterid which is getting from the appdelegate.listOfFiltersArray array.
but i would like to call the server for each filterid.
if anyone know please let me know.thanks in advance.
Create one count variable int count in .h file.
int count = 0 //in .m file
Now use this method:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
//one request finished
count ++;
//request another using count
}
The solution that Prince proposed is not general as you will face the problem of defining the
"filterid" for each request.
And what you are doing is a bad approach. Dont mix the web request with your business logic code.The web stuff should be handled by a separate file handling all the requests throughout the app. And that class will implement delegation.
For delegation you need to do the following.
In your Network class header (Networkclass.h) add the protocol
#protocol NetworkControllerDelegate <NSObject>
-(void) UrlResponseRecieved :(NSData *)responseData;
-(void) UrlResponseFailed:(NSError *)error;
#end
and in NetworkClass.m (implementation fie) do the following
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
if (receivedData != nil)
{
if([delegate respondsToSelector:#selector(UrlResponseRecieved:)])
[self.delegate UrlResponseRecieved:receivedData];
[receivedData release];
receivedData = nil;
}
[connection release];
}
if you still get stuck you may refer to the following
What's wrong on following URLConnection?
or you may read any asynchronous downloading tutorial first.
Related
I am trying to set up a NSURLRequest to download a simple index.html with its externa style.css sheet but I am not quite sure how to do this.. I have only ever just formatted the URL of the request to the file I want.. but this has to be slightly different and I cannot find a good example of what I am trying to do.
this is my code so far:
#pragma mark - NSURLConnection methods
- (void)htmlRequest
{
// Create the request.
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.mywebsite.com/index.html"]
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:60.0];
// create the connection with the request
// and start loading the data
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
if (theConnection) {
// Create the NSMutableData to hold the received data.
// receivedData is an instance variable declared elsewhere.
receivedData = [NSMutableData data];
} else {
// Inform the user that the connection failed.
NSLog(#"Connection Fail");
}
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
// This method is called when the server has determined that it
// has enough information to create the NSURLResponse.
// It can be called multiple times, for example in the case of a
// redirect, so each time we reset the data.
// receivedData is an instance variable declared elsewhere.
[receivedData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// Append the new data to receivedData.
// receivedData is an instance variable declared elsewhere.
[receivedData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
// inform the developer of error type
}
// This method uses methodName to determin which Initalizer method to send the response data to in EngineResponses.m
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
// EngineResponses *engineResponses = [EngineResponses sharedManager];
// [engineResponses GetManufacturers:receivedData];
NSString *myString = [[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding];
NSLog(#"%#", myString);
}
as you can see I am just calling index.html directly.. I would like to know how to format my request so i get the index.html as well as style.css
any help would be greatly appreciated.
I always create a new data structure,which has a -connection property and a -request property,like this
#interface connectionWrapper : NSObject
#property(retain) NSURLRequest *request
#property(retain) NSURLConnection *connection
by retaining this data structure in an mutable array, you can distinguish the connections in callback methods by iterate the array and compare each connectionWrapper instance's -connection property with the connection parameter the of the callback method, if they match(points to a same object), then you can retrieve the -request property of the connectionWrapper instance, then -url property of NSURLRequest instance.
as I'm not an native English speaker, I think code is a better tutor.
-(NSURLRequest*)getRequestByConnection:(NSURLConnection*)connection
{
for(connectionWrapper *w in theArrayContainingAllConnectionWrappers)
{
if(w == connection)
return w.request;
}
}
In callback method:
-(void)connection:(NSURLConnection*)connection didReceiveResponse(NSURLResponse*)response
{
NSURLRequest *request = [self getRequestByConnection:connection];
NSURL *url = [request url];
/*apply different approach to different url/*
}
PS:it's very sad that NSURLConnection don't have a -request property so that we can retrieve the request associated with the connection easily.
One way or another, you will have to make 2 requests. Even if you open a web page directly in a web browser, the browser will make a separate request for the CSS file referenced in the HTML it downloads. If your application needs both the HTML and the CSS file, then you want it to make 2 separate URL requests, first to get the HTML and then to get the CSS file.
Now, just because 2 requests need to be made, that doesn't mean you will always need to write the code that makes those 2 requests. It may be that libraries like the ones recommended by #Slee automatically take the results of a first request, parse them out, and make requests for any referenced CSS files. I have not worked with them so I am not sure what they support, or if any libraries will do this for you.
One thing you may want to consider is loading the HTML and CSS through a UIWebView rather than handling it all manually. UIWebView will attempt to load, parse, and render an HTML file into a UI component. In the process it will load referenced CSS and JavaScript files and apply them to its rendering. If you want to do anything special like intercept the calls it makes to load the CSS file(s), you can implement the UIWebViewDelegate protocol and set the delegate of the the UIWebView. Within that delegate you can implement the -webView:shouldStartLoadWithRequest:navigationType: method to be notified when the web view is loading the CSS file. You can use the call to that method to look at the request that is being issued for the CSS and do something else interesting with the request.
do you know the name of the .css file?
If so I would just make 2 requests otherwise you will have to write a parser to look for the link to the css and make a second request anyways.
I'd also suggest looking into a library to handle the downlading of stuff - lot's of great libraries that can do the heavy lifting for you with advanced features.
Here's 3 I have used:
http://blog.mugunthkumar.com/coding/ios-tutorial-advanced-networking-with-mknetworkkit/
https://github.com/tonymillion/TMHTTPRequest
https://github.com/pokeb/asi-http-request
Here is my actual issue, as some suggested I want to write a class for handling the multiple download progress in a UITableView. I have no idea how to write a class for this, can somebody help with some tips or ideas?
The group to look at is NSURLRequest and NSURLConnection. The former let's you specify the request (URL, http method, params, etc) and the latter runs it.
Since you want to update status as it goes (I answered a sketch of this in your other question, I think), you'll need to implement the NSURLConnectionDelegate protocol that hands over chunks of data as it arrives from the connection. If you know how much data to expect, you can use the amount received to calculate a downloadProgress float as I suggested earlier:
float downloadProgress = [responseData length] / bytesExpected;
Here's some nice looking example code in SO. You can extend for multiple connections like this...
MyLoader.m
#interface MyLoader ()
#property (strong, nonatomic) NSMutableDictionary *connections;
#end
#implementation MyLoader
#synthesize connections=_connections; // add a lazy initializer for this, not shown
// make it a singleton
+ (MyLoader *)sharedInstance {
#synchronized(self) {
if (!_sharedInstance) {
_sharedInstance = [[MyLoader alloc] init];
}
}
return _sharedInstance;
}
// you can add a friendlier one that builds the request given a URL, etc.
- (void)startConnectionWithRequest:(NSURLRequest *)request {
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
NSMutableData *responseData = [[NSMutableData alloc] init];
[self.connections setObject:responseData forKey:connection];
}
// now all the delegate methods can be of this form. just like the typical, except they begin with a lookup of the connection and it's associated state
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
NSMutableData *responseData = [self.connections objectForKey:connection];
[responseData appendData:data];
// to help you with the UI question you asked earlier, this is where
// you can announce that download progress is being made
NSNumber *bytesSoFar = [NSNumber numberWithInt:[responseData length]];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
[connection URL], #"url", bytesSoFar, #"bytesSoFar", nil];
[[NSNotificationCenter defaultCenter] postNotificationName:#"MyDownloaderDidRecieveData"
object:self userInfo:userInfo];
// the url should let you match this connection to the database object in
// your view controller. if not, you could pass that db object in when you
// start the connection, hang onto it (in the connections dictionary) and
// provide it in userInfo when you post progress
}
I wrote this library to do exactly that. You can checkout the implementation in the github repo.
I'm in the process of trying to move code from a UITableViewController class to a "helper" class.
The code utilizes NSURLConnection to grab and parse JSON and then populate an NSMutableArray.
What I'd like to do is call a method in my helper class that returns a NSMutableArray. What I don't understand is how to return the array from the connectionDidFinishLoading delegate class of NSURLConnection (where the array is actually built) as though it was from the originally called method that started the connection. In other words, how does the method that calls NSURLConnection get control back so it can return a value from the whole operation?
Here are the relevant methods from the helper class. How do I get the getMovies method to return the listOfMovies that is built in the connectionDidFinishLoading delegate class?
-(NSMutableArray)getMovies:(NSURL*)url {
responseData = [[NSMutableData data] retain];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//NSURLRequest* request = [NSURLRequest requestWithURL: url cachePolicy: NSURLRequestUseProtocolCachePolicy timeoutInterval: 30.0];
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[responseData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[responseData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
//TODO error handling for connection
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
//---initialize the array---
listOfMovies = [[NSMutableArray alloc] init];
tmdbMovies = [[NSArray alloc] init];
posters = [[NSArray alloc] init];
thumbs = [[NSDictionary alloc] init];
NSString *responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
SBJsonParser *json = [[SBJsonParser new] autorelease];
tmdbMovies = [json objectWithString:responseString];
// loop through all the top level elements in JSON
for (id movie in tmdbMovies) {
// 0 - Name
// 1 - Meta
// 2 - Url
if ((NSNull *)[movie objectForKey:#"name"] != [NSNull null]) {
if (![[movie objectForKey:#"name"] isEqualToString:#""]) {
name = [movie objectForKey:#"name"];
}
}
if ((NSNull *)[movie objectForKey:#"info"] != [NSNull null]) {
if (![[movie objectForKey:#"info"] isEqualToString:#""]) {
meta = [movie objectForKey:#"info"];
}
}
if ((NSNull *)[movie objectForKey:#"thumb"] != [NSNull null]) {
if (![[movie objectForKey:#"thumb"] isEqualToString:#""]) {
thumbUrl = [movie objectForKey:#"thumb"];
}
}
NSLog(#"Name: %#", name);
NSLog(#"Info: %#", meta);
NSLog(#"Thumb: %#", thumbUrl);
NSMutableArray *movieData = [[NSMutableArray alloc] initWithObjects:name,meta,thumbUrl,nil];
// add movieData array to listOfJMovies array
[listOfMovies addObject:movieData];
[movieData release];
}
//FIXME: Connection warning
if (connection!=nil) {
[connection release];
}
[responseData release];
[responseString release];
}
What you really need to do here is create a #protocol that creates a delegate for your helper class. Then change -(NSMutableArray)getMovies:(NSURL*)url to -(void)getMovies:(NSURL*)url
The class that is calling your helper method needs to implement your helper method's delegate.
Then - (void)connectionDidFinishLoading:(NSURLConnection *)connection calls the delegate method(s). It's best to have a one for success and one for failure.
=Update Begin=
You will need to also define an id delegate in your helper file which the calling class sets to self after init but before calling -(void)getMovies:(NSURL*)url. That way the helper file knows where to call back to.
getMovies *movieListCall = [[getMovies alloc] init];
movieListCall.delegate = self;
[movieListCall getMovies:<your NSURL goes here>];
You will see some additional lines for the inclusion of a delegate in both the getMovies.h and getMovies.m files.
=Update End=
in your getMovies.h file add:
#protocol getMoviesDelegate
#required
- (void)getMoviesSucceeded:(NSMutableArray *)movieArray;
- (void)getMoviesFailed:(NSString *)failedMessage;
#end
#interface getMovies : NSOBject {
id delegate;
}
#property (nonatomic, assign) id delegate;
in your getMovies.m file add:
#synthesize delegate;
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
//TODO error handling for connection
if ([delegate respondsToSelector:#selector(getMoviesFailed:)]) {
[delegate getMoviesFailed:[error localizedDescription]];
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
//finishes with
if ([delegate respondsToSelector:#selector(getMoviesSucceeded:)]) {
[delegate getMoviesSucceeded:listOfMovies];
}
}
update your calling class .h file to use getMoviesDelegate:
#interface MoviesView : UIViewController <getMoviesDelegate>{
.
.
.
}
add the getMoviesDelegate methods to your calling class .m file
- (void)getMoviesSucceeded:(NSMutableArray *)movieArray {
//deal with movieArray here
}
- (void)getMoviesFailed:(NSString *)failedMessage {
//deal with failure here
}
This is not tested but hopefully gives you a road map to work with.
Protocols are nice because you can make both required and optional delegate methods and it helps in refining your helper methods to become very reusable across projects. The compiler will also warn you if you have implemented a protocol but not implemented the protocol's required delegate methods. If you follow this path be sure to use conformsToProtocol: and respondsToSelector:
Fundamentally, what's happening is that you're starting an asynchronous network load (asynchronous is the right way to do this, almost assuredly), and then you need some way to resume whatever operation you were doing before the load began. You have a few options:
Create your own delegate protocol. Your UITableViewController would then set itself as the helper's delegate, and the helper would call helperDidLoad or whatever you named that method. There's more information on writing delegates in the Cocoa Programming Guide.
Use blocks and continuation passing style. This is a bit more advanced but I like it. In your UITableViewController you'd write something like this:
[helper doSomething:^ (id loaded) {
[modelObject refresh:loaded]; // or whatever you need to do
}];
And then in your helper you'd write:
- (void)doSomething:(void ^ (id))continuation {
_continuation = continuation;
//kick off network load
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
_continuation(_data);
}
Use notifications. Read the NSNotificationCenter docs.
Use KVO. The KVO programming guide has a lot of good info on Key-Value Observing.
How to i get the getMovies method to return the listOfMovies that is built in the connectionDidFinishLoading delegate class?
I'm going to argue that you should not do that.
Network requests should be made asynchronously. If your getMovies were to make a synchronous request and return only when it had data you would block that entire thread while you waiting for a network connection to finish. This is a bad idea in general and a terrible idea if your main thread is calling getMovies. Blocking the main thread will prevent you from responding to touches or updating the UI, your app will appear frozen, and the OS will terminate it if your users don't quit in frustration first.
Instead have the helper class notify the caller when data is available (or when it failed to retrieve data) through a delegate call back, notification, KVO, or whatever mechanism you prefer.
Here are the steps, pseudocode like style:
[helperInstance setDelegate:self]; // where self is your UITableViewController class
in your helper class, in the connectionDidFinishLoading do something like this:
[delegate finishedLoadingData:JSONData];
Also you can define a protocol for your delegate, and the declare the delegate like this in your helper class:
#property (nonatomic, assign) id<YourProtocol> delegate;
Hope this helps,
Moszi
I'm trying to download several images in response to a single http request. On the server side (java) I'm using oreilly multipart response and I'm getting my datas in my iPhone Simulator in didReceiveData (approximately one call for each image) after a call to didReceiveResponse (approximately one call for each image as well) in my delegate.
The problem is this approximately... Has anyone ever managed to handle correctly multipart/x-mixed-re with iPhone SDK ? If yes what is the best strategy here ? Should I play with the expected length ? on server side ? on client side ? should I wait until I've received everything... mmmh that doesn't even seen enough as the calls to didReceiveData happens in a random order (I'm asking picture1,picture2 and I'm sometimes receiving picture2,picture1 even though the order is respected on server side !). Should i temporize between pictures on server side ?
Or should I drop multipart/x-mixed-replace ? what would be the easiest then ?
That's a lot of questions but I'm really stuck here ! Thanks for you help !
I'm not sure what your final use for the images is, but the intended purpose of the multipart/x-midex-replace content type is for each received part to completely replace the previously received responses. Think of it like frames of a video; only one picture is displayed at a time and the previous ones are discarded.
Temporizing is almost never a foolproof solution. Especially on the iPhone you're going to encounter an unimaginable variety of network situations and relying on a magic number delay between frames will probably still fail some of the time.
Since you have control of the server, I'd recommend dropping the multipart. Make sure when you are sending multiple requests to the server that you don't block the main thread of your iPhone app. Use NSOperations or an alternative HTTP library (like ASIHTTPRequest) to make your image fetch operations asynchronous.
I did that successfully using this code. The important thing is to create 2 buffers to receive your data. If you use only one you will have some double access problems (stream access and jpg CODEC access) and corrupted JPG data.
Do not hesitate to ask me for more details.
- (IBAction)startDowload:(id)sender {
NSURL *url = [NSURL URLWithString:#"http://210.236.173.198/axis-cgi/mjpg/video.cgi?resolution=320x240&fps=5"];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
[req setHTTPMethod:#"GET"];
/*
I create 2 NSMutableData buffers. This points is very important.
I swap them each frame.
*/
receivedData = [[NSMutableData data] retain];
receivedData2 = [[NSMutableData data] retain];
currentData = receivedData;
urlCon = [[NSURLConnection alloc] initWithRequest:req delegate:self];
noImg = YES;
frame = 0;
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSHTTPURLResponse *)response
{
// this method is called when the server has determined that it
// has enough information to create the NSURLResponse
// it can be called multiple times, for example in the case of a
// redirect, so each time we reset the data.
// receivedData is declared as a method instance elsewhere
UIImage *_i;
#try
{
_i = [UIImage imageWithData:currentData];
}
#catch (NSException * e)
{
NSLog(#"%#",[e description]);
}
#finally
{
}
CGSize _s = [_i size];
[imgView setImage:_i];
[imgView setNeedsDisplay];
[[self view] setNeedsDisplay];
}
/*
Buffers swap
*/
if (currentData == receivedData)
{
currentData = receivedData2;
}
else
{
currentData = receivedData;
}
[currendData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// append the new data to the currentData (NSData buffer)
[currendData appendData:data];
}
Has anyone integrated an iPhone application with a Shibboleth Identity Provider? Googling did not come up with anything so I am asking the gurus directly.
If it has not been previously dones, is it feasible to do so?
The answer to both is "Yes."
I'm a Java guy, so being asked two weeks ago to:
Learn Objective-C
Write an native iPhone App
Authenticated programmatically with Shibboleth
Download an display Shibboleth protected datafile
...Was a little daunting. Compound that with the absence of any forum posts to help out has prompted me to share my experience.
Here's an overview followed by some hopefully very helpful sample code. Please vote for my answer if this helps! It worth a few weeks of my time :)
For an application on the iPhone to download Shibbolized resources, the following needs to happen:
Use the URL API's in Cocoa to submit the HTTP request for the resource in question.
Implement a delegate class for the request to:
Respond to the SP re-direct to the IdP (automatic courtesy of Cocoa)
Respond to server certificate trust challenges
Respond to user credential challenges
Respond to errors (if needed)
Receive IdP's "binding template" for the authenticated user, an HTML form which re-directs the user back to the SP with two parameters
Programmatically HTTP POST the two parameters from the IdP back to the SP.
Cookies are automatically stored and forwards courtesy of Cocoa again
Implement a second URL Request delegate to receive the originally request data.
Here are some useful references from Apple and Shibboleth:
http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html
https://spaces.internet2.edu/display/SHIB2/IdPSPLocalTestInstall
And hopefully I can include all the source for a quick demonstration.
ApplicationDelegate.h
----------
#import <UIKit/UIKit.h>
#import "ConsoleViewController.h"
/*
The application delegate will hold references to the application's UIWindow and a ConsoleViewController.
The console does all of the interesting Shibboleth activities.
*/
#interface ApplicationDelegate : NSObject <UIApplicationDelegate> {
UIWindow *window;
ConsoleViewController *consoleViewController;
}
#end
ApplicationDelegate.m
----------
#import "ApplicationDelegate.h"
#import "ConsoleViewController.h"
/*
The implementation for the ApplicationDelegate initializes the console view controller and assembles everything.
The console does all of the interesting Shibboleth activities.
*/
#implementation ApplicationDelegate
- (void)applicationDidFinishLaunching:(UIApplication *)application {
// Initialize the console.
consoleViewController = [[ConsoleViewController alloc] init];
window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
[window setBackgroundColor:[UIColor lightGrayColor]];
[window addSubview:[consoleViewController view]];
[window makeKeyAndVisible];
}
- (void)dealloc {
[window release];
[ConsoleViewController release];
[super dealloc];
}
#end
ConsoleController.h
----------
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
/*
The ConsoleViewController's interface declares references to the network data used in negotiating with Shibboleth
and a UITextView used to display the final result or errors.
*/
#interface ConsoleViewController : UIViewController {
NSMutableData *responseData;
NSString *responseString;
UITextView *console;
}
#end
ConsoleController.m
----------
#import "ApplicationDelegate.h"
#import "ConsoleViewController.h"
/*
This delegate is used when making the second HTTP request with Shibboleth. If you're just getting here, start
by reading the comments for ConsoleViewController below.
All we need to do now is receive the response from the SP and display it.
If all goes well, this should be the secured page originally requested.
*/
#interface AuthenticationRedirectDelegate : NSObject {
NSMutableData *authResponseData;
NSString *authResponseString;
UITextView *console;
}
#property (nonatomic retain) UITextView *console;
#end
/*
Refer to the comments for the interface above.
*/
#implementation AuthenticationRedirectDelegate
#synthesize console;
-(id)init {
authResponseData = [[NSMutableData alloc] retain];
return self;
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[authResponseData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[authResponseData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[console setText:[error localizedDescription]];
}
/*
Once the data is received from Shibboleth's SP, display it.
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
authResponseString = [[NSString alloc] initWithData:authResponseData encoding:NSUTF8StringEncoding];
[console setText:authResponseString];
[connection release];
}
#end
/*
The implementation of the ConsoleViewController, and AuthenticationRedirectDelegate above, contain the real logic of
this Shibboleth exercise. The ConsoleViewController performs the following:
1. Prepare the initial HTTP request to a Shibboleth protected resource.
2. Act as the delegate whilst Cocoa's URL Loading API receives the HTTP Response.
NOTE: We instruct Cocoa in advance to take care of the SP redirecting to the IdP, accepting the server certificate,
and submitting the user credentials
3. Once the HTTP Response is finished loading, parse the <form action, RelayState and SAMLResponse from the IdP's
response
4. Call a utility method to prepare a second HTTP POST Request to the <form action/SP with the IdP's parameters
NOTE: We do not need to transfer over any of Shibboleth's cookies, since Cocoa is doing this automatically
5. Use a new instance of AuthenticationRedirectDelegate to receive the POST's response, which should be the secured
page originally requested.
6. Display the final content in the UITextView known as console.
*/
#implementation ConsoleViewController
/*
A handy utility method for extracting a substring marked by two provided token strings.
Used in parsing the HTML form returned by the IdP after the first HTTP Request.
*/
+(id)substringFromString:(NSString *)source BetweenOpenToken:(NSString *)openToken AndCloseToken:(NSString *)closeToken {
NSUInteger l = [source length];
NSUInteger openTokenLen = [openToken length];
NSUInteger openTokenLoc = ([source rangeOfString:openToken]).location;
NSUInteger valueLoc = openTokenLoc + openTokenLen;
NSRange searchRange = NSMakeRange(valueLoc, l - valueLoc);
NSUInteger closeTokenLoc = ([source rangeOfString:closeToken options:NSCaseInsensitiveSearch range:searchRange]).location;
searchRange = NSMakeRange(valueLoc, closeTokenLoc - valueLoc);
NSString *result = [source substringWithRange:searchRange];
return result;
}
/*
This function takes the three properties returned by the IdP after the first HTTP request and
HTTP POSTs them to the SP as specified by the IdP in the "url" parameter.
*/
-(void)authReturnTo:(NSURL *)url WithRelay:(NSString *)relayState AndSAML:(NSString *)samlResponse {
// Here we assemble the HTTP POST body as usual.
NSString *preBody = [[NSString alloc] initWithString:#"RelayState="];
preBody = [preBody stringByAppendingString:relayState];
preBody = [preBody stringByAppendingString:#"&"];
preBody = [preBody stringByAppendingString:#"SAMLResponse="];
preBody = [preBody stringByAppendingString:samlResponse];
/* The SAMLResponse parameter contains characters (+) that the SP expects to be URL encoded.
Here we simply manually URL encode those characters. You may wish to harden this with proper
URL encoding for production use.
*/
NSString *httpBody = [preBody stringByReplacingOccurrencesOfString:#"+" withString:#"%2B"];
NSData *httpBodyData = [httpBody dataUsingEncoding:NSUTF8StringEncoding];
NSString *httpContentLength = [NSString stringWithFormat:#"%d", [httpBodyData length]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:12.0];
[request setHTTPMethod:#"POST"];
[request setValue:httpContentLength forHTTPHeaderField:#"Content-Length"];
[request setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:httpBodyData];
// Submit the HTTP POST using the second delegate class to receive the response
AuthenticationRedirectDelegate *delegate = [[AuthenticationRedirectDelegate alloc] init];
delegate.console=console;
[[NSURLConnection alloc] initWithRequest:request delegate:delegate];
}
/*
When this UIViewController finishes loading, automatically prepare and send a request to the Shibboleth SP Web Server
for a secured resource.
*/
- (void)viewDidLoad {
[super viewDidLoad];
console = [[UITextView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
[[self view] addSubview:console];
responseData = [[NSMutableData data] retain];
// TODO: Enter your own URL for a Shibboleth secured resource.
NSURL *url = [NSURL URLWithString:#"<URL>"];
NSURLRequest *request = [NSURLRequest requestWithURL:url
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:12.0];
[[NSURLConnection alloc] initWithRequest:request delegate:self];
/* Control flows to the delegate methods below */
}
/*
Refer to Apple's docs on the URL Loading System for details.
http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[responseData setLength:0];
}
/*
Refer to Apple's docs on the URL Loading System for details.
http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[responseData appendData:data];
}
/*
This implementation in the delegate let's Cocoa trust my SP Web Server's self-signed certificate.
TODO: You will want to harden this for production use.
Refer to Apple's docs on the URL Loading System for details.
http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html
*/
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust] || [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic];
}
/*
This implementation for the delegate does two things:
1. Respond to challenges for my server's self-signed certificate
2. Respond to the IdP's challenge for the username and password.
TODO: Enter your own username and password here.
Refer to Apple's docs on the URL Loading System for details.
http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html
*/
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
// TODO: Enter the correct username and password below.
/*
WARNING: Using an incorrect user name and password will result in your application being re-challenged
by the IdP. Cocoa will return to this function in a never-ending loop. This can result in the message
"NSPosixErrorDomain Too many open files". You'll need to perform additional coding to handle this.
*/
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
[challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
else if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic])
[challenge.sender useCredential:[NSURLCredential credentialWithUser:#"<USERNAME>" password:#"<PASSWORD>" persistence:NSURLCredentialPersistenceNone] forAuthenticationChallenge:challenge];
else
[challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}
/*
You may wish to add more code here to log errors.
Refer to Apple's docs on the URL Loading System for details.
http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html
*/
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[console setText:[error localizedDescription]];
}
/*
Once Cocoa has received a (hopefully) authenticated response from the IdP, we parse out the relevant pieces and prepare to
HTTP POST them back to the SP as specified by the IdP in the <form action attribute.
Refer to Apple's docs on the URL Loading System for details.
http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[connection release];
responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
if([responseString rangeOfString:#"SAMLResponse"].length < 1)
{
[console setText:[#"Unexpected response:\n]n" stringByAppendingString:responseString]];
return;
}
NSString *relayState = [ConsoleViewController substringFromString:responseString BetweenOpenToken:#"RelayState\" value=\"" AndCloseToken:#"\"/>"];
NSString *SAMLResponse = [ConsoleViewController substringFromString:responseString BetweenOpenToken:#"SAMLResponse\" value=\"" AndCloseToken:#"\"/>"];
NSString *formAction = [ConsoleViewController substringFromString:responseString BetweenOpenToken:#"<form action=\"" AndCloseToken:#"\""];
NSURL *formActionURL = [[NSURL alloc] initWithString:formAction];
[self authReturnTo:formActionURL WithRelay:relayState AndSAML:SAMLResponse];
}
#end
I managed to do just that, but it took me some time to understand every step of the process and to reproduce it perfectly. If I have time, I might write a detailed tutorial, because I didn't find any help for a lot of problems I got. The thing is, it also depends on the website you want to connect to, so yours maybe does not follow the same path as mine (its process is the same as the one described here).
To see every request fired by my browser (Chrome) to connect, I used the developer tools Network panel, with 'Preserve log' checked.
A few hints :
1°) You need to get "_idp_authn_lc_key..." cookie. There's a request that set it for you, find it.
2°) You need the login ticket (LT-...). You'll probably find it in the body of the page that asks you your credentials.
3°) You need a service ticket (ST-...). Again, you will find it in the page that the previous request returned.
4°) You need SAMLResponse. Again, you will find it in the page that the previous request returned.
5°) Finally, you can log in by sending back SAMLResponse to the service provider. You should take care of the encoding, here. I had a few '+' or '=' that I needed to change to '%2B' and '%3D'. You will be given a "_idp_session" cookie, that will allow you to reconnect without all this mess.
If someone tries to do the same, I'd be happy to help ! Just send me a message.
I successfully implemented using EC's solution as a starting point. The only other thing I'd add is that you really have to pay attention to keeping only one request going at a time. In our implementation the authentication process would get confused between multiple asynchronous requests running concurrently. Using NSOperation to throttle the queue seemed to work great for me.