How to hook NSURLSession methods with theos? - iphone

I created a tweak project using rpetrich's theos and wanted to hook NSURLSession methods but the hooks don't seem to get invoked? Why? This is my Tweak.xm code:
%hook NSURLSession
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler
{
NSLog(#"testhook dataTaskWithRequest:completionHandler:");
return %orig(request, completionHandler);
}
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
{
NSLog(#"testhook dataTaskWithRequest");
return %orig(request);
}
%end
%hook NSMutableURLRequest
+ (id)requestWithURL:(NSURL *)URL
{
NSLog(#"testhook NSMutableURLRequest");
return %orig(URL);
}
%end
I added the NSMutableURLRequest hook to make sure that the file and the whole tweak was being loaded. I can verify that it does hook requestWithURL: but not any of the NSURLSession methods. I am testing against the code from NSURLSessionExample.
What's missing here? Has anybody successfully hooked NSURLSession?

NSURLSession is a class cluster, and you are hooking the toplevel class that contains no (or scarce little) code.
You should investigate the subclasses of NSURLSession—potentially by logging the real class of an NSURLSession object in-situ. In my limited testing, I received an object whose class was truly named __NSURLSessionLocal.

Related

Can't get OHHTTPStubs to work with NSURLSession

I'm trying to use OHHTTPStubs in my XCTest class,
This is how I configured OHTTPStubs in my test file.
//
// Tests Configuration
//
- (void)setUp
{
[super setUp];
_bundle = [NSBundle bundleForClass:[self class]];
[self configureHTTPStubs];
[self installHTTPStubs];
}
- (void)configureHTTPStubs
{
[OHHTTPStubs onStubActivation:^(NSURLRequest *request, id<OHHTTPStubsDescriptor> stub) {
NSLog(#"[OHHTTPStubs] Request to %# has been stubbed with %#", request.URL, stub.name);
}];
}
- (void)installHTTPStubs
{
HIAPIRequests *requester = [[HIAPIOperator sharedOperator] requester];
[OHHTTPStubs setEnabled:YES forSessionConfiguration:requester.session.configuration];
[OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
return [request.URL.path isEqualToString:#"/image_upload"];
} withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) {
return [[OHHTTPStubsResponse responseWithFileAtPath:OHPathForFileInBundle(#"image_upload_ws_response.json", nil)
statusCode:201
headers:#{#"Content-Type":#"text/json"}] responseTime:OHHTTPStubsDownloadSpeed3G];
}].name = #"Image Upload OK";
}
//
// In my Requester class this is how I setup the NSURLSession configuration
//
- (void)configureURLSession
{
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
_session = [NSURLSession sessionWithConfiguration:config];
}
And this is how I'm performing a request
- (void)uploadImage:(UIImage *)image
completionBlock:(operationCompletionBlock)completionBlock
progressBlock:(operationProgressBlock)progressBlock
{
NSData *imageData = UIImageJPEGRepresentation(image, 0.80);
NSURLRequest *request = [NSURLRequest requestWithURLString:#"/image_upload"];
NSURLSessionUploadTask *uploadTask = [_session uploadTaskWithRequest:request
fromData:imageData
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
completionBlock(data, error);
}];
[_progressTable setObject:progressBlock forKey:uploadTask];
[uploadTask resume];
}
In the completionHandler callback I'm basically getting a no domain found error (error NSURLError * domain: #"NSURLErrorDomain" - code: -1003 0x08a70740) , #"A server with the specified hostname could not be found."
I'm completely sure that I'm querying the correct URL (the one I stubbed with OHHTTPStubs) in my test.
What could be going on here? Bug maybe?
#Goles I know we already discussed that directly on the related issue you created on my GitHub, but I'm answering it here to so that other SO readers can have the solution too.
The issue in #Goles ' code is that he uses [OHHTTPStubs setEnabled:YES forSessionConfiguration:requester.session.configuration]; on an NSURLSessionConfiguration that was already used to create its NSURLSession.
As the Apple documentation explains, you can't modify an NSURLSessionConfiguration once it has been used to create an NSURLSession. Well, you can, but it won't have any effect on the already-created NSURLSession. If you modify an NSURLSessionConfiguration, you will have to create a new NSURLSession with that modified NSURLSessionConfiguration to take the new configuration into account.
Moreover, in the latest versions of OHHTTPStubs, it is not necessary anymore to explicitly call +[setEnabled:forSessionConfiguration:]: as explained in the documentation, every NSURLSessionConfiguration created with defaultSessionConfiguration or ephemeralSessionConfiguration will automatically have OHHTTPStubs enabled on them if you use my library.
This means that you don't need to change anything in your working code for your stubs to hook into your sessions and network requests (even if you create the NSURLSessionConfiguration and NSURLSession somewhere deeply hidden in your app code).
For those of you who want to use OHHTTPStubs with NSURLSession I strongly recommend to use the latest 3.0.2 version of OHHTTPStubs. NSURLSession support was indeed introduced in version 2.4.0 of OHHTTPStubs, but there were some glitches left in that version that have been fixed in version 3.x since.

How to pass info with NSURLConnection instance so I can get it from connectionDidFinishLoading

I am using NSURLConnection to load data from a response. It works as it should, the delegate method connectionDidFinishLoading has the connection instance with the data I need. The problem is that I want to pass some information along with the request so that I can get it when the connection finishes loading:
User wants to share the content of a URL via (Facebook, Twitter,
C, D).
NSURLConnection is used to get the content of the URL
Once I have the content, I use the SL framework
SLComposeViewController:composeViewControllerForServiceType and need
to give it the service type
At this point I don't know what service the user selected in step 1. I'd like to send that with the NSURLConnection.
Can I extend NSURLConnection with a property for this? That seems very heavy-handed. There must be a "right way" to do this.
Many Thanks
Assuming you don't need the delegate-based version of the NSURLConnection process for some other reason, this is a good use case for the block-based version:
- (void)shareContentAtURL:(NSURL *)shareURL viaService:(NSString *)service
{
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:shareURL];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if ([data length] == 0 && error == nil) {
// handle empty response
} else if (error != nil) {
// handle error
} else {
// back to the main thread for UI stuff
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// do whatever you do to get something you want to post from the url content
NSString *postText = [self postTextFromData:data];
// present the compose view
SLComposeViewController *vc = [SLComposeViewController composeViewControllerForServiceType:service];
[vc setInitialText:postText];
[self presentViewController:vc animated:YES];
}];
}
}];
}
Since blocks can capture variables from their surrounding scope, you can just use whatever context you already had for the user's choice of service inside the NSURLConnection's completion block.
If you're still wed to the delegate-based NSURLConnection API for whatever reason, you can always use an ivar or some other piece of state attached to whatever object is handling this process: set self.serviceType or some such when the user chooses a service, then refer back to it once you get your content from the NSURLConnectionDelegate methods and are ready to show a compose view.
You could check the URL property of an NSURLConnection instance and determine the service by parsing the baseURL or absoluteString property of the URL with something like - (ServiceType)serviceTypeForURL:(NSURL *)theURL;
All the NSURLConnectionDelegate methods pass the calling NSURLConnection object-so you could get it from
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
or
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error

AFNetworking HTTPClient subclass with XMLParser

I am writing a small iOS app that queries a XML REST webservice. The networking framework in use is AFNetworking.
Situation
To query the webservice I subclassed AFHTTPClient:
#interface MyApiClient : AFHTTPClient
and in the implementation I make that available as a singleton:
+ (MyApiClient *)sharedClient {
static MySharedClient *_sharedClient = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_sharedClient = [[self alloc] initWithBaseUrl:[NSUrl URLWithString:#"http://url.to.the.webservice"]];
});
return self;
}
and in initWithBaseURL I tell AFNetworking to expect XML content:
[self registerHTTPOperationClass:[AFXMLRequestOperation class]];
Now I can call getPatch on the singleton from my ViewController and in the success block start parsing my returned XML. In NSXMLParserDelegate methods in the ViewController I can then pick the parts of the XML I am interested in and do stuff with it.
Problem
I want to have methods in my HTTPClient singleton that handle everything related to the webservice and return data models or list of models instead of XML.
For example I want to do something like this:
ServerModel *status = [[MyApiClient sharedClient] getServerStatus];
The ApiClient would then internally call the webservice, parse the XML and return the model.
How can I do that? Normally I would use a delegate that gets called once the XML is parsed, but due to the singleton nature of the ApiClient there could be multiple delegates?
Hope someone can shed light on this, thanks!
Use blocks instead of delegates.
From my ApiClient class:
- (void)getPath:(NSString *)path
parameters:(NSDictionary *)parameters
success:(void (^)(id response))success
failure:(void (^)(NSError *error))failure
{
NSURLRequest *request = [self requestWithMethod:#"GET" path:path parameters:parameters];
[self enqueueHTTPOperationWithRequest:request success:success failure:failure];
}
-(void)fetchAllUsersSuccess:(void (^)(id))success
failure:(void (^)(NSError *))failure
{
[self getPath:#"/api/mobile/user/"
parameters:nil
success:^(id response) {
if([response isKindOfClass:[NSXMLParser class]]){
//parse here to new dict
success(newDict);
} else
success(response);
} failure:^(NSError *error) {
failure(error);
}];
}
Now I can use it like:
ServiceApiClient *apiClient = [ServiceApiClient sharedClient];
[apiClient fetchAllUsersSuccess:^(id dict) {
for (NSDictionary *object in [dict objectForKey:#"objects"]) {
[ServiceUser addUserFromDictionary:object
inContext:self.managedObjectContext];
}
NSError *error= nil;
[self.managedObjectContext save:&error];
if (error) {
NSLog(#"%#", error);
}
} failure:^(NSError * error) {
NSLog(#"%#", error);
}];
(Apologies in advance for this "sort-of" answer, but we're working towards a better solution...)
You need to take a step back and think about your design carefully.
You're having problems because you've got an idea that something in your design needs to be a singleton, but either:
1) that's not actually necessary,
2) something might already exist that does that job for you (e.g. the HTTP lib you're using),
or
3) You're making the wrong thing a singleton, or you haven't portioned out your design into the appropriate parts to work well with the singleton idea
So, can you tell me explicitly why you're going for a singleton approach? Is it just to ensure that only one network request can happen at once? Is there any notion of statefulness in your singleton object? Then I'll update this answer or comment, etc.
(Digression: I would also add that in some cases there might be a true need for a 'strong' singleton -- by which I mean that there really is only one possible instance, and that mechanism is baked right into your object, as you are doing - but this isn't it. The alternative is a 'weak' singleton, by which I mean your core object that actually does the work has a plain init method as usual, but shared access to a common object goes via another object, which is a kind of simple 'factory' that instantiates/holds the shared instance. The advantage of this weak singleton idea is that your code is more re-usable in different contexts - e.g. you could decide to do multiple HTTP requests/sessions concurrently at a later time - and it sometimes makes writing tests less problematic).

How do I get the last HTTP Status Code from a UIWebView?

I would like to detect when a page load request give to a UIWebView has returned a status code in the 5xx or 4xx range.
I've setup the delegate for the web view and have provided a -webView:didFailLoadWithError:error method but although it gets called fine for timeouts, it is not called for HTTP Status Code errors.
Any suggestions?
Hmmm... I'm not an iPhone developer, but....
Could you try creating an NSURLRequest with the URL you want to load? Then you could make the connection using NSURLConnection.
NSURLConnection has a delegate method
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
which will give the the response from the server. Please note that if you are making the connection over HTTP, the response will actually be of class NSHTTPURLResponse. The NSHTTPURLResponse can be used to get the status using the following instance method
- (NSInteger)statusCode
NSURLConnection has another delegate method
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
that can be used to get the data from the URL Connection. You could then manually load the data into your UIWebView using:
- (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)encodingName baseURL:(NSURL *)baseURL
That seems like a ton of work, but it could be done. Hopefully someone else will come up with the easier way, though I don't see it.
I struggled with this for quite a while trying to find a good answer. The requirements that I was working under was that I needed to be able to determine the status of the FIRST page load, and any load after that I would assume that the user was clicking links which shouldn't be broken (not guaranteed, I know, but a lot better than the alternatives).
What I ended up doing was making the initial call myself via a NSURLConnection (synchronously), and then passing the data on to the UIWebView.
NSURL *googleURL = [NSURL URLWithString:#"http://www.google.com"];
NSURLRequest *googleRequest = [NSURLRequest requestWithURL:googleURL];
NSHTTPURLResponse *response;
NSError *error;
NSData *responseData = [NSURLConnection sendSynchronousRequest:googleRequest
returningResponse:&response
error:&error];
if ([response statusCode] >= 400 || error)
{
// handle error condition
} else {
[webView_ loadData:responseData MIMEType:[response MIMEType]
textEncodingName:[response textEncodingName]
baseURL:[response URL]];
[self setView:webView_];
}
If you desire to get the information for every request, you could simply use the method
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
to intercept all requests and make them yourself. You would have to have some kind of request management technique, because when you call the loadData on the UIWebView, it will invoke the shouldStartLoadWithRequest callback, and you want to make sure you don't do an infinite loop of making the same request over and over.
I struggled very hard on this topic when things are on Swift 3.0 now. I even created a custom URLProtocol and tried to intercept all web requests, just to realize eventually that it was unnecessary. The reason for the confusion for me is because that they moved the didReceiveResponse function:
optional public func connection(_ connection: NSURLConnection, didReceive response: URLResponse)
to NSURLConnectionDataDelegate, which inherits from NSURLConnectionDelegate.
Anyway, Here is the Swift 3.0 version that works:
// You first need to have NSURLConnectionDataDelegate on your UIWebView
// MARK: - get HTTP status code
// setup urlconnectiondelegate
// so that the didReceiveResponse is called
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
let conn: NSURLConnection? = NSURLConnection(request: request, delegate: self)
if conn == nil {
print("cannot create connection")
}
return true;
}
// intercept the actual http status code
func connection(_ connection: NSURLConnection, didReceive response: URLResponse) {
let httpResponse: HTTPURLResponse = response as! HTTPURLResponse;
print(httpResponse.statusCode)
}
Currently, UIWebView does not provide any functionality for getting HTTP status codes for the requests it loads. One workaround is to intercept the request loading process of UIWebView using the UIWebViewDelegate methods and use NSURLConnection to detect how the server responds to that request. Then you can take an appropriate action suitable for the situation. This article explains the workaround in detail on a demo project.
And you don't need to continue loading the request after you received a response. You can just cancel the connection in - (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response method after learning the HTTP status code. This way you prevent the connection from loading any unnecessary response data. Then you can load the request in UIWebView again or show an appropriate error message to the user depending on the HTTP status code, etc.
Here is the article
and here is the demo project on github
Here's a work-around to get HTTP response code, but with sending just one request to each URL:-
BOOL isLoad;
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
NSLog(#"Requesting: %# - %d - %#", request.URL.absoluteString, navigationType, request.URL.host);
if (navigationType != UIWebViewNavigationTypeOther) {
//Store last selected URL
self.loadedURL = request.URL.absoluteString;
}
if (!isLoad && [request.URL.absoluteString isEqualToString:loadedURL]) {
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (connectionError || ([response respondsToSelector:#selector(statusCode)] && [((NSHTTPURLResponse *)response) statusCode] != 200 && [((NSHTTPURLResponse *)response) statusCode] != 302)) {
//Show error message
[self showErrorMessage];
}else {
isLoad = YES;
[_wbView loadData:data MIMEType:[response MIMEType]
textEncodingName:[response textEncodingName]
baseURL:[response URL]];
}
}];
return NO;
}
isLoad = NO;
return YES;
}
As I just posted on another thread, it is also possible to intercept any NSURLRequest at the level of the NSURLProtocol and create your NSURLResponse there, instead of in your UIWebView delegate/controller. The reason why this is preferable in my opinion is that it maintains the back/forward navigation stack of the UIWebView. The outline of the approach can be found in this excellent blog post by Rob Napier:
http://robnapier.net/blog/offline-uiwebview-nsurlprotocol-588
and there's code on GitHub:
https://github.com/rnapier/RNCachingURLProtocol
Here is a nice example where they use a combination of creating a NSURLConnection for the first loading, and the UIWebView for the next pages:
http://www.ardalahmet.com/2011/08/18/how-to-detect-and-handle-http-status-codes-in-uiwebviews/
Basically this is the main trick, using the YES/NO return value of shouldStartLoadRequest:
- (BOOL) webView:(UIWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType
{
if (_sessionChecked) {
// session already checked.
return YES;
}
// will check session.
NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
if (conn == nil) {
NSLog(#"cannot create connection");
}
return NO;
}
This should get you going without using synchronous requests, especially combined with Jeff's answer.
Sadly at present it looks like the best available option is to use -stringByEvaluatingJavaScriptFromString: to run some sort of small script that queries the document for its status code.
What about implementing webViewDidFinishLoad: on your UIWebViewDelegate, and using the request property of the UIWebView to access the headers you're interested in? Something like this (untested):
- (void)webViewDidFinishLoad:(UIWebView *)webView {
NSString* theStatus = [[webView request] valueForHTTPHeaderField:#"Status"];
}

NSURLConnection - how to wait for completion

Our iPhone app code currently uses NSURLConnection sendSynchronousRequest and that works fine except we need more visibility into the connection progress and caching so we're moving to an async NSURLConnection.
What's the simplest way to wait for the async code to complete? Wrap it in a NSOperation/NSOperationQueue, performSelector..., or what?
Thanks.
I'm answering this in case anyone else bumps into the issue in the future. Apple's URLCache sample code is a fine example of how this is done. You can find it at:
iOS Developer Library - URLCache
As John points out in the comment above - don't block/wait - notify.
To use NSURLConnection asynchronously you supply a delegate when you init it in initWithRequest:delegate:. The delegate should implement the NSURLConnection delegate methods. NSURLConnection processing takes place on another thread but the delegate methods are called on the thread that started the asynchronous load operation for the associated NSURLConnection object.
Apart from notifications mentioned prior, a common approach is to have the class that needs to know about the URL load finishing set itself as a delegate of the class that's handling the URL callbacks. Then when the URL load is finished the delegate is called and told the load has completed.
Indeed, if you blocked the thread the connection would never go anywhere since it works on the same thread (yes, even if you are using the asynch methods).
I ran into this because our app used NSURLConnection sendSynchronousRequest in quite a few places where it made sense, like having some processing occurring on a background thread occasionally needing extra data to complete the processing. Something like this:
// do some processing
NSData * data = someCachedData;
if (data = nil) {
data = [NSURLConnection sendSynchronousRequest....]
someCachedData = data;
}
// Use data for further processing
If you have something like 3 different places in the same flow that do that, breaking it up into separate functions might not be desirable(or simply not doable if you have a large enough code base).
At some point, we needed to have a delegate for our connections(to do SSL certificate pinning) and I went trolling the internet for solutions and everything was of the form: "just use async and don't fight the framework!". Well, sendSynchronousRequest exists for a reason, this is how to reproduce it with an underlying async connection:
+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse *__autoreleasing *)response error:(NSError *__autoreleasing *)error
{
static NSOperationQueue * requestsQueue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
requestsQueue = [[NSOperationQueue alloc] init];
requestsQueue.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount;
});
NSCondition * waitLock = [NSCondition new];
[waitLock lock];
__block NSError * returnedError;
__block NSURLResponse * returnedResponse;
__block NSData * returnedData;
__block BOOL done = NO;
[NSURLConnection sendAsynchronousRequest:request
queue:requestsQueue
completionHandler:^(NSURLResponse * response, NSData * data, NSError * connectionError){
returnedError = connectionError;
returnedResponse = response;
returnedData = data;
[waitLock lock];
done = YES;
[waitLock signal];
[waitLock unlock];
}];
if (!done) {
[waitLock wait];
}
[waitLock unlock];
*response = returnedResponse;
*error = returnedError;
return returnedData;
}
Posted here in case anyone comes looking as I did.
Note that NSURLConnection sendAsynchrounousRequest can be replaced by whatever way you use to send an async request, like creating an NSURLConnection object with a delegate or something.