Как отправить PushNotification на устройство iOS с C # asp.net .netcore в webapi?

Я пытаюсь отправить сообщение PushNotification в список iOS device. Мои ограничения:

  • код находится внутри контроллера webapi
  • веб-сервер - Windows 2012 R2
  • с использованием ядра asp.net/.net (на самом деле 3.1)

Во-первых, я решил использовать файл p8 вместо p12 (кажется, Apple предпочитает его).

Я просканировал много вопросов SO, пробуя около 5-6 решений, но все равно получаю те же результаты:

При обработке запроса произошло необработанное исключение. Win32Exception: Сообщение получено без внимания или неправильно отформатировано. Неизвестное местоположение

AuthenticationException: Ошибка аутентификации, см. внутреннее исключение. System.Net.Security.SslStream.StartSendAuthResetSignal (сообщение ProtocolToken, AsyncProtocolRequest asyncRequest, исключение ExceptionDispatchInfo)

HttpRequestException: SSL-соединение не может быть установлено, см. внутреннее исключение. System.Net.Http.ConnectHelper.EstablishSslConnectionAsyncCore (поток потока, SslClientAuthenticationOptions sslOptions, CancellationToken CancellationToken)

С французского «Win32Exception: Le message reçu était inattendu ou formaté de façon correcte» на английском языке будет «Win32Exception: полученное сообщение было неожиданным или плохо отформатировано».

Для форматирования JWT я использовал решение здесь от Bourne Koloh.

Я также пробовал PushSharp и CoreSharp с той же ошибкой.

Когда я извлекаю данные из файла p8, у меня есть весь файл без первой и последней строки и без разрыва строки.

Когда я использую приложение Push Notifications Tester, оно работает, сообщение доставляется на устройство.

Кстати, я добавил оба сертификата p12 (dev и prod) на сервер Windows 2012 R2. Я, вероятно, пропустил что-то важное, но до сих пор не знаю, что.

Поскольку в сообщении говорится, что это проблема аутентификации, я думаю, это связано с JWT и файлом p8.

Как я извлекаю данные файла p8:

var data = System.IO.File.ReadAllText("AuthKey.p8");
var list = data.Split('\n').ToList();
var prk = list.Where((s, i) => i != 0 && i != list.Count - 1).Aggregate((agg, s) => agg + s);

var key = new ECDsaCng(CngKey.Import(Convert.FromBase64String(prk), CngKeyBlobFormat.Pkcs8PrivateBlob));

Il также пробовал этот способ (с BouncyCastle):

using (var reader = System.IO.File.OpenText("AuthKey.p8"))
{
    var ecPrivateKeyParameters = (ECPrivateKeyParameters)new PemReader(reader).ReadObject();
    var x = ecPrivateKeyParameters.Parameters.G.AffineXCoord.GetEncoded();
    var y = ecPrivateKeyParameters.Parameters.G.AffineYCoord.GetEncoded();
    var d = ecPrivateKeyParameters.D.ToByteArrayUnsigned();
    var key = ECDsaCng(EccKey.New(x, y, d));
}

А теперь, как я создаю JWT:

попытка №1: использование пакета Jose.

private string GetProviderToken(ECDsaCng key)
{
    var epochNow = (int)DateTime.UtcNow.Subtract(new DateTime(1970,1,1)).TotalSeconds;
    var payload = new Dictionary<string, object>()
    {
        { "iss", "THETEAMID" },
        { "iat", epochNow }
    };

    var extraHeaders = new Dictionary<string, object>()
    {
        { "kid", "THEKEYID"}
    };

    return JWT.Encode(payload, key, JwsAlgorithm.ES256, extraHeaders);
}
var token = GetProviderToken(key);

попытка №2:

private string CreateToken(ECDsa key, string keyID, string teamID)
{
    var securityKey = new ECDsaSecurityKey(key) { KeyId = keyID };
    var credentials = new SigningCredentials(securityKey, "ES256");

    var descriptor = new SecurityTokenDescriptor
    {
        IssuedAt = DateTime.Now,
        Issuer = teamID,
        SigningCredentials = credentials
    };
    descriptor.Expires = null;
    descriptor.NotBefore = null;
    var handler = new JwtSecurityTokenHandler();
    var encodedToken = handler.CreateEncodedJwt(descriptor);
    return encodedToken;
}
var token = CreateToken(key, "THEKEYID", "THETEAMID");

попробуй №3

private string SignES256(string privateKey, string header, string payload)
{
    CngKey key = CngKey.Import(Convert.FromBase64String(privateKey), CngKeyBlobFormat.Pkcs8PrivateBlob);
    using (ECDsaCng dsa = new ECDsaCng(key))
    {
        dsa.HashAlgorithm = CngAlgorithm.Sha256;
        var unsignedJwtData =              Microsoft.AspNetCore.WebUtilities.WebEncoders.Base64UrlEncode(System.Text.Encoding.UTF8.GetBytes(header)) + "." + Microsoft.AspNetCore.WebUtilities.WebEncoders.Base64UrlEncode(System.Text.Encoding.UTF8.GetBytes(payload));
        var signature = dsa.SignData(System.Text.Encoding.UTF8.GetBytes(unsignedJwtData));
        return unsignedJwtData + "." + Microsoft.AspNetCore.WebUtilities.WebEncoders.Base64UrlEncode(signature);
    }
}
var token = SignES256(prk, "{\"alg\":\"ES256\" ,\"kid\":\"THEKEYID\"}", "{ \"iss\": \"THETEAMID\",\"iat\":" + (int)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds + "\" }");

Если я не использую библиотеку (CorePush и т. д.) для отправки http-сообщения, я делаю так:

var url = string.Format("https://api.sandbox.push.apple.com/3/device/{0}", deviceToken);
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Headers.TryAddWithoutValidation("apns-push-type", "alert"); // or background
request.Headers.TryAddWithoutValidation("apns-id", Guid.NewGuid().ToString("D"));
request.Headers.TryAddWithoutValidation("apns-expiration", Convert.ToString(0));
request.Headers.TryAddWithoutValidation("apns-priority", Convert.ToString(10));
request.Headers.TryAddWithoutValidation("apns-topic", "com.company.project");
request.Content = new StringContent("{\"aps\":{\"alert\":\"Hello\"},\"yourCustomKey\":\"1\"}");
// also tried without yourcustomkey
request.Version = new Version(2, 0); // tried directly with System.Net.HttpVersion.Version20;
var handler = new HttpClientHandler();
handler.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls; // Tried with only Tls12
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true;
using (HttpClient client = new HttpClient(handler))
{
    HttpResponseMessage resp = await client.SendAsync(request).ContinueWith(responseTask =>
    {
        return responseTask.Result; // line of error
    });
    if (resp != null)
    {
        string apnsResponseString = await resp.Content.ReadAsStringAsync();
        handler.Dispose();
        //ALL GOOD ....
        return Ok(apnsResponseString);
    }

    handler.Dispose();
}

person Nicolas Vieira    schedule 07.06.2020    source источник
comment
Что, если попробовать Firebase от Google? Это более просто и бесплатно.   -  person Artavazd    schedule 07.06.2020
comment
Я прочитал несколько страниц о Firebase и не понял, как это работает, для меня это звучит сложнее, даже если это может работать как для устройств Apple, так и для Android.   -  person Nicolas Vieira    schedule 07.06.2020
comment
Firebase также работает для Android и iOS. Им просто нужно иметь токен сообщения Firebase, который вы могли бы отправить им.   -  person Artavazd    schedule 07.06.2020
comment
@NicolasVieira, у тебя когда-нибудь это работало? После этого руководства я получил те же ошибки, что и вы: medium.com/@sunnysun_5694/ Я собираюсь просто использовать CorePush, но не вижу хорошей документации, любая информация будет полезна!   -  person Jh2170    schedule 23.09.2020
comment
@ Jh2170 Jh2170 Я использовал обходной путь, предложенный Артаваздом: Google Firebase. По крайней мере, цель достигнута: уведомление отправлено и получено.   -  person Nicolas Vieira    schedule 26.09.2020