ios – CFHTTPMessageAddAuthentication无法向请求添加身份验证数据

我正在尝试扩展 SocketRocket库的功能.我想添加身份验证功能.

由于此库使用CFNetwork CFHTTPMessage* API进行HTTP功能(需要启动Web套接字连接),我正在尝试使用此API来提供身份验证.
有完全匹配的功能:CFHTTPMessageAddAuthentication,但它不能像我期望的那样工作(据我所知documentation).

以下是显示问题的代码示例:

- (CFHTTPMessageRef)createAuthenticationHandShakeRequest: (CFHTTPMessageRef)chalengeMessage {
    CFHTTPMessageRef request = [self createHandshakeRequest];
    BOOL result = CFHTTPMessageAddAuthentication(request,chalengeMessage,(__bridge CFStringRef)self.credentials.user,(__bridge CFStringRef)self.credentials.password,kcfHTTPAuthenticationSchemeDigest,/* I've also tried NULL for use strongest supplied authentication */
                                                 NO);
    if (!result) {
        Nsstring *chalengeDescription = [[Nsstring alloc] initWithData: CFBridgingrelease(CFHTTPMessagecopySerializedMessage(chalengeMessage))
                                                              encoding: NSUTF8StringEncoding];
        Nsstring  *requestDescription = [[Nsstring alloc] initWithData: CFBridgingrelease(CFHTTPMessagecopySerializedMessage(request))
                                                              encoding: NSUTF8StringEncoding];
        SRFastLog(@"Failed to add authentication data `%@` to a request:\n%@After a chalenge:\n%@",self.credentials,requestDescription,chalengeDescription);
    }
    return request;
}

requestDescription内容是:

GET /digest-auth/auth/user/passwd HTTP/1.1
Host: httpbin.org
Sec-WebSocket-Version: 13
Upgrade: websocket
Sec-WebSocket-Key: 3P5YiQDt+g/wgxHe71Af5Q==
Connection: Upgrade
Origin: http://httpbin.org/

chalengeDescription包含:

HTTP/1.1 401 UNAUTHORIZED
Server: Nginx
Content-Type: text/html; charset=utf-8
Set-Cookie: fake=fake_value
Access-Control-Allow-Origin: http://httpbin.org/
Access-Control-Allow-Credentials: true
Date: Mon,29 Jun 2015 12:21:33 GMT
Proxy-Support: Session-Based-Authentication
Www-Authenticate: Digest nonce="0c7479b412e665b8685bea67580cf391",opaque="4ac236a2cec0fc3b07ef4d628a4aa679",realm="me@kennethreitz.com",qop=auth
Content-Length: 0
Connection: keep-alive

用户和密码值有效(“user”“passwd”).

为什么CFHTTPMessageAddAuthentication返回NO?不知道问题是什么.我也尝试使用凭据更新空请求,但没有运气.

我已经使用http://httpbin.org/进行测试(在此步骤中,Web套接字的功能无关紧要).

请注意,使用过的代码不会使用(并且永远不会)NSURLRequst或NSURLSession或NSURLConnection /

我尝试使用不同的函数:CFHTTPAuthenticationCreateFromresponse和CFHTTPMessageApplyCredentials,结果相同.
至少CFHTTPMessageApplyCredentials以CFStreamError的形式返回一些错误信息.问题是这个错误信息是无用的:error.domain = 4,error.error = -1000这些值没有记录在任何地方.
唯一记录的值如下所示:

typedef CF_ENUM(CFIndex,CFStreamErrorDomain) {
    kcfStreamErrorDomainCustom = -1L,/* custom to the kind of stream in question */
    kcfStreamErrorDomainPOSIX = 1,/* POSIX errno; interpret using <sys/errno.h> */
    kcfStreamErrorDomainMacOsstatus      /* Osstatus type from Carbon APIs; interpret using <MacTypes.h> */
};

CFHTTPAuthenticationCreateFromresponse返回无效对象,该描述返回:

<CFHTTPAuthentication 0x108810450>{state = Failed; scheme = <undecided>,forProxy = false}

我在文档中发现了这些值的含义:domain = kcfStreamErrorDomainHTTP,error = kcfStreamErrorHTTPAuthenticationTypeUnsupported(感谢@JensAlfke我在你的评论之前找到了它).为什么它不受支持?文档声称支持摘要一个常量kcfHTTPAuthenticationSchemeDigest,CFHTTPMessageAddAuthentication接受并期望它!

我已经挖掘了source code of CFNetwork authentication并试图找出问题所在.

我必须犯一些错误,因为这个简单的tast应用程序也会失败:

#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];
}

void testAuthenticationHeader(Nsstring *authenticatiohHeader) {
    CFHTTPMessageRef response = CFHTTPMessageCreateResponse(kcfAllocatorDefault,401,NULL,kcfHTTPVersion1_1);
    CFAutorelease(response);

    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;
}

日志显示

2015-07-01 16:33:57.659 cfauthtest[24742:600143] 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",opaque="5ccc069c403ebaf9f0171e9517f40e41"

authData are INVALID   error.domain=kcfStreamErrorDomainHTTP  error.error=kcfStreamErrorHTTPAuthenticationTypeUnsupported

2015-07-01 16:33:57.660 cfauthtest[24742:600143] testing header value: Digest nonce="b6921981b6437a4f138ba7d631bcda37",opaque="3de7d2bd5708ac88904acbacbbebc4a2",qop=auth
HTTP/1.1 401 Unauthorized
Www-Authenticate: Digest nonce="b6921981b6437a4f138ba7d631bcda37",qop=auth

authData are INVALID   error.domain=kcfStreamErrorDomainHTTP  error.error=kcfStreamErrorHTTPAuthenticationTypeUnsupported

2015-07-01 16:33:57.660 cfauthtest[24742:600143] testing header value: Basic realm="Fake Realm"
HTTP/1.1 401 Unauthorized
Www-Authenticate: Basic realm="Fake Realm"

authData are INVALID   error.domain=kcfStreamErrorDomainHTTP  error.error=kcfStreamErrorHTTPAuthenticationTypeUnsupported

在我自己的回答后编辑:

替代解决方

其他可能的解决方案是手动解析WWW-Authenticate响应头并进行它并为新请求生成Authorization头.

是否有一些我可以在商业应用程序中使用的简单库或示例代码(只有这个)?我能做到这一点,但这需要宝贵的时间.赏金仍然可用:).

解决方法

回答自己的问题:(

Apple CFNetwork API很糟糕

问题是CFHTTPMessageRef中的响应具有隐藏的属性URL.
您可以阅读它:CFHTTPMessagecopyRequestURL未设置它,并且需要从CFHTTPMessageRef正确创建身份验证对象.如果URL属性为空,则身份验证将失败.

那么为什么有些案例的响应与身份验证质询包含URL在其他情况下不是?
此工作响应来自CFReadStreamCreateForHTTPRequest创建的CFReadStreamRef作为此流的属性. Here is crappy example.因此,由于SocketRocket不使用CFReadStreamCreateForHTTPRequest,这是一个无法简单克服的大问题.

令人遗憾的是,CFHTTPMessageAddAuthentication可以从请求修改的URL获取此URL,如果在响应中找不到它.

解决方法

在这个问题上有完美的解决方法!但它涉及使用私有API(所以很可能它不会通过Apple审查).以下是完整的示例代码及其解决方法(与问题相同,但应用此解决方法),解决方法只需两行:公开私有API并使用它.

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

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

static Nsstring * const kHTTPDigestChallengeExample1 = @"Digest realm=\"testrealm@host.com\",(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,kcfHTTPVersion1_1);
    CFAutorelease(response);

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

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

日志结果如下:

2015-07-03 11:47:02.849 cfauthtest[42766:934054] testing header value: Digest realm="testrealm@host.com",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",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

所以解决方法有效.

我将继续寻找仅使用公共API的其他解决方法.至少现在我知道问题是什么.

相关文章

UITabBarController 是 iOS 中用于管理和显示选项卡界面的一...
UITableView的重用机制避免了频繁创建和销毁单元格的开销,使...
Objective-C中,类的实例变量(instance variables)和属性(...
从内存管理的角度来看,block可以作为方法的传入参数是因为b...
WKWebView 是 iOS 开发中用于显示网页内容的组件,它是在 iO...
OC中常用的多线程编程技术: 1. NSThread NSThread是Objecti...