CFHTTPMessageAddAuthentication无法向请求添加身份validation数据

我正在尝试扩展SocketRocket库的function。 我想添加身份validationfunction。

由于此库使用CFNetwork CFHTTPMessage* API来实现HTTPfunction(启动Web套接字连接所需),我正在尝试使用此API来提供身份validation。
有完全匹配的function: CFHTTPMessageAddAuthentication ,但它不能像我期望的那样工作(据我所知文档 )。

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

 - (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 

userpassword值有效(“user”“passwd”)。

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

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

请注意,使用过的代码不会使用(并且永远不会) NSURLRequstNSURLSessionNSURLConnection /


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

 typedef CF_ENUM(CFIndex, CFStreamErrorDomain) { kCFStreamErrorDomainCustom = -1L, /* custom to the kind of stream in question */ kCFStreamErrorDomainPOSIX = 1, /* POSIX errno; interpret using  */ kCFStreamErrorDomainMacOSStatus /* OSStatus type from Carbon APIs; interpret using  */ }; 

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

 {state = Failed; scheme = , forProxy = false} 

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


我挖掘了CFNetwork身份validation的源代码,并试图找出问题所在。

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

 #import  #import  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", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", 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", 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 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正确创建身份validation对象。 如果URL属性为空,则身份validation将失败。

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

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

解决方法

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

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

日志结果如下:

 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 

所以解决方法有效。

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

如果你得到kCFStreamErrorHTTPAuthenticationTypeUnsupported

kCFHTTPAuthenticationSchemeBasic有效吗?

只是一个想法?

编辑另一个想法,我在使用错误的协议和端口时看到了这一点,即

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

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

几个月前我写了一些CFHTTPAuthentication代码,模糊地回忆起类似的怪异。 我认为这些调用只能与CFStream结合使用。

意思是, kCFStreamPropertyHTTPResponseHeader与通过CFHTTPMessageCreateEmptyCFHTTPMessageCreateResponse创建的CFHTTPMessage有某种不同。

虽然我现在还没有时间进行测试,但我并不是百分之百。