I am currently serving videos in my iOS application with MPMoviePlayerController. The files are streamed from our backend server that requires authentication. It is key-based authenticated set in the Authorization HTTP Header.
It used to work perfectly with single video files. Now we’re trying to implement HLS adaptive streaming and we have faced a wall. I am currently using a custom NSURLProtocol subclass to catch requests made to our backend server and inject the proper Authorization header. For HLS it simply doesn’t work.
When we looked at the server logs, we clearly saw that the first request to the m3u8 file worked fine. Then all subsequent calls made (other m3u8 files and ts also) are 403 forbidden. It seems that MPMoviePlayerController doesn’t use NSURLProtocol for the other files. (side note: It does work on the simulator thought, but not on a physical device which let me think that both are not implemented in the same way).
MPMoviePlayerController instantiation
self.videoController = [[MPMoviePlayerController alloc] initWithContentURL:video.videoURL];
The URL Protocol interception
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *newRequest = request.mutableCopy;
[newRequest setValue:#"HIDDEN" forHTTPHeaderField:#"Authorization"];
return newRequest;
}
Any Ideas, suggestions, work arounds?
After verification with Apple Developer Technical Support, I figured what I wanted to achieve is impossible (and unsupported).
Here's the quote from the reply :
The problem you're seeing with NSURLProtocol and so on is that the movie playback subsystem does not run its HTTP requests within your process. Rather, these requests are run from within a separate system process, mediaserverd. Thus, all your efforts to affect the behaviour of that playback are futile.
By using NSURLProtocol, you can intercept the communication between MPMoviePlayerController and the streamed requests. To inject cookies along the way, or possibly save the stream offline videos. To do this, you should to create a new class extending NSURLProtocol:
Hope this helps you:
GAURLProtocol.h
#import <Foundation/Foundation.h>
#interface GAURLProtocol : NSURLProtocol
+ (void) register;
+ (void) injectURL:(NSString*) urlString cookie:(NSString*)cookie;
#end
GAURLProtocol.m
#import "GAURLProtocol.h"
#interface GAURLProtocol() <NSURLConnectionDelegate> {
NSMutableURLRequest* myRequest;
NSURLConnection * connection;
}
#end
static NSString* injectedURL = nil;
static NSString* myCookie = nil;
#implementation GAURLProtocol
+ (void) register
{
[NSURLProtocol registerClass:[self class]];
}
// public static function to call when injecting a cookie
+ (void) injectURL:(NSString*) urlString cookie:(NSString*)cookie
{
injectedURL = urlString;
myCookie = cookie;
}
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
if([[[request allHTTPHeaderFields] objectForKey:#"Heeehey"] isEqualToString:#"Huuu"])
{
return NO;
}
return [[[request URL] absoluteString] isEqualToString:injectedURL];
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
// intercept the request and handle it yourself
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client {
if (self = [super initWithRequest:request cachedResponse:cachedResponse client:client]) {
myRequest = request.mutableCopy;
[myRequest setValue:#"Huuu" forHTTPHeaderField:#"Heeehey"]; // add your own signature to the request
}
return self;
}
// load the request
- (void)startLoading {
// inject your cookie
[myRequest setValue:myCookie forHTTPHeaderField:#"Cookie"];
connection = [[NSURLConnection alloc] initWithRequest:myRequest delegate:self];
}
// overload didReceive data
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[[self client] URLProtocol:self didLoadData:data];
}
// overload didReceiveResponse
- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse *)response {
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:[myRequest cachePolicy]];
}
// overload didFinishLoading
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[[self client] URLProtocolDidFinishLoading:self];
}
// overload didFail
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[[self client] URLProtocol:self didFailWithError:error];
}
// handle load cancelation
- (void)stopLoading {
[connection cancel];
}
#end
Register
// register protocol
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[NSURLProtocol registerClass:[GAURLProtocol class]];
return YES;
}
Usage
[GAURLProtocol injectURL:#"http://example.com/video.mp4" cookie:#"cookie=f23r3121"];
MPMoviePlayerController * moviePlayer = [[MPMoviePlayerController alloc]initWithContentURL:#"http://example.com/video.mp4"];
[moviePlayer play];
#Marc-Alexandre Bérubé I can think of below workaround:
Run a proxy server in your app to proxy all the video URL's. Download all the video content by injecting the necessary auth headers to the request and relay back the content via the proxy server to the media player to render it. This approach may not work for large videos as the video rendering would only start after entire video is downloaded.
Related
I'm trying to use delegate methods from NMSSH library in iOS but could not get it working. Let's take an example.
CustomViewController.h
#import <UIKit/UIKit.h>
#import <NMSSH/NMSSH.h>
#interface CustomViewController : UIViewController<NMSSHSessionDelegate, NMSSHChannelDelegate>
- (IBAction)connectButton:(UIButton *)sender;
#end
CustomViewController.m
#import "CustomViewController.h"
#implementation CustomViewController
-(void)viewDidLoad{
[super viewDidLoad];
}
- (IBAction)connectButton:(UIButton *)sender {
[self serverConnect:#"10.0.0.1"];
}
-(void)serverConnect:(NSString *)address{
NMSSHSession *session = [NMSSHSession connectToHost:address withUsername:#"username"];
NMSSHChannel *myChannel = [[NMSSHChannel alloc]init];
if (session.isConnected) {
[session authenticateByPassword:#"password"];
if (session.isAuthorized) {
NSLog(#"Authentication succeeded");
[session setDelegate:self];
[myChannel setDelegate:self];
}
}
NSError *error = nil;
//session.channel.requestPty = YES; (tried and later ignored)
NSString *response = [session.channel execute:#"mkdir" error:&error];
NSLog(#"Response from device: %#", response);
}
- (void)session:(NMSSHSession *)session didDisconnectWithError:(NSError *)error{
NSLog(#"log if session disconnects...Delegate method");
}
- (void)channel:(NMSSHChannel *)channel didReadError:(NSString *)error{
NSLog(#"Error received...Delegate method");
}
- (void)channel:(NMSSHChannel *)channel didReadRawData:(NSData *)data{
NSLog(#"Read Raw Data...Delegate method");
}
Connection to the server, sending a single line command and acknowledgement back from the server in Console is OK.
I have decent idea how to pass values from one View Controller to another using delegate (went through few tutorials with practical implementation).
With the same knowledge I am attempting to get response from delegate methods parts of NMSSH library but it's driving me round and round. I've found http://cocoadocs.org/docsets/NMSSH/2.2.1/ pretty nice API of this library but with my limited knowledge of iOS, I'm bit stuck.
Please help me.
My search finally came to an end with NMSSH AsyncAPI (branch) which supports multithreading.
When the user presses a button, I need to know whether the device is connected to the internet at that very instant--not whether he was connected 3 seconds ago. The reachability (tonymillion) notifier takes about that long to update after there is a change in network reachability.
I thought that I would be able to check for actual access in real time using the following methods:
if (!([[Reachability reachabilityWithHostname:#"www.google.com"] currentReachabilityStatus] == NotReachable)) NSLog(#"reachable");
if ([[Reachability reachabilityWithHostname:#"www.google.com"] currentReachabilityStatus] == NotReachable) NSLog(#"not reachable");
But results indicated that in fact currentReachabilityStatus does not check for internet access; it only checks the same flag that is updated with ~3 seconds' delay.
What's an efficient way of actually checking for network access on the spot?
As you wished in the comments above here is a solution using a "HEAD" request.
Make your class conforming to the
NSURLConnectionDelegate.
Implement the connection:didReceiveResponse: delegate method
Optionally implement the connection:didFailWithError: delegate method
So your setup could look like this:
YourClass.m
#interface YourClass () <NSURLConnectionDelegate>
#property (strong, nonatomic) NSURLConnection *headerConnection;
#end
#implementation YourClass
- (void)viewDidLoad {
// You can do this in whatever method you want
NSMutableURLRequest *headerRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"http://www.google.com"] cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:10.0];
headerRequest.HTTPMethod = #"HEAD";
self.headerConnection = [[NSURLConnection alloc] initWithRequest:headerRequest delegate:self];
}
#pragma mark - NSURLConnectionDelegate Methods
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
if (connection == self.headerConnection) {
// Handle the case that you have Internet; if you receive a response you are definitely connected to the Internet
}
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
// Note: Check the error using `error.localizedDescription` for getting the reason of failing
NSLog(#"Failed: %#", error.localizedDescription);
}
Have you tried putting an observer on the reachability status?
The Reachabilty extension (NPReachability) that I used to use, allows KVO on the status.
I have an NSURLConnection in a tableview cell subclass that can download most files. I noticed, however, that some fail to start downloading, and time out. An example would be this URL, which is just a test zip file that downloads fine in any other browser. Heres my code for the download
-(void)downloadFileAtURL:(NSURL *)url{
self.downloadedData = [[NSMutableData alloc] init];
self.url = url;
conn = [[NSURLConnection alloc] initWithRequest:[NSURLRequest requestWithURL:self.url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:1200.0] delegate:self startImmediately:YES];
}
- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSHTTPURLResponse*)response
{
int statusCode = [response statusCode];
if (statusCode == 200){
self.fileName.text = response.URL.lastPathComponent;
self.respo = response;
expectedLength = [response expectedContentLength];
}
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
[self.downloadedData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
CFStringRef mimeType = (__bridge CFStringRef)[_respo MIMEType];
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, NULL);
CFStringRef extension = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension);
NSString *fileName = [NSString stringWithFormat:#"%#.%#", [[_respo suggestedFilename] stringByDeletingPathExtension], (__bridge NSString *)extension];
[[NSFileManager defaultManager] createFileAtPath:[[self docsDir] stringByAppendingPathComponent:[NSString stringWithFormat:#"Downloads/%#", fileName]] contents:_downloadedData attributes:nil];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
NSLog(#"Download failed with error: %#", error);
}
Anybody see anything that might cause this?
Heres the error:
Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo=0x1fd2c650
{NSErrorFailingURLStringKey=http://download.thinkbroadband.com/10MB.zip,
NSErrorFailingURLKey=http://download.thinkbroadband.com/10MB.zip,
NSLocalizedDescription=The request timed out., NSUnderlyingError=0x1fdc90b0 "The request timed out."}
"I have an NSURLConnection in a tableview cell subclass " - never do this. As Sung-Pil Lim already pointed out correctly, TableView Cells will be reused which may cause this issue.
Anyway, the response data of your connection is a property of the model. The model might encapsulate how it gets to this data. If that data is not immediately available once it will be accessed, it should provide a "placeholder" value instead and start an asynchronous task which retrieves this data.
Suppose a model's property, an image, will be accessed by the view controller in order to be displayed by a view. The model has not yet loaded its actual image - and thus it returns a "placeholder image" in order to let the view display something. But at the same time the model is starting an asynchronous task to load the image. When this connection is finished loading with the data, the model updates internally its property - thereby replacing the placeholder with the real image. The update of the property should be performed on the main thread - since the UIKit views may access the same property as well.
During initialization, the View Controller has registered as an observer of the model's property (see KVO). When the model's property is updated, the controller gets notified. The View Controller then performs appropriate actions so that the view will be redrawn and displays the new updated value.
Your model should have a "cancel" method, which will be send to the model from the controller when the actual value of the model's property is not required anymore. For example, the user switched to another view (see viewWillDisappear).
I tried your codes.
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
[self.downloadedData appendData:data];
NSLog(#"%d", data.length);
}
2013-05-04 01:51:13.811 SomethingTodo[2732:c07] 1124
2013-05-04 01:51:13.856 SomethingTodo[2732:c07] 1448
2013-05-04 01:51:14.075 SomethingTodo[2732:c07] 1448
2013-05-04 01:51:17.180 SomethingTodo[2732:c07] 1448
2013-05-04 01:51:17.295 SomethingTodo[2732:c07] 1448
It's working... on ViewController
'request timeout error' was brought to network connection. or...
Are you resuing UITableViewCell? If you initialize for cell reuse codes deal with connection. maybe bring to trouble. Just i thought.
If you attach more your codes. Could I help you more then this.
I would start with a clean slate and just use basic code to work the download. Load in lots of NSLog(s) to track everything. If that works, keep adding your custom code and see if you stumble across an error. I suggest basic NSURLConnection code:
-(void)startDownloading:(NSString *)URLaddress{
NSLog(#"start downloading from: %#",URLaddress);
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:[URLaddress stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
__unused NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self startImmediately:YES];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
NSLog(#"didReceiveResponse: %#", response);
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
NSLog(#"didReceiveData");
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
NSLog(#"Connection failed! Error - %# %#",[error localizedDescription], [[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
NSLog(#"connectionDidFinishLoading");
}
try with HCDownloadViewController and you can check which url is not downloaded. and next time sync for that particular url which is not downloaded.
.h file
#import "HCDownloadViewController.h"
#interface HomeViewController_iPhone : UIViewController<HCDownloadViewControllerDelegate>
{
HCDownloadViewController *tblDownloadHairStyle;
}
#property (nonatomic,retain) HCDownloadViewController *tblDownloadHairStyle;
.m file
#define kAppDirectoryPath NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)
#synthesize tblDownloadHairStyle
- (void)viewDidLoad
{
[super viewDidLoad];
tblDownloadHairStyle=[[HCDownloadViewController alloc] init];
tblDownloadHairStyle.delegate=self;
}
- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSHTTPURLResponse*)response
{
[self createDocumentDirectory:#"Downloaded_HairStyle"];
NSString *pathHair=[self getDocumentDirectoryPath:#"Downloaded_HairStyle"];
tblDownloadHairStyle.downloadDirectory = pathHair;
////You can put url in for loop, it create queue for downloading.
[tblDownloadHairStyle downloadURL:[NSURL URLWithString:#"yourUrl"] userInfo:YourResponseDictonary];
}
-(void)createDocumentDirectory:(NSString*)pStrDirectoryName
{
NSString *dataPath = [self getDocumentDirectoryPath:pStrDirectoryName];
if (![[NSFileManager defaultManager] fileExistsAtPath:dataPath])
[[NSFileManager defaultManager] createDirectoryAtPath:dataPath withIntermediateDirectories:NO attributes:nil error:NULL];
}
-(NSString*)getDocumentDirectoryPath:(NSString*)pStrPathName
{
NSString *strPath = #"";
if(pStrPathName)
strPath = [[kAppDirectoryPath objectAtIndex:0] stringByAppendingPathComponent:pStrPathName];
return strPath;
}
#pragma mark-
#pragma mark-HCDownloadViewController Delegate Method
- (void)downloadController:(HCDownloadViewController *)vc startedDownloadingURL:(NSURL *)url userInfo:(NSDictionary *)userInfo {
}
- (void)downloadController:(HCDownloadViewController *)vc finishedDownloadingURL:(NSURL *)url toFile:(NSString *)fileName userInfo:(NSDictionary *)userInfo {
if (vc==tblDownloadHairStyle) {
if ([tblDownloadHairStyle numberOfDownloads]==0) {
NSLog(#"AllDownLoad are complete");
}
}
}
- (void)downloadController:(HCDownloadViewController *)vc failedDownloadingURL:(NSURL *)url withError:(NSError *)error userInfo:(NSDictionary *)userInfo {
NSLog(#"failedDownloadingURL=%#",url);
}
https://github.com/H2CO3/HCDownload
accept any response with http response code range 200-299 and disable caching on the http-connector.
double check your url address conforms to RFC 2396. so it must include HTTP://
Do you have any libraries (TestFlight, UA, etc) in the project? Try removing them and re-test. We had an app that used NSUrlConnection with TestFlight SDK that caused all sorts of sporadic network problems.
NSURLConnection timing out
ASIHTTPRequest request times out
https://github.com/AFNetworking/AFNetworking/issues/307
In my first ViewController (MonitorViewController) this is in the interface file MonitorViewController.h:
#import <RestKit/RestKit.h>
#interface MonitorViewController : UIViewController <RKRequestDelegate>
In MonitorViewController.m ViewDidLoad method, I have this at the end:
RKClient* client = [RKClient clientWithBaseURL:#"http://192.168.2.3:8000/DataRecorder/ExternalControl"];
NSLog(#"I am your RKClient singleton : %#", [RKClient sharedClient]);
[client get:#"/json/get_Signals" delegate:self];
The implementation of delegate methods in MonitorViewController.m:
- (void) request: (RKRequest *) request didLoadResponse: (RKResponse *) response {
if ([request isGET]) {
NSLog (#"Retrieved : %#", [response bodyAsString]);
}
}
- (void) request:(RKRequest *)request didFailLoadWithError:(NSError *)error
{
NSLog (#"Retrieved an error");
}
- (void) requestDidTimeout:(RKRequest *)request
{
NSLog(#"Did receive timeout");
}
- (void) request:(RKRequest *)request didReceivedData:(NSInteger)bytesReceived totalBytesReceived:(NSInteger)totalBytesReceived totalBytesExectedToReceive:(NSInteger)totalBytesExpectedToReceive
{
NSLog(#"Did receive data");
}
My AppDelegate method DidFinishLaunchingWithOptions method only returns YES and nothing else.
I recommend using RestKit framework. With restkit, you simply do:
// create the parameters dictionary for the params that you want to send with the request
NSDictionary* paramsDictionary = [NSDictionary dictionaryWithObjectsAndKeys: #"00003",#"SignalId", nil];
// send your request
RKRequest* req = [client post:#"your/resource/path" params:paramsDictionary delegate:self];
// set the userData property, it can be any object
[req setUserData:#"SignalId = 00003"];
And then, in the delegate method:
- (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)response {
// check which request is responsible for the response
// to achieve this, you can do two things
// check the parameters of the request like this
NSLog(#"%#", [request URL]); // this will print your request url with the parameters
// something like http://myamazingrestservice.org/resource/path?SignalId=00003
// the second option will work if your request is not a GET request
NSLog(#"%#", request.params); // this will print paramsDictionary
// or you can get it from userData if you decide to go this way
NSString* myData = [request userData];
NSLog(#"%#", myData); // this will log "SignalId = 00003" in the debugger console
}
So you will never need to send the parameters that are not used on the server side, just to distinguish your requests. Additionally, the RKRequest class has lots of other properties that you can use to check which request corresponds to the given response. But if you send a bunch of identical requests, I think the userData is the best solution.
RestKit will also help you with other common rest interface tasks.
I would like to support HTTP Basic Authentication in my UIWebView.
At the moment, I am canceling requests in
webView:shouldStartLoadWithRequest:navigationType: then handle them in my own NSURLConnectionDelegate to check for and provide credentials if needed. I then use loadData:MIMEType:textEncodingName:baseURL: to present HTML in the web view. That works fine for any URLs that are passed to the delegate.
My problem is that the delegate is never called for embedded elements, like images, JavaScript or CSS files. So if I have an HTML page which references an image which is protected with basic authentication, that image cannot be loaded properly. Additionally, webView:didFinishLoad: is never called, because the web view could not fully load the page.
I have checked that case with Terra, a third-party browser available on the App Store, and it can fully cope with that situation. I think it would be possible to solve this by providing my own NSURLProtocol, but that seems too complicated. What am I missing?
Try to use sharedCredentialStorage for all domains you need to authenticate.
Here is working sample for UIWebView it was tested against Windows IIS having only BasicAuthentication enabled
This is how to add your site credentials:
NSString* login = #"MYDOMAIN\\myname";
NSURLCredential *credential = [NSURLCredential credentialWithUser:login
password:#"mypassword"
persistence:NSURLCredentialPersistenceForSession];
NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc]
initWithHost:#"myhost"
port:80
protocol:#"http"
realm:#"myhost" // check your web site settigns or log messages of didReceiveAuthenticationChallenge
authenticationMethod:NSURLAuthenticationMethodDefault];
[[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential
forProtectionSpace:protectionSpace];
[protectionSpace release];
Edit: same code in Swift 4
let login = "MYDOMAIN\\myname"
let credential = URLCredential(user:login, password:"mypassword", persistence:.forSession)
let protectionSpace = URLProtectionSpace(host:"myhost", port:80, protocol:"http", realm:"myhost", authenticationMethod:NSURLAuthenticationMethodDefault)
URLCredentialStorage.shared.setDefaultCredential(credential, for:protectionSpace)
Your webView is supposed to work now, if it does not work use next code to debug, especially check log messages of didReceiveAuthenticationChallenge.
#import "TheSplitAppDelegate.h"
#import "RootViewController.h"
#implementation TheSplitAppDelegate
#synthesize window = _window;
#synthesize splitViewController = _splitViewController;
#synthesize rootViewController = _rootViewController;
#synthesize detailViewController = _detailViewController;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
// Add the split view controller's view to the window and display.
self.window.rootViewController = self.splitViewController;
[self.window makeKeyAndVisible];
NSLog(#"CONNECTION: Add credentials");
NSString* login = #"MYDOMAIN\\myname";
NSURLCredential *credential = [NSURLCredential credentialWithUser:login
password:#"mypassword"
persistence:NSURLCredentialPersistenceForSession];
NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc]
initWithHost:#"myhost"
port:80
protocol:#"http"
realm:#"myhost" // check your web site settigns or log messages of didReceiveAuthenticationChallenge
authenticationMethod:NSURLAuthenticationMethodDefault];
[[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential forProtectionSpace:protectionSpace];
[protectionSpace release];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"http://myhost/index.html"]
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:12
];
NSLog(#"CONNECTION: Run request");
[[NSURLConnection alloc] initWithRequest:request delegate:self];
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application
{
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
}
- (void)applicationWillTerminate:(UIApplication *)application
{
}
- (void)dealloc
{
[_window release];
[_splitViewController release];
[_rootViewController release];
[_detailViewController release];
[super dealloc];
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
{
NSLog(#"CONNECTION: got auth challange");
NSString* message = [NSString stringWithFormat:#"CONNECTION: cred cout = %i", [[[NSURLCredentialStorage sharedCredentialStorage] allCredentials] count]];
NSLog(message);
NSLog([connection description]);
NSLog([NSString stringWithFormat:#"CONNECTION: host = %#", [[challenge protectionSpace] host]]);
NSLog([NSString stringWithFormat:#"CONNECTION: port = %i", [[challenge protectionSpace] port]]);
NSLog([NSString stringWithFormat:#"CONNECTION: protocol = %#", [[challenge protectionSpace] protocol]]);
NSLog([NSString stringWithFormat:#"CONNECTION: realm = %#", [[challenge protectionSpace] realm]]);
NSLog([NSString stringWithFormat:#"CONNECTION: authenticationMethod = %#", [[challenge protectionSpace] authenticationMethod]]);
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
// release the connection, and the data object
[connection release];
// inform the user
NSLog(#"CONNECTION: failed! Error - %# %#",
[error localizedDescription],
[[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
{
NSLog(#"CONNECTION: received response via nsurlconnection");
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection;
{
NSLog(#"CONNECTION: USE!");
return YES;
}
#end
The final solution for WebView authentication was based on custom protocol implementation. All protocols registered as a stack, so if you redefine HTTP protocol it would intercept all requests coming from webView, so you have to check attributes assotiated with incoming request and repack it into new request and send it again via your own connection. Since you are in stack, your request immidiatly comes to you again and you have to ignore it. So it goes down protocol stack to real HTTP protocol implementation, since your request is not athenticated you'll get authenticaiton request. And after authenticaiton you'll get a real response from server, so you repack response and reply to original request received from webView and that's it.
Don;t try to create new requests or responses bodies, you have to just resend them. The final code would be aproximetly 30-40 lines of code and it is quite simple, but requires a lot of debuging and tetsing.
Unfortunatlly I cannot provide code here, since I am assigned to different project already, I just wanted to say that my post is wrong way, it stucks when user changes password.
The secret to HTTP basic authentication using cocoa is knowing NSURL and the related classes.
NSURL
NSURLRequest/NSMutableURLRequest
NSURLConnection
NSURLCredential
NSURLCredentialStorage
NSURLProtectionSpace
UIWebView/WebView/NIWebController etc.
The real magic comes from NSURLConnection. In the words of the devDocs, "An NSURLConnection object provides support to perform the loading of a URL request." If you want to load some a URL in the background without displaying it you would use NSURLConnection. The real power of the NSURLConnection is in the method
+ (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id < NSURLConnectionDelegate >)delegate
The NSURLConnectionDelegate protocol has methods for responding to successful connections, fatal errors, and authentication challenges. If you are trying to access data Protected by HTTP basic authentication this is how Cocoa does it. At this point an example should bring some clarity.
//basic HTTP authentication
NSURL *url = [NSURL URLWithString: urlString];
NSMutableURLRequest *request;
request = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:12];
[self.webView openRequest:request];
(void)[NSURLConnection connectionWithRequest:request delegate:self];
This creates a URL. From the URL a URLRequest is created. The URLRequest is then loaded in the web view. The Request is also used to make a URLConnection. We don't really use the connection, but we need to receive notifications about authentication so we set the delegate. There are only two methods we need from the delegate.
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
{
NSURLCredential * cred = [NSURLCredential credentialWithUser:#"username"
password:#"password"
persistence:NSURLCredentialPersistenceForSession];
[[NSURLCredentialStorage sharedCredentialStorage]setCredential:cred forProtectionSpace:[challenge protectionSpace]];
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection;
{
return YES;
}
Whenever there is an authentication challenge a credential is added to the credential storage. You also tell the connection to use the credential storage.
I've just implemented this by setting basic auth credentials using an NSMutableURLRequest for the UIWebView. This also avoids the round trip incurred when implementing sharedCredentialStorage (of course there are tradeoffs involved).
Solution:
NSString *url = #"http://www.my-url-which-requires-basic-auth.io"
NSString *authStr = [NSString stringWithFormat:#"%#:%#", username, password];
NSData *authData = [authStr dataUsingEncoding:NSASCIIStringEncoding];
NSString *authValue = [NSString stringWithFormat:#"Basic %#", [authData base64EncodedString]];
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]];
[mutableRequest setValue:authValue forHTTPHeaderField:#"Authorization"];
NSURLRequest *request = [mutableRequest copy];
NSURLRequest *request = [NSURLRequest basicAuthHTTPURLRequestForUrl:url];
[self.webView loadRequest:request];
You can grab the NSData+Base64 category which implements the base64EncodedString for NSData from Matt Gallagher's page (it was at the bottom of the blog post when I downloaded it)
For TKAURLProtocolPro [http://kadao.dir.bg/cocoa.htm]
For SVWebViewController [https://github.com/samvermette/SVWebViewController]
Make sure to remember that logging out is not so easy with sessions and UIWebView credentials. See answer here: https://stackoverflow.com/a/18143902/2116338.