From d3c9015ac8251f145fe072b481dd97257740b19a Mon Sep 17 00:00:00 2001 From: Afshin Arani Date: Tue, 30 Aug 2022 16:42:16 +0430 Subject: [PATCH] WIP: dotAPNS integration --- .gitmodules | 3 - HttpTwo | 1 - PushSharp.Apple/ApnsConfiguration.cs | 119 +---- PushSharp.Apple/ApnsConnection.cs | 464 +----------------- PushSharp.Apple/ApnsFeedbackService.cs | 127 ----- PushSharp.Apple/ApnsHttp2Configuration.cs | 156 ------ PushSharp.Apple/ApnsHttp2Connection.cs | 135 ----- PushSharp.Apple/ApnsHttp2Notification.cs | 89 ---- PushSharp.Apple/ApnsHttp2ServiceConnection.cs | 43 -- PushSharp.Apple/ApnsNotification.cs | 160 ++---- PushSharp.Apple/ApnsServiceConnection.cs | 10 +- PushSharp.Apple/Exceptions.cs | 63 --- PushSharp.Apple/PushSharp.Apple.csproj | 1 + PushSharp.Tests/ApnsRealTest.cs | 18 +- PushSharp.Tests/ApnsTests.cs | 169 ------- PushSharp.Tests/Servers/TestApnsServer.cs | 343 ------------- 16 files changed, 68 insertions(+), 1833 deletions(-) delete mode 160000 HttpTwo delete mode 100644 PushSharp.Apple/ApnsFeedbackService.cs delete mode 100644 PushSharp.Apple/ApnsHttp2Configuration.cs delete mode 100644 PushSharp.Apple/ApnsHttp2Connection.cs delete mode 100644 PushSharp.Apple/ApnsHttp2Notification.cs delete mode 100644 PushSharp.Apple/ApnsHttp2ServiceConnection.cs delete mode 100644 PushSharp.Apple/Exceptions.cs delete mode 100644 PushSharp.Tests/ApnsTests.cs delete mode 100644 PushSharp.Tests/Servers/TestApnsServer.cs diff --git a/.gitmodules b/.gitmodules index 5e19cd95..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "HttpTwo"] - path = HttpTwo - url = git@github.com:Redth/HttpTwo.git diff --git a/HttpTwo b/HttpTwo deleted file mode 160000 index 7ba7b0e8..00000000 --- a/HttpTwo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7ba7b0e814d326b063f45e95376b264bfd288980 diff --git a/PushSharp.Apple/ApnsConfiguration.cs b/PushSharp.Apple/ApnsConfiguration.cs index 464a5e62..162b59ba 100644 --- a/PushSharp.Apple/ApnsConfiguration.cs +++ b/PushSharp.Apple/ApnsConfiguration.cs @@ -7,21 +7,6 @@ namespace PushSharp.Apple { public class ApnsConfiguration { - #region Constants - const string APNS_SANDBOX_HOST = "gateway.sandbox.push.apple.com"; - const string APNS_PRODUCTION_HOST = "gateway.push.apple.com"; - - const string APNS_SANDBOX_FEEDBACK_HOST = "feedback.sandbox.push.apple.com"; - const string APNS_PRODUCTION_FEEDBACK_HOST = "feedback.push.apple.com"; - - const int APNS_SANDBOX_PORT = 2195; - const int APNS_PRODUCTION_PORT = 2195; - - const int APNS_SANDBOX_FEEDBACK_PORT = 2196; - const int APNS_PRODUCTION_FEEDBACK_PORT = 2196; - - #endregion - public ApnsConfiguration (ApnsServerEnvironment serverEnvironment, string certificateFile, string certificateFilePwd, bool validateIsApnsCertificate) : this (serverEnvironment, System.IO.File.ReadAllBytes (certificateFile), certificateFilePwd, validateIsApnsCertificate) { @@ -44,15 +29,6 @@ public ApnsConfiguration (ApnsServerEnvironment serverEnvironment, byte[] certif { } - public ApnsConfiguration (string overrideHost, int overridePort, bool skipSsl = true) - { - SkipSsl = skipSsl; - - Initialize (ApnsServerEnvironment.Sandbox, null, false); - - OverrideServer (overrideHost, overridePort); - } - public ApnsConfiguration (ApnsServerEnvironment serverEnvironment, X509Certificate2 certificate) { Initialize (serverEnvironment, certificate, true); @@ -67,37 +43,10 @@ void Initialize (ApnsServerEnvironment serverEnvironment, X509Certificate2 certi { ServerEnvironment = serverEnvironment; - var production = serverEnvironment == ApnsServerEnvironment.Production; - - Host = production ? APNS_PRODUCTION_HOST : APNS_SANDBOX_HOST; - FeedbackHost = production ? APNS_PRODUCTION_FEEDBACK_HOST : APNS_SANDBOX_FEEDBACK_HOST; - Port = production ? APNS_PRODUCTION_PORT : APNS_SANDBOX_PORT; - FeedbackPort = production ? APNS_PRODUCTION_FEEDBACK_PORT : APNS_SANDBOX_FEEDBACK_PORT; - Certificate = certificate; - MillisecondsToWaitBeforeMessageDeclaredSuccess = 3000; - ConnectionTimeout = 10000; - MaxConnectionAttempts = 3; - - FeedbackIntervalMinutes = 10; - FeedbackTimeIsUTC = false; - - AdditionalCertificates = new List (); - AddLocalAndMachineCertificateStores = false; - if (validateIsApnsCertificate) CheckIsApnsCertificate (); - - ValidateServerCertificate = false; - - KeepAlivePeriod = TimeSpan.FromMinutes (20); - KeepAliveRetryPeriod = TimeSpan.FromSeconds (30); - - InternalBatchSize = 1000; - InternalBatchingWaitPeriod = TimeSpan.FromMilliseconds (750); - - InternalBatchFailureRetryCount = 1; } @@ -124,76 +73,10 @@ void CheckIsApnsCertificate () } } - public void OverrideServer (string host, int port) - { - Host = host; - Port = port; - } - - public void OverrideFeedbackServer (string host, int port) - { - FeedbackHost = host; - FeedbackPort = port; - } - - public string Host { get; private set; } - - public int Port { get; private set; } - - public string FeedbackHost { get; private set; } - - public int FeedbackPort { get; private set; } + public bool UseBackupPort { get; set; } public X509Certificate2 Certificate { get; private set; } - public List AdditionalCertificates { get; private set; } - - public bool AddLocalAndMachineCertificateStores { get; set; } - - public bool SkipSsl { get; set; } - - public int MillisecondsToWaitBeforeMessageDeclaredSuccess { get; set; } - - public int FeedbackIntervalMinutes { get; set; } - - public bool FeedbackTimeIsUTC { get; set; } - - public bool ValidateServerCertificate { get; set; } - - public int ConnectionTimeout { get; set; } - - public int MaxConnectionAttempts { get; set; } - - /// - /// The internal connection to APNS servers batches notifications to send before waiting for errors for a short time. - /// This value will set a maximum size per batch. The default value is 1000. You probably do not want this higher than 7500. - /// - /// The size of the internal batch. - public int InternalBatchSize { get; set; } - - /// - /// How long the internal connection to APNS servers should idle while collecting notifications in a batch to send. - /// Setting this value too low might result in many smaller batches being used. - /// - /// The internal batching wait period. - public TimeSpan InternalBatchingWaitPeriod { get; set; } - - /// - /// How many times the internal batch will retry to send in case of network failure. The default value is 1. - /// - /// The internal batch failure retry count. - public int InternalBatchFailureRetryCount { get; set; } - - /// - /// Gets or sets the keep alive period to set on the APNS socket - /// - public TimeSpan KeepAlivePeriod { get; set; } - - /// - /// Gets or sets the keep alive retry period to set on the APNS socket - /// - public TimeSpan KeepAliveRetryPeriod { get; set; } - /// /// Gets the configured APNS server environment /// diff --git a/PushSharp.Apple/ApnsConnection.cs b/PushSharp.Apple/ApnsConnection.cs index 371151b3..5b646caa 100644 --- a/PushSharp.Apple/ApnsConnection.cs +++ b/PushSharp.Apple/ApnsConnection.cs @@ -8,471 +8,51 @@ using System.Threading; using System.Net; using PushSharp.Core; +using dotAPNS; namespace PushSharp.Apple { public class ApnsConnection { - static int ID = 0; - public ApnsConnection (ApnsConfiguration configuration) { - id = ++ID; - if (id >= int.MaxValue) - ID = 0; - Configuration = configuration; certificate = Configuration.Certificate; - certificates = new X509CertificateCollection (); - - // Add local/machine certificate stores to our collection if requested - if (Configuration.AddLocalAndMachineCertificateStores) { - var store = new X509Store (StoreLocation.LocalMachine); - certificates.AddRange (store.Certificates); - - store = new X509Store (StoreLocation.CurrentUser); - certificates.AddRange (store.Certificates); - } - - // Add optionally specified additional certs into our collection - if (Configuration.AdditionalCertificates != null) { - foreach (var addlCert in Configuration.AdditionalCertificates) - certificates.Add (addlCert); - } - - // Finally, add the main private cert for authenticating to our collection - if (certificate != null) - certificates.Add (certificate); - - timerBatchWait = new Timer (new TimerCallback (async state => { - - await batchSendSemaphore.WaitAsync (); - try { - await SendBatch ().ConfigureAwait (false); - } finally { - batchSendSemaphore.Release (); - } + client = + ApnsClient + .CreateUsingCert(certificate); - }), null, Timeout.Infinite, Timeout.Infinite); + if (configuration.UseBackupPort) + client.UseBackupPort(); } public ApnsConfiguration Configuration { get; private set; } - X509CertificateCollection certificates; - X509Certificate2 certificate; - TcpClient client; - SslStream stream; - Stream networkStream; - byte[] buffer = new byte[6]; - int id; - - - SemaphoreSlim connectingSemaphore = new SemaphoreSlim (1); - SemaphoreSlim batchSendSemaphore = new SemaphoreSlim (1); - object notificationBatchQueueLock = new object (); - - //readonly object connectingLock = new object (); - Queue notifications = new Queue (); - List sent = new List (); - - Timer timerBatchWait; - - public void Send (CompletableApnsNotification notification) - { - lock (notificationBatchQueueLock) { - - notifications.Enqueue (notification); - - if (notifications.Count >= Configuration.InternalBatchSize) { - - // Make the timer fire immediately and send a batch off - timerBatchWait.Change (0, Timeout.Infinite); - return; - } - - // Restart the timer to wait for more notifications to be batched - // This timer will keep getting 'restarted' before firing as long as notifications - // are queued before the timer's due time - // if the timer is actually called, it means no more notifications were queued, - // so we should flush out the queue instead of waiting for more to be batched as they - // might not ever come and we don't want to leave anything stranded in the queue - timerBatchWait.Change (Configuration.InternalBatchingWaitPeriod, Timeout.InfiniteTimeSpan); - } - } - - long batchId = 0; - - async Task SendBatch () - { - batchId++; - if (batchId >= long.MaxValue) - batchId = 1; - - // Pause the timer - timerBatchWait.Change (Timeout.Infinite, Timeout.Infinite); - - if (notifications.Count <= 0) - return; - - // Let's store the batch items to send internally - var toSend = new List (); - - while (notifications.Count > 0 && toSend.Count < Configuration.InternalBatchSize) { - var n = notifications.Dequeue (); - toSend.Add (n); - } - - - Log.Info ("APNS-Client[{0}]: Sending Batch ID={1}, Count={2}", id, batchId, toSend.Count); - - try { - - var data = createBatch (toSend); - - if (data != null && data.Length > 0) { - - for (var i = 0; i <= Configuration.InternalBatchFailureRetryCount; i++) { - - await connectingSemaphore.WaitAsync (); + private readonly X509Certificate2 certificate; + private readonly ApnsClient client; - try { - // See if we need to connect - if (!socketCanWrite () || i > 0) - await connect (); - } finally { - connectingSemaphore.Release (); - } - - try { - await networkStream.WriteAsync(data, 0, data.Length).ConfigureAwait(false); - break; - } catch (Exception ex) when (i != Configuration.InternalBatchFailureRetryCount) { - Log.Info("APNS-CLIENT[{0}]: Retrying Batch: Batch ID={1}, Error={2}", id, batchId, ex); - } - } - - foreach (var n in toSend) - sent.Add (new SentNotification (n)); - } - - } catch (Exception ex) { - Log.Error ("APNS-CLIENT[{0}]: Send Batch Error: Batch ID={1}, Error={2}", id, batchId, ex); - foreach (var n in toSend) - n.CompleteFailed (new ApnsNotificationException (ApnsNotificationErrorStatusCode.ConnectionError, n.Notification, ex)); - } - - Log.Info ("APNS-Client[{0}]: Sent Batch, waiting for possible response...", id); - - try { - await Reader (); - } catch (Exception ex) { - Log.Error ("APNS-Client[{0}]: Reader Exception: {1}", id, ex); - } - - Log.Info ("APNS-Client[{0}]: Done Reading for Batch ID={1}, reseting batch timer...", id, batchId); - - // Restart the timer for the next batch - timerBatchWait.Change (Configuration.InternalBatchingWaitPeriod, Timeout.InfiniteTimeSpan); - } - - byte[] createBatch (List toSend) + public async Task SendAsync (ApnsNotification notification) { - if (toSend == null || toSend.Count <= 0) - return null; - - var batchData = new List (); - - // Add all the frame data - foreach (var n in toSend) - batchData.AddRange (n.Notification.ToBytes ()); - - return batchData.ToArray (); - } - - async Task Reader () - { - var readCancelToken = new CancellationTokenSource (); - - // We are going to read from the stream, but the stream *may* not ever have any data for us to - // read (in the case that all the messages sent successfully, apple will send us nothing - // So, let's make our read timeout after a reasonable amount of time to wait for apple to tell - // us of any errors that happened. - readCancelToken.CancelAfter (750); - - int len = -1; - - while (!readCancelToken.IsCancellationRequested) { - - // See if there's data to read - if (client.Client.Available > 0) { - Log.Info ("APNS-Client[{0}]: Data Available...", id); - len = await networkStream.ReadAsync (buffer, 0, buffer.Length).ConfigureAwait (false); - Log.Info ("APNS-Client[{0}]: Finished Read.", id); - break; - } - - // Let's not tie up too much CPU waiting... - await Task.Delay (50).ConfigureAwait (false); - } - - Log.Info ("APNS-Client[{0}]: Received {1} bytes response...", id, len); - - // If we got no data back, and we didn't end up canceling, the connection must have closed - if (len == 0) { - - Log.Info ("APNS-Client[{0}]: Server Closed Connection...", id); - - // Connection was closed - disconnect (); - return; - - } else if (len < 0) { //If we timed out waiting, but got no data to read, everything must be ok! - - Log.Info ("APNS-Client[{0}]: Batch (ID={1}) completed with no error response...", id, batchId); - - //Everything was ok, let's assume all 'sent' succeeded - foreach (var s in sent) - s.Notification.CompleteSuccessfully (); - - sent.Clear (); - return; - } - - // If we make it here, we did get data back, so we have errors - - Log.Info ("APNS-Client[{0}]: Batch (ID={1}) completed with error response...", id, batchId); - - // If we made it here, we did receive some data, so let's parse the error - var status = buffer [1]; - var identifier = IPAddress.NetworkToHostOrder (BitConverter.ToInt32 (buffer, 2)); - - // Let's handle the failure - //Get the index of our failed notification (by identifier) - var failedIndex = sent.FindIndex (n => n.Identifier == identifier); - - // If we didn't find an index for the failed notification, something is wrong - // Let's just return - if (failedIndex < 0) - return; + ApnsResponse response; - // Get all the notifications before the failed one and mark them as sent! - if (failedIndex > 0) { - // Get all the notifications sent before the one that failed - // We can assume that these all succeeded - var successful = sent.GetRange (0, failedIndex); //TODO: Should it be failedIndex - 1? - - // Complete all the successfully sent notifications - foreach (var s in successful) - s.Notification.CompleteSuccessfully (); - - // Remove all the successful notifications from the sent list - // This should mean the failed notification is now at index 0 - sent.RemoveRange (0, failedIndex); - } - - //Get the failed notification itself - var failedNotification = sent [0]; - - //Fail and remove the failed index from the list - Log.Info ("APNS-Client[{0}]: Failing Notification {1}", id, failedNotification.Identifier); - failedNotification.Notification.CompleteFailed ( - new ApnsNotificationException (status, failedNotification.Notification.Notification)); - - // Now remove the failed notification from the sent list - sent.RemoveAt (0); - - // The remaining items in the list were sent after the failed notification - // we can assume these were ignored by apple so we need to send them again - // Requeue the remaining notifications - foreach (var s in sent) - notifications.Enqueue (s.Notification); - - // Clear our sent list - sent.Clear (); - - // Apple will close our connection after this anyway - disconnect (); - } - - bool socketCanWrite () - { - if (client == null) - return false; - - if (networkStream == null || !networkStream.CanWrite) - return false; - - if (!client.Client.Connected) - return false; - - var p = client.Client.Poll (1000, SelectMode.SelectWrite); - - Log.Info ("APNS-Client[{0}]: Can Write? {1}", id, p); - - return p; - } - - async Task connect () - { - if (client != null) - disconnect (); - - Log.Info ("APNS-Client[{0}]: Connecting (Batch ID={1})", id, batchId); - - client = new TcpClient (); - - try { - await client.ConnectAsync (Configuration.Host, Configuration.Port).ConfigureAwait (false); - - //Set keep alive on the socket may help maintain our APNS connection - try { - client.Client.SetSocketOption (SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); - } catch { - } - - //Really not sure if this will work on MONO.... - // This may help windows azure users - try { - SetSocketKeepAliveValues (client.Client, (int)Configuration.KeepAlivePeriod.TotalMilliseconds, (int)Configuration.KeepAliveRetryPeriod.TotalMilliseconds); - } catch { - } - } catch (Exception ex) { - throw new ApnsConnectionException ("Failed to Connect, check your firewall settings!", ex); - } - - // We can configure skipping ssl all together, ie: if we want to hit a test server - if (Configuration.SkipSsl) { - networkStream = client.GetStream (); - } else { - - // Create our ssl stream - stream = new SslStream (client.GetStream (), - false, - ValidateRemoteCertificate, - (sender, targetHost, localCerts, remoteCert, acceptableIssuers) => certificate); - - try { - var tls = System.Security.Authentication.SslProtocols.Tls | System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls12; - stream.AuthenticateAsClient(Configuration.Host, certificates, tls, false); - } catch (System.Security.Authentication.AuthenticationException ex) { - throw new ApnsConnectionException ("SSL Stream Failed to Authenticate as Client", ex); - } - - if (!stream.IsMutuallyAuthenticated) - throw new ApnsConnectionException ("SSL Stream Failed to Authenticate", null); - - if (!stream.CanWrite) - throw new ApnsConnectionException ("SSL Stream is not Writable", null); - - networkStream = stream; - } - - Log.Info ("APNS-Client[{0}]: Connected (Batch ID={1})", id, batchId); - } - - void disconnect () - { - Log.Info ("APNS-Client[{0}]: Disconnecting (Batch ID={1})", id, batchId); - - //We now expect apple to close the connection on us anyway, so let's try and close things - // up here as well to get a head start - //Hopefully this way we have less messages written to the stream that we have to requeue - try { stream.Close (); } catch { } - try { stream.Dispose (); } catch { } - - try { networkStream.Close (); } catch { } - try { networkStream.Dispose (); } catch { } - - try { client.Client.Shutdown (SocketShutdown.Both); } catch { } - try { client.Client.Dispose (); } catch { } - - try { client.Close (); } catch { } - - client = null; - networkStream = null; - stream = null; - - Log.Info ("APNS-Client[{0}]: Disconnected (Batch ID={1})", id, batchId); - } - - bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors policyErrors) - { - if (Configuration.ValidateServerCertificate) - return policyErrors == SslPolicyErrors.None; - - return true; - } - - - - public class SentNotification - { - public SentNotification (CompletableApnsNotification notification) - { - this.Notification = notification; - this.SentAt = DateTime.UtcNow; - this.Identifier = notification.Notification.Identifier; - } - - public CompletableApnsNotification Notification { get; set; } - - public DateTime SentAt { get; set; } - - public int Identifier { get; set; } - } - - public class CompletableApnsNotification - { - public CompletableApnsNotification (ApnsNotification notification) - { - Notification = notification; - completionSource = new TaskCompletionSource (); - } - - public ApnsNotification Notification { get; private set; } - - TaskCompletionSource completionSource; - - public Task WaitForComplete () - { - return completionSource.Task; - } - - public void CompleteSuccessfully () + if (Configuration.ServerEnvironment == ApnsConfiguration.ApnsServerEnvironment.Production) + response = await client.SendAsync(notification.ToApplePush()); + else { - completionSource.SetResult (null); - } + var applePushToSend = + notification + .ToApplePush() + .SendToDevelopmentServer(); - public void CompleteFailed (Exception ex) - { - completionSource.SetResult (ex); + response = await client.SendAsync(applePushToSend); } - } - /// - /// Using IOControl code to configue socket KeepAliveValues for line disconnection detection(because default is toooo slow) - /// - /// TcpClient - /// The keep alive time. (ms) - /// The keep alive interval. (ms) - public static void SetSocketKeepAliveValues (Socket socket, int KeepAliveTime, int KeepAliveInterval) - { - //KeepAliveTime: default value is 2hr - //KeepAliveInterval: default value is 1s and Detect 5 times - - uint dummy = 0; //lenth = 4 - byte[] inOptionValues = new byte[System.Runtime.InteropServices.Marshal.SizeOf (dummy) * 3]; //size = lenth * 3 = 12 - - BitConverter.GetBytes ((uint)1).CopyTo (inOptionValues, 0); - BitConverter.GetBytes ((uint)KeepAliveTime).CopyTo (inOptionValues, System.Runtime.InteropServices.Marshal.SizeOf (dummy)); - BitConverter.GetBytes ((uint)KeepAliveInterval).CopyTo (inOptionValues, System.Runtime.InteropServices.Marshal.SizeOf (dummy) * 2); - // of course there are other ways to marshal up this byte array, this is just one way - // call WSAIoctl via IOControl - - // .net 3.5 type - socket.IOControl (IOControlCode.KeepAliveValues, inOptionValues, null); + if (response.IsSuccessful) + return; + else + throw new NotificationException($"Notification send failed, Reason = {response.ReasonString}", notification); } } } diff --git a/PushSharp.Apple/ApnsFeedbackService.cs b/PushSharp.Apple/ApnsFeedbackService.cs deleted file mode 100644 index 1d078318..00000000 --- a/PushSharp.Apple/ApnsFeedbackService.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Net; -using System.Net.Sockets; -using System.Net.Security; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; - -namespace PushSharp.Apple -{ - public class FeedbackService - { - public FeedbackService (ApnsConfiguration configuration) - { - Configuration = configuration; - } - - public ApnsConfiguration Configuration { get; private set; } - - public delegate void FeedbackReceivedDelegate (string deviceToken, DateTime timestamp); - public event FeedbackReceivedDelegate FeedbackReceived; - - public void Check () - { - var encoding = Encoding.ASCII; - - var certificate = Configuration.Certificate; - - var certificates = new X509CertificateCollection(); - certificates.Add(certificate); - - var client = new TcpClient (Configuration.FeedbackHost, Configuration.FeedbackPort); - - var stream = new SslStream (client.GetStream(), true, - (sender, cert, chain, sslErrs) => { return true; }, - (sender, targetHost, localCerts, remoteCert, acceptableIssuers) => { return certificate; }); - - var tls = System.Security.Authentication.SslProtocols.Tls | System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls12; - stream.AuthenticateAsClient(Configuration.FeedbackHost, certificates, tls, false); - - - //Set up - byte[] buffer = new byte[4096]; - int recd = 0; - var data = new List (); - - //Get the first feedback - recd = stream.Read(buffer, 0, buffer.Length); - - //Continue while we have results and are not disposing - while (recd > 0) - { - // Add the received data to a list buffer to work with (easier to manipulate) - for (int i = 0; i < recd; i++) - data.Add (buffer [i]); - - //Process each complete notification "packet" available in the buffer - while (data.Count >= (4 + 2 + 32)) // Minimum size for a valid packet - { - var secondsBuffer = data.GetRange (0, 4).ToArray (); - var tokenLengthBuffer = data.GetRange (4, 2).ToArray (); - - // Get our seconds since epoch - // Check endianness and reverse if needed - if (BitConverter.IsLittleEndian) - Array.Reverse (secondsBuffer); - var seconds = BitConverter.ToInt32 (secondsBuffer, 0); - - //Add seconds since 1970 to that date, in UTC - var timestamp = new DateTime (1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds (seconds); - - //flag to allow feedback times in UTC or local, but default is local - if (!Configuration.FeedbackTimeIsUTC) - timestamp = timestamp.ToLocalTime(); - - - if (BitConverter.IsLittleEndian) - Array.Reverse (tokenLengthBuffer); - var tokenLength = BitConverter.ToInt16 (tokenLengthBuffer, 0); - - if (data.Count >= 4 + 2 + tokenLength) { - - var tokenBuffer = data.GetRange (6, tokenLength).ToArray (); - // Strings shouldn't care about endian-ness... this shouldn't be reversed - //if (BitConverter.IsLittleEndian) - // Array.Reverse (tokenBuffer); - var token = BitConverter.ToString (tokenBuffer).Replace ("-", "").ToLower ().Trim (); - - // Remove what we parsed from the buffer - data.RemoveRange (0, 4 + 2 + tokenLength); - - // Raise the event to the consumer - var evt = FeedbackReceived; - if (evt != null) - evt (token, timestamp); - } else { - continue; - } - - } - - //Read the next feedback - recd = stream.Read (buffer, 0, buffer.Length); - } - - try - { - stream.Close (); - stream.Dispose(); - } - catch { } - - try - { - client.Client.Shutdown (SocketShutdown.Both); - client.Client.Dispose (); - } - catch { } - - try { client.Close (); } catch { } - - } - } -} diff --git a/PushSharp.Apple/ApnsHttp2Configuration.cs b/PushSharp.Apple/ApnsHttp2Configuration.cs deleted file mode 100644 index d3ba0e76..00000000 --- a/PushSharp.Apple/ApnsHttp2Configuration.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Security.Cryptography.X509Certificates; - -namespace PushSharp.Apple -{ - public class ApnsHttp2Configuration - { - #region Constants - const string APNS_SANDBOX_HOST = "api.development.push.apple.com"; - const string APNS_PRODUCTION_HOST = "api.push.apple.com"; - - const uint APNS_SANDBOX_PORT = 443; - const uint APNS_PRODUCTION_PORT = 443; - #endregion - - public ApnsHttp2Configuration (ApnsServerEnvironment serverEnvironment, string certificateFile, string certificateFilePwd) - : this (serverEnvironment, System.IO.File.ReadAllBytes (certificateFile), certificateFilePwd) - { - } - - public ApnsHttp2Configuration (ApnsServerEnvironment serverEnvironment, byte[] certificateData, string certificateFilePwd) - : this (serverEnvironment, new X509Certificate2 (certificateData, certificateFilePwd, - X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable)) - { - } - - public ApnsHttp2Configuration (string overrideHost, uint overridePort, bool skipSsl = true) - { - SkipSsl = skipSsl; - - Initialize (ApnsServerEnvironment.Sandbox, null); - - OverrideServer (overrideHost, overridePort); - } - - public ApnsHttp2Configuration (ApnsServerEnvironment serverEnvironment, X509Certificate2 certificate) - { - Initialize (serverEnvironment, certificate); - } - - void Initialize (ApnsServerEnvironment serverEnvironment, X509Certificate2 certificate) - { - var production = serverEnvironment == ApnsServerEnvironment.Production; - - Host = production ? APNS_PRODUCTION_HOST : APNS_SANDBOX_HOST; - Port = production ? APNS_PRODUCTION_PORT : APNS_SANDBOX_PORT; - - Certificate = certificate; - - MillisecondsToWaitBeforeMessageDeclaredSuccess = 3000; - ConnectionTimeout = 10000; - MaxConnectionAttempts = 3; - - FeedbackIntervalMinutes = 10; - FeedbackTimeIsUTC = false; - - AdditionalCertificates = new List (); - AddLocalAndMachineCertificateStores = false; - - CheckIsApnsCertificate (); - - ValidateServerCertificate = false; - - KeepAlivePeriod = TimeSpan.FromMinutes (20); - KeepAliveRetryPeriod = TimeSpan.FromSeconds (30); - - InternalBatchSize = 1000; - InternalBatchingWaitPeriod = TimeSpan.FromMilliseconds (750); - - InternalBatchFailureRetryCount = 1; - } - - - void CheckIsApnsCertificate () - { - if (Certificate != null) { - var issuerName = Certificate.IssuerName.Name; - var commonName = Certificate.SubjectName.Name; - - if (!issuerName.Contains ("Apple")) - throw new ApnsConnectionException ("Your Certificate does not appear to be issued by Apple! Please check to ensure you have the correct certificate!"); - if (!commonName.Contains ("Apple Push Services:")) - throw new ApnsConnectionException ("Your Certificate is not in the new combined Sandbox/Production APNS certificate format, please create a new single certificate to use"); - - } else { - throw new ApnsConnectionException ("You must provide a Certificate to connect to APNS with!"); - } - } - - public void OverrideServer (string host, uint port) - { - Host = host; - Port = port; - } - - public string Host { get; private set; } - - public uint Port { get; private set; } - - public X509Certificate2 Certificate { get; private set; } - - public List AdditionalCertificates { get; private set; } - - public bool AddLocalAndMachineCertificateStores { get; set; } - - public bool SkipSsl { get; set; } - - public int MillisecondsToWaitBeforeMessageDeclaredSuccess { get; set; } - - public int FeedbackIntervalMinutes { get; set; } - - public bool FeedbackTimeIsUTC { get; set; } - - public bool ValidateServerCertificate { get; set; } - - public int ConnectionTimeout { get; set; } - - public int MaxConnectionAttempts { get; set; } - - /// - /// The internal connection to APNS servers batches notifications to send before waiting for errors for a short time. - /// This value will set a maximum size per batch. The default value is 1000. You probably do not want this higher than 7500. - /// - /// The size of the internal batch. - public int InternalBatchSize { get; set; } - - /// - /// How long the internal connection to APNS servers should idle while collecting notifications in a batch to send. - /// Setting this value too low might result in many smaller batches being used. - /// - /// The internal batching wait period. - public TimeSpan InternalBatchingWaitPeriod { get; set; } - - /// - /// How many times the internal batch will retry to send in case of network failure. The default value is 1. - /// - /// The internal batch failure retry count. - public int InternalBatchFailureRetryCount { get; set; } - - /// - /// Gets or sets the keep alive period to set on the APNS socket - /// - public TimeSpan KeepAlivePeriod { get; set; } - - /// - /// Gets or sets the keep alive retry period to set on the APNS socket - /// - public TimeSpan KeepAliveRetryPeriod { get; set; } - - public enum ApnsServerEnvironment { - Sandbox, - Production - } - } -} \ No newline at end of file diff --git a/PushSharp.Apple/ApnsHttp2Connection.cs b/PushSharp.Apple/ApnsHttp2Connection.cs deleted file mode 100644 index 543ec43c..00000000 --- a/PushSharp.Apple/ApnsHttp2Connection.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; -using System.Collections.Specialized; -using System.Collections.Generic; -using System.Text; -using System.Linq; -using Newtonsoft.Json.Linq; -using System.Net; - -namespace PushSharp.Apple -{ - public class ApnsHttp2Connection - { - static int ID = 0; - - public ApnsHttp2Connection (ApnsHttp2Configuration configuration) - { - id = ++ID; - if (id >= int.MaxValue) - ID = 0; - - Configuration = configuration; - - certificate = Configuration.Certificate; - - certificates = new X509CertificateCollection (); - - // Add local/machine certificate stores to our collection if requested - if (Configuration.AddLocalAndMachineCertificateStores) { - var store = new X509Store (StoreLocation.LocalMachine); - certificates.AddRange (store.Certificates); - - store = new X509Store (StoreLocation.CurrentUser); - certificates.AddRange (store.Certificates); - } - - // Add optionally specified additional certs into our collection - if (Configuration.AdditionalCertificates != null) { - foreach (var addlCert in Configuration.AdditionalCertificates) - certificates.Add (addlCert); - } - - // Finally, add the main private cert for authenticating to our collection - if (certificate != null) - certificates.Add (certificate); - - var http2Settings = new HttpTwo.Http2ConnectionSettings ( - Configuration.Host, - (uint)Configuration.Port, - true, - certificates); - - http2 = new HttpTwo.Http2Client (http2Settings); - } - - public ApnsHttp2Configuration Configuration { get; private set; } - - X509CertificateCollection certificates; - X509Certificate2 certificate; - int id = 0; - HttpTwo.Http2Client http2; - - public async Task Send (ApnsHttp2Notification notification) - { - var url = string.Format ("https://{0}:{1}/3/device/{2}", - Configuration.Host, - Configuration.Port, - notification.DeviceToken); - var uri = new Uri (url); - - var payload = notification.Payload.ToString (); - - var data = Encoding.ASCII.GetBytes (payload); - - var headers = new NameValueCollection (); - headers.Add ("apns-id", notification.Uuid); // UUID - - if (notification.Expiration.HasValue) { - var sinceEpoch = notification.Expiration.Value.ToUniversalTime () - new DateTime (1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - var secondsSinceEpoch = (long)sinceEpoch.TotalSeconds; - headers.Add ("apns-expiration", secondsSinceEpoch.ToString ()); //Epoch in seconds - } - - if (notification.Priority.HasValue) - headers.Add ("apns-priority", notification.Priority == ApnsPriority.Low ? "5" : "10"); // 5 or 10 - - headers.Add ("content-length", data.Length.ToString ()); - - if (!string.IsNullOrEmpty (notification.Topic)) - headers.Add ("apns-topic", notification.Topic); // string topic - - var response = await http2.Post (uri, headers, data); - - if (response.Status == HttpStatusCode.OK) { - // Check for matching uuid's - var responseUuid = response.Headers ["apns-id"]; - if (responseUuid != notification.Uuid) - throw new Exception ("Mismatched APNS-ID header values"); - } else { - // Try parsing json body - var json = new JObject (); - - if (response.Body != null && response.Body.Length > 0) { - var body = Encoding.ASCII.GetString (response.Body); - json = JObject.Parse (body); - } - - if (response.Status == HttpStatusCode.Gone) { - - var timestamp = DateTime.UtcNow; - if (json != null && json["timestamp"] != null) { - var sinceEpoch = json.Value ("timestamp"); - timestamp = new DateTime (1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds (sinceEpoch); - } - - // Expired - throw new PushSharp.Core.DeviceSubscriptonExpiredException { - OldSubscriptionId = notification.DeviceToken, - NewSubscriptionId = null, - ExpiredAt = timestamp - }; - } - - // Get the reason - var reasonStr = json.Value ("reason"); - - var reason = (ApnsHttp2FailureReason)Enum.Parse (typeof (ApnsHttp2FailureReason), reasonStr, true); - - throw new ApnsHttp2NotificationException (reason, notification); - } - } - } -} - diff --git a/PushSharp.Apple/ApnsHttp2Notification.cs b/PushSharp.Apple/ApnsHttp2Notification.cs deleted file mode 100644 index 5a390d31..00000000 --- a/PushSharp.Apple/ApnsHttp2Notification.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using PushSharp.Core; -using Newtonsoft.Json.Linq; -using System.Net; -using System.Text; -using System.Collections.Generic; - -namespace PushSharp.Apple -{ - public class ApnsHttp2Notification : INotification - { - /// - /// Store whatever associated information you'd like here - /// - /// The tag. - public object Tag { get; set; } - - /// - /// Unique Identifier to match server responses with - /// - /// The UUID. - public string Uuid { get; set; } - - /// - /// Device Token to send notifications to - /// - /// The device token. - public string DeviceToken { get; set; } - - /// - /// JSON APNS Payload - /// - /// The payload. - public JObject Payload { get; set; } - - /// - /// The expiration date after which Apple will no longer store and forward this push notification. - /// If no value is provided, an assumed value of one year from now is used. If you do not wish - /// for Apple to store and forward, set this value to Notification.DoNotStore. - /// - public DateTime? Expiration { get; set; } - - public ApnsPriority? Priority { get; set; } - - public string Topic { get;set; } - - public const int MAX_PAYLOAD_SIZE = 4096; - public static readonly DateTime DoNotStore = DateTime.MinValue; - - public ApnsHttp2Notification () : this (string.Empty, new JObject ()) - { - } - - public ApnsHttp2Notification (string deviceToken) : this (deviceToken, new JObject ()) - { - } - - public ApnsHttp2Notification (string deviceToken, JObject payload) - { - DeviceToken = deviceToken; - Payload = payload; - - Uuid = Guid.NewGuid ().ToString ("D"); - } - - public bool IsDeviceRegistrationIdValid () - { - var r = new System.Text.RegularExpressions.Regex (@"^[0-9A-F]+$", System.Text.RegularExpressions.RegexOptions.IgnoreCase); - return r.Match (DeviceToken).Success; - } - - public override string ToString () - { - try { - if (Payload != null) - return Payload.ToString (); - } catch { - } - - return "{}"; - } - } - - public enum ApnsPriority - { - Low = 5, - High = 10 - } -} diff --git a/PushSharp.Apple/ApnsHttp2ServiceConnection.cs b/PushSharp.Apple/ApnsHttp2ServiceConnection.cs deleted file mode 100644 index 39d8587c..00000000 --- a/PushSharp.Apple/ApnsHttp2ServiceConnection.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using PushSharp.Core; -using System.Threading.Tasks; - -namespace PushSharp.Apple -{ - public class ApnsHttp2ServiceConnectionFactory : IServiceConnectionFactory - { - public ApnsHttp2ServiceConnectionFactory (ApnsHttp2Configuration configuration) - { - Configuration = configuration; - } - - public ApnsHttp2Configuration Configuration { get; private set; } - - public IServiceConnection Create() - { - return new ApnsHttp2ServiceConnection (Configuration); - } - } - - public class ApnsHttp2ServiceBroker : ServiceBroker - { - public ApnsHttp2ServiceBroker (ApnsHttp2Configuration configuration) : base (new ApnsHttp2ServiceConnectionFactory (configuration)) - { - } - } - - public class ApnsHttp2ServiceConnection : IServiceConnection - { - readonly ApnsHttp2Connection connection; - - public ApnsHttp2ServiceConnection (ApnsHttp2Configuration configuration) - { - connection = new ApnsHttp2Connection (configuration); - } - - public async Task Send (ApnsHttp2Notification notification) - { - await connection.Send (notification); - } - } -} diff --git a/PushSharp.Apple/ApnsNotification.cs b/PushSharp.Apple/ApnsNotification.cs index b9a4ea9e..8095aabe 100644 --- a/PushSharp.Apple/ApnsNotification.cs +++ b/PushSharp.Apple/ApnsNotification.cs @@ -4,73 +4,54 @@ using System.Net; using System.Text; using System.Collections.Generic; +using dotAPNS; namespace PushSharp.Apple { public class ApnsNotification : INotification { - static readonly object nextIdentifierLock = new object (); - static int nextIdentifier = 1; + public object Tag { get; set; } - static int GetNextIdentifier () - { - lock (nextIdentifierLock) { - if (nextIdentifier >= int.MaxValue - 10) - nextIdentifier = 1; + public int Identifier { get; private set; } - return nextIdentifier++; - } - } + public string DeviceToken { get; set; } - /// - /// DO NOT Call this unless you know what you are doing! - /// - public static void ResetIdentifier () - { - lock (nextIdentifierLock) - nextIdentifier = 0; - } + public string Title { get; set; } - public object Tag { get; set; } + public string Subtitle { get; set; } - public int Identifier { get; private set; } + public string Body { get; set; } = string.Empty; - public string DeviceToken { get; set; } + public int? Badge { get; set; } - public JObject Payload { get; set; } + public string Sound { get; set; } /// /// The expiration date after which Apple will no longer store and forward this push notification. /// If no value is provided, an assumed value of one year from now is used. If you do not wish /// for Apple to store and forward, set this value to Notification.DoNotStore. /// - public DateTime? Expiration { get; set; } + public DateTimeOffset? Expiration { get; set; } public bool LowPriority { get; set; } - public const int DEVICE_TOKEN_BINARY_MIN_SIZE = 32; - public const int DEVICE_TOKEN_STRING_MIN_SIZE = 64; - public const int MAX_PAYLOAD_SIZE = 2048; //will be 4096 soon - public static readonly DateTime DoNotStore = DateTime.MinValue; - private static readonly DateTime UNIX_EPOCH = new DateTime (1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private const int DEVICE_TOKEN_STRING_MIN_SIZE = 64; - public ApnsNotification () : this (string.Empty, new JObject ()) - { - } + public static readonly DateTimeOffset DoNotStore = DateTimeOffset.MinValue; - public ApnsNotification (string deviceToken) : this (deviceToken, new JObject ()) + public ApnsNotification () : this (null, null, null, null) { } - public ApnsNotification (string deviceToken, JObject payload) + public ApnsNotification(string deviceToken, string title, string subtitle, string body) { - if (!string.IsNullOrEmpty (deviceToken) && deviceToken.Length < DEVICE_TOKEN_STRING_MIN_SIZE) - throw new NotificationException ("Invalid DeviceToken Length", this); + if (!string.IsNullOrEmpty(deviceToken) && deviceToken.Length < DEVICE_TOKEN_STRING_MIN_SIZE) + throw new NotificationException("Invalid DeviceToken Length", this); DeviceToken = deviceToken; - Payload = payload; - - Identifier = GetNextIdentifier (); + Title = title; + Subtitle = subtitle; + Body = body; } public bool IsDeviceRegistrationIdValid () @@ -79,90 +60,33 @@ public bool IsDeviceRegistrationIdValid () return r.Match (this.DeviceToken).Success; } - public override string ToString () + internal ApplePush ToApplePush() { - try { - if (Payload != null) - return Payload.ToString(Newtonsoft.Json.Formatting.None); - } catch { - } + if (Body != null) + throw new Exception($"{nameof(Body)} can't be null or empty"); - return "{}"; - } + if (string.IsNullOrWhiteSpace(Sound)) + throw new Exception($"{nameof(Sound)} can't be null or empty"); + if (string.IsNullOrWhiteSpace(DeviceToken) || DeviceToken.Length < DEVICE_TOKEN_STRING_MIN_SIZE) + throw new Exception($"{nameof(DeviceToken)} can't be null or empty"); - public byte[] ToBytes () - { - var builder = new List (); - - // 1 - Device Token - if (string.IsNullOrEmpty (this.DeviceToken)) - throw new NotificationException ("Missing DeviceToken", this); - - if (!IsDeviceRegistrationIdValid ()) - throw new NotificationException ("Invalid DeviceToken", this); - - // Turn the device token into bytes - byte[] deviceToken = new byte[DeviceToken.Length / 2]; - for (int i = 0; i < deviceToken.Length; i++) { - try { - deviceToken [i] = byte.Parse (DeviceToken.Substring (i * 2, 2), System.Globalization.NumberStyles.HexNumber); - } catch (Exception) { - throw new NotificationException ("Invalid DeviceToken", this); - } - } - - if (deviceToken.Length < DEVICE_TOKEN_BINARY_MIN_SIZE) - throw new NotificationException ("Invalid DeviceToken Length", this); - - builder.Add (0x01); // Device Token ID - builder.AddRange (BitConverter.GetBytes (IPAddress.HostToNetworkOrder (Convert.ToInt16 (deviceToken.Length)))); - builder.AddRange (deviceToken); - - // 2 - Payload - var payload = Encoding.UTF8.GetBytes (ToString ()); - if (payload.Length > MAX_PAYLOAD_SIZE) - throw new NotificationException ("Payload too large (must be " + MAX_PAYLOAD_SIZE + " bytes or smaller", this); - - builder.Add (0x02); // Payload ID - builder.AddRange (BitConverter.GetBytes (IPAddress.HostToNetworkOrder (Convert.ToInt16 (payload.Length)))); - builder.AddRange (payload); - - // 3 - Identifier - builder.Add (0x03); - builder.AddRange (BitConverter.GetBytes (IPAddress.HostToNetworkOrder ((Int16)4))); - builder.AddRange (BitConverter.GetBytes (IPAddress.HostToNetworkOrder (Identifier))); - - // 4 - Expiration - // APNS will not store-and-forward a notification with no expiry, so set it one year in the future - // if the client does not provide it. - int expiryTimeStamp = -1; - if (Expiration != DoNotStore) { - DateTime concreteExpireDateUtc = (Expiration ?? DateTime.UtcNow.AddMonths (1)).ToUniversalTime (); - TimeSpan epochTimeSpan = concreteExpireDateUtc - UNIX_EPOCH; - expiryTimeStamp = (int)epochTimeSpan.TotalSeconds; - } - - builder.Add (0x04); // 4 - Expiry ID - builder.AddRange (BitConverter.GetBytes (IPAddress.HostToNetworkOrder ((Int16)4))); - builder.AddRange (BitConverter.GetBytes (IPAddress.HostToNetworkOrder (expiryTimeStamp))); - - // 5 - Priority - //TODO: Add priority - var priority = LowPriority ? (byte)5 : (byte)10; - builder.Add (0x05); // 5 - Priority - builder.AddRange (BitConverter.GetBytes (IPAddress.HostToNetworkOrder ((Int16)1))); - builder.Add (priority); - - var frameLength = builder.Count; - - builder.Insert (0, 0x02); // COMMAND 2 for new format - - // Insert the frame length - builder.InsertRange (1, BitConverter.GetBytes (IPAddress.HostToNetworkOrder ((Int32)frameLength))); - - return builder.ToArray (); - } + var applePush = + new ApplePush(ApplePushType.Alert) + .AddAlert(Title, Subtitle, Body) + .AddToken(DeviceToken) + .SetPriority(LowPriority ? 5 : 10); + + if (!string.IsNullOrWhiteSpace(Sound)) + applePush.AddSound(Sound); + + if (Badge.HasValue) + applePush.AddBadge(Badge.Value); + if (Expiration.HasValue) + applePush.AddExpiration(Expiration.Value); + + return applePush; + } } } diff --git a/PushSharp.Apple/ApnsServiceConnection.cs b/PushSharp.Apple/ApnsServiceConnection.cs index 2dc760cb..66311f8b 100644 --- a/PushSharp.Apple/ApnsServiceConnection.cs +++ b/PushSharp.Apple/ApnsServiceConnection.cs @@ -37,17 +37,9 @@ public ApnsServiceConnection (ApnsConfiguration configuration) public async Task Send (ApnsNotification notification) { - var completableNotification = new ApnsConnection.CompletableApnsNotification (notification); - - connection.Send (completableNotification); - - var ex = await completableNotification.WaitForComplete ().ConfigureAwait (false); + await connection.SendAsync (notification).ConfigureAwait(false); //Log.Info ("Finished Waiting for Notification: {0} (Had Exception? {1})", notification.Identifier, ex != null); - - if (ex != null) { - throw ex; - } } } } diff --git a/PushSharp.Apple/Exceptions.cs b/PushSharp.Apple/Exceptions.cs deleted file mode 100644 index 33d28192..00000000 --- a/PushSharp.Apple/Exceptions.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using PushSharp.Core; - -namespace PushSharp.Apple -{ - public enum ApnsNotificationErrorStatusCode - { - NoErrors = 0, - ProcessingError = 1, - MissingDeviceToken = 2, - MissingTopic = 3, - MissingPayload = 4, - InvalidTokenSize = 5, - InvalidTopicSize = 6, - InvalidPayloadSize = 7, - InvalidToken = 8, - Shutdown = 10, - ConnectionError = 254, - Unknown = 255 - } - - public class ApnsNotificationException : NotificationException - { - public ApnsNotificationException(byte errorStatusCode, ApnsNotification notification) - : this(ToErrorStatusCode(errorStatusCode), notification) - { } - - public ApnsNotificationException (ApnsNotificationErrorStatusCode errorStatusCode, ApnsNotification notification) - : base ("Apns notification error: '" + errorStatusCode + "'", notification) - { - Notification = notification; - ErrorStatusCode = errorStatusCode; - } - - public ApnsNotificationException (ApnsNotificationErrorStatusCode errorStatusCode, ApnsNotification notification, Exception innerException) - : base ("Apns notification error: '" + errorStatusCode + "'", notification, innerException) - { - Notification = notification; - ErrorStatusCode = errorStatusCode; - } - - public new ApnsNotification Notification { get; set; } - public ApnsNotificationErrorStatusCode ErrorStatusCode { get; private set; } - - private static ApnsNotificationErrorStatusCode ToErrorStatusCode(byte errorStatusCode) - { - var s = ApnsNotificationErrorStatusCode.Unknown; - Enum.TryParse(errorStatusCode.ToString(), out s); - return s; - } - } - - public class ApnsConnectionException : Exception - { - public ApnsConnectionException (string message) : base (message) - { - } - - public ApnsConnectionException (string message, Exception innerException) : base (message, innerException) - { - } - } -} diff --git a/PushSharp.Apple/PushSharp.Apple.csproj b/PushSharp.Apple/PushSharp.Apple.csproj index 77cf4026..e6ca3f05 100644 --- a/PushSharp.Apple/PushSharp.Apple.csproj +++ b/PushSharp.Apple/PushSharp.Apple.csproj @@ -18,6 +18,7 @@ bin\Release + diff --git a/PushSharp.Tests/ApnsRealTest.cs b/PushSharp.Tests/ApnsRealTest.cs index 40e24294..f83c0172 100644 --- a/PushSharp.Tests/ApnsRealTest.cs +++ b/PushSharp.Tests/ApnsRealTest.cs @@ -30,7 +30,7 @@ public void APNS_Send_Single () attempted++; broker.QueueNotification (new ApnsNotification { DeviceToken = dt, - Payload = JObject.Parse ("{ \"aps\" : { \"alert\" : \"Hello PushSharp!\" } }") + Body = "Hello PushSharp!" }); } @@ -39,22 +39,6 @@ public void APNS_Send_Single () Assert.AreEqual (attempted, succeeded); Assert.AreEqual (0, failed); } - - [Test] - public void APNS_Feedback_Service () - { - var config = new ApnsConfiguration ( - ApnsConfiguration.ApnsServerEnvironment.Sandbox, - Settings.Instance.ApnsCertificateFile, - Settings.Instance.ApnsCertificatePassword); - - var fbs = new FeedbackService (config); - fbs.FeedbackReceived += (string deviceToken, DateTime timestamp) => { - // Remove the deviceToken from your database - // timestamp is the time the token was reported as expired - }; - fbs.Check (); - } } } diff --git a/PushSharp.Tests/ApnsTests.cs b/PushSharp.Tests/ApnsTests.cs deleted file mode 100644 index b7e5ab3f..00000000 --- a/PushSharp.Tests/ApnsTests.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System; -using System.Threading.Tasks; -using System.Collections.Generic; -using PushSharp.Apple; -using System.Threading; -using Newtonsoft.Json.Linq; -using NUnit.Framework; -using PushSharp.Core; - -namespace PushSharp.Tests -{ - [TestFixture] - [Category ("Fake")] - public class ApnsTests - { - [Test] - public async Task APNS_Single_Succeeds () - { - await Apns (0, 1, new List ()); - } - - [Test] - public async Task APNS_Multiple_Succeed () - { - await Apns (0, 3, new List ()); - } - - //[Test] - public async Task APNS_Send_Many () - { - await Apns (10, 1010, new List { - new ApnsResponseFilter ((id, deviceToken, payload) => { - return id % 100 == 0; - }) - }); - } - - [Test] - public async Task APNS_Send_Small () - { - await Apns (2, 256, new List () { - new ApnsResponseFilter ((id, deviceToken, payload) => { - return id % 100 == 0; - }) - }); - } - - //[Test] - public async Task APNS_Scale_Brokers () - { - await Apns (10, 10100, new List { - new ApnsResponseFilter ((id, deviceToken, payload) => { - return id % 1000 == 0; - }) - }, scale: 2); - } - - [Test] - public async Task Should_Fail_Connect () - { - int count = 2; - long failed = 0; - long success = 0; - - var server = new TestApnsServer (); - #pragma warning disable 4014 - server.Start (); - #pragma warning restore 4014 - - var config = new ApnsConfiguration ("invalidhost", 2195); - var broker = new ApnsServiceBroker (config); - broker.OnNotificationFailed += (notification, exception) => { - Interlocked.Increment (ref failed); - Console.WriteLine ("Failed: " + notification.Identifier); - }; - broker.OnNotificationSucceeded += (notification) => Interlocked.Increment (ref success); - broker.Start (); - - for (int i = 0; i < count; i++) { - broker.QueueNotification (new ApnsNotification { - DeviceToken = (i + 1).ToString ().PadLeft (64, '0'), - Payload = JObject.Parse (@"{""aps"":{""badge"":" + (i + 1) + "}}") - }); - } - - broker.Stop (); - await server.Stop ().ConfigureAwait (false); - - var actualFailed = failed; - var actualSuccess = success; - - Console.WriteLine ("Success: {0}, Failed: {1}", actualSuccess, actualFailed); - - Assert.AreEqual (count, actualFailed);//, "Expected Failed Count not met"); - Assert.AreEqual (0, actualSuccess);//, "Expected Success Count not met"); - } - - public async Task Apns (int expectFailed, int numberNotifications, IEnumerable responseFilters, int batchSize = 1000, int scale = 1) - { - var notifications = new List (); - - for (int i = 0; i < numberNotifications; i++) { - notifications.Add (new ApnsNotification { - DeviceToken = (i + 1).ToString ().PadLeft (64, '0'), - Payload = JObject.Parse (@"{""aps"":{""badge"":" + (i + 1) + "}}") - }); - } - - await Apns (expectFailed, notifications, responseFilters, batchSize, scale).ConfigureAwait (false); - } - - public async Task Apns (int expectFailed, List notifications, IEnumerable responseFilters, int batchSize = 1000, int scale = 1) - { - long success = 0; - long failed = 0; - - var server = new TestApnsServer (); - server.ResponseFilters.AddRange (responseFilters); - - // We don't want to await this, so we can start the server and listen without blocking - #pragma warning disable 4014 - server.Start (); - #pragma warning restore 4014 - - var config = new ApnsConfiguration ("127.0.0.1", 2195) { - InternalBatchSize = batchSize - }; - - - var broker = new ApnsServiceBroker (config); - broker.OnNotificationFailed += (notification, exception) => { - Interlocked.Increment (ref failed); - }; - broker.OnNotificationSucceeded += (notification) => Interlocked.Increment (ref success); - - broker.Start (); - - if (scale != 1) - broker.ChangeScale (scale); - - var c = Log.StartCounter (); - - foreach (var n in notifications) - broker.QueueNotification (n); - - broker.Stop (); - - c.StopAndLog ("Test Took {0} ms"); - - await server.Stop ().ConfigureAwait (false); - - var expectedSuccess = notifications.Count - expectFailed; - - var actualFailed = failed; - var actualSuccess = success; - - Console.WriteLine("EXPECT: Successful: {0}, Failed: {1}", expectedSuccess, expectFailed); - Console.WriteLine("SERVER: Successful: {0}, Failed: {1}", server.Successful, server.Failed); - Console.WriteLine("CLIENT: Successful: {0}, Failed: {1}", actualSuccess, actualFailed); - - Assert.AreEqual (expectFailed, actualFailed); - Assert.AreEqual (expectedSuccess, actualSuccess); - - Assert.AreEqual (server.Failed, actualFailed); - Assert.AreEqual (server.Successful, actualSuccess); - } - } -} - diff --git a/PushSharp.Tests/Servers/TestApnsServer.cs b/PushSharp.Tests/Servers/TestApnsServer.cs deleted file mode 100644 index 1be38243..00000000 --- a/PushSharp.Tests/Servers/TestApnsServer.cs +++ /dev/null @@ -1,343 +0,0 @@ -using System; -using System.Threading.Tasks; -using System.Net.Sockets; -using System.Net; -using System.Collections.Generic; -using System.Threading; - -namespace PushSharp.Tests -{ - public class TestApnsServer - { - const string TAG = "TestApnsServer"; - - public TestApnsServer () - { - ResponseFilters = new List (); - } - - Socket listener; - bool running = false; - long totalBytesRx = 0; - - public int Successful { get; set; } - public int Failed { get; set; } - - ManualResetEvent waitStop = new ManualResetEvent (false); - - public List ResponseFilters { get; set; } - - #pragma warning disable 1998 - public async Task Stop () - #pragma warning restore 1998 - { - running = false; - - try { listener.Shutdown (SocketShutdown.Both); } - catch { } - - try { listener.Close (); } - catch { } - - try { listener.Dispose (); } - catch { } - - waitStop.WaitOne (); - } - - public async Task Start () - { - Console.WriteLine (TAG + " -> Starting Mock APNS Server..."); - running = true; - - listener = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - - listener.Bind (new IPEndPoint (IPAddress.Any, 2195)); - - listener.Listen (100); - - Console.WriteLine (TAG + " -> Started Mock APNS Server."); - - while (running) { - - Socket socket = null; - - try { - // Get a client connection - socket = await Task.Factory.FromAsync ( - listener.BeginAccept (null, null), - listener.EndAccept).ConfigureAwait (false); - } catch { - } - - if (socket == null) - break; - - Console.WriteLine (TAG + " -> Client Connected."); - - - // Start receiving from the client connection on a new thread - #pragma warning disable 4014 - Task.Factory.StartNew (() => { - #pragma warning restore 4014 - - var sentErrorResponse = false; - var s = socket; - byte[] buffer = new byte[1024000]; // 1 MB - - var data = new List (); - - // Do processing, continually receiving from the socket - while (true) - { - var received = s.Receive (buffer); - - if (received <= 0 && data.Count <= 0) - break; - - totalBytesRx += received; - - Console.WriteLine (TAG + " -> Received {0} bytes...", received); - - // Add the received data to our data list - for (int i = 0; i < received; i++) - data.Add (buffer [i]); - - ApnsServerNotification notification = null; - - try { - - while ((notification = Parse (data)) != null) { - - if (!sentErrorResponse) - Successful++; - - // Console.WriteLine (TAG + " -> Rx'd ID: {0}, DeviceToken: {1}, Payload: {2}", notification.Identifier, notification.DeviceToken, notification.Payload); - - foreach (var rf in ResponseFilters) { - if (rf.IsMatch (notification.Identifier, notification.DeviceToken, notification.Payload)) { - if (!sentErrorResponse) - SendErrorResponse (s, rf.Status, notification.Identifier); - sentErrorResponse = true; - break; - } - } - } - - - } catch (ApnsNotificationException ex) { - - Console.WriteLine (TAG + " -> Notification Exception: {0}", ex); - - if (!sentErrorResponse) - SendErrorResponse (s, ex.ErrorStatusCode, ex.NotificationId); - sentErrorResponse = true; - - break; - } - - } - - try { - s.Shutdown (SocketShutdown.Both); - } catch { - } - try { - s.Close (); - } catch { - } - try { - s.Dispose (); - } catch { - } - - Console.WriteLine (TAG + " -> Client Disconnected..."); - }); - } - - waitStop.Set (); - - Console.WriteLine (TAG + " -> Stopped APNS Server."); - } - - - void SendErrorResponse (Socket s, ApnsNotificationErrorStatusCode statusCode, int identifier) - { - Failed++; - Successful--; - - var errorResponseData = new byte[6]; - errorResponseData[0] = 0x01; - errorResponseData[1] = BitConverter.GetBytes ( (short)statusCode)[0]; - - var id = BitConverter.GetBytes (IPAddress.HostToNetworkOrder (identifier)); - Buffer.BlockCopy (id, 0, errorResponseData, 2, 4); - - var sent = Task.Factory.FromAsync ( - s.BeginSend (errorResponseData, 0, errorResponseData.Length, SocketFlags.None, null, null), - s.EndSend).Result; - } - - ApnsServerNotification Parse (List data) - { - // COMMAND FRAME LENGTH FRAME - // 1 byte (0x02) 4 bytes ITEM ... ITEM ... ITEM ... - - // ITEM ID ITEM LENGTH ITEM DATA - // 1 byte 2 bytes variable length - - // ITEMS: - // 1: 32 bytes Device Token - // 2: 2048 bytes (up to) Payload - // 3: 4 bytes Notification identifier - // 4: 4 bytes Expiration - // 5: 1 byte Priority (10 - immediate, or 5 - normal) - - var notification = new ApnsServerNotification (); - - ApnsNotificationException exception = null; - - // If there aren't even 5 bytes, we can't even check if the length of notification is correct - if (data.Count < 5) - return null; - - // Let's check to see if the notification is all here in the buffer - var apnsCmd = data [0]; - var apnsFrameLength = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(data.GetRange (1, 4).ToArray (), 0)); - - // If we don't have all of the notification's frame data that we should have, we need to keep waiting - if (data.Count - 5 < apnsFrameLength) - return null; - - var frameData = data.GetRange (5, apnsFrameLength); - - // Remove the data we are processing - data.RemoveRange (0, apnsFrameLength + 5); - - // Now process each item from the frame - while (frameData.Count > 0) { - - // We need at least 4 bytes to count as a full item (1 byte id + 2 bytes length + at least 1 byte data) - if (frameData.Count < 4) { - exception = new ApnsNotificationException (ApnsNotificationErrorStatusCode.ProcessingError, "Invalid Frame Data"); - break; - } - - var itemId = frameData [0]; - var itemLength = IPAddress.NetworkToHostOrder (BitConverter.ToInt16 (frameData.GetRange (1, 2).ToArray (), 0)); - - // Make sure the item data is all there - if (frameData.Count - 3 < itemLength) { - exception = new ApnsNotificationException (ApnsNotificationErrorStatusCode.ProcessingError, "Invalid Item Data"); - break; - } - - var itemData = frameData.GetRange (3, itemLength); - frameData.RemoveRange (0, itemLength + 3); - - if (itemId == 1) { // Device Token - - notification.DeviceToken = BitConverter.ToString(itemData.ToArray()).Replace("-", ""); - if (notification.DeviceToken.Length != 64) - exception = new ApnsNotificationException (ApnsNotificationErrorStatusCode.InvalidTokenSize, "Invalid Token Size"); - - } else if (itemId == 2) { // Payload - - notification.Payload = BitConverter.ToString(itemData.ToArray()); - if (notification.Payload.Length > 2048) - exception = new ApnsNotificationException (ApnsNotificationErrorStatusCode.InvalidPayloadSize, "Invalid Payload Size"); - - } else if (itemId == 3) { // Identifier - - if (itemData.Count > 4) - exception = new ApnsNotificationException (ApnsNotificationErrorStatusCode.ProcessingError, "Identifier too long"); - else - notification.Identifier = IPAddress.NetworkToHostOrder (BitConverter.ToInt32 (itemData.ToArray (), 0)); - - } else if (itemId == 4) { // Expiration - - int secondsSinceEpoch = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(itemData.ToArray (), 0)); - - var expire = new DateTime (1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds (secondsSinceEpoch); - notification.Expiration = expire; - - } else if (itemId == 5) { // Priority - notification.Priority = itemData [0]; - } - } - - if (exception == null && string.IsNullOrEmpty (notification.DeviceToken)) - exception = new ApnsNotificationException (ApnsNotificationErrorStatusCode.MissingDeviceToken, "Missing Device Token"); - if (exception == null && string.IsNullOrEmpty (notification.Payload)) - exception = new ApnsNotificationException (ApnsNotificationErrorStatusCode.MissingPayload, "Missing Payload"); - - // See if there was an error and we can assign an ID to it - if (exception != null) { - exception.NotificationId = notification.Identifier; - - throw exception; - } - - return notification; - } - } - - public class ApnsServerNotification - { - public string DeviceToken { get;set; } - public string Payload { get;set; } - public int Identifier { get;set; } - public DateTime? Expiration { get;set; } - public short Priority { get;set; } - } - - public enum ApnsNotificationErrorStatusCode - { - NoErrors = 0, - ProcessingError = 1, - MissingDeviceToken = 2, - MissingTopic = 3, - MissingPayload = 4, - InvalidTokenSize = 5, - InvalidTopicSize = 6, - InvalidPayloadSize = 7, - InvalidToken = 8, - Shutdown = 10, - Unknown = 255 - } - - public class ApnsNotificationException : Exception - { - public ApnsNotificationException () : base () - { - } - - public ApnsNotificationException (ApnsNotificationErrorStatusCode statusCode, string msg) : base (msg) - { - ErrorStatusCode = statusCode; - } - - public int NotificationId { get; set; } - public ApnsNotificationErrorStatusCode ErrorStatusCode { get; set; } - } - - public class ApnsResponseFilter - { - public ApnsResponseFilter (IsMatchDelegate isMatchHandler) : this (ApnsNotificationErrorStatusCode.ProcessingError, isMatchHandler) - { - } - - public ApnsResponseFilter (ApnsNotificationErrorStatusCode status, IsMatchDelegate isMatchHandler) - { - IsMatch = isMatchHandler; - Status = status; - } - - public delegate bool IsMatchDelegate (int identifier, string deviceToken, string payload); - - public IsMatchDelegate IsMatch { get;set; } - - public ApnsNotificationErrorStatusCode Status { get; set; } - } -} -