UIWebView capturing the response headers
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
- 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;
}
ACP
Updated on June 04, 2022Comments
-
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 over 11 yearshow do you get it from a web view though .. THAT is the thing :)
-
Richard J. Ross III over 11 yearsIt 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 over 11 yearsThe 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 over 11 yearsNSURLConnection is bad as richard pointed out . I wanted to use that too at first. :/
-
Daij-Djan over 11 yearsuse of private api is frowned upon by apple
-
Daij-Djan over 11 yearsgod are use constructive and nice! :P
-
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 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 over 11 years_initWithCFURLResponse is in no header that I can see
-
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 over 11 yearsand that I believe anyway ^^ I have so much stuff that went through
-
Daij-Djan over 11 yearsBTW thats why I wrote if that works for you, great because I am aware it wont work always!
-
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 over 11 yearsI upvoted it even before, it is a cool approach. Just wanted to state this this as a disclaimer
-
ACP over 11 yearsHI 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 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 over 10 yearsyou should also #import <objc/message.h>
-
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 over 10 years@RichardJ.RossIII are you talking about swizzling the constructors for
NSURLResponse
? -
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 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 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 over 9 years@RichardJ.RossIII one last thing: have you actually XORed the selector to "help" the app go through?
-
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 almost 6 yearsI get a compile error stating that
originalImp(self, _cmd, cf)
should have zero parameters (Xcode 10 beta 4, 2018 here) -
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, ...)
totypedef id (*IMP)()
due to concerns around the prior signature's lack of safety on new ABIs (mainly ARM64).