Runtime: APNs + HTTP/2 + HttpClient + ClientCertificate throws WinHttpException

Created on 24 Aug 2017  路  32Comments  路  Source: dotnet/runtime

I'm trying to sent message to APNs using HttpClient and p12 certificate.

Unfortunatelly I'm getting The server returned an invalid or unrecognized response error on SendAsync.

Here is Code Example:

public async Task SendAsync(ApnsHttp2Notification notification)
{
    var url = string.Format("https://{0}:{1}/3/device/{2}",
      _options.Host,
      _options.Port,
      notification.DeviceToken);

    var uri = new Uri(url);

    using (var certificate = SecurityHelperMethods.GetCertificateFromFile(_options.CertificateFileName, _options.CertificatePassword))
    using (var httpHandler = new HttpClientHandler { SslProtocols = SslProtocols.Tls12 })
    {
        httpHandler.ClientCertificates.Add(certificate);
        using (var httpClient = new HttpClient(httpHandler, true))
        using (var request = new HttpRequestMessage(HttpMethod.Post, url))
        {
            request.Content = new StringContent("Test");
            request.Version = new Version(2, 0);
            try
            {
                using (var httpResponseMessage = await httpClient.SendAsync(request))
                {
                    var responseContent = await httpResponseMessage.Content.ReadAsStringAsync();
                    var result = $"status: {(int)httpResponseMessage.StatusCode} content: {responseContent}";
                }
            }
            catch (Exception e)
            {
                throw;
            }
        }
    }
}

Exception:

{System.Net.Http.HttpRequestException: Error while copying content to a stream. ---> System.IO.IOException: The write operation failed, see inner exception. ---> System.Net.Http.WinHttpException: The server returned an invalid or unrecognized response
   --- End of inner exception stack trace ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Net.Http.HttpContent.<CopyToAsyncCore>d__44.MoveNext()
   --- End of inner exception stack trace ---
   at System.Net.Http.HttpContent.<CopyToAsyncCore>d__44.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Net.Http.WinHttpHandler.<InternalSendRequestBodyAsync>d__131.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Net.Http.WinHttpHandler.<StartRequest>d__105.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
   at System.Net.Http.HttpClient.<FinishSendAsyncBuffered>d__58.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at APNs.ApnService.<SendAsync>d__4.MoveNext() in D:\projects\ipl\src\Services\Notifiers\IPL.APNsNotifier.Worker\APNs\ApnService.cs:line 69}
    Data: {System.Collections.ListDictionaryInternal}
    HResult: -2146232800
    HelpLink: null
    InnerException: {System.IO.IOException: The write operation failed, see inner exception. ---> System.Net.Http.WinHttpException: The server returned an invalid or unrecognized response
   --- End of inner exception stack trace ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Net.Http.HttpContent.<CopyToAsyncCore>d__44.MoveNext()}
    Message: "Error while copying content to a stream."
    Source: "System.Net.Http"
    StackTrace: "   at System.Net.Http.HttpContent.<CopyToAsyncCore>d__44.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at System.Net.Http.WinHttpHandler.<InternalSendRequestBodyAsync>d__131.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at System.Net.Http.WinHttpHandler.<StartRequest>d__105.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at Sys
tem.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()\r\n   at System.Net.Http.HttpClient.<FinishSendAsyncBuffered>d__58.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()\r\n   at APNs.ApnService.<SendAsync>d__4.MoveNext() in ...\\ApnService.cs:line 69"
    TargetSite: {Void MoveNext()}

Target Framework: netcoreapp2.0
OS: Windows 10
OS Version: 1703
OS Build: 15063.332

area-System.Net.Http bug tracking-external-issue

Most helpful comment

Yes @cgyan009 this is the code i use.
If using .NET Framework you have to get the System.Net.Http.WinHttpHandler (4.4x) nuget package (.NET Core supports HTTP2)


public class CustomHttpHandler : WinHttpHandler { }

public class ApnsProvider : IDisposable
{
    HttpClient _client;
    CustomHttpHandler _handler;

    private readonly string _appBundleId;
    private readonly string _apnBaseUrl;

    public ApnsProvider(string apnUrl, string appBundleId)
    {
        _appBundleId = appBundleId;
        _apnBaseUrl = apnUrl;
        _handler = new CustomHttpHandler();
        _client = new HttpClient(_handler);
    }

    public async Task<bool> SendAsync(string message, string deviceToken, string jwtToken, bool playSound, string debugInfo = null)
    {
        var success = false;
        var headers = GetHeaders();

        var obj = new
        {
            aps = new
            {
                alert = message,
                sound = playSound ? "default" : null
            }
        };

        var json = Newtonsoft.Json.JsonConvert.SerializeObject(obj);
        var data = new StringContent(json);

        using (var request = new HttpRequestMessage(HttpMethod.Post, _apnBaseUrl + "/3/device/" + deviceToken))
        {
            request.Version = new Version(2, 0);
            request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", jwtToken);
            request.Content = data;

            foreach (var header in headers)
            {
                request.Headers.Add(header.Key, header.Value);
            }

            using (var response = await _client.SendAsync(request))
            {
                success = response.IsSuccessStatusCode;
            }
        }

        return success;
    }

    private Dictionary<string, string> GetHeaders()
    {
        var messageGuid = Guid.NewGuid().ToString();
        var headers = new Dictionary<string, string>();

        headers.Add("apns-id", messageGuid);
        headers.Add("apns-expiration", "0");
        headers.Add("apns-priority", "10");
        headers.Add("apns-topic", _appBundleId);

        return headers;
    }

    public void Dispose()
    {
        _handler.Dispose();
        _client.Dispose();
    }
}

All 32 comments

What is APNs? Without understanding what that is and what server is involved, it won't be possible to diagnose this.

Also, please attach a WireShark trace.

APNs is Apple Push Notification Service. We are using similar code to what @vad3x posted. The code is being used to send a HTTP/2 request to https://api.development.push.apple.com:443 with TLS1.2 and using p12 client certificate. We can confirm the reproduction.

Thanks @eccelor!
I want to mention that this code works well on Linux docker container (the container uses Curl http handler implementation)

Opened an internal tracking bug with WinHttp team
Ref: 13561564

Hello, any progress on this issue?

Hello!
We are also interested in any progress or workaround.
Goal is the same: .net core + HTTP2 + APNs
We can confirm the reproduction.

I prepared short example. Create new net core console project and copy-paste this code into Program.cs:
```c#
using System;
using System.Net.Http;

namespace Http2TestProject
{
public class Program
{
public static void Main(string[] args)
{
var client = new HttpClient();

        var request = new HttpRequestMessage(HttpMethod.Post, "https://api.development.push.apple.com/3/device/");
        request.Version = new Version(2, 0);
        request.Content = new StringContent("{\"aps\":{\"alert\":\"Hello\"}}");

        var response = client.SendAsync(request).GetAwaiter().GetResult();
        Console.WriteLine("Response: " + response.Content.ReadAsStringAsync().GetAwaiter().GetResult());
    }
}

}


The same request using cURL for Windows:
```cmd
C:\Utils\curl-7.56.1-win64-mingw\bin>curl -v -d "{\"aps\":{\"alert\":\"Hello\"}}" --http2 https://api.development.push.apple.com/3/device/
*   Trying 17.188.165.219...
* TCP_NODELAY set
* Connected to api.development.push.apple.com (17.188.165.219) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: C:\Utils\curl-7.56.1-win64-mingw\bin\curl-ca-bundle.crt
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Request CERT (13):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-CHACHA20-POLY1305
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=api.development.push.apple.com; OU=management:idms.group.533599; O=Apple Inc.; ST=California; C=US
*  start date: Jul 25 19:35:19 2017 GMT
*  expire date: Aug 24 19:35:19 2019 GMT
*  subjectAltName: host "api.development.push.apple.com" matched cert's "api.development.push.apple.com"
*  issuer: CN=Apple IST CA 2 - G1; OU=Certification Authority; O=Apple Inc.; C=US
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x1e57af540e0)
> POST /3/device/ HTTP/2
> Host: api.development.push.apple.com
> User-Agent: curl/7.56.1
> Accept: */*
> Content-Length: 25
> Content-Type: application/x-www-form-urlencoded
>
* Connection state changed (MAX_CONCURRENT_STREAMS updated)!
* We are completely uploaded and fine
< HTTP/2 403
< apns-id: E15048F8-E258-9D04-A55D-6A88D10A9FC1
<
{"reason":"MissingProviderToken"}* Connection #0 to host api.development.push.apple.com left intact

C:\Utils\curl-7.56.1-win64-mingw\bin>

Hello, any progress on this issue?

cc: @karelz

There are no updates to report at this time.

@freshe
could you share the codes?

Yes @cgyan009 this is the code i use.
If using .NET Framework you have to get the System.Net.Http.WinHttpHandler (4.4x) nuget package (.NET Core supports HTTP2)


public class CustomHttpHandler : WinHttpHandler { }

public class ApnsProvider : IDisposable
{
    HttpClient _client;
    CustomHttpHandler _handler;

    private readonly string _appBundleId;
    private readonly string _apnBaseUrl;

    public ApnsProvider(string apnUrl, string appBundleId)
    {
        _appBundleId = appBundleId;
        _apnBaseUrl = apnUrl;
        _handler = new CustomHttpHandler();
        _client = new HttpClient(_handler);
    }

    public async Task<bool> SendAsync(string message, string deviceToken, string jwtToken, bool playSound, string debugInfo = null)
    {
        var success = false;
        var headers = GetHeaders();

        var obj = new
        {
            aps = new
            {
                alert = message,
                sound = playSound ? "default" : null
            }
        };

        var json = Newtonsoft.Json.JsonConvert.SerializeObject(obj);
        var data = new StringContent(json);

        using (var request = new HttpRequestMessage(HttpMethod.Post, _apnBaseUrl + "/3/device/" + deviceToken))
        {
            request.Version = new Version(2, 0);
            request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", jwtToken);
            request.Content = data;

            foreach (var header in headers)
            {
                request.Headers.Add(header.Key, header.Value);
            }

            using (var response = await _client.SendAsync(request))
            {
                success = response.IsSuccessStatusCode;
            }
        }

        return success;
    }

    private Dictionary<string, string> GetHeaders()
    {
        var messageGuid = Guid.NewGuid().ToString();
        var headers = new Dictionary<string, string>();

        headers.Add("apns-id", messageGuid);
        headers.Add("apns-expiration", "0");
        headers.Add("apns-priority", "10");
        headers.Add("apns-topic", _appBundleId);

        return headers;
    }

    public void Dispose()
    {
        _handler.Dispose();
        _client.Dispose();
    }
}

@freshe Thank you sooooo much!

Hi, using the JWT is only valid if you have an Apple Developer Account. For iOS MDM push a customer is able to acquire the certificate without having an Apple Developer Account. So any update on whether this should work now using a certificate?

Thanks!!

@freshe your code looks great, however I had a problem saying that WinHttpHandler is not visible class.... then i gave up...

@sisoje Make sure you reference the latest System.Net.Http package with "Exclude Assets".


All

<PackageReference Include="System.Net.Http" Version="4.3.3">
      <ExcludeAssets>All</ExcludeAssets>
</PackageReference>

@davidsh

Is there a possibility that 'tracking-external-issue' label not really correct?

Considering that issue is reproducible with SocketsHttpHandler but works with CurlHandler (when enabled with environment variable in NET Core 2.1 on linux)

Is there a possibility that 'tracking-external-issue' label not really correct?
Considering that issue is reproducible with SocketsHttpHandler but works with CurlHandler (when enabled with environment variable in NET Core 2.1 on linux)

This issue is tracking only WinHttpHandler. So, the 'tracking-external-issue' label is appropriate.

This issue is not tracking SocketsHttpHandler work to enable this scenario. SocketsHttpHandler currently doesn't support HTTP/2.0. So, the scenario is not even possible in SocketsHttpHandler. A future version of .NET Core will update SocketsHttpHandler to support HTTP/2.0.

Still not working.
Having same issue here.
.Net Core is so great.

@davidsh Is there any update from WinHttp team?

@davidsh Is there any update from WinHttp team?

I don't have any updates.

cc: @karelz

It would help us tremendously if everyone affected by this problem would upvote top-post.
We can use it to create more pressure on the OS component team & ask them to communicate their plans here (or in other public place). Thanks!

I'm having the same problem when trying to connect to Apple APNS servers. on .NET Core 2.2 preview.

Setting the environment variable DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER to 0 worked for me.

Would love to see this fixed with the default sockets implementation.

Maybe a slightly improved and 100% working APN wrapper:

```c#
public class ApnHttp2Sender : IDisposable
{
private static readonly Dictionary servers = new Dictionary
{
{ApnServerType.Development, "https://api.development.push.apple.com:443" },
{ApnServerType.Production, "https://api.push.apple.com:443" }
};

    private const string apnidHeader = "apns-id";

    private readonly string p8privateKey;
    private readonly string p8privateKeyId;
    private readonly string teamId;
    private readonly string appBundleIdentifier;
    private readonly ApnServerType server;
    private readonly Lazy<string> jwtToken;
    private readonly Lazy<HttpClient> http;
    private readonly Lazy<Http2Handler> handler;

    /// <summary>
    /// Initialize sender
    /// </summary>
    /// <param name="p8privateKey">p8 certificate string</param>
    /// <param name="privateKeyId">10 digit p8 certificate id. Usually a part of a downloadable certificate filename</param>
    /// <param name="teamId">Apple 10 digit team id</param>
    /// <param name="appBundleIdentifier">App slug / bundle name</param>
    /// <param name="server">Development or Production server</param>
    public ApnHttp2Sender(string p8privateKey, string p8privateKeyId, string teamId, string appBundleIdentifier, ApnServerType server)
    {
        this.p8privateKey = p8privateKey;
        this.p8privateKeyId = p8privateKeyId;
        this.teamId = teamId;
        this.server = server;
        this.appBundleIdentifier = appBundleIdentifier;
        this.jwtToken = new Lazy<string>(() => CreateJwtToken());
        this.handler = new Lazy<Http2Handler>(() => new Http2Handler());
        this.http = new Lazy<HttpClient>(() => new HttpClient(handler.Value));
    }

    public async Task<ApnSendResult> SendAsync<TNotification>(
        string deviceToken,
        TNotification notification,
        string apnsId = null,
        string apnsExpiration = "0",
        string apnsPriority = "10")
    {
        var path = $"/3/device/{deviceToken}";
        var json = JsonConvert.SerializeObject(notification);
        var result = new ApnSendResult { NotificationId = apnsId };
        var headers = new NameValueCollection
        {
            { ":method", "POST" },
            { ":path", path },
            { "authorization", $"bearer {jwtToken.Value}" },
            { "apns-topic", appBundleIdentifier },
            { "apns-expiration", apnsExpiration },
            { "apns-priority", apnsPriority }
        };

        if (!string.IsNullOrWhiteSpace(apnsId))
        {
            headers.Add(apnidHeader, apnsId);
        }

        try
        {
            Retry:
            var request = new HttpRequestMessage(HttpMethod.Post, new Uri(servers[server] + path))
            {
                Version = new Version(2, 0),
                Content = new StringContent(json)
            };
            request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", jwtToken.Value);
            request.Headers.TryAddWithoutValidation(":method", "POST");
            request.Headers.TryAddWithoutValidation(":path", path);
            request.Headers.Add("apns-topic", appBundleIdentifier);
            request.Headers.Add("apns-expiration", apnsExpiration);
            request.Headers.Add("apns-priority", apnsPriority);

            var response = await http.Value.SendAsync(request);
            if (response.StatusCode == HttpStatusCode.TooManyRequests)
            {
                Console.WriteLine("Retrying in a second");
                await Task.Delay(1000);
                goto Retry;
            }

            result.Success = response.IsSuccessStatusCode;
            result.Status = response.StatusCode;
            result.NotificationId = response.Headers.GetValues(apnidHeader).FirstOrDefault();
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
            result.Exception = ex;
        }

        return result;
    }

    public void Dispose()
    {
        if (http.IsValueCreated)
        {
            handler.Value.Dispose();
            http.Value.Dispose();
        }
    }

    private string CreateJwtToken()
    {
        var header = JsonConvert.SerializeObject(new { alg = "ES256", kid = p8privateKeyId });
        var payload = JsonConvert.SerializeObject(new { iss = teamId, iat = ToEpoch(DateTime.UtcNow) });

        var key = CngKey.Import(Convert.FromBase64String(p8privateKey), CngKeyBlobFormat.Pkcs8PrivateBlob);
        using (var dsa = new ECDsaCng(key))
        {
            dsa.HashAlgorithm = CngAlgorithm.Sha256;
            var headerBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(header));
            var payloadBasae64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(payload));
            var unsignedJwtData = $"{headerBase64}.{payloadBasae64}";
            var signature = dsa.SignData(Encoding.UTF8.GetBytes(unsignedJwtData));
            return $"{unsignedJwtData}.{Convert.ToBase64String(signature)}";
        }
    }

    private static int ToEpoch(DateTime time)
    {
        var span = DateTime.UtcNow - new DateTime(1970, 1, 1);
        return Convert.ToInt32(span.TotalSeconds);
    }

    private class Http2Handler : WinHttpHandler { }
}

public class ApnSendResult
{
    public bool Success { get; set; }
    public HttpStatusCode Status { get; set; }
    public string NotificationId { get; set; }
    public Exception Exception { get; set; }

    public override string ToString()
    {
        return $"{Success}, {Status}, {Exception?.Message}";
    }
}

public enum ApnServerType
{
    Development,
    Production
}

```

I still need a solution for this!
APNS + MDM -> No JWT -> need client cert support, Yesterday!

Maybe a slightly improved and 100% working APN wrapper:

    public class ApnHttp2Sender : IDisposable
    {
        private static readonly Dictionary<ApnServerType, string> servers = new Dictionary<ApnServerType, string>
        {
            {ApnServerType.Development, "https://api.development.push.apple.com:443" },
            {ApnServerType.Production, "https://api.push.apple.com:443" }
        };

        private const string apnidHeader = "apns-id";

        private readonly string p8privateKey;
        private readonly string p8privateKeyId;
        private readonly string teamId;
        private readonly string appBundleIdentifier;
        private readonly ApnServerType server;
        private readonly Lazy<string> jwtToken;
        private readonly Lazy<HttpClient> http;
        private readonly Lazy<Http2Handler> handler;

        /// <summary>
        /// Initialize sender
        /// </summary>
        /// <param name="p8privateKey">p8 certificate string</param>
        /// <param name="privateKeyId">10 digit p8 certificate id. Usually a part of a downloadable certificate filename</param>
        /// <param name="teamId">Apple 10 digit team id</param>
        /// <param name="appBundleIdentifier">App slug / bundle name</param>
        /// <param name="server">Development or Production server</param>
        public ApnHttp2Sender(string p8privateKey, string p8privateKeyId, string teamId, string appBundleIdentifier, ApnServerType server)
        {
            this.p8privateKey = p8privateKey;
            this.p8privateKeyId = p8privateKeyId;
            this.teamId = teamId;
            this.server = server;
            this.appBundleIdentifier = appBundleIdentifier;
            this.jwtToken = new Lazy<string>(() => CreateJwtToken());
            this.handler = new Lazy<Http2Handler>(() => new Http2Handler());
            this.http = new Lazy<HttpClient>(() => new HttpClient(handler.Value));
        }

        public async Task<ApnSendResult> SendAsync<TNotification>(
            string deviceToken,
            TNotification notification,
            string apnsId = null,
            string apnsExpiration = "0",
            string apnsPriority = "10")
        {
            var path = $"/3/device/{deviceToken}";
            var json = JsonConvert.SerializeObject(notification);
            var result = new ApnSendResult { NotificationId = apnsId };
            var headers = new NameValueCollection
            {
                { ":method", "POST" },
                { ":path", path },
                { "authorization", $"bearer {jwtToken.Value}" },
                { "apns-topic", appBundleIdentifier },
                { "apns-expiration", apnsExpiration },
                { "apns-priority", apnsPriority }
            };

            if (!string.IsNullOrWhiteSpace(apnsId))
            {
                headers.Add(apnidHeader, apnsId);
            }

            try
            {
                Retry:
                var request = new HttpRequestMessage(HttpMethod.Post, new Uri(servers[server] + path))
                {
                    Version = new Version(2, 0),
                    Content = new StringContent(json)
                };
                request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", jwtToken.Value);
                request.Headers.TryAddWithoutValidation(":method", "POST");
                request.Headers.TryAddWithoutValidation(":path", path);
                request.Headers.Add("apns-topic", appBundleIdentifier);
                request.Headers.Add("apns-expiration", apnsExpiration);
                request.Headers.Add("apns-priority", apnsPriority);

                var response = await http.Value.SendAsync(request);
                if (response.StatusCode == HttpStatusCode.TooManyRequests)
                {
                    Console.WriteLine("Retrying in a second");
                    await Task.Delay(1000);
                    goto Retry;
                }

                result.Success = response.IsSuccessStatusCode;
                result.Status = response.StatusCode;
                result.NotificationId = response.Headers.GetValues(apnidHeader).FirstOrDefault();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
                result.Exception = ex;
            }

            return result;
        }

        public void Dispose()
        {
            if (http.IsValueCreated)
            {
                handler.Value.Dispose();
                http.Value.Dispose();
            }
        }

        private string CreateJwtToken()
        {
            var header = JsonConvert.SerializeObject(new { alg = "ES256", kid = p8privateKeyId });
            var payload = JsonConvert.SerializeObject(new { iss = teamId, iat = ToEpoch(DateTime.UtcNow) });

            var key = CngKey.Import(Convert.FromBase64String(p8privateKey), CngKeyBlobFormat.Pkcs8PrivateBlob);
            using (var dsa = new ECDsaCng(key))
            {
                dsa.HashAlgorithm = CngAlgorithm.Sha256;
                var headerBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(header));
                var payloadBasae64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(payload));
                var unsignedJwtData = $"{headerBase64}.{payloadBasae64}";
                var signature = dsa.SignData(Encoding.UTF8.GetBytes(unsignedJwtData));
                return $"{unsignedJwtData}.{Convert.ToBase64String(signature)}";
            }
        }

        private static int ToEpoch(DateTime time)
        {
            var span = DateTime.UtcNow - new DateTime(1970, 1, 1);
            return Convert.ToInt32(span.TotalSeconds);
        }

        private class Http2Handler : WinHttpHandler { }
    }

    public class ApnSendResult
    {
        public bool Success { get; set; }
        public HttpStatusCode Status { get; set; }
        public string NotificationId { get; set; }
        public Exception Exception { get; set; }

        public override string ToString()
        {
            return $"{Success}, {Status}, {Exception?.Message}";
        }
    }

    public enum ApnServerType
    {
        Development,
        Production
    }

good, i dont see TNotification? but can you make a method that accepts json directly ?

With current .NET Core 3.0 master branch, I now see this:

```c#
var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Post, "https://api.development.push.apple.com/3/device/");
request.Version = new Version(2, 0);
request.Content = new StringContent("{\"aps\":{\"alert\":\"Hello\"}}");

HttpResponseMessage response = client.SendAsync(request).GetAwaiter().GetResult();
Console.WriteLine(response);
string body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
Console.WriteLine(body);


Results:

StatusCode: 403, ReasonPhrase: 'Forbidden', Version: 2.0, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
{
apns-id: 93F7E8EF-168C-9298-61EA-ABB6C0133097
}
{"reason":"MissingProviderToken"}
```

This means that we are successfully connecting with HTTP/2.0.

I didn't test the full scenario since there is no client certificate available for us to try out.

We enabled HTTP/2.0 now as of .NET Core 3.0 Preview 6 (not yet released). The default HTTP stack in .NET Core 3.0 (similar to .NET Core 2.1, 2.2) is our new managed HTTP stack (SocketsHttpHandler).

You can try out our daily builds if you want to explore your scenario more. Let us know if you see other problems after you start using your client certificate.

the same case with me on windows , httpclient( System.Net.Http.WinHttpHandler fix http2 problem ) work with X509Certificate2( apple ) will throw the same exception , so i must use jwt-token to fix it ???

The current .NET Core 3.0 Preview is a go-live release. The default HTTP stack (which is not WinHttpHandler) in .NET Core supports HTTP/2.0 protocol and should work with client-certificates as well.

Please try it out:
https://devblogs.microsoft.com/dotnet/announcing-net-core-3-0-preview-9/

If you see problems with the APNS scenario using .NET Core 3.0, please open a new issue.

There is already an external issue regarding WinHttpHandler (via the Windows team native WinHTTP (see https://github.com/dotnet/corefx/issues/23518#issuecomment-326714456). So, we will close this issue here since there is no actionable change for .NET Core.

Yes @cgyan009 this is the code i use.
If using .NET Framework you have to get the System.Net.Http.WinHttpHandler (4.4x) nuget package (.NET Core supports HTTP2)

public class CustomHttpHandler : WinHttpHandler { }

public class ApnsProvider : IDisposable
{
    HttpClient _client;
    CustomHttpHandler _handler;

    private readonly string _appBundleId;
    private readonly string _apnBaseUrl;

    public ApnsProvider(string apnUrl, string appBundleId)
    {
        _appBundleId = appBundleId;
        _apnBaseUrl = apnUrl;
        _handler = new CustomHttpHandler();
        _client = new HttpClient(_handler);
    }

    public async Task<bool> SendAsync(string message, string deviceToken, string jwtToken, bool playSound, string debugInfo = null)
    {
        var success = false;
        var headers = GetHeaders();

        var obj = new
        {
            aps = new
            {
                alert = message,
                sound = playSound ? "default" : null
            }
        };

        var json = Newtonsoft.Json.JsonConvert.SerializeObject(obj);
        var data = new StringContent(json);

        using (var request = new HttpRequestMessage(HttpMethod.Post, _apnBaseUrl + "/3/device/" + deviceToken))
        {
            request.Version = new Version(2, 0);
            request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", jwtToken);
            request.Content = data;

            foreach (var header in headers)
            {
                request.Headers.Add(header.Key, header.Value);
            }

            using (var response = await _client.SendAsync(request))
            {
                success = response.IsSuccessStatusCode;
            }
        }

        return success;
    }

    private Dictionary<string, string> GetHeaders()
    {
        var messageGuid = Guid.NewGuid().ToString();
        var headers = new Dictionary<string, string>();

        headers.Add("apns-id", messageGuid);
        headers.Add("apns-expiration", "0");
        headers.Add("apns-priority", "10");
        headers.Add("apns-topic", _appBundleId);

        return headers;
    }

    public void Dispose()
    {
        _handler.Dispose();
        _client.Dispose();
    }
}

@freshe for the jwtToken, what cert format u using? is it .p12 or .p8?

@Luqis a .p8 cert

Was this page helpful?
0 / 5 - 0 ratings