【问题标题】:CFHTTPMessageAddAuthentication fails to add authentication data to requestCFHTTPMessageAddAuthentication 未能将身份验证数据添加到请求
【发布时间】:2015-09-15 22:23:54
【问题描述】:

我正在尝试扩展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

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

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

我使用http://httpbin.org/ 只是为了测试(在这一步中网络套接字的功能无关紧要)。

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


我尝试使用不同的功能:CFHTTPAuthenticationCreateFromResponseCFHTTPMessageApplyCredentials 具有相同的结果。 至少CFHTTPMessageApplyCredentialsCFStreamError 的形式返回一些错误信息。问题是这个错误信息是无用的: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=kCFStreamErrorDomainHTTPerror=kCFStreamErrorHTTPAuthenticationTypeUnsupported(感谢@JensAlfke,我在您发表评论之前就找到了它)。为什么不支持?文档声称支持摘要,有一个常量 kCFHTTPAuthenticationSchemeDigestCFHTTPMessageAddAuthentication 接受和预期!


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

我不得不犯一些错误,因为这个简单的品尝应用程序也失败了:

#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", 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 标头。

是否有一些我可以在商业应用程序中使用的简单库或示例代码(仅此)?我可以自己做,但这将花费宝贵的时间。赏金仍然可用:)。

【问题讨论】:

  • 请注意,这是低级 API CFHTTPMessage,它在 CFStream 上运行,您指的是更高级别的 API NSURLConnectionNSURLSession。出于某种奇怪的原因,CFHTTPMessageAddAuthentication 拒绝将身份验证数据添加到我的请求中,并且没有任何信息说明原因。
  • 我明白你对 CFStream 的意思。您是否尝试过使用- (id)initWithURLRequest:(NSURLRequest *)request; 传入NSURLRequest(实际上是NSMutableURLRequest 对象)中的Auth 对象?只是不确定当它通过_urlRequest.allHTTPHeaderFields 时,它会使用CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)key, (__bridge CFStringRef)obj); 正确添加Auth 对象
  • @sbarow:显然你不明白这个问题。 NSURLRequest 此处不可用,因为 HTTP 仅用作握手以启动 Web 套接字连接,因此较低级别的 API 用于 HTTP。
  • 您使用的是 Socket Rocket 对吗?我不知道你的实现的进出,但是如果你在头文件(SRWebSocket.h)中查看64 行,你会看到- (id)initWithURLRequest:(NSURLRequest *)request; 所以NSURLRequest 实际上是可用的。就像我说我不知道​​你的实现,所以这就是我能给出的所有建议。祝你好运。
  • the source code of initWithURLRequestNSURLRequest 仅用作标题和 URL 的临时存储,没有其他用途。对于 HTTP 协议,仅使用 CFNetwork API。

标签: ios objective-c macos digest-authentication cfnetwork


【解决方案1】:

回答自己的问题:(

Apple CFNetwork API 很烂

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

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

可悲的是,CFHTTPMessageAddAuthentication 可以从请求中获取此 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\", "
    "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 的其他解决方法。至少现在我知道问题出在哪里了。

【讨论】:

    【解决方案2】:

    如果您收到kCFStreamErrorHTTPAuthenticationTypeUnsupported

    kCFHTTPAuthenticationSchemeBasic 有效吗?

    只是一个想法?

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

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

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

    【讨论】:

    • 我已经测试了这个也用于基本身份验证,结果相同。我必须做一些错误的事情,错误报告并没有显示到底是什么问题。结帐上次更新的问题,有完整的示例应用程序代码显示出了问题。
    • 这肯定不是端口问题。我在其他测试中使用了这个 URL(NSURLSessionPOCO library 和其他平台上的其他库)并且它正在工作。另外问题不是网络通信,而是解析身份验证数据(如果您阅读日志,我的代码证明了这一点)。
    • 您是否通过 tcpdump 或其他数据包分析器(如 packetpeeper form packetpeeper.org)确认了这一点?知道它是在网络端还是在身份验证数据端会减少很多地方看:)
    • 这不是网络问题。我收到了正确的身份验证挑战,通过调试,我缩小了在构建新请求时添加凭据的问题。请仔细阅读问题。
    【解决方案3】:

    几个月前我写了一些CFHTTPAuthentication 代码,隐约记得类似的怪事。我认为这些调用只有在与CFStream 结合使用时才能正常工作。

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

    虽然我不是 100% 的,但现在没有时间测试。

    【讨论】:

    • socket 火箭使用CFStreamCreatePairWithSocketToHostCFHTTPMessageCreateEmpty 所以这可能是一些线索。我会对此进行调查。
    • 形成其他来源(我合作公司的技术负责人)我有解释为什么它不起作用。他声称没有严格的解决方法,但是您的线索让我知道如何在不自己实施摘要身份验证的情况下解决问题。这可能会解决这个问题,但它可能会破坏其他东西。当我清楚地了解问题和解决方案时,我会发布答案。
    猜你喜欢
    • 2017-04-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-03-24
    • 2014-05-07
    • 1970-01-01
    • 2016-01-31
    相关资源
    最近更新 更多