CFHTTPMessageAddAuthentication не может добавить данные аутентификации для запроса

Я пытаюсь расширить функциональность библиотеки SocketRocket. Я хочу добавить функцию аутентификации.

Поскольку эта библиотека использует CFNetwork CFHTTPMessage* API для функциональности HTTP (необходимой для запуска подключения к веб-сокету). Я пытаюсь использовать этот API для обеспечения аутентификации.
Для этого есть идеально подходящая функция: 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="[email protected]", qop=auth
Content-Length: 0
Connection: keep-alive

Значения user и password допустимы ("user" "passwd").

Почему CFHTTPMessageAddAuthentication возвращает NO? Нет понятия, в чем проблема. Я также пытался обновить учетные данные пустым запросом, но безуспешно.

Я использовал http://httpbin.org/ только для тестирования (функциональность веб-сокета на этом этапе не имеет значения).

Обратите внимание, что используемый код не использует (и никогда не будет) 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!


Я откопал исходный код CFNetwork аутентификации и попытаться выяснить, в чем проблема.

Я должен сделать некоторую ошибку, так как это простое приложение для вкуса также терпит неудачу:

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

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

static NSString * const kHTTPDigestChallengeExample1 = @"Digest realm=\"[email protected]\", "
    "qop=\"auth,auth-int\", "
    "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
    "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";

static NSString * const kHTTPDigestChallengeExample2 = @"Digest nonce=\"b6921981b6437a4f138ba7d631bcda37\", "
    "opaque=\"3de7d2bd5708ac88904acbacbbebc4a2\", "
    "realm=\"[email protected]\", "
    "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="[email protected]", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"
HTTP/1.1 401 Unauthorized
Www-Authenticate: Digest realm="[email protected]", 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="[email protected]", qop=auth
HTTP/1.1 401 Unauthorized
Www-Authenticate: Digest nonce="b6921981b6437a4f138ba7d631bcda37", opaque="3de7d2bd5708ac88904acbacbbebc4a2", realm="[email protected]", 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 для нового запроса.

Есть ли какая-то простая библиотека или пример кода, который я мог бы использовать в коммерческом приложении, которое будет делать это (только это)? Я мог бы сделать это сам, но это займет драгоценное время. Баунти все еще доступна :).


person Marek R    schedule 29.06.2015    source источник
comment
Обратите внимание, что это API низкого уровня CFHTTPMessage, который работает с CFStream, и вы имеете в виду API более высокого уровня NSURLConnection или NSURLSession. По какой-то странной причине CFHTTPMessageAddAuthentication отказался добавить данные для аутентификации в мой запрос, и нет никакой информации, почему.   -  person Marek R    schedule 29.06.2015
comment
Я понимаю, что вы имеете в виду о CFStream. Вы пытались передать объект Auth внутри объекта NSURLRequest (на самом деле это объект NSMutableURLRequest) с помощью - (id)initWithURLRequest:(NSURLRequest *)request;? Просто не уверен, что при прохождении через _urlRequest.allHTTPHeaderFields он правильно добавит объект Auth с помощью CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);   -  person sbarow    schedule 29.06.2015
comment
@sbarow: видимо, ты не понимаешь проблемы. NSURLRequest здесь недоступен, так как HTTP используется только как рукопожатие для запуска подключения к веб-сокету, поэтому для HTTP используется API более низкого уровня.   -  person Marek R    schedule 29.06.2015
comment
Вы используете Socket Rocket, верно? Я не знаю входов и выходов вашей реализации, но если вы посмотрите в заголовочный файл (SRWebSocket.h) в строке 64, вы увидите - (id)initWithURLRequest:(NSURLRequest *)request;, поэтому NSURLRequest на самом деле доступно. Как я уже сказал, я не знаю вашей реализации, так что это все, что я могу посоветовать. Удачи.   -  person sbarow    schedule 29.06.2015
comment
см. исходный код initWithURLRequest. NSURLRequest используется только как временное хранилище для заголовков и URL и больше ничего. Для протокола HTTP используется только CFNetwork API.   -  person Marek R    schedule 30.06.2015
comment
Я нашел ошибку -1000 на osstatus.com — это kCFStreamErrorHTTPAuthenticationTypeUnsupported (как определено в CFHTTPAuthentication.h.), что означает, что дайджест-аутентификация не поддерживается; это странно. Кроме того, домен 4 — это kCFStreamErrorDomainHTTP.   -  person Jens Alfke    schedule 01.07.2015
comment
Должен ли источник начинаться как https, чтобы выполнить обновление до wss?   -  person uchuugaka    schedule 01.07.2015
comment
@uchuugaka на этом этапе (рукопожатие HTTP) функциональность веб-сокета не важна. Это дайджест-аутентификация через HTTP.   -  person Marek R    schedule 01.07.2015
comment
@JensAlfke спасибо! Вчера я нашел эти значения в документации. Я согласен, что это странно, поскольку в документации четко указано, что дайджест поддерживается. Я также добавил ссылку на исходный код CFHTTPAuthentication.c   -  person Marek R    schedule 01.07.2015


Ответы (3)


Отвечая на собственный вопрос :(

Apple CFNetwork API отстой

Проблема в том, что ответ в CFHTTPMessageRef имеет скрытое свойство URL. Вы можете прочитать это: CFHTTPMessageCopyRequestURL не задано, и это необходимо для правильного создания объекта аутентификации из CFHTTPMessageRef. Если свойство URL пусто, аутентификация завершится ошибкой.

Так почему же в некоторых случаях ответ с запросом аутентификации содержит URL, а в других нет? Этот рабочий ответ исходит от CFReadStreamRef, созданного CFReadStreamCreateForHTTPRequest как свойство этого потока. Вот дерьмовый пример. Итак, поскольку 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=\"[email protected]\", "
    "qop=\"auth,auth-int\", "
    "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
    "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";

static NSString * const kHTTPDigestChallengeExample2 = @"Digest nonce=\"b6921981b6437a4f138ba7d631bcda37\", "
    "opaque=\"3de7d2bd5708ac88904acbacbbebc4a2\", "
    "realm=\"[email protected]\", "
    "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="[email protected]", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"
HTTP/1.1 401 Unauthorized
Www-Authenticate: Digest realm="[email protected]", 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="[email protected]", qop=auth
HTTP/1.1 401 Unauthorized
Www-Authenticate: Digest nonce="b6921981b6437a4f138ba7d631bcda37", opaque="3de7d2bd5708ac88904acbacbbebc4a2", realm="[email protected]", 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. По крайней мере, теперь я знаю, в чем проблема.

person Marek R    schedule 03.07.2015

Если вы получаете kCFStreamErrorHTTPAuthenticationTypeUnsupported

kCFHTTPAuthenticationSchemeBasic работает?

Просто мысль?

редактировать еще одну мысль, я видел это при использовании неправильного протокола и порта, т.е.

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

и

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

person Adrian Sluyters    schedule 01.07.2015
comment
Я проверил это также для базовой аутентификации с тем же результатом. Я должен сделать что-то не так, и отчет об ошибках не показывает, в чем именно проблема. Проверьте последнее обновление вопроса, есть полный код примера приложения, показывающий, что что-то не так. - person Marek R; 01.07.2015
comment
Это точно не проблема с портом. Я использовал этот URL-адрес в другом тесте (где NSURLSession, POCO library и другие библиотеки на других платформах), и он работал. Также проблема заключается не в сетевой связи, а в анализе данных аутентификации (мой код доказывает это, если вы читаете журналы). - person Marek R; 02.07.2015
comment
Подтвердили ли вы это с помощью tcpdump или другого анализатора пакетов, такого как packetpeeper, из packagepeeper.org? Знание того, находится ли он на стороне сети или на стороне данных аутентификации, сократит множество мест для поиска :) - person Adrian Sluyters; 02.07.2015
comment
Это не проблема сети. Я получаю правильный вызов аутентификации, и путем отладки я сужаю проблему до проблемы с добавлением учетных данных при создании нового запроса. Пожалуйста, прочитайте вопрос внимательнее. - person Marek R; 02.07.2015

Я написал код CFHTTPAuthentication несколько месяцев назад и смутно припоминаю подобные странности. Я думаю, что вызовы работали правильно только в сочетании с CFStream.

Это означает, что kCFStreamPropertyHTTPResponseHeader каким-то образом отличался от CFHTTPMessage, созданного с помощью CFHTTPMessageCreateEmpty или CFHTTPMessageCreateResponse.

Я не уверен в этом на 100%, и у меня нет времени проверять прямо сейчас.

person Community    schedule 03.07.2015
comment
socket Rocket использует CFStreamCreatePairWithSocketToHost и CFHTTPMessageCreateEmpty, так что это может быть подсказкой. Я расследую это. - person Marek R; 03.07.2015
comment
Из другого источника (технический руководитель компании, с которой я сотрудничаю) у меня есть объяснение, почему это не работает. Он утверждает, что простого обходного пути нет, но ваша подсказка дает мне представление о том, как я могу найти обходной путь, не применяя дайджест-аутентификацию самостоятельно. Это может решить эту проблему, но может сломать другие вещи. Я опубликую ответ, когда у меня будет четкое представление о проблеме и решении. - person Marek R; 03.07.2015