7
votes

It seems everyone uses PushSharp for sending push notifications to iOS devices from C#. But that library has a queue it uses instead of sending the notification directly, which then means you need a Windows Service or something to host it properly (per its own documentation) which is overkill for me. I have an incoming web request to my ASP.NET web service and as part of handling that, I want to immediately send a push notification. Simple as that.

Can anyone tell me either how to use PushSharp to send one immediately (bypassing its queue mechanism) or how to properly send the push notification myself? I already have the code that formulates the JSON message, but I don't know how to apply the .p12 file to the request. I can't find any Apple documentation for how to do that.

2
I would like to know how to do this tooMichaelMcCabe
I did find the documentation from Apple where they say it's a TCP binary channel that must be held open for long periods rather than opened for just one message. So I guess queuing is important. Apple says they'll block you as a DoS attack if you don't do it.Andrew Arnott

2 Answers

1
votes

This is a old question, but the answer is not complete.

Here my code:

// private fields
private static readonly string _apnsHostName = ConfigurationManager.AppSettings["APNS:HostName"];
private static readonly int _apnsPort = int.Parse(ConfigurationManager.AppSettings["APNS:Port"]);
private static readonly string _apnsCertPassword = ConfigurationManager.AppSettings["APNS:CertPassword"];
private static readonly string _apnsCertSubject = ConfigurationManager.AppSettings["APNS:CertSubject"];
private static readonly string _apnsCertPath = ConfigurationManager.AppSettings["APNS:CertPath"];

private readonly ILogger _log;

private X509Certificate2Collection _certificatesCollection;


ctor <TAB key>(ILogger log)
{
    _log = log ?? throw new ArgumentNullException(nameof(log));

    // load .p12 certificate in the collection
    var cert = new X509Certificate2(_apnsCertPath, _apnsCertPassword);
    _certificatesCollection = new X509Certificate2Collection(cert);
}

public async Task SendAppleNativeNotificationAsync(string payload, Registration registration)
{
    try
    {
        // handle is the iOS device Token
        var handle = registration.Handle;

        // instantiate new TcpClient with ApnsHostName and Port
        var client = new TcpClient(_apnsHostName, _apnsPort);

        // add fake validation
        var sslStream = new SslStream(client.GetStream(), false, new RemoteCertificateValidationCallback(ValidateServerCertificate), null);

        try
        {
            // authenticate ssl stream on ApnsHostName with your .p12 certificate
            sslStream.AuthenticateAsClient(_apnsHostName, _certificatesCollection, SslProtocols.Tls, false);
            var memoryStream = new MemoryStream();
            var writer = new BinaryWriter(memoryStream);
            // command
            writer.Write((byte)0);
            // first byte of the deviceId length (big-endian first byte)
            writer.Write((byte)0);
            // deviceId length (big-endian second byte)
            writer.Write((byte)32);
            // deviceId data (byte[])
            writer.Write(HexStringToByteArray(handle.ToUpper()));
            // first byte of payload length; (big-endian first byte)
            writer.Write((byte)0);
            // payload length (big-endian second byte)
            writer.Write((byte)Encoding.UTF8.GetByteCount(payload));
            byte[] b1 = Encoding.UTF8.GetBytes(payload);
            // payload data (byte[])
            writer.Write(b1);

            writer.Flush();
            byte[] array = memoryStream.ToArray();

            await sslStream.WriteAsync(array, 0, array.Length);

            // TIP: do not wait a response from APNS because APNS return a response only when an error occurs; 
            // so if you wait the response your code will remain stuck here.
            // await ReadTcpResponse();

            sslStream.Flush();

            // close client
            client.Close();
        }
        catch (AuthenticationException ex)
        {
            _log.Error($"Error sending APNS notification. Exception: {ex}");
            client.Close();
        }
        catch (Exception ex)
        {
            _log.Error($"Error sending APNS notification. Exception: {ex}");
            client.Close();
        }
    }
    catch (Exception ex)
    {
        _log.Error($"Error sending APNS notification. Exception: {ex}");
    }
}

private static byte[] HexStringToByteArray(string hex)
{
    if (hex == null)
    {
        return null;
    }

    // added for newest devices (>= iPhone 8)
    if (hex.Length % 2 == 1)
    {
        hex = '0' + hex;
    }

    return Enumerable.Range(0, hex.Length)
                     .Where(x => x % 2 == 0)
                     .Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
                     .ToArray();
}

private static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
    return true;
    //if (sslPolicyErrors == SslPolicyErrors.None)
    //    return true;

    //// do not allow this client to communicate with unauthenticated servers.
    //return false;
}

private async Task<byte[]> ReadTcpResponse(SslStream sslStream)
{
    MemoryStream ms = new MemoryStream();

    byte[] buffer = new byte[2048];

    int bytes = -1;
    do
    {
        bytes = await sslStream.ReadAsync(buffer, 0, buffer.Length);

        await ms.WriteAsync(buffer, 0, bytes);

    } while (bytes != 0);

    return ms.ToArray();
}

TIP: with iOS13, device token is received differently.

> iOS 12 (deviceToken as NSData).description -> "< your_token_here >"
> iOS 13 (deviceToken as NSData).description -> "{ length = 32, bytes = 0x321e1ba1c1ba...token_in_bytes }"

With iOS13 you must convert token to string or skip the method 'HexStringToByteArray' because you already have a byte[].

If you have question, I'm glad to answer.

-2
votes

I spent many hours trying to find a way to push notifications, then I found a piece of code that did it for me.

First of all make sure that you installed the certificates correctly, here is a link which will help you. https://arashnorouzi.wordpress.com/2011/04/13/sending-apple-push-notifications-in-asp-net-%E2%80%93-part-3-apns-certificates-registration-on-windows/

Here is a code I used to push notifications:

public static bool ConnectToAPNS(string deviceId, string message)
    {
        X509Certificate2Collection certs = new X509Certificate2Collection();

        // Add the Apple cert to our collection
        certs.Add(getServerCert());

        // Apple development server address
        string apsHost;
        /*
        if (getServerCert().ToString().Contains("Production"))
            apsHost = "gateway.push.apple.com";
        else*/
        apsHost = "gateway.sandbox.push.apple.com";

        // Create a TCP socket connection to the Apple server on port 2195
        TcpClient tcpClient = new TcpClient(apsHost, 2195);

        // Create a new SSL stream over the connection
        SslStream sslStream1 = new SslStream(tcpClient.GetStream());

        // Authenticate using the Apple cert
        sslStream1.AuthenticateAsClient(apsHost, certs, SslProtocols.Default, false);

        PushMessage(deviceId, message, sslStream1);

        return true;
    }

private static X509Certificate getServerCert()
    {
        X509Certificate test = new X509Certificate();

        //Open the cert store on local machine
        X509Store store = new X509Store(StoreLocation.CurrentUser);

        if (store != null)
        {
            // store exists, so open it and search through the certs for the Apple Cert
            store.Open(OpenFlags.ReadOnly);
            X509Certificate2Collection certs = store.Certificates;

            if (certs.Count > 0)
            {
                int i;
                for (i = 0; i < certs.Count; i++)
                {
                    X509Certificate2 cert = certs[i];

                    if (cert.FriendlyName.Contains("Apple Development IOS Push Services"))
                    {
                        //Cert found, so return it.
                        Console.WriteLine("Found It!");
                        return certs[i];
                    }
                }
            }
            return test;
        }
        return test;
    }

private static byte[] HexToData(string hexString)
    {
        if (hexString == null)
            return null;

        if (hexString.Length % 2 == 1)
            hexString = '0' + hexString; // Up to you whether to pad the first or last byte

        byte[] data = new byte[hexString.Length / 2];

        for (int i = 0; i < data.Length; i++)
            data[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);

        return data;
    }

Note that this code is for development certificates "Apple Development IOS Push Services".