UIWebView capturing the response headers

14,749

Solution 1

I love the objective-c runtime. Is there something that you want to do but don't have an API for it? DM;HR.

Alright, on a more serious note, here's the solution to this. It will capture every URL Response that initiated from CFNetwork, which is what UIWebView happens to use behind the scenes. It will also capture AJAX requests, image loads, and more.

Adding a filter to this should probably be as easy as doing a regex on the contents of the headers.

@implementation NSURLResponse(webViewHack)

static IMP originalImp;

static char *rot13decode(const char *input)
{
    static char output[100];

    char *result = output;

    // rot13 decode the string
    while (*input) {
        if (isalpha(*input))
        {
            int inputCase = isupper(*input) ? 'A' : 'a';

            *result = (((*input - inputCase) + 13) % 26) + inputCase;
        }
        else {
            *result = *input;
        }

        input++;
        result++;
    }

    *result = '\0';
    return output;
}

+(void) load {
    SEL oldSel = sel_getUid(rot13decode("_vavgJvguPSHEYErfcbafr:"));

    Method old = class_getInstanceMethod(self, oldSel);
    Method new = class_getInstanceMethod(self, @selector(__initWithCFURLResponse:));

    originalImp = method_getImplementation(old);
    method_exchangeImplementations(old, new);
}

-(id) __initWithCFURLResponse:(void *) cf {
    if ((self = originalImp(self, _cmd, cf))) {
        printf("-[%s %s]: %s", class_getName([self class]), sel_getName(_cmd), [[[self URL] description] UTF8String]);

        if ([self isKindOfClass:[NSHTTPURLResponse class]])
        {
            printf(" - %s", [[[(NSHTTPURLResponse *) self allHeaderFields] description] UTF8String]);
        }

        printf("\n");
    }

    return self;
}

@end

Solution 2

There is no way to get the response object from the UIWebView (file a bug with apple for that, id say)

BUT two workarounds

1) via the shared NSURLCache

- (void)viewDidAppear:(BOOL)animated {
    NSURL *u = [NSURL URLWithString:@"http://www.google.de"];
    NSURLRequest *r = [NSURLRequest requestWithURL:u];
    [self.webView loadRequest:r];
}

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    NSCachedURLResponse *resp = [[NSURLCache sharedURLCache] cachedResponseForRequest:webView.request];
    NSLog(@"%@",[(NSHTTPURLResponse*)resp.response allHeaderFields]);
}
@end

if this works for you this is ideal


ELSE

  1. you could use NSURLConnection altogether and then just use the NSData you downloaded to feed the UIWebView :)

that'd be a bad workaround for this! (as Richard pointed out in the comments.) It DOES have major drawbacks and you have to see if it is a valid solution in your case

NSURL *u = [NSURL URLWithString:@"http://www.google.de"];
NSURLRequest *r = [NSURLRequest requestWithURL:u];
[NSURLConnection sendAsynchronousRequest:r queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *resp, NSData *d, NSError *e) {
    [self.webView loadData:d MIMEType:nil textEncodingName:nil baseURL:u];
    NSLog(@"%@", [(NSHTTPURLResponse*)resp allHeaderFields]);
}];

Solution 3

If you want a more high level API code of what @Richard J. Ross III's wrote, you need to subclass NSURLProtocol.

An NSURLProtocol is an object which handles URL Requests. So you can use it for specific tasks which are described better on NSHipster and Ray Wenderlich, which includes your case of getting the HTTP Headers from the Response.

Code

Create a new class subclassing from NSURLProtocol and your .h file should look like this:

@interface CustomURLProtocol : NSURLProtocol <NSURLConnectionDelegate>

@property (nonatomic, strong) NSURLConnection *connection;

@end

Your .m file should have these methods to handle what you wish for

@implementation CustomURLProtocol

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    // Here you can add custom filters to init or not specific requests
    return YES;
}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    // Here you can modify your request
    return request;
}

+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
    return [super requestIsCacheEquivalent:a toRequest:b];
}

- (void)startLoading {
    // Start request
    self.connection = [NSURLConnection connectionWithRequest:self.request delegate:self];
}

- (void) stopLoading {
    [self.connection cancel];
    self.connection = nil;
}

#pragma mark - Delegation

#pragma mark NSURLConnectionDelegate

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
        NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
        
        // Here we go with the headers
        NSDictionary *allHeaderFields = [httpResponse allHeaderFields];
    }
    
    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [self.client URLProtocol:self didLoadData:data];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    [self.client URLProtocolDidFinishLoading:self];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [self.client URLProtocol:self didFailWithError:error];
}

Also last thing to do is to register this protocol to the loading system which is easy achievable on AppDelegate:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [NSURLProtocol registerClass:[CustomURLProtocol class]];
    return YES;
}
Share:
14,749
ACP
Author by

ACP

Updated on June 04, 2022

Comments

  • ACP
    ACP about 2 years

    I searched/googled a lot but could not get the answer on how to capture HTTP response headers in UIWebview. Say I redirect to user to registration gateway(which is already active) in UIWebview on App Launch and when user finishes the registration, the app should be notified with the successful unique id assigned to the user on registration which is passed back in HTTP Response Headers.

    Is there any direct way to capture/print the HTTP Response headers using UIWebview?

  • Daij-Djan
    Daij-Djan over 11 years
    how do you get it from a web view though .. THAT is the thing :)
  • Richard J. Ross III
    Richard J. Ross III over 11 years
    It also isn't clean as the web-view cannot use AJAX and have you intercept that, and you don't get the incremental load that you would with a webview.
  • Richard J. Ross III
    Richard J. Ross III over 11 years
    The cache won't work for POST requests (or even GET requests with caching disabled). By overriding a specific constructor of NSURLResponse I have been able to dump every URL request made by the WebView (even AJAX ones) successfully.
  • Daij-Djan
    Daij-Djan over 11 years
    NSURLConnection is bad as richard pointed out . I wanted to use that too at first. :/
  • Daij-Djan
    Daij-Djan over 11 years
    use of private api is frowned upon by apple
  • Daij-Djan
    Daij-Djan over 11 years
    god are use constructive and nice! :P
  • Richard J. Ross III
    Richard J. Ross III over 11 years
    @Daij-Djan there's no private APIs in use here. Every method call is public in the objective-c runtime. I simply reference a selector that is usually hidden. I've used this same idea in an app that passed review, and if you're really paranoid just XOR the selector string.
  • Richard J. Ross III
    Richard J. Ross III over 11 years
    @Daij-Djan no documentation != private. NSExpression happens to have a constructor (expressionWithFormat:) that's in the public headers, but for whatever reason doesn't have any documentation. That doesn't make it private, however.
  • Daij-Djan
    Daij-Djan over 11 years
    _initWithCFURLResponse is in no header that I can see
  • Richard J. Ross III
    Richard J. Ross III over 11 years
    @Daij-Djan that doesn't make it illegal to reference the selector. This WILL make it past review, I promise.
  • Daij-Djan
    Daij-Djan over 11 years
    and that I believe anyway ^^ I have so much stuff that went through
  • Daij-Djan
    Daij-Djan over 11 years
    BTW thats why I wrote if that works for you, great because I am aware it wont work always!
  • Richard J. Ross III
    Richard J. Ross III over 11 years
    @Daij-Djan alright, I did update my answer to have a better hiding of the selector. Unless apple is entirely precognitive (which is possible!) It shouldn't be possible for them to know you're using a private API.
  • Daij-Djan
    Daij-Djan over 11 years
    I upvoted it even before, it is a cool approach. Just wanted to state this this as a disclaimer
  • ACP
    ACP over 11 years
    HI Richard,Thanks for your answer. but since I am new on this, I am bit confuse how to integrate this code with UIWebview. Can you throw some more light on this? Sorry for asking this!
  • Richard J. Ross III
    Richard J. Ross III over 11 years
    @ACP this snippet of code automatically logs every URL request sent with the APIs used by a UIWebView, so there's no integration per-SE that needs to be done. Your question wasnt clear on what precisely you needed to do with the response headers, so this just prints them all out.
  • Muhammad Zeeshan
    Muhammad Zeeshan over 10 years
    you should also #import <objc/message.h>
  • Richard J. Ross III
    Richard J. Ross III over 10 years
    @MuhammadZeeshan actually no - this wasn't the full file. You need include objc/runtime for this, not objc/message.
  • ArpitM
    ArpitM over 10 years
    @RichardJ.RossIII are you talking about swizzling the constructors for NSURLResponse?
  • Don Miguel
    Don Miguel over 9 years
    @RichardJ.RossIII have you submitted more than one apps using this solution? I 'm a bit anxious as I will be uploading an app to the store soon and I am using your approach. Also what do you mean by XORing the string? What's the point of encrypting it?
  • Richard J. Ross III
    Richard J. Ross III over 9 years
    @DonMiguel only one application. But the reason you encrypt the string is so that apple doesn't find it when they look through your binary for private API calls, which just checks the strings used in your application against a set of known private APIs.
  • Don Miguel
    Don Miguel over 9 years
    @RichardJ.RossIII so there is a private api in use after all. Since it will be my first time doing this, which selector string should I be encrypting, oldSel, "_vavgJvguPSHEYErfcbafr:" or __initWithCFURLResponse:? In case I have to encrypt oldSel how will I be supposed to use it in class_getInstanceMethod(self, oldSel)?
  • Don Miguel
    Don Miguel over 9 years
    @RichardJ.RossIII one last thing: have you actually XORed the selector to "help" the app go through?
  • Richard J. Ross III
    Richard J. Ross III over 9 years
    @DonMiguel I have used this tactic before, yes. You can just copy my code verbatim and you should be fine.
  • Rivera
    Rivera almost 6 years
    I get a compile error stating that originalImp(self, _cmd, cf) should have zero parameters (Xcode 10 beta 4, 2018 here)
  • Richard J. Ross III
    Richard J. Ross III almost 6 years
    @Rivera you should cast the function pointer to the proper function signature before invoking, I believe the signature from IMP changed from typedef id (*IMP)(id, SEL, ...) to typedef id (*IMP)() due to concerns around the prior signature's lack of safety on new ABIs (mainly ARM64).