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.
Related
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.
I am implementing my own authentication framework for OAuth 2.0.
As far as my understanding is concerned server sends 401 if token has been expired.
I implemented NSURLConnection's delegate
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
to catch these error and refresh token.
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge{
// refresh token and send it to server
if ([challenge previousFailureCount] > 0) {
// do something may be alert message
}
else
{
//refreshToken
}
}
But it seems to be that there is no way I can append the token to url.
you can't - changing the url means a new request. the current request is in progress and it is for this URL.
one straightforward way is to have a 'wrapper object' around the url connection itself, that could transparently do a 2nd request should the 1st fail
e.g. PSEUDOCODE
#interface MyConnection
- loadRequest:(NSURLRequest*)r completionHandler:handler //not fully felshed out for this example
#end
#implementation MyConnection
- loadRequest:(NSURLRequest*)r completionHandler:handler //not fully felshed out for this example
{
[NSURLConnection sendRequest:r completion:{
if(response.status == 401)
{
NSMutableURLRequest *r2 = [NSMutableURLRequest requestWithURL:r.URL + token];
//refresh by sending a new request r2
[NSURLConnection sendRequest:r2 completion:{
handler(response);
}];
}
else {
handler(response);
}
}];
}
#end
I have a class, "WebAPI", that handles all web API calls, the class uses NSURLConnection through its asynchronous delegate-based calls.
Whenever an object needs to communicate with the web API it will use an instance of WebAPI and call the required method as shown below in the case of signing in I make the folowing call from the AppDelegate:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
WebAPI *webAPI = [[WebAPI alloc] init];
[webAPI performLoginWithUserName:#"test1#myserver.com" andPassword:#"password"];
}
The problem is that once the performLoginWithUserName:andPassword call is made, the code progresses on and any/all response is received in the delegate methods that are implemented in WebAPI.m.
This is a real issue because I need to be able to get response codes and any data received within the class method from where the call to the WebAPI, originated . I would like to be able to this :
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
WebAPI *webAPI = [[WebAPI alloc] init];
WebAPIResponse * webAPIRespnse = [webAPI performLoginWithUserName:#"test1#myserver.com" andPassword:#"password"];
}
Where WebAPIResponse class is a custom class that will contain the HTTP Status code and any data that is received.
This is achievable if I change WebAPI.m to use NSURLConnection sendSynchronousRequest, but that doesnt enable me to receive all HTTP codes.
What would be the best way to fulfill this requirement?
Thank you for your help.
You could use blocks to handle responses.
For example:
WebApi.h
- (void)performLoginWithUsername:(NSString *)userName
andPassword:(NSString *)password
successBlock:(void(^)(NSData *response))successBlock
failureBlock:(void(^)(NSError *error))failureBlock;
WebApi.m
#interface WebAPI()
#property (nonatomic, copy) void(^authorizationSuccessBlock)(NSData *response);
#property (nonatomic, copy) void(^authorizationFailureBlock)(NSError *error);
#end
#implementation WebAPI
- (void)performLoginWithUsername:(NSString *)userName
andPassword:(NSString *)password
successBlock:(void(^)(NSData *response))successBlock
failureBlock:(void(^)(NSError *error))failureBlock {
self.authorizationSuccessBlock = successBlock;
self.authorizationFailureBlock = failureBlock;
// NSURLConnection call for authorization here
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if (self.authorizationSuccessBlock != nil) {
self.authorizationSuccessBlock(data);
self.authorizationSuccessBlock = nil;
}
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
if (self.authorizationFailureBlock != nil) {
self.authorizationFailureBlock(error);
self.authorizationFailureBlock = nil;
}
}
AppDelegate.m
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
WebAPI *webAPI = [[WebAPI alloc] init];
[webAPI performLoginWithUserName:#"test1#myserver.com" andPassword:#"password" successBlock:^(NSData *response) {
// Handle result here
} failureBlock:^(NSError *error) {
// Handle error here
}];
}
Change your WebAPI class to provide a delegate interface of its own, or to use completion blocks on the request which are called when the asynchronous connection completes.
In order to send HTTP request, I am using NSURLConnection like this:
NSURLConnection *connection = [[NSURLConnection alloc]
initWithRequest:request
delegate:self
startImmediately:YES];
At the end of connectionDidFinishLoading, I need to post different notifications, depending on the HTTP request that was just completed.
However inside connectionDidFinishLoading I don't have a clear logical identifier to the type of the request that was send:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// here i want to post various notifications, depending on the HTTP request that was completed
}
What is the best solution here? Thanks!
Connection did finish method passes the NSURLConnection object, which has the request and url:
-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(#"currentRequest: %#", connection.currentRequest);
NSLog(#"originalRequest: %#", connection.originalRequest);
// here do a if statement that compares url
if ([connection.currentRequest.URL.absoluteString isEqualToString:#"http://google.co.uk"]) {
NSLog(#"Equal to google");
// post notification
}
}
You can use framework like MKNetworkKit. In this framework you can write like this:
- (void) sendRequest: (NSString*) aRequestPath
httpMethod: (NSString*) aHttpMethod
paramsBlock: (SMFillParametersForRequestBlock) aParamsBlock
successBlock: (SMSaveRequestResultBlock) aSuccessBlock
errorBlock: (SMErrorRequestResultBlock) aErrorBlock
userInfo: (id) anUserInfo
{
MKNetworkEngine* network_engine= [[MKNetworkEngine alloc] initWithHostName: MuseumsHostName];
NSMutableDictionary* params = [[NSMutableDictionary alloc] init];
if (aParamsBlock)
{
aParamsBlock(params);
}
MKNetworkOperation* operation = [network_engine operationWithPath: aRequestPath
params: params
httpMethod: aHttpMethod
ssl: NO];
[operation onCompletion: ^(MKNetworkOperation* completedOperation){
// parse response of current request:
aSuccessBlock(completedOperation, anUserInfo, ...);
} onError: ^(NSError *error){
// error handler: call block
}];
[network_engine enqueueOperation: operation];
}
Believe me, this is the best solution
My app goes to a viewcontroller, makes two automatic server requests, makes the connection, retrieves the data and correctly displays it, and is done. The user clicks a "likes" button and two more server requests are made - successfully. Displays are correct. Should be done. Then it crashes, with the error:
[__NSCFNumber isEqualToString:]: unrecognized selector sent to instance
I'm using the very handy SimplePost class (by Nicolas Goles). Here are my requests, which are both called in viewDidLoad:
- (void) setScore {
Profile *newPf = [[Profile alloc] initID:thisUser profil:#"na" scor:score];
NSMutableURLRequest *reqPost = [SimplePost urlencodedRequestWithURL:[NSURL URLWithString:kMyProfileURL] andDataDictionary:[newPf toDictPf]];
(void) [[NSURLConnection alloc] initWithRequest:reqPost delegate:self];
}
- (void) saveHist {
History *newH = [[History alloc] initHistID:thisUser hQid:thisQstn hPts:score hLiked:NO];
NSMutableURLRequest *reqHpost = [SimplePost urlencodedRequestWithURL:[NSURL URLWithString:kMyHistURL] andDataDictionary:[newH toDictH]];
(void) [[NSURLConnection alloc] initWithRequest:reqHpost delegate:self];
}
The only "new" thing with my custom classes (Profile and History) is the BOOL for hLiked, but it's "working" - the database is updating correctly.
Then, the user can click a "Likes" button (+ or -). Here are the other requests:
- (IBAction)likeClick:(id)sender {
double stepperValue = _likeStepper.value;
_likesLbl.text = [NSString stringWithFormat:#"%.f", stepperValue];
[self updateLikes];
[self updateHist];
}
- (void) updateLikes {
// update the question with the new "liked" score
NSInteger likesN = [_likesLbl.text integerValue];
Questn *qInfo = [[Questn alloc] initQwID:thisQstn askID:0 wCat:#"na" wSit:#"na" wAns1:#"na" wPts1:0 wAns2:#"na" wPts2:0 wAns3:#"na" wPts3:0 wAns4:#"na" wPts4:0 wJust:#"na" wLikes:likesN ];
NSMutableURLRequest *reqPost = [SimplePost urlencodedRequestWithURL:[NSURL URLWithString:kLikesURL] andDataDictionary:[qInfo toDictQ]];
(void) [[NSURLConnection alloc] initWithRequest:reqPost delegate:self];
}
- (void) updateHist {
History *newH = [[History alloc] initHistID:thisUser hQid:thisQstn hPts:98989 hLiked:YES];
NSMutableURLRequest *reqHpost = [SimplePost urlencodedRequestWithURL:[NSURL URLWithString:kHistURL] andDataDictionary:[newH toDictH]];
(void) [[NSURLConnection alloc] initWithRequest:reqHpost delegate:self];
}
Messy, right? Here's my connection code:
// connection to URL finished with Plist-formatted user data array returned from PHP
- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
NSDictionary *array = (NSDictionary *)[NSPropertyListSerialization propertyListFromData:data mutabilityOption:NSPropertyListImmutable format:0 errorDescription:nil];
BOOL keyLikeExists = [array objectForKey:#"likes"] != nil;
if( keyLikeExists ) {
_likesLbl.text = [array objectForKey:#"likes"];
}
}
- (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(#"Connection did fail." );
}
It all does a good job, and then a couple of seconds later it crashes with that "unrecognized selector" error mentioned above, like there's still some URL activity happening. There shouldn't be.
Anybody seen this kind of thing before? Many thanks for any help!
Somewhere in your code there's a call to the method isEqualToString:. The thing that's being sent that message is a NSNumber object rather than a string. Either there's a logic problem concerning the object type or there's a memory problem where a string was over-released and its memory is being re-used to hold a number.
Without seeing the context for the call, it's hard to guess.
If you break on the exception, the stack trace should tell you where in the code it's failing.