CFHTTPMessageAddAuthentication fails to add authentication data to request

拥有回忆 提交于 2019-12-05 03:52:03

Answering own question :(

Apple CFNetwork API sucks

Problem is that response in CFHTTPMessageRef have hidden property URL. You can read it: CFHTTPMessageCopyRequestURL not set it and it is needed to properly create authentication object from CFHTTPMessageRef. If URL property is empty authentication will fail.

So how come that is some cases response with authentication challenge contains URL in other cases not? This working response comes from CFReadStreamRef created by CFReadStreamCreateForHTTPRequest as property of this stream. Here is crappy example. So since SocketRocket doesn't use CFReadStreamCreateForHTTPRequest this is a big problem which can't be simply overcome.

What is sad that CFHTTPMessageAddAuthentication could fetch this URL from request it modifies if it can't be found in response.

Workaround

There is perfectly working workaround on this issue! But it involves use of private API (so most probably it will not pass Apple review). Here is full sample code with workaround (same as in question but applying this workaround), the workaround it self it just two lines: exposing private API and using it.

#import <Foundation/Foundation.h>
#import <CFNetwork/CFNetwork.h>

static NSString * const kHTTPAuthHeaderName = @"WWW-Authenticate";

static NSString * const kHTTPDigestChallengeExample1 = @"Digest realm=\"testrealm@host.com\", "
    "qop=\"auth,auth-int\", "
    "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
    "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";

static NSString * const kHTTPDigestChallengeExample2 = @"Digest nonce=\"b6921981b6437a4f138ba7d631bcda37\", "
    "opaque=\"3de7d2bd5708ac88904acbacbbebc4a2\", "
    "realm=\"me@kennethreitz.com\", "
    "qop=auth";

static NSString * const kHTTPBasicChallengeExample1 = @"Basic realm=\"Fake Realm\"";

#define RETURN_STRING_IF_CONSTANT(a, x) if ((a) == (x)) return @ #x

NSString *NSStringFromCFErrorDomain(CFIndex domain) {
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainHTTP);
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainFTP);
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainSSL);
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainSystemConfiguration);
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainSOCKS);
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainPOSIX);
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainMacOSStatus);

    return [NSString stringWithFormat: @"UnknownDomain=%ld", domain];
}

NSString *NSStringFromCFErrorError(SInt32 error) {
    RETURN_STRING_IF_CONSTANT(error, kCFStreamErrorHTTPAuthenticationTypeUnsupported);
    RETURN_STRING_IF_CONSTANT(error, kCFStreamErrorHTTPAuthenticationBadUserName);
    RETURN_STRING_IF_CONSTANT(error, kCFStreamErrorHTTPAuthenticationBadPassword);

    return [NSString stringWithFormat: @"UnknownError=%d", (int)error];
}

NSString *NSStringFromCFHTTPMessage(CFHTTPMessageRef message) {
    return [[NSString alloc] initWithData: CFBridgingRelease(CFHTTPMessageCopySerializedMessage(message))
                                 encoding: NSUTF8StringEncoding];
}

// exposing private API for workaround
extern void _CFHTTPMessageSetResponseURL(CFHTTPMessageRef, CFURLRef);

void testAuthenticationHeader(NSString *authenticatiohHeader) {
    CFHTTPMessageRef response = CFHTTPMessageCreateResponse(kCFAllocatorDefault,
                                                            401,
                                                            NULL,
                                                            kCFHTTPVersion1_1);
    CFAutorelease(response);

    // workaround: use of private API
    _CFHTTPMessageSetResponseURL(response, (__bridge CFURLRef)[NSURL URLWithString: @"http://some.test.url.com/"]);

    CFHTTPMessageSetHeaderFieldValue(response,
                                     (__bridge CFStringRef)kHTTPAuthHeaderName,
                                     (__bridge CFStringRef)authenticatiohHeader);


    CFHTTPAuthenticationRef authData = CFHTTPAuthenticationCreateFromResponse(kCFAllocatorDefault, response);
    CFAutorelease(authData);

    CFStreamError error;
    BOOL validAuthData = CFHTTPAuthenticationIsValid(authData, &error);

    NSLog(@"testing header value: %@\n%@authData are %@   error.domain=%@  error.error=%@\n\n",
          authenticatiohHeader, NSStringFromCFHTTPMessage(response),
          validAuthData?@"Valid":@"INVALID",
          NSStringFromCFErrorDomain(error.domain), NSStringFromCFErrorError(error.error));
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        testAuthenticationHeader(kHTTPDigestChallengeExample1);
        testAuthenticationHeader(kHTTPDigestChallengeExample2);
        testAuthenticationHeader(kHTTPBasicChallengeExample1);
    }
    return 0;
}

And result in logs looks like that:

2015-07-03 11:47:02.849 cfauthtest[42766:934054] testing header value: Digest realm="testrealm@host.com", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"
HTTP/1.1 401 Unauthorized
Www-Authenticate: Digest realm="testrealm@host.com", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"

authData are Valid   error.domain=UnknownDomain=0  error.error=UnknownError=0

2015-07-03 11:47:02.852 cfauthtest[42766:934054] testing header value: Digest nonce="b6921981b6437a4f138ba7d631bcda37", opaque="3de7d2bd5708ac88904acbacbbebc4a2", realm="me@kennethreitz.com", qop=auth
HTTP/1.1 401 Unauthorized
Www-Authenticate: Digest nonce="b6921981b6437a4f138ba7d631bcda37", opaque="3de7d2bd5708ac88904acbacbbebc4a2", realm="me@kennethreitz.com", qop=auth

authData are Valid   error.domain=UnknownDomain=0  error.error=UnknownError=0

2015-07-03 11:47:02.852 cfauthtest[42766:934054] testing header value: Basic realm="Fake Realm"
HTTP/1.1 401 Unauthorized
Www-Authenticate: Basic realm="Fake Realm"

authData are Valid   error.domain=UnknownDomain=0  error.error=UnknownError=0

So workaround works.

I will keep looking for other workaround which will use public API only. At least now I know what is the problem.

If you're getting kCFStreamErrorHTTPAuthenticationTypeUnsupported

Does kCFHTTPAuthenticationSchemeBasic work?

Just a thought?

edit another thought, I've seen this when using the wrong protocol & port i.e.

http://myauth.com/auth/.../foobar (on port 443 despite being http)

and

https://myauth.com/auth/.../foobar (on port 80 despite being https)

I wrote some CFHTTPAuthentication code several months ago, and vaguely recall similar weirdness. I think the calls only worked correctly in combination with CFStream.

Meaning, kCFStreamPropertyHTTPResponseHeader was somehow different from a CFHTTPMessage created via CFHTTPMessageCreateEmpty or CFHTTPMessageCreateResponse.

I'm not 100% on that though & don't have time to test right now.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!