I'm trying to create an single UIWebView-based application. Its main purpose is to provide authentication to a few websites using NTLM or Basic authentication, without the user having to input the username/password for every website when he/she changes his password (the websites share the user/password database).
In order to do that, I'm "overriding" the loading of page data for the UIWebView, so that I can intercept the HTTP response codes and ask the user for a password every time I detect a 401 Unauthorized response.
I've set up a NSURLConnectionDataDelegate to handle the requests as such:
#interface LPURLConnectionDelegate : NSObject<NSURLConnectionDataDelegate>
{
UIWebView* _webView;
NSMutableURLRequest* _request;
NSURLConnection* _connection;
NSString* _encoding;
NSString* _mimeType;
NSMutableData* _data;
}
- (LPURLConnectionDelegate*) initWithWebView:(UIWebView *)webView;
- (void) loadPageWithRequest: (NSURLRequest*)request;
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection;
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;
#end
#implementation LPURLConnectionDelegate
- (void) loadPageWithRequest: (NSURLRequest*)request
{
_request = [request mutableCopy];
if(!_data)
{
_data = [NSMutableData dataWithCapacity:0];
[_data retain];
}
[_data setLength:0];
if(_connection)
{
[_connection cancel];
[_connection release];
_connection = nil;
}
_connection = [[NSURLConnection alloc] initWithRequest:_request delegate:self startImmediately:YES];
}
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
LPAppDelegate* delegate = [LPAppDelegate getInstance];
NSURLCredential* credential = [NSURLCredential credentialWithUser:[delegate getUsername] password:[delegate getPassword] persistence:NSURLCredentialPersistenceForSession];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
//handle connection failed
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
NSLog(#"Connection did receive response");
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
_mimeType = [httpResponse MIMEType];
_encoding = [httpResponse textEncodingName];
//handle status - if the status is 401, the connection is canceled and the user is asked for his new password
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection
{
return YES;
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[_data appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[_webView loadData:_data MIMEType:_mimeType textEncodingName:_encoding baseURL:[_request URL]];
}
#end
My UIWebViewDelegate looks like this:
#interface LPWebViewDelegate : NSObject<UIWebViewDelegate>
- (void)webViewDidStartLoad:(UIWebView *)webView;
- (void)webViewDidFinishLoad:(UIWebView *)webView;
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error;
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
#end
#implementation LPWebViewDelegate
- (void) webViewDidStartLoad:(UIWebView *)webView
{
}
- (void) webViewDidFinishLoad:(UIWebView *)webView
{
//handle success
}
- (BOOL) webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
LPURLConnectionDelegate* connectionDelegate = [[LPURLConnectionDelegate alloc] initWithWebView:webView];
[connectionDelegate loadPageWithRequest:request];
return NO;
}
- (void) webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
//handle fail
}
#end
Basically, what I'm trying to do is tell the UIWebView not to load any page by itself, letting me handle the request to load the page. When the request completes and the user is authenticated, I'm trying to set the data using the UIWebView::loadData:MIMEType:textEncodingName:baseUrl method.
The problem here is that after setting the UIWebView's data from the NSURLConnectionDataDelegate, the UIWebView reloads the page set as baseURL (at least in the simulator - I haven't tried the code on an actual device just yet).
Does anyone know a way to set data to UIWebViews in such way that it doesn't reload the page(*)?
*) I know that I could just let the UIWebView load the page after I make the NSURLConnection to check for authentication (or in parallel), but I'm trying to avoid requesting the page header twice for each page.
Note: the code is not complete, I've extracted what I consider to be the most relevant parts for this issue.
Implement NSURLProtocol class. It essentially a abstract class that allows subclasses to define the URL loading behavior of HTTP schemes
Check this tutorial out here
Related
I want to log all the NSURLRequests which are initiated from within my app and response for those requests. I wrote a custom NSURLProtocol to achieve this. I was able to trap all the request but not the response.
In canInitWithRequest method I can log all the requests regardless of method returns YES/NO.
Right now I am returning YES in hope the of actual response.
In -startLoading method I am supposed to inform NSURLProtocolClient with response and progress. I am not interested in modifying/creating my own response instead I am interested in actual response and want to log it. When and where would I find actual response for the request?
I am not interested in modifying URL loading behavior.
Am I on the right track with custom protocol or is there something else I need to do to log all the requests and responses?
#implementation TestURLProtocol
+(BOOL)canInitWithRequest:(NSURLRequest *)request
{
//here i can log all the requests
NSLog(#"TestURLProtocol : canInitWithRequest");
return YES;
}
+(NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}
-(void)startLoading
{
// I don't want to create or modify response.
//I have nothing todo with response instead I need actual response for logging purpose.
NSURLResponse * response = [[NSURLResponse alloc] initWithURL:[self.request URL] MIMEType:#"text/html" expectedContentLength:-1 textEncodingName:#"utf8"];
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[[self client] URLProtocolDidFinishLoading:self];
}
-(void)stopLoading
{
}
#end
Well, I dropped the idea of custom NSURLProtocol. I wrote a class which confirms to NSURLConnectionDelegate, NSURLConnectionDataDelegate etc as per your need. This class serves as a common delegate to all NSURLConnection instances. It has a property of type id(depend upon requirements), this property holds the object which is creating NSURLConnection and interested in delegate callbacks as well.
URLRequestLogger instance serves as a common delegate to all NSURLConnection instances.
Each NSURLConnection has a URLRequestLogger instance as a delegate.
#import <Foundation/Foundation.h>
#interface URLRequestLogger : NSObject<NSURLConnectionDataDelegate>
-(id)initWithConnectionDelegate:(id<NSURLConnectionDataDelegate>)connectionDelegate;
#end
implementation file
#import "URLRequestLogger.h"
#interface URLRequestLogger ()
#property(nonatomic, assign) id<NSURLConnectionDataDelegate> connectionDelegate;
#end
#implementation URLRequestLogger
-(id)initWithConnectionDelegate:(id<NSURLConnectionDataDelegate>)connectionDelegate
{
self = [super init];
if (self)
{
_connectionDelegate = connectionDelegate;
}
return self;
}
//one can implement all the delegates related to NSURLConnection as per need
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
//log reponse info
if ([response isKindOfClass:[NSHTTPURLResponse class]])
{
NSHTTPURLResponse * httpResponse = (NSHTTPURLResponse *)response;
NSDictionary * responseHeaders = [httpResponse allHeaderFields];
NSInteger statusCode = [httpResponse statusCode];
//save as many info as we can
}
//if connectionDelegate is interested in it, inform it
if ([self.connectionDelegate respondsToSelector:#selector(connection:didReceiveResponse:)])
{
[self.connectionDelegate connection:connection didReceiveResponse:response];
}
}
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
//log the error and other info
//if connectionDelegate is interested in it, inform it
if ([self.connectionDelegate respondsToSelector:#selector(connection:didFailWithError:)])
{
[self.connectionDelegate connection:connection didFailWithError:error];
}
}
-(void)connectionDidFinishLoading:(NSURLConnection *)connection
{
//log the connection finish info
//response time and download time etc
//if connectionDelegate is interested in it, inform it
if ([self.connectionDelegate respondsToSelector:#selector(connectionDidFinishLoading:)])
{
[self.connectionDelegate connectionDidFinishLoading:connection];
}
}
#end
MyViewController creates an NSURLConnection and is interested in delegate methods as well. At the same time we want to log all the details about request and response.
#import <UIKit/UIKit.h>
#interface MyViewController : UIViewController<NSURLConnectionDataDelegate>
#end
implementation file
#import "MyViewController.h"
#import "URLRequestLogger.h"
#implementation MyViewController
//MyViewController class creates a request and interested in connection delegate callbacks
//MyViewController confirms to NSURLConnectionDelegate.
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
[self sendRequest];
}
-(void)sendRequest
{
/*
connection delegate here would be our URLRequestLogger class instance which holds MyViewController instance
to return the callbacks here after logging operations are finished
*/
URLRequestLogger * requestLogger = [[URLRequestLogger alloc] initWithConnectionDelegate:self];
[NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.example.org"]] delegate:requestLogger];
}
#pragma mark - NSURLConnection delegate methods
-(void)connectionDidFinishLoading:(NSURLConnection *)connection
{
//this callback is received from URLRequestLogger after logging operation is completed
//do something. Update UI etc...
}
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
//this callback is received from URLRequestLogger after logging operation is completed
//do something. Update UI etc...
}
#end
Better solution with custom NSURLProtocol:
One can log all outgoing URL request and alter request properties, response and do other things like loading encrypted html files from disk into webview etc.
.h file
#import <Foundation/Foundation.h>
#interface URLRequestLoggerProtocol : NSURLProtocol
#end
.m file
#import "URLRequestLoggerProtocol.h"
#interface URLRequestLoggerProtocol ()<NSURLConnectionDelegate,NSURLConnectionDataDelegate>
#property(nonatomic, strong) NSURLConnection * connection;
#end
#implementation URLRequestLoggerProtocol
#synthesize connection;
+(BOOL)canInitWithRequest:(NSURLRequest *)request
{
//logging only HTTP and HTTPS requests which are not being logged
NSString * urlScheme = request.URL.scheme;
if (([urlScheme isEqualToString:#"http"] || [urlScheme isEqualToString:#"https"]) && ![[NSURLProtocol propertyForKey:#"isBeingLogged" inRequest:request] boolValue])
{
return YES;
}
return NO;
}
+(NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}
-(void)startLoading
{
NSMutableURLRequest * newRequest = [self.request mutableCopy];
[NSURLProtocol setProperty:[NSNumber numberWithBool:YES] forKey:#"isBeingLogged" inRequest:newRequest];
self.connection = [NSURLConnection connectionWithRequest:newRequest delegate:self];
}
-(void)stopLoading
{
[self.connection cancel];
}
#pragma mark - NSURLConnectionDelegate methods
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
//log all things along with error and finally inform URL client that you are done
[self.client URLProtocol:self didFailWithError:error];
}
#pragma mark - NSURLConnectionDataDelegate methods
-(NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response
{
//start logging a request properties from here
return request;
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
//log the response and other things and inform client that you are done.
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
//log the data and other things and inform client that you are done.
[self.client URLProtocol:self didLoadData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
//log the finish info and other things and inform client that you are done.
[self.client URLProtocolDidFinishLoading:self];
}
#end
I subclassed a NSURLConnection in order to call web services using ssl.
I thought about doing it synchronous and not only asynchronous connection as it's now.
My code:
#import "SecureSSLConnection.h"
static NSMutableArray *sharedConnectionList = nil;
#implementation SecureSSLConnection
#synthesize request, completionBlock, internalConnection;
- (id)initWithRequest:(NSMutableURLRequest *)req
{
self = [super init];
if (self) {
self.request = req;
}
return self;
}
- (void)start
{
container = [NSMutableData new];
internalConnection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:YES];
if (!sharedConnectionList) {
sharedConnectionList = [NSMutableArray new];
}
[sharedConnectionList addObject:self];
}
#pragma mark - NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[container appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
if (self.completionBlock) {
self.completionBlock(container, nil);
}
[sharedConnectionList removeObject:self];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
if (self.completionBlock) {
self.completionBlock(nil, error);
}
[sharedConnectionList removeObject:self];
}
#pragma mark - SSL Connection Addition
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
NSLog(#"challenge.protectionSpace.host: %#", challenge.protectionSpace.host);
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
// We only trust our own domain
if ([challenge.protectionSpace.host isEqualToString:WebSiteURL]) {
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
[challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
}
}
[challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}
I started thinking about implementing:
+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error
In order to keep it the same as original delegate.
One way is using NSNotificationCenter but my goal is much easier method for it.
Any ideas how should I doing it?
I would not suggest making this synchronous. You invariably want to keep your network requests asynchronous.
The simple solution is to employ your completion block property. Simply put other tasks that are dependent upon your initial SSL request inside the completion block of the first request. That way, they won't start until the first completes.
The more elegant solution is to take this a step further, and wrap your SSL request in a concurrent NSOperation subclass. That way, you can employ not only the above completion block pattern, but you can use the addDependency of subsequent operations and maxConcurrentOperationCount of the NSOperationQueue to really refine the dynamics between various operations. This is non-trivial, but you can see AFNetworking for an example of a relatively well thought-out implementation of this pattern.
I am trying to download a JSON with NSURLConnection, but unless I force the app to pause some seconds the data I get isn't complete. It is always around 2600 bytes and my response should be around 70000.
Any clue why is this happening?
Thank You
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
_responseData = [[NSMutableData alloc] init];
//sleep(10);
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[_responseData appendData:data];
[self getDataJSON: _responseData];
}
didReceiveData is called a lot of times, while it is receiving data
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[_responseData appendData:data];
}
You have to wait until it finish receiving data
- (void)connectionDidFinishLoading:(NSURLConnection*)connection
{
[self getDataJSON: _responseData];
}
You can get complete data when connection is finished. NSURLConnection finishes when it's connectionDidFinishLoading: delegate method is called. Try [self getDataJSON: _responseData]; in that method. Good Luck!
I'm building an iPhone app that aggregates data from several different data sources and displays them together in a single table. For each data source, I've created a class (WeatherProvider, TwitterProvider) that handles connecting to the datasource, download the data and storing it in an object.
I have another class ConnectionManager, that instantiates and calls each of the two provider classes, and merges the results into a single NSArray which is displayed in the table.
When I run the program calling just the WeatherProvider or just the TwitterProvider, it works perfectly. I can call each of those objects again and again to keep them up-to-date without a problem. I can also call TwitterProvider and then WeatherProvider without a problem.
However, if I call WeatherProvider then TwitterProvider, the TwitterProvider hangs just as I call: [[NSURLConnection alloc] initWithRequest:request delegate:self];.
Other points:
- it doesn't seem to matter how long between when I call WeatherProvider and TwitterProvider, TwitterProvider still hangs.
- in WeatherProvider, I'm using NSXMLParser with NSAutoreleasePool to parse the output from the WeatherProvider.
- ConnectionManager creates an instance of WeatherProvider and TwitterProvider when the application is started, and reuses those instances when the user requests a data refresh.
- I ran the app with the activity monitor connected and it verifies that the app is basically just hanging. No CPU usage, or additional memory allocations or network activity seems to be happening.
There's a bunch of code spread across a couple files, so I've tried to include the relevant bits (as far as I can tell!) here. I very much appreciate any help you can provide, even if it is just additional approaches to debugging.
WeatherProvider
-(void)getCurrentWeather: (NSString*)lat lon:(NSString*)lon lastUpdate:(double)lastUpdate
{
double now = [[NSDate date] timeIntervalSince1970];
NSString *noaaApiUrl;
// don't update if current forecast is < than 1 hour old
if(now - lastUpdate < 3600)
{
[[self delegate] weatherUpdaterComplete:1];
return;
}
// if we already have a forecast, delete and refill it.
if(forecast)
{
[forecast release];
forecast = [[WeatherForecast alloc] init];
}
forecast.clickThroughUrl = [NSString stringWithFormat:#"http://forecast.weather.gov/MapClick.php?lat=%#&lon=%#",
lat, lon];
noaaApiUrl = [NSString stringWithFormat:#"http://www.weather.gov/forecasts/xml/sample_products/browser_interface/ndfdBrowserClientByDay.php?lat=%#&lon=%#&format=24+hourly",
lat, lon];
NSURLRequest *noaaUrlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:noaaApiUrl]];
[[NSURLConnection alloc] initWithRequest:noaaUrlRequest delegate:self];
}
#pragma mark -
#pragma mark NSURLConnection delegate methods
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
self.noaaData = [NSMutableData data];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[noaaData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
self.noaaConnection = nil;
[[self delegate] weatherUpdaterError:error];
[connection release];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
// Spawn a thread to fetch the data so UI isn't blocked while we parse
// the data. IMPORTANT - don't access UIKit objects on 2ndary threads
[NSThread detachNewThreadSelector:#selector(parseNoaaData:) toTarget:self withObject:noaaData];
// the noaaData will be retailed by the thread until parseNoaaData: has finished executing
// so, we'll no longer need a reference to it in the main thread.
self.noaaData = nil;
[connection release];
}
#pragma mark -
#pragma mark NSXMLParser delegate methods
- (void)parseNoaaData:(NSData *)data
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
[parser setDelegate:self];
[parser parse];
[parser release];
[pool release];
}
TwitterProvider
-(void)getLocationTimeline:(NSString*)lat lon:(NSString*)lon lastUpdate:(double)lastUpdate refreshUrl:(NSString*)newUrl
{
NSString *updateURL;
if(tweets.count > 1)
[tweets removeAllObjects];
updateURL = [NSString stringWithFormat:#"http://search.twitter.com/search.json?geocode=%#,%#,1mi&rpp=%#&page=1", lat, lon, TWITTER_LOCAL_UPDATES_URL_RESULTS];
_serviceResponse = [[NSMutableData data] retain];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:updateURL]];
[[NSURLConnection alloc] initWithRequest:request delegate:self];
}
#pragma mark -
#pragma mark NSURLConnection delegate methods
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSString *responseString = [[NSString alloc] initWithData:_serviceResponse encoding:NSUTF8StringEncoding];
// parse tweets
[responseString release];
[_serviceResponse release];
[connection release];
// tell our delegate that we're done!
[[self delegate] twitterUpdaterComplete:tweets.count];
}
- (void)connection:(NSURLConnection *)connection
didReceiveResponse:(NSURLResponse *)response
{
[_serviceResponse setLength:0];
}
- (void)connection:(NSURLConnection *)connection
didReceiveData:(NSData *)data
{
[_serviceResponse appendData:data];
}
- (void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error
{
[connection release];
[[self delegate] twitterUpdaterError:error];
}
I was able to fix this by removing the threading in WeatherUpdater that was being used for the NSXMLParser. I had 'barrowed' that code from Apple's SiesmicXML sample app.
The SKProductsRequestDelegate has one single method:
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
Usually, I find these sorts of delegates will have several methods for handling multiple cases, rather than just success. For example:
-(void) connection:(NSURLConnection *)connection didFailWithError:(NSError *) error
How can I check if this code fails for some reason, eg. the user is offline?
SKProductsRequest *productsRequest = [[SKProductsRequest alloc] ... ];
productsRequest.delegate = self;
[productsRequest start];
SKProductsRequestDelegate conforms to the SKRequestDelegate protocol.
There you find
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
- (void)requestDidFinish:(SKRequest *)request