From 39cd3b53caec491e3cef96b1ce4ddf20536cd0dd Mon Sep 17 00:00:00 2001 From: Karimi Date: Mon, 24 Oct 2016 13:25:06 +0330 Subject: [PATCH 1/5] Reformat all cods and update packages and .net --- .gitignore | 1 + PushSharp.Amazon/AdmConfiguration.cs | 51 +- PushSharp.Amazon/AdmConnection.cs | 291 +++--- PushSharp.Amazon/AdmNotification.cs | 80 +- PushSharp.Amazon/Exceptions.cs | 43 +- PushSharp.Amazon/Properties/AssemblyInfo.cs | 18 +- PushSharp.Amazon/PushSharp.Amazon.csproj | 14 +- PushSharp.Amazon/packages.config | 2 +- PushSharp.Apple/ApnsConfiguration.cs | 296 +++--- PushSharp.Apple/ApnsConnection.cs | 946 +++++++++--------- PushSharp.Apple/ApnsFeedbackService.cs | 227 +++-- PushSharp.Apple/ApnsNotification.cs | 329 +++--- PushSharp.Apple/ApnsServiceConnection.cs | 96 +- PushSharp.Apple/Exceptions.cs | 114 +-- PushSharp.Apple/Properties/AssemblyInfo.cs | 18 +- PushSharp.Apple/PushSharp.Apple.csproj | 14 +- PushSharp.Apple/packages.config | 2 +- .../BlackberryConfiguration.cs | 92 +- PushSharp.Blackberry/BlackberryConnection.cs | 150 +-- PushSharp.Blackberry/BlackberryHttpClient.cs | 54 +- .../BlackberryNotification.cs | 276 ++--- PushSharp.Blackberry/Enums.cs | 264 ++--- PushSharp.Blackberry/Exceptions.cs | 26 +- .../Properties/AssemblyInfo.cs | 18 +- .../PushSharp.Blackberry.csproj | 7 +- PushSharp.Core/Exceptions.cs | 91 +- PushSharp.Core/INotification.cs | 10 +- PushSharp.Core/IServiceBroker.cs | 18 +- PushSharp.Core/IServiceConnection.cs | 12 +- PushSharp.Core/IServiceConnectionFactory.cs | 12 +- PushSharp.Core/Log.cs | 302 +++--- .../NotificationBlockingCollection.cs | 2 +- PushSharp.Core/Properties/AssemblyInfo.cs | 18 +- PushSharp.Core/PushHttpClient.cs | 188 ++-- PushSharp.Core/PushSharp.Core.csproj | 7 +- PushSharp.Core/ServiceBroker.cs | 438 ++++---- PushSharp.Firefox/Exceptions.cs | 18 +- PushSharp.Firefox/FirefoxConfiguration.cs | 12 +- PushSharp.Firefox/FirefoxConnection.cs | 81 +- PushSharp.Firefox/FirefoxNotification.cs | 66 +- PushSharp.Firefox/Properties/AssemblyInfo.cs | 18 +- PushSharp.Firefox/PushSharp.Firefox.csproj | 7 +- PushSharp.Google/Exceptions.cs | 48 +- PushSharp.Google/GcmConfiguration.cs | 57 +- PushSharp.Google/GcmMessageResult.cs | 104 +- PushSharp.Google/GcmNotification.cs | 326 +++--- PushSharp.Google/GcmResponse.cs | 145 ++- PushSharp.Google/GcmServiceConnection.cs | 438 ++++---- PushSharp.Google/Properties/AssemblyInfo.cs | 18 +- PushSharp.Google/PushSharp.Google.csproj | 14 +- PushSharp.Google/packages.config | 2 +- PushSharp.Tests/AdmRealTests.cs | 76 +- PushSharp.Tests/ApnsRealTest.cs | 112 ++- PushSharp.Tests/ApnsTests.cs | 316 +++--- PushSharp.Tests/BrokerTests.cs | 143 ++- PushSharp.Tests/GcmRealTests.cs | 91 +- PushSharp.Tests/GcmTests.cs | 48 +- PushSharp.Tests/PushSharp.Tests.csproj | 17 +- PushSharp.Tests/Servers/TestApnsServer.cs | 603 +++++------ PushSharp.Tests/Settings.cs | 118 +-- PushSharp.Tests/TestServiceConnection.cs | 68 +- PushSharp.Tests/WnsRealTests.cs | 141 +-- PushSharp.Tests/packages.config | 3 +- PushSharp.Tests/settings.sample.json | 22 - PushSharp.Windows/Exceptions.cs | 28 +- PushSharp.Windows/Properties/AssemblyInfo.cs | 18 +- PushSharp.Windows/PushSharp.Windows.csproj | 14 +- PushSharp.Windows/WnsConfiguration.cs | 24 +- PushSharp.Windows/WnsConnection.cs | 379 +++---- PushSharp.Windows/WnsNotification.cs | 96 +- PushSharp.Windows/WnsNotificationStatus.cs | 82 +- PushSharp.Windows/WnsTokenAccessManager.cs | 122 +-- PushSharp.Windows/packages.config | 2 +- PushSharp.sln | 41 +- 74 files changed, 4308 insertions(+), 4137 deletions(-) delete mode 100644 PushSharp.Tests/settings.sample.json diff --git a/.gitignore b/.gitignore index b8dbc72c..8d1474ce 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ node_modules/ .vs/ settings.json +/UnitTestProject1 diff --git a/PushSharp.Amazon/AdmConfiguration.cs b/PushSharp.Amazon/AdmConfiguration.cs index 529fbbc1..2ed9034e 100644 --- a/PushSharp.Amazon/AdmConfiguration.cs +++ b/PushSharp.Amazon/AdmConfiguration.cs @@ -2,34 +2,33 @@ namespace PushSharp.Amazon { - public class AdmConfiguration - { - const string ADM_SEND_URL = "https://api.amazon.com/messaging/registrations/{0}/messages"; - const string ADM_AUTH_URL = "https://api.amazon.com/auth/O2/token"; + public class AdmConfiguration + { + const string ADM_SEND_URL = "https://api.amazon.com/messaging/registrations/{0}/messages"; + const string ADM_AUTH_URL = "https://api.amazon.com/auth/O2/token"; - public AdmConfiguration (string clientId, string clientSecret) - { - ClientId = clientId; - ClientSecret = clientSecret; - AdmSendUrl = ADM_SEND_URL; - AdmAuthUrl = ADM_AUTH_URL; - } + public AdmConfiguration(string clientId, string clientSecret) + { + ClientId = clientId; + ClientSecret = clientSecret; + AdmSendUrl = ADM_SEND_URL; + AdmAuthUrl = ADM_AUTH_URL; + } - public string ClientId { get; private set; } - public string ClientSecret { get; private set; } + public string ClientId { get; private set; } + public string ClientSecret { get; private set; } - public string AdmSendUrl { get; private set; } - public string AdmAuthUrl { get; private set; } + public string AdmSendUrl { get; private set; } + public string AdmAuthUrl { get; private set; } - public void OverrideSendUrl(string url) - { - AdmSendUrl = url; - } - - public void OverrideAuthUrl(string url) - { - AdmAuthUrl = url; - } - } -} + public void OverrideSendUrl(string url) + { + AdmSendUrl = url; + } + public void OverrideAuthUrl(string url) + { + AdmAuthUrl = url; + } + } +} \ No newline at end of file diff --git a/PushSharp.Amazon/AdmConnection.cs b/PushSharp.Amazon/AdmConnection.cs index 3ce9d346..56be8292 100644 --- a/PushSharp.Amazon/AdmConnection.cs +++ b/PushSharp.Amazon/AdmConnection.cs @@ -1,154 +1,155 @@ using System; -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using System.Net; using PushSharp.Core; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Threading; namespace PushSharp.Amazon { - public class AdmServiceConnectionFactory : IServiceConnectionFactory - { - public AdmServiceConnectionFactory (AdmConfiguration configuration) - { - Configuration = configuration; - } - - public AdmConfiguration Configuration { get; private set; } - - public IServiceConnection Create() - { - return new AdmServiceConnection (Configuration); - } - } - - public class AdmServiceBroker : ServiceBroker - { - public AdmServiceBroker (AdmConfiguration configuration) : base (new AdmServiceConnectionFactory (configuration)) - { - } - } - - public class AdmServiceConnection : IServiceConnection - { - public AdmServiceConnection (AdmConfiguration configuration) - { - Configuration = configuration; - - Expires = DateTime.UtcNow.AddYears(-1); - - http.DefaultRequestHeaders.Add ("X-Amzn-Type-Version", "com.amazon.device.messaging.ADMMessage@1.0"); - http.DefaultRequestHeaders.Add ("X-Amzn-Accept-Type", "com.amazon.device.messaging.ADMSendResult@1.0"); - http.DefaultRequestHeaders.Add ("Accept", "application/json"); - http.DefaultRequestHeaders.ConnectionClose = true; - - http.DefaultRequestHeaders.Remove("connection"); - } - - public AdmConfiguration Configuration { get; private set; } - - public DateTime Expires { get; set; } - public DateTime LastRequest { get; private set; } - public string LastAmazonRequestId { get; private set; } - public string AccessToken { get; private set; } - - readonly HttpClient http = new HttpClient (); - - public async Task Send (AdmNotification notification) - { - try - { - if (string.IsNullOrEmpty(AccessToken) || Expires <= DateTime.UtcNow) { - await UpdateAccessToken (); - http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "Bearer " + AccessToken); - //http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken); - } - - var sc = new StringContent(notification.ToJson ()); - sc.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - - var response = await http.PostAsync (string.Format(Configuration.AdmSendUrl, notification.RegistrationId), sc); - - // We're done here if it was a success - if (response.IsSuccessStatusCode) { - return; - } - - var data = await response.Content.ReadAsStringAsync(); - - var json = JObject.Parse (data); - - var reason = json ["reason"].ToString (); - - var regId = notification.RegistrationId; - - if (json["registrationID"] != null) - regId = json["registrationID"].ToString(); - - switch (response.StatusCode) - { - case HttpStatusCode.BadGateway: //400 - case HttpStatusCode.BadRequest: // - if ("InvalidRegistrationId".Equals (reason, StringComparison.InvariantCultureIgnoreCase)) { - throw new DeviceSubscriptionExpiredException (notification) { - OldSubscriptionId = regId, - ExpiredAt = DateTime.UtcNow - }; - } - throw new NotificationException ("Notification Failed: " + reason, notification); - case HttpStatusCode.Unauthorized: //401 - //Access token expired - AccessToken = null; - throw new UnauthorizedAccessException ("Access token failed authorization"); - case HttpStatusCode.Forbidden: //403 - throw new AdmRateLimitExceededException (reason, notification); - case HttpStatusCode.RequestEntityTooLarge: //413 - throw new AdmMessageTooLargeException (notification); - default: - throw new NotificationException ("Unknown ADM Failure", notification); - } - } - catch (Exception ex) - { - throw new NotificationException ("Unknown ADM Failure", notification, ex); - } - } - - - async Task UpdateAccessToken() - { - var http = new HttpClient (); - - var param = new Dictionary (); - param.Add ("grant_type", "client_credentials"); - param.Add ("scope", "messaging:push"); - param.Add ("client_id", Configuration.ClientId); - param.Add ("client_secret", Configuration.ClientSecret); - - - var result = await http.PostAsync (Configuration.AdmAuthUrl, new FormUrlEncodedContent (param)); - var data = await result.Content.ReadAsStringAsync(); - - var json = JObject.Parse (data); - - AccessToken = json ["access_token"].ToString (); - - JToken expiresJson = new JValue(3540); - if (json.TryGetValue("expires_in", out expiresJson)) - Expires = DateTime.UtcNow.AddSeconds(expiresJson.ToObject() - 60); - else - Expires = DateTime.UtcNow.AddSeconds(3540); - - if (result.Headers.Contains ("X-Amzn-RequestId")) - this.LastAmazonRequestId = string.Join("; ", result.Headers.GetValues("X-Amzn-RequestId")); - - LastRequest = DateTime.UtcNow; - } - - } + public class AdmServiceConnectionFactory : IServiceConnectionFactory + { + public AdmServiceConnectionFactory(AdmConfiguration configuration) + { + Configuration = configuration; + } + + public AdmConfiguration Configuration { get; private set; } + + public IServiceConnection Create() + { + return new AdmServiceConnection(Configuration); + } + } + + public class AdmServiceBroker : ServiceBroker + { + public AdmServiceBroker(AdmConfiguration configuration) : base(new AdmServiceConnectionFactory(configuration)) + { + } + } + + public class AdmServiceConnection : IServiceConnection + { + public AdmServiceConnection(AdmConfiguration configuration) + { + Configuration = configuration; + + Expires = DateTime.UtcNow.AddYears(-1); + + http.DefaultRequestHeaders.Add("X-Amzn-Type-Version", "com.amazon.device.messaging.ADMMessage@1.0"); + http.DefaultRequestHeaders.Add("X-Amzn-Accept-Type", "com.amazon.device.messaging.ADMSendResult@1.0"); + http.DefaultRequestHeaders.Add("Accept", "application/json"); + http.DefaultRequestHeaders.ConnectionClose = true; + + http.DefaultRequestHeaders.Remove("connection"); + } + + public AdmConfiguration Configuration { get; private set; } + + public DateTime Expires { get; set; } + public DateTime LastRequest { get; private set; } + public string LastAmazonRequestId { get; private set; } + public string AccessToken { get; private set; } + + readonly HttpClient http = new HttpClient(); + + public async Task Send(AdmNotification notification) + { + try + { + if (string.IsNullOrEmpty(AccessToken) || Expires <= DateTime.UtcNow) + { + await UpdateAccessToken(); + http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "Bearer " + AccessToken); + //http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken); + } + + var sc = new StringContent(notification.ToJson()); + sc.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + var response = await http.PostAsync(string.Format(Configuration.AdmSendUrl, notification.RegistrationId), sc); + + // We're done here if it was a success + if (response.IsSuccessStatusCode) + { + return; + } + + var data = await response.Content.ReadAsStringAsync(); + + var json = JObject.Parse(data); + + var reason = json["reason"].ToString(); + + var regId = notification.RegistrationId; + + if (json["registrationID"] != null) + regId = json["registrationID"].ToString(); + + switch (response.StatusCode) + { + case HttpStatusCode.BadGateway: //400 + case HttpStatusCode.BadRequest: // + if ("InvalidRegistrationId".Equals(reason, StringComparison.InvariantCultureIgnoreCase)) + { + throw new DeviceSubscriptionExpiredException(notification) + { + OldSubscriptionId = regId, + ExpiredAt = DateTime.UtcNow + }; + } + throw new NotificationException("Notification Failed: " + reason, notification); + case HttpStatusCode.Unauthorized: //401 + //Access token expired + AccessToken = null; + throw new UnauthorizedAccessException("Access token failed authorization"); + case HttpStatusCode.Forbidden: //403 + throw new AdmRateLimitExceededException(reason, notification); + case HttpStatusCode.RequestEntityTooLarge: //413 + throw new AdmMessageTooLargeException(notification); + default: + throw new NotificationException("Unknown ADM Failure", notification); + } + } + catch (Exception ex) + { + throw new NotificationException("Unknown ADM Failure", notification, ex); + } + } + + + async Task UpdateAccessToken() + { + var http = new HttpClient(); + + var param = new Dictionary(); + param.Add("grant_type", "client_credentials"); + param.Add("scope", "messaging:push"); + param.Add("client_id", Configuration.ClientId); + param.Add("client_secret", Configuration.ClientSecret); + + + var result = await http.PostAsync(Configuration.AdmAuthUrl, new FormUrlEncodedContent(param)); + var data = await result.Content.ReadAsStringAsync(); + + var json = JObject.Parse(data); + + AccessToken = json["access_token"].ToString(); + + JToken expiresJson = new JValue(3540); + if (json.TryGetValue("expires_in", out expiresJson)) + Expires = DateTime.UtcNow.AddSeconds(expiresJson.ToObject() - 60); + else + Expires = DateTime.UtcNow.AddSeconds(3540); + + if (result.Headers.Contains("X-Amzn-RequestId")) + this.LastAmazonRequestId = string.Join("; ", result.Headers.GetValues("X-Amzn-RequestId")); + + LastRequest = DateTime.UtcNow; + } + + } } diff --git a/PushSharp.Amazon/AdmNotification.cs b/PushSharp.Amazon/AdmNotification.cs index 74dad5b4..657f031e 100644 --- a/PushSharp.Amazon/AdmNotification.cs +++ b/PushSharp.Amazon/AdmNotification.cs @@ -1,59 +1,59 @@ -using System; -using System.Collections.Generic; -using PushSharp.Core; +using System.Collections.Generic; using Newtonsoft.Json.Linq; +using PushSharp.Core; namespace PushSharp.Amazon { - public class AdmNotification : INotification - { - public AdmNotification () - { - Data = new Dictionary (); - } + public class AdmNotification : INotification + { + public AdmNotification() + { + Data = new Dictionary(); + } - public Dictionary Data { get; set; } + public Dictionary Data { get; set; } - public string ConsolidationKey { get;set; } + public string ConsolidationKey { get; set; } - public int? ExpiresAfter { get;set; } + public int? ExpiresAfter { get; set; } - public string RegistrationId { get;set; } + public string RegistrationId { get; set; } - public string ToJson() - { - var json = new JObject (); - var data = new JObject (); + public string ToJson() + { + var json = new JObject(); + var data = new JObject(); - if (Data != null && Data.Count > 0) { - foreach (var key in Data.Keys) - data [key] = Data [key]; - } + if (Data != null && Data.Count > 0) + { + foreach (var key in Data.Keys) + data[key] = Data[key]; + } - json ["data"] = data; + json["data"] = data; - if (!string.IsNullOrEmpty (ConsolidationKey)) - json ["consolidationKey"] = ConsolidationKey; + if (!string.IsNullOrEmpty(ConsolidationKey)) + json["consolidationKey"] = ConsolidationKey; - if (ExpiresAfter.HasValue && ExpiresAfter.Value >= 0) - json ["expiresAfter"] = ExpiresAfter.Value; + if (ExpiresAfter.HasValue && ExpiresAfter.Value >= 0) + json["expiresAfter"] = ExpiresAfter.Value; - return json.ToString(); - } + return json.ToString(); + } - public override string ToString () - { - return ToJson (); - } + public override string ToString() + { + return ToJson(); + } - #region INotification implementation - public bool IsDeviceRegistrationIdValid () - { - return !string.IsNullOrWhiteSpace (RegistrationId); - } + #region INotification implementation + public bool IsDeviceRegistrationIdValid() + { + return !string.IsNullOrWhiteSpace(RegistrationId); + } - public object Tag { get; set; } - #endregion - } + public object Tag { get; set; } + #endregion + } } diff --git a/PushSharp.Amazon/Exceptions.cs b/PushSharp.Amazon/Exceptions.cs index b3f145b9..fc125729 100644 --- a/PushSharp.Amazon/Exceptions.cs +++ b/PushSharp.Amazon/Exceptions.cs @@ -1,29 +1,28 @@ -using System; -using PushSharp.Core; +using PushSharp.Core; namespace PushSharp.Amazon { - public class AdmRateLimitExceededException : NotificationException - { - public AdmRateLimitExceededException (string reason, AdmNotification notification) - : base ("Rate Limit Exceeded (" + reason + ")", notification) - { - Notification = notification; - Reason = reason; - } + public class AdmRateLimitExceededException : NotificationException + { + public AdmRateLimitExceededException(string reason, AdmNotification notification) + : base("Rate Limit Exceeded (" + reason + ")", notification) + { + Notification = notification; + Reason = reason; + } - public new AdmNotification Notification { get; set; } - public string Reason { get; private set; } - } + public new AdmNotification Notification { get; set; } + public string Reason { get; private set; } + } - public class AdmMessageTooLargeException : NotificationException - { - public AdmMessageTooLargeException (AdmNotification notification) - : base ("ADM Message too Large, must be <= 6kb", notification) - { - Notification = notification; - } + public class AdmMessageTooLargeException : NotificationException + { + public AdmMessageTooLargeException(AdmNotification notification) + : base("ADM Message too Large, must be <= 6kb", notification) + { + Notification = notification; + } - public new AdmNotification Notification { get; set; } - } + public new AdmNotification Notification { get; set; } + } } diff --git a/PushSharp.Amazon/Properties/AssemblyInfo.cs b/PushSharp.Amazon/Properties/AssemblyInfo.cs index a2f8a614..2a42711f 100644 --- a/PushSharp.Amazon/Properties/AssemblyInfo.cs +++ b/PushSharp.Amazon/Properties/AssemblyInfo.cs @@ -4,20 +4,20 @@ // Information about this assembly is defined by the following attributes. // Change them to the values specific to your project. -[assembly: AssemblyTitle ("PushSharp.Amazon")] -[assembly: AssemblyDescription ("")] -[assembly: AssemblyConfiguration ("")] -[assembly: AssemblyCompany ("")] -[assembly: AssemblyProduct ("")] -[assembly: AssemblyCopyright ("redth")] -[assembly: AssemblyTrademark ("")] -[assembly: AssemblyCulture ("")] +[assembly: AssemblyTitle("PushSharp.Amazon")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("redth")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion ("1.0.*")] +[assembly: AssemblyVersion("1.0.*")] // The following attributes are used to specify the signing key for the assembly, // if desired. See the Mono documentation for more information about signing. diff --git a/PushSharp.Amazon/PushSharp.Amazon.csproj b/PushSharp.Amazon/PushSharp.Amazon.csproj index 594d22c3..b6b858a8 100644 --- a/PushSharp.Amazon/PushSharp.Amazon.csproj +++ b/PushSharp.Amazon/PushSharp.Amazon.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -7,9 +7,10 @@ Library PushSharp.Amazon PushSharp.Amazon - v4.5 + v4.6.1 true ..\PushSharp-Signing.snk + true @@ -30,11 +31,12 @@ false + + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + True + - - ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll - diff --git a/PushSharp.Amazon/packages.config b/PushSharp.Amazon/packages.config index 505e5883..e1fae9c6 100644 --- a/PushSharp.Amazon/packages.config +++ b/PushSharp.Amazon/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/PushSharp.Apple/ApnsConfiguration.cs b/PushSharp.Apple/ApnsConfiguration.cs index 464a5e62..0a28cbe7 100644 --- a/PushSharp.Apple/ApnsConfiguration.cs +++ b/PushSharp.Apple/ApnsConfiguration.cs @@ -5,204 +5,208 @@ 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"; + 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 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_PORT = 2195; + const int APNS_PRODUCTION_PORT = 2195; - const int APNS_SANDBOX_FEEDBACK_PORT = 2196; - const int APNS_PRODUCTION_FEEDBACK_PORT = 2196; + const int APNS_SANDBOX_FEEDBACK_PORT = 2196; + const int APNS_PRODUCTION_FEEDBACK_PORT = 2196; - #endregion + #endregion - public ApnsConfiguration (ApnsServerEnvironment serverEnvironment, string certificateFile, string certificateFilePwd, bool validateIsApnsCertificate) - : this (serverEnvironment, System.IO.File.ReadAllBytes (certificateFile), certificateFilePwd, validateIsApnsCertificate) - { - } + public ApnsConfiguration(ApnsServerEnvironment serverEnvironment, string certificateFile, string certificateFilePwd, bool validateIsApnsCertificate) + : this(serverEnvironment, System.IO.File.ReadAllBytes(certificateFile), certificateFilePwd, validateIsApnsCertificate) + { + } - public ApnsConfiguration (ApnsServerEnvironment serverEnvironment, string certificateFile, string certificateFilePwd) - : this (serverEnvironment, System.IO.File.ReadAllBytes (certificateFile), certificateFilePwd) - { - } + public ApnsConfiguration(ApnsServerEnvironment serverEnvironment, string certificateFile, string certificateFilePwd) + : this(serverEnvironment, System.IO.File.ReadAllBytes(certificateFile), certificateFilePwd) + { + } - public ApnsConfiguration (ApnsServerEnvironment serverEnvironment, byte[] certificateData, string certificateFilePwd, bool validateIsApnsCertificate) - : this (serverEnvironment, new X509Certificate2 (certificateData, certificateFilePwd, - X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable), validateIsApnsCertificate) - { - } + public ApnsConfiguration(ApnsServerEnvironment serverEnvironment, byte[] certificateData, string certificateFilePwd, bool validateIsApnsCertificate) + : this(serverEnvironment, new X509Certificate2(certificateData, certificateFilePwd, + X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable), validateIsApnsCertificate) + { + } - public ApnsConfiguration (ApnsServerEnvironment serverEnvironment, byte[] certificateData, string certificateFilePwd) - : this (serverEnvironment, new X509Certificate2 (certificateData, certificateFilePwd, - X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable)) - { - } + public ApnsConfiguration(ApnsServerEnvironment serverEnvironment, byte[] certificateData, string certificateFilePwd) + : this(serverEnvironment, new X509Certificate2(certificateData, certificateFilePwd, + X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable)) + { + } - public ApnsConfiguration (string overrideHost, int overridePort, bool skipSsl = true) - { - SkipSsl = skipSsl; + public ApnsConfiguration(string overrideHost, int overridePort, bool skipSsl = true) + { + SkipSsl = skipSsl; - Initialize (ApnsServerEnvironment.Sandbox, null, false); + Initialize(ApnsServerEnvironment.Sandbox, null, false); - OverrideServer (overrideHost, overridePort); - } + OverrideServer(overrideHost, overridePort); + } - public ApnsConfiguration (ApnsServerEnvironment serverEnvironment, X509Certificate2 certificate) - { - Initialize (serverEnvironment, certificate, true); - } + public ApnsConfiguration(ApnsServerEnvironment serverEnvironment, X509Certificate2 certificate) + { + Initialize(serverEnvironment, certificate, true); + } - public ApnsConfiguration (ApnsServerEnvironment serverEnvironment, X509Certificate2 certificate, bool validateIsApnsCertificate) - { - Initialize (serverEnvironment, certificate, validateIsApnsCertificate); - } + public ApnsConfiguration(ApnsServerEnvironment serverEnvironment, X509Certificate2 certificate, bool validateIsApnsCertificate) + { + Initialize(serverEnvironment, certificate, validateIsApnsCertificate); + } - void Initialize (ApnsServerEnvironment serverEnvironment, X509Certificate2 certificate, bool validateIsApnsCertificate) - { - ServerEnvironment = serverEnvironment; + void Initialize(ApnsServerEnvironment serverEnvironment, X509Certificate2 certificate, bool validateIsApnsCertificate) + { + ServerEnvironment = serverEnvironment; - var production = serverEnvironment == ApnsServerEnvironment.Production; + 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; + 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; + Certificate = certificate; - MillisecondsToWaitBeforeMessageDeclaredSuccess = 3000; - ConnectionTimeout = 10000; - MaxConnectionAttempts = 3; + MillisecondsToWaitBeforeMessageDeclaredSuccess = 3000; + ConnectionTimeout = 10000; + MaxConnectionAttempts = 3; - FeedbackIntervalMinutes = 10; - FeedbackTimeIsUTC = false; + FeedbackIntervalMinutes = 10; + FeedbackTimeIsUTC = false; - AdditionalCertificates = new List (); - AddLocalAndMachineCertificateStores = false; + AdditionalCertificates = new List(); + AddLocalAndMachineCertificateStores = false; - if (validateIsApnsCertificate) - CheckIsApnsCertificate (); + if (validateIsApnsCertificate) + CheckIsApnsCertificate(); - ValidateServerCertificate = false; + ValidateServerCertificate = false; - KeepAlivePeriod = TimeSpan.FromMinutes (20); - KeepAliveRetryPeriod = TimeSpan.FromSeconds (30); + KeepAlivePeriod = TimeSpan.FromMinutes(20); + KeepAliveRetryPeriod = TimeSpan.FromSeconds(30); - InternalBatchSize = 1000; - InternalBatchingWaitPeriod = TimeSpan.FromMilliseconds (750); + InternalBatchSize = 1000; + InternalBatchingWaitPeriod = TimeSpan.FromMilliseconds(750); - InternalBatchFailureRetryCount = 1; - } + InternalBatchFailureRetryCount = 1; + } - void CheckIsApnsCertificate () - { - if (Certificate != null) { - var issuerName = Certificate.IssuerName.Name; - var commonName = Certificate.SubjectName.Name; + void CheckIsApnsCertificate() + { + if (Certificate != null) + { + var issuerName = Certificate.IssuerName.Name; + var commonName = Certificate.SubjectName.Name; - if (!issuerName.Contains ("Apple")) - throw new ArgumentOutOfRangeException ("Your Certificate does not appear to be issued by Apple! Please check to ensure you have the correct certificate!"); + if (!issuerName.Contains("Apple")) + throw new ArgumentOutOfRangeException("Your Certificate does not appear to be issued by Apple! Please check to ensure you have the correct certificate!"); - if (!Regex.IsMatch (commonName, "Apple.*?Push Services") - && !commonName.Contains ("Website Push ID:")) - throw new ArgumentOutOfRangeException ("Your Certificate is not a valid certificate for connecting to Apple's APNS servers"); + if (!Regex.IsMatch(commonName, "Apple.*?Push Services") + && !commonName.Contains("Website Push ID:")) + throw new ArgumentOutOfRangeException("Your Certificate is not a valid certificate for connecting to Apple's APNS servers"); - if (commonName.Contains ("Development") && ServerEnvironment != ApnsServerEnvironment.Sandbox) - throw new ArgumentOutOfRangeException ("You are using a certificate created for connecting only to the Sandbox APNS server but have selected a different server environment to connect to."); + if (commonName.Contains("Development") && ServerEnvironment != ApnsServerEnvironment.Sandbox) + throw new ArgumentOutOfRangeException("You are using a certificate created for connecting only to the Sandbox APNS server but have selected a different server environment to connect to."); - if (commonName.Contains ("Production") && ServerEnvironment != ApnsServerEnvironment.Production) - throw new ArgumentOutOfRangeException ("You are using a certificate created for connecting only to the Production APNS server but have selected a different server environment to connect to."); - } else { - throw new ArgumentOutOfRangeException ("You must provide a Certificate to connect to APNS with!"); - } - } + if (commonName.Contains("Production") && ServerEnvironment != ApnsServerEnvironment.Production) + throw new ArgumentOutOfRangeException("You are using a certificate created for connecting only to the Production APNS server but have selected a different server environment to connect to."); + } + else + { + throw new ArgumentOutOfRangeException("You must provide a Certificate to connect to APNS with!"); + } + } - public void OverrideServer (string host, int port) - { - Host = host; - Port = port; - } + public void OverrideServer(string host, int port) + { + Host = host; + Port = port; + } - public void OverrideFeedbackServer (string host, int port) - { - FeedbackHost = host; - FeedbackPort = port; - } + public void OverrideFeedbackServer(string host, int port) + { + FeedbackHost = host; + FeedbackPort = port; + } - public string Host { get; private set; } + public string Host { get; private set; } - public int Port { get; private set; } + public int Port { get; private set; } - public string FeedbackHost { get; private set; } + public string FeedbackHost { get; private set; } - public int FeedbackPort { get; private set; } + public int FeedbackPort { get; private set; } - public X509Certificate2 Certificate { get; private set; } + public X509Certificate2 Certificate { get; private set; } - public List AdditionalCertificates { get; private set; } + public List AdditionalCertificates { get; private set; } - public bool AddLocalAndMachineCertificateStores { get; set; } + public bool AddLocalAndMachineCertificateStores { get; set; } - public bool SkipSsl { get; set; } + public bool SkipSsl { get; set; } - public int MillisecondsToWaitBeforeMessageDeclaredSuccess { get; set; } + public int MillisecondsToWaitBeforeMessageDeclaredSuccess { get; set; } - public int FeedbackIntervalMinutes { get; set; } + public int FeedbackIntervalMinutes { get; set; } - public bool FeedbackTimeIsUTC { get; set; } + public bool FeedbackTimeIsUTC { get; set; } - public bool ValidateServerCertificate { get; set; } + public bool ValidateServerCertificate { get; set; } - public int ConnectionTimeout { get; set; } + public int ConnectionTimeout { get; set; } - public int MaxConnectionAttempts { 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; } + /// + /// 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 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; } + /// + /// 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 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 or sets the keep alive retry period to set on the APNS socket + /// + public TimeSpan KeepAliveRetryPeriod { get; set; } - /// - /// Gets the configured APNS server environment - /// - /// The server environment. - public ApnsServerEnvironment ServerEnvironment { get; private set; } + /// + /// Gets the configured APNS server environment + /// + /// The server environment. + public ApnsServerEnvironment ServerEnvironment { get; private set; } - public enum ApnsServerEnvironment { - Sandbox, - Production - } - } + public enum ApnsServerEnvironment + { + Sandbox, + Production + } + } } \ No newline at end of file diff --git a/PushSharp.Apple/ApnsConnection.cs b/PushSharp.Apple/ApnsConnection.cs index 0908b166..a465dafb 100644 --- a/PushSharp.Apple/ApnsConnection.cs +++ b/PushSharp.Apple/ApnsConnection.cs @@ -1,477 +1,521 @@ using System; +using System.Collections.Generic; using System.IO; -using System.Net.Sockets; +using System.Net; using System.Net.Security; -using System.Threading.Tasks; +using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; -using System.Collections.Generic; using System.Threading; -using System.Net; +using System.Threading.Tasks; using PushSharp.Core; 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 (); - } - - }), null, Timeout.Infinite, Timeout.Infinite); - } - - 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 (); - - 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) - { - 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; - - // 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); + 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); + } - // 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); + // Add optionally specified additional certs into our collection + if (Configuration.AdditionalCertificates != null) + { + foreach (var addlCert in Configuration.AdditionalCertificates) + certificates.Add(addlCert); + } - // Clear our sent list - sent.Clear (); + // Finally, add the main private cert for authenticating to our collection + if (certificate != null) + certificates.Add(certificate); - // Apple will close our connection after this anyway - disconnect (); - } + timerBatchWait = new Timer(new TimerCallback(async state => + { - bool socketCanWrite () - { - if (client == null) - return false; + await batchSendSemaphore.WaitAsync(); + try + { + await SendBatch().ConfigureAwait(false); + } + finally + { + batchSendSemaphore.Release(); + } - if (networkStream == null || !networkStream.CanWrite) - return false; + }), null, Timeout.Infinite, Timeout.Infinite); + } + + 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(); + + try + { + // See if we need to connect + if (!socketCanWrite() || i > 0) + await connect(); + } + finally + { + connectingSemaphore.Release(); + } - if (!client.Client.Connected) - return false; + 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); + } + } - var p = client.Client.Poll (1000, SelectMode.SelectWrite); + foreach (var n in toSend) + sent.Add(new SentNotification(n)); + } - Log.Info ("APNS-Client[{0}]: Can Write? {1}", id, p); + } + 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)); + } - return p; - } + Log.Info("APNS-Client[{0}]: Sent Batch, waiting for possible response...", id); - async Task connect () - { - if (client != null) - disconnect (); - - Log.Info ("APNS-Client[{0}]: Connecting (Batch ID={1})", id, batchId); + 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) + { + 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(); - client = new TcpClient (); + // 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! - try { - await client.ConnectAsync (Configuration.Host, Configuration.Port).ConfigureAwait (false); + Log.Info("APNS-Client[{0}]: Batch (ID={1}) completed with no error response...", id, batchId); - //Set keep alive on the socket may help maintain our APNS connection - try { - client.Client.SetSocketOption (SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); - } catch { - } + //Everything was ok, let's assume all 'sent' succeeded + foreach (var s in sent) + s.Notification.CompleteSuccessfully(); + + sent.Clear(); + return; + } - //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); - } + // If we make it here, we did get data back, so we have errors - // We can configure skipping ssl all together, ie: if we want to hit a test server - if (Configuration.SkipSsl) { - networkStream = client.GetStream (); - } else { + 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; + + // 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); - // Create our ssl stream - stream = new SslStream (client.GetStream (), - false, - ValidateRemoteCertificate, - (sender, targetHost, localCerts, remoteCert, acceptableIssuers) => certificate); - - try { - stream.AuthenticateAsClient (Configuration.Host, certificates, System.Security.Authentication.SslProtocols.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 () - { - completionSource.SetResult (null); - } - - public void CompleteFailed (Exception ex) - { - completionSource.SetResult (ex); - } - } - - /// - /// 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 + 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 + { + stream.AuthenticateAsClient(Configuration.Host, certificates, System.Security.Authentication.SslProtocols.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() + { + completionSource.SetResult(null); + } + + public void CompleteFailed(Exception ex) + { + completionSource.SetResult(ex); + } + } + + /// + /// 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); - } - } -} + 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); + } + } +} \ No newline at end of file diff --git a/PushSharp.Apple/ApnsFeedbackService.cs b/PushSharp.Apple/ApnsFeedbackService.cs index 57fb887a..832c2335 100644 --- a/PushSharp.Apple/ApnsFeedbackService.cs +++ b/PushSharp.Apple/ApnsFeedbackService.cs @@ -1,126 +1,125 @@ 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; }); - - stream.AuthenticateAsClient(Configuration.FeedbackHost, certificates, System.Security.Authentication.SslProtocols.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 { } + public class FeedbackService + { + public FeedbackService(ApnsConfiguration configuration) + { + Configuration = configuration; + } - try { client.Close (); } catch { } + 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; }); + + stream.AuthenticateAsClient(Configuration.FeedbackHost, certificates, System.Security.Authentication.SslProtocols.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/ApnsNotification.cs b/PushSharp.Apple/ApnsNotification.cs index b9a4ea9e..6fe73902 100644 --- a/PushSharp.Apple/ApnsNotification.cs +++ b/PushSharp.Apple/ApnsNotification.cs @@ -1,168 +1,177 @@ using System; -using PushSharp.Core; -using Newtonsoft.Json.Linq; +using System.Collections.Generic; using System.Net; using System.Text; -using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using PushSharp.Core; namespace PushSharp.Apple { - public class ApnsNotification : INotification - { - static readonly object nextIdentifierLock = new object (); - static int nextIdentifier = 1; - - static int GetNextIdentifier () - { - lock (nextIdentifierLock) { - if (nextIdentifier >= int.MaxValue - 10) - nextIdentifier = 1; - - return nextIdentifier++; - } - } - - /// - /// DO NOT Call this unless you know what you are doing! - /// - public static void ResetIdentifier () - { - lock (nextIdentifierLock) - nextIdentifier = 0; - } - - public object Tag { get; set; } - - public int Identifier { get; private set; } - - public string DeviceToken { get; set; } - - 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 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); - - public ApnsNotification () : this (string.Empty, new JObject ()) - { - } - - public ApnsNotification (string deviceToken) : this (deviceToken, new JObject ()) - { - } - - public ApnsNotification (string deviceToken, JObject payload) - { - if (!string.IsNullOrEmpty (deviceToken) && deviceToken.Length < DEVICE_TOKEN_STRING_MIN_SIZE) - throw new NotificationException ("Invalid DeviceToken Length", this); - - DeviceToken = deviceToken; - Payload = payload; - - Identifier = GetNextIdentifier (); - } - - public bool IsDeviceRegistrationIdValid () - { - var r = new System.Text.RegularExpressions.Regex (@"^[0-9A-F]+$", System.Text.RegularExpressions.RegexOptions.IgnoreCase); - return r.Match (this.DeviceToken).Success; - } - - public override string ToString () - { - try { - if (Payload != null) - return Payload.ToString(Newtonsoft.Json.Formatting.None); - } catch { - } - - return "{}"; - } - - 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 (); - } - - } + public class ApnsNotification : INotification + { + static readonly object nextIdentifierLock = new object(); + static int nextIdentifier = 1; + + static int GetNextIdentifier() + { + lock (nextIdentifierLock) + { + if (nextIdentifier >= int.MaxValue - 10) + nextIdentifier = 1; + + return nextIdentifier++; + } + } + + /// + /// DO NOT Call this unless you know what you are doing! + /// + public static void ResetIdentifier() + { + lock (nextIdentifierLock) + nextIdentifier = 0; + } + + public object Tag { get; set; } + + public int Identifier { get; private set; } + + public string DeviceToken { get; set; } + + 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 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); + + public ApnsNotification() : this(string.Empty, new JObject()) + { + } + + public ApnsNotification(string deviceToken) : this(deviceToken, new JObject()) + { + } + + public ApnsNotification(string deviceToken, JObject payload) + { + if (!string.IsNullOrEmpty(deviceToken) && deviceToken.Length < DEVICE_TOKEN_STRING_MIN_SIZE) + throw new NotificationException("Invalid DeviceToken Length", this); + + DeviceToken = deviceToken; + Payload = payload; + + Identifier = GetNextIdentifier(); + } + + public bool IsDeviceRegistrationIdValid() + { + var r = new System.Text.RegularExpressions.Regex(@"^[0-9A-F]+$", System.Text.RegularExpressions.RegexOptions.IgnoreCase); + return r.Match(this.DeviceToken).Success; + } + + public override string ToString() + { + try + { + if (Payload != null) + return Payload.ToString(Newtonsoft.Json.Formatting.None); + } + catch + { + } + + return "{}"; + } + + 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(); + } + + } } diff --git a/PushSharp.Apple/ApnsServiceConnection.cs b/PushSharp.Apple/ApnsServiceConnection.cs index 2dc760cb..30f2acbb 100644 --- a/PushSharp.Apple/ApnsServiceConnection.cs +++ b/PushSharp.Apple/ApnsServiceConnection.cs @@ -1,53 +1,53 @@ -using System; +using System.Threading.Tasks; using PushSharp.Core; -using System.Threading.Tasks; namespace PushSharp.Apple { - public class ApnsServiceConnectionFactory : IServiceConnectionFactory - { - public ApnsServiceConnectionFactory (ApnsConfiguration configuration) - { - Configuration = configuration; - } - - public ApnsConfiguration Configuration { get; private set; } - - public IServiceConnection Create() - { - return new ApnsServiceConnection (Configuration); - } - } - - public class ApnsServiceBroker : ServiceBroker - { - public ApnsServiceBroker (ApnsConfiguration configuration) : base (new ApnsServiceConnectionFactory (configuration)) - { - } - } - - public class ApnsServiceConnection : IServiceConnection - { - readonly ApnsConnection connection; - - public ApnsServiceConnection (ApnsConfiguration configuration) - { - connection = new ApnsConnection (configuration); - } - - public async Task Send (ApnsNotification notification) - { - var completableNotification = new ApnsConnection.CompletableApnsNotification (notification); - - connection.Send (completableNotification); - - var ex = await completableNotification.WaitForComplete ().ConfigureAwait (false); - - //Log.Info ("Finished Waiting for Notification: {0} (Had Exception? {1})", notification.Identifier, ex != null); - - if (ex != null) { - throw ex; - } - } - } + public class ApnsServiceConnectionFactory : IServiceConnectionFactory + { + public ApnsServiceConnectionFactory(ApnsConfiguration configuration) + { + Configuration = configuration; + } + + public ApnsConfiguration Configuration { get; private set; } + + public IServiceConnection Create() + { + return new ApnsServiceConnection(Configuration); + } + } + + public class ApnsServiceBroker : ServiceBroker + { + public ApnsServiceBroker(ApnsConfiguration configuration) : base(new ApnsServiceConnectionFactory(configuration)) + { + } + } + + public class ApnsServiceConnection : IServiceConnection + { + readonly ApnsConnection connection; + + public ApnsServiceConnection(ApnsConfiguration configuration) + { + connection = new ApnsConnection(configuration); + } + + public async Task Send(ApnsNotification notification) + { + var completableNotification = new ApnsConnection.CompletableApnsNotification(notification); + + connection.Send(completableNotification); + + var ex = await completableNotification.WaitForComplete().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 index 33d28192..97eb3176 100644 --- a/PushSharp.Apple/Exceptions.cs +++ b/PushSharp.Apple/Exceptions.cs @@ -3,61 +3,61 @@ 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) - { - } - } + 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/Properties/AssemblyInfo.cs b/PushSharp.Apple/Properties/AssemblyInfo.cs index 3b074103..076fbe53 100644 --- a/PushSharp.Apple/Properties/AssemblyInfo.cs +++ b/PushSharp.Apple/Properties/AssemblyInfo.cs @@ -4,20 +4,20 @@ // Information about this assembly is defined by the following attributes. // Change them to the values specific to your project. -[assembly: AssemblyTitle ("PushSharp.Apple")] -[assembly: AssemblyDescription ("")] -[assembly: AssemblyConfiguration ("")] -[assembly: AssemblyCompany ("")] -[assembly: AssemblyProduct ("")] -[assembly: AssemblyCopyright ("redth")] -[assembly: AssemblyTrademark ("")] -[assembly: AssemblyCulture ("")] +[assembly: AssemblyTitle("PushSharp.Apple")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("redth")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion ("1.0.*")] +[assembly: AssemblyVersion("1.0.*")] // The following attributes are used to specify the signing key for the assembly, // if desired. See the Mono documentation for more information about signing. diff --git a/PushSharp.Apple/PushSharp.Apple.csproj b/PushSharp.Apple/PushSharp.Apple.csproj index 3c9344a0..bca698c4 100644 --- a/PushSharp.Apple/PushSharp.Apple.csproj +++ b/PushSharp.Apple/PushSharp.Apple.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -7,9 +7,10 @@ Library PushSharp.Apple PushSharp.Apple - v4.5 + v4.6.1 true ..\PushSharp-Signing.snk + true @@ -30,10 +31,11 @@ false - - - ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + True + diff --git a/PushSharp.Apple/packages.config b/PushSharp.Apple/packages.config index 505e5883..e1fae9c6 100644 --- a/PushSharp.Apple/packages.config +++ b/PushSharp.Apple/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/PushSharp.Blackberry/BlackberryConfiguration.cs b/PushSharp.Blackberry/BlackberryConfiguration.cs index 80fbd376..294ce197 100644 --- a/PushSharp.Blackberry/BlackberryConfiguration.cs +++ b/PushSharp.Blackberry/BlackberryConfiguration.cs @@ -2,51 +2,51 @@ namespace PushSharp.Blackberry { - public class BlackberryConfiguration - { - const string SEND_URL = "https://pushapi.eval.blackberry.com/mss/PD_pushRequest"; - - public BlackberryConfiguration () - { - SendUrl = SEND_URL; - } - - public BlackberryConfiguration (string applicationId, string password) - { - ApplicationId = applicationId; - Password = password; - SendUrl = SEND_URL; - } - - public string ApplicationId { get; set; } - public string Password { get; set; } - public string Boundary { get { return "ASDFaslkdfjasfaSfdasfhpoiurwqrwm"; } } - - - /// - /// Push Proxy Gateway (PPG) Url is used for submitting push requests - /// Default value is BIS PPG evaluation url - /// https://pushapi.eval.blackberry.com/mss/PD_pushRequest - /// - public string SendUrl { get; private set; } - - /// - /// Overrides SendUrl with any PPG url: BIS or BES - /// - /// Push Proxy Gateway (PPG) Url, - /// For BIS,it's PPG production url is in format: http://cpxxx.pushapi.na.blackberry.com - /// where xxx should be replaced with CPID (Content Provider ID) - public void OverrideSendUrl(string url) - { - if (!string.IsNullOrWhiteSpace(url)) - { - if (url.EndsWith("pushapi.na.blackberry.com", StringComparison.InvariantCultureIgnoreCase) || - url.EndsWith("pushapi.eval.blackberry.com", StringComparison.InvariantCultureIgnoreCase)) - url = url + @"/mss/PD_pushRequest"; - } - SendUrl = url; - } - - } + public class BlackberryConfiguration + { + const string SEND_URL = "https://pushapi.eval.blackberry.com/mss/PD_pushRequest"; + + public BlackberryConfiguration() + { + SendUrl = SEND_URL; + } + + public BlackberryConfiguration(string applicationId, string password) + { + ApplicationId = applicationId; + Password = password; + SendUrl = SEND_URL; + } + + public string ApplicationId { get; set; } + public string Password { get; set; } + public string Boundary { get { return "ASDFaslkdfjasfaSfdasfhpoiurwqrwm"; } } + + + /// + /// Push Proxy Gateway (PPG) Url is used for submitting push requests + /// Default value is BIS PPG evaluation url + /// https://pushapi.eval.blackberry.com/mss/PD_pushRequest + /// + public string SendUrl { get; private set; } + + /// + /// Overrides SendUrl with any PPG url: BIS or BES + /// + /// Push Proxy Gateway (PPG) Url, + /// For BIS,it's PPG production url is in format: http://cpxxx.pushapi.na.blackberry.com + /// where xxx should be replaced with CPID (Content Provider ID) + public void OverrideSendUrl(string url) + { + if (!string.IsNullOrWhiteSpace(url)) + { + if (url.EndsWith("pushapi.na.blackberry.com", StringComparison.InvariantCultureIgnoreCase) || + url.EndsWith("pushapi.eval.blackberry.com", StringComparison.InvariantCultureIgnoreCase)) + url = url + @"/mss/PD_pushRequest"; + } + SendUrl = url; + } + + } } diff --git a/PushSharp.Blackberry/BlackberryConnection.cs b/PushSharp.Blackberry/BlackberryConnection.cs index 14f17573..993c3dbd 100644 --- a/PushSharp.Blackberry/BlackberryConnection.cs +++ b/PushSharp.Blackberry/BlackberryConnection.cs @@ -7,80 +7,80 @@ namespace PushSharp.Blackberry { - public class BlackberryServiceConnectionFactory : IServiceConnectionFactory - { - public BlackberryServiceConnectionFactory (BlackberryConfiguration configuration) - { - Configuration = configuration; - } - - public BlackberryConfiguration Configuration { get; private set; } - - public IServiceConnection Create() - { - return new BlackberryServiceConnection (Configuration); - } - } - - public class BlackberryServiceBroker : ServiceBroker - { - public BlackberryServiceBroker (BlackberryConfiguration configuration) : base (new BlackberryServiceConnectionFactory (configuration)) - { - } - } - - public class BlackberryServiceConnection : IServiceConnection - { - public BlackberryServiceConnection (BlackberryConfiguration configuration) - { - Configuration = configuration; - http = new BlackberryHttpClient (Configuration); - } - - public BlackberryConfiguration Configuration { get; private set; } - - readonly BlackberryHttpClient http; - - public async Task Send (BlackberryNotification notification) - { - var response = await http.PostNotification (notification); - var description = string.Empty; - - var status = new BlackberryMessageStatus - { - Notification = notification, - HttpStatus = HttpStatusCode.ServiceUnavailable - }; - - var bbNotStatus = string.Empty; - status.HttpStatus = response.StatusCode; - - var xmlContent = await response.Content.ReadAsStreamAsync (); - var doc = XDocument.Load (xmlContent); - - XElement result = doc.Descendants().FirstOrDefault(desc => - desc.Name == "response-result" || - desc.Name == "badmessage-response"); - if (result != null) - { - bbNotStatus = result.Attribute("code").Value; - description = result.Attribute("desc").Value; - } - - BlackberryNotificationStatus notStatus; - Enum.TryParse(bbNotStatus, true, out notStatus); - status.NotificationStatus = notStatus; - - if (status.NotificationStatus == BlackberryNotificationStatus.NoAppReceivePush) - throw new DeviceSubscriptionExpiredException (notification); - - if (status.HttpStatus == HttpStatusCode.OK - && status.NotificationStatus == BlackberryNotificationStatus.RequestAcceptedForProcessing) - return; - - throw new BlackberryNotificationException (status, description, notification); - - } - } + public class BlackberryServiceConnectionFactory : IServiceConnectionFactory + { + public BlackberryServiceConnectionFactory(BlackberryConfiguration configuration) + { + Configuration = configuration; + } + + public BlackberryConfiguration Configuration { get; private set; } + + public IServiceConnection Create() + { + return new BlackberryServiceConnection(Configuration); + } + } + + public class BlackberryServiceBroker : ServiceBroker + { + public BlackberryServiceBroker(BlackberryConfiguration configuration) : base(new BlackberryServiceConnectionFactory(configuration)) + { + } + } + + public class BlackberryServiceConnection : IServiceConnection + { + public BlackberryServiceConnection(BlackberryConfiguration configuration) + { + Configuration = configuration; + http = new BlackberryHttpClient(Configuration); + } + + public BlackberryConfiguration Configuration { get; private set; } + + readonly BlackberryHttpClient http; + + public async Task Send(BlackberryNotification notification) + { + var response = await http.PostNotification(notification); + var description = string.Empty; + + var status = new BlackberryMessageStatus + { + Notification = notification, + HttpStatus = HttpStatusCode.ServiceUnavailable + }; + + var bbNotStatus = string.Empty; + status.HttpStatus = response.StatusCode; + + var xmlContent = await response.Content.ReadAsStreamAsync(); + var doc = XDocument.Load(xmlContent); + + XElement result = doc.Descendants().FirstOrDefault(desc => + desc.Name == "response-result" || + desc.Name == "badmessage-response"); + if (result != null) + { + bbNotStatus = result.Attribute("code").Value; + description = result.Attribute("desc").Value; + } + + BlackberryNotificationStatus notStatus; + Enum.TryParse(bbNotStatus, true, out notStatus); + status.NotificationStatus = notStatus; + + if (status.NotificationStatus == BlackberryNotificationStatus.NoAppReceivePush) + throw new DeviceSubscriptionExpiredException(notification); + + if (status.HttpStatus == HttpStatusCode.OK + && status.NotificationStatus == BlackberryNotificationStatus.RequestAcceptedForProcessing) + return; + + throw new BlackberryNotificationException(status, description, notification); + + } + } } diff --git a/PushSharp.Blackberry/BlackberryHttpClient.cs b/PushSharp.Blackberry/BlackberryHttpClient.cs index c54c2878..d9415800 100644 --- a/PushSharp.Blackberry/BlackberryHttpClient.cs +++ b/PushSharp.Blackberry/BlackberryHttpClient.cs @@ -6,45 +6,45 @@ namespace PushSharp.Blackberry { - public class BlackberryHttpClient : HttpClient - { - public BlackberryConfiguration Configuration { get; private set; } + public class BlackberryHttpClient : HttpClient + { + public BlackberryConfiguration Configuration { get; private set; } - public BlackberryHttpClient (BlackberryConfiguration configuration) : base() - { - Configuration = configuration; + public BlackberryHttpClient(BlackberryConfiguration configuration) : base() + { + Configuration = configuration; - var authInfo = Configuration.ApplicationId + ":" + Configuration.Password; - authInfo = Convert.ToBase64String (Encoding.Default.GetBytes(authInfo)); + var authInfo = Configuration.ApplicationId + ":" + Configuration.Password; + authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); - this.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue ("Basic", authInfo); - this.DefaultRequestHeaders.ConnectionClose = true; + this.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authInfo); + this.DefaultRequestHeaders.ConnectionClose = true; - this.DefaultRequestHeaders.Remove("connection"); - } + this.DefaultRequestHeaders.Remove("connection"); + } - public Task PostNotification (BlackberryNotification notification) - { - var c = new MultipartContent ("related", Configuration.Boundary); - c.Headers.Remove("Content-Type"); - c.Headers.TryAddWithoutValidation("Content-Type", "multipart/related; boundary=" + Configuration.Boundary + "; type=application/xml"); + public Task PostNotification(BlackberryNotification notification) + { + var c = new MultipartContent("related", Configuration.Boundary); + c.Headers.Remove("Content-Type"); + c.Headers.TryAddWithoutValidation("Content-Type", "multipart/related; boundary=" + Configuration.Boundary + "; type=application/xml"); - var xml = notification.ToPapXml (); + var xml = notification.ToPapXml(); - c.Add (new StringContent (xml, Encoding.UTF8, "application/xml")); + c.Add(new StringContent(xml, Encoding.UTF8, "application/xml")); - var bc = new ByteArrayContent(notification.Content.Content); - bc.Headers.Add("Content-Type", notification.Content.ContentType); + var bc = new ByteArrayContent(notification.Content.Content); + bc.Headers.Add("Content-Type", notification.Content.ContentType); - foreach (var header in notification.Content.Headers) - bc.Headers.Add(header.Key, header.Value); + foreach (var header in notification.Content.Headers) + bc.Headers.Add(header.Key, header.Value); - c.Add(bc); + c.Add(bc); - return PostAsync (Configuration.SendUrl, c); - } - } + return PostAsync(Configuration.SendUrl, c); + } + } } diff --git a/PushSharp.Blackberry/BlackberryNotification.cs b/PushSharp.Blackberry/BlackberryNotification.cs index 64b1fa17..30122ee8 100644 --- a/PushSharp.Blackberry/BlackberryNotification.cs +++ b/PushSharp.Blackberry/BlackberryNotification.cs @@ -7,162 +7,162 @@ namespace PushSharp.Blackberry { - public enum QualityOfServiceLevel - { - NotSpecified, - Unconfirmed, - PreferConfirmed, - Confirmed - } + public enum QualityOfServiceLevel + { + NotSpecified, + Unconfirmed, + PreferConfirmed, + Confirmed + } - public class BlackberryNotification : INotification - { - public BlackberryNotification() - { - PushId = Guid.NewGuid ().ToString (); - Recipients = new List (); - DeliverBeforeTimestamp = DateTime.UtcNow.AddMinutes(5); - QualityOfService = QualityOfServiceLevel.Unconfirmed; - } + public class BlackberryNotification : INotification + { + public BlackberryNotification() + { + PushId = Guid.NewGuid().ToString(); + Recipients = new List(); + DeliverBeforeTimestamp = DateTime.UtcNow.AddMinutes(5); + QualityOfService = QualityOfServiceLevel.Unconfirmed; + } - public bool IsDeviceRegistrationIdValid () - { - return true; - } + public bool IsDeviceRegistrationIdValid() + { + return true; + } - public object Tag { get;set; } + public object Tag { get; set; } - public string PushId { get; private set; } + public string PushId { get; private set; } - public QualityOfServiceLevel QualityOfService { get;set; } + public QualityOfServiceLevel QualityOfService { get; set; } - /// - /// Address (e.g. URL) that Blackberry push service could use for notification - /// of results related to the message - /// - public string PpgNotifyRequestedTo { get; set; } + /// + /// Address (e.g. URL) that Blackberry push service could use for notification + /// of results related to the message + /// + public string PpgNotifyRequestedTo { get; set; } - /// - /// Date and time by which the content must be delivered,expressed as UTC - /// Message that has aged beyond this date will not be transmitted - /// - public DateTime? DeliverBeforeTimestamp { get; set; } + /// + /// Date and time by which the content must be delivered,expressed as UTC + /// Message that has aged beyond this date will not be transmitted + /// + public DateTime? DeliverBeforeTimestamp { get; set; } - /// - /// Date and time after which the content should be delivered,expressed as UTC - /// Message will not be transmitted before this date - /// - public DateTime? DeliverAfterTimestamp { get; set; } + /// + /// Date and time after which the content should be delivered,expressed as UTC + /// Message will not be transmitted before this date + /// + public DateTime? DeliverAfterTimestamp { get; set; } - public List Recipients { get;set; } - - public string SourceReference { get; set; } - - public BlackberryMessageContent Content { get; set; } - - public string ToPapXml() - { - var doc = new XDocument (); - - var docType = new XDocumentType("pap", "-//WAPFORUM//DTD PAP 2.1//EN", "http://www.openmobilealliance.org/tech/DTD/pap_2.1.dtd", ""); - - doc.AddFirst (docType); - - var pap = new XElement ("pap"); - - var pushMsg = new XElement ("push-message"); - - pushMsg.Add (new XAttribute ("push-id", this.PushId)); - pushMsg.Add(new XAttribute("source-reference", this.SourceReference)); - - if (!string.IsNullOrEmpty (this.PpgNotifyRequestedTo)) - pushMsg.Add(new XAttribute("ppg-notify-requested-to", this.PpgNotifyRequestedTo)); - - if (this.DeliverAfterTimestamp.HasValue) - pushMsg.Add (new XAttribute ("deliver-after-timestamp", this.DeliverAfterTimestamp.Value.ToUniversalTime ().ToString("s", CultureInfo.InvariantCulture) + "Z")); - if (this.DeliverBeforeTimestamp.HasValue) - pushMsg.Add (new XAttribute ("deliver-before-timestamp", this.DeliverBeforeTimestamp.Value.ToUniversalTime ().ToString("s", CultureInfo.InvariantCulture) + "Z")); - - //Add all the recipients - foreach (var r in Recipients) - { - var address = new XElement("address"); - - var addrValue = r.Recipient; - - if (!string.IsNullOrEmpty(r.RecipientType)) - { - addrValue = string.Format("WAPPUSH={0}%3A{1}/TYPE={2}", System.Web.HttpUtility.UrlEncode(r.Recipient), - r.Port, r.RecipientType); - } - - address.Add(new XAttribute("address-value", addrValue)); - pushMsg.Add (address); - } - - pushMsg.Add (new XElement ("quality-of-service", new XAttribute ("delivery-method", this.QualityOfService.ToString ().ToLowerInvariant ()))); - - pap.Add(pushMsg); - doc.Add (pap); - - return "" + Environment.NewLine + doc.ToString (SaveOptions.None); - } - - - protected string XmlEncode(string text) - { - return System.Security.SecurityElement.Escape(text); - } + public List Recipients { get; set; } + + public string SourceReference { get; set; } + + public BlackberryMessageContent Content { get; set; } + + public string ToPapXml() + { + var doc = new XDocument(); + + var docType = new XDocumentType("pap", "-//WAPFORUM//DTD PAP 2.1//EN", "http://www.openmobilealliance.org/tech/DTD/pap_2.1.dtd", ""); + + doc.AddFirst(docType); + + var pap = new XElement("pap"); + + var pushMsg = new XElement("push-message"); + + pushMsg.Add(new XAttribute("push-id", this.PushId)); + pushMsg.Add(new XAttribute("source-reference", this.SourceReference)); + + if (!string.IsNullOrEmpty(this.PpgNotifyRequestedTo)) + pushMsg.Add(new XAttribute("ppg-notify-requested-to", this.PpgNotifyRequestedTo)); + + if (this.DeliverAfterTimestamp.HasValue) + pushMsg.Add(new XAttribute("deliver-after-timestamp", this.DeliverAfterTimestamp.Value.ToUniversalTime().ToString("s", CultureInfo.InvariantCulture) + "Z")); + if (this.DeliverBeforeTimestamp.HasValue) + pushMsg.Add(new XAttribute("deliver-before-timestamp", this.DeliverBeforeTimestamp.Value.ToUniversalTime().ToString("s", CultureInfo.InvariantCulture) + "Z")); + + //Add all the recipients + foreach (var r in Recipients) + { + var address = new XElement("address"); + + var addrValue = r.Recipient; + + if (!string.IsNullOrEmpty(r.RecipientType)) + { + addrValue = string.Format("WAPPUSH={0}%3A{1}/TYPE={2}", System.Web.HttpUtility.UrlEncode(r.Recipient), + r.Port, r.RecipientType); + } + + address.Add(new XAttribute("address-value", addrValue)); + pushMsg.Add(address); + } + + pushMsg.Add(new XElement("quality-of-service", new XAttribute("delivery-method", this.QualityOfService.ToString().ToLowerInvariant()))); + + pap.Add(pushMsg); + doc.Add(pap); + + return "" + Environment.NewLine + doc.ToString(SaveOptions.None); + } + + + protected string XmlEncode(string text) + { + return System.Security.SecurityElement.Escape(text); + } - } + } - public class BlackberryRecipient - { - public BlackberryRecipient(string recipient) - { - Recipient = recipient; - } + public class BlackberryRecipient + { + public BlackberryRecipient(string recipient) + { + Recipient = recipient; + } - public BlackberryRecipient(string recipient, int port, string recipientType) - { - Recipient = recipient; - Port = port; - RecipientType = recipientType; - } + public BlackberryRecipient(string recipient, int port, string recipientType) + { + Recipient = recipient; + Port = port; + RecipientType = recipientType; + } - public string Recipient { get;set; } - public int Port { get;set; } - public string RecipientType { get;set; } - } + public string Recipient { get; set; } + public int Port { get; set; } + public string RecipientType { get; set; } + } - public class BlackberryMessageContent - { + public class BlackberryMessageContent + { - public BlackberryMessageContent(string contentType, string content) - { - this.Headers = new Dictionary(); - this.ContentType = contentType; - this.Content = Encoding.UTF8.GetBytes(content); - } + public BlackberryMessageContent(string contentType, string content) + { + this.Headers = new Dictionary(); + this.ContentType = contentType; + this.Content = Encoding.UTF8.GetBytes(content); + } - public BlackberryMessageContent(string content) - { - this.Headers = new Dictionary(); - this.ContentType = "text/plain"; - this.Content = Encoding.UTF8.GetBytes(content); - } + public BlackberryMessageContent(string content) + { + this.Headers = new Dictionary(); + this.ContentType = "text/plain"; + this.Content = Encoding.UTF8.GetBytes(content); + } - public BlackberryMessageContent(string contentType, byte[] content) - { - this.Headers = new Dictionary(); - this.ContentType = contentType; - this.Content = content; - } + public BlackberryMessageContent(string contentType, byte[] content) + { + this.Headers = new Dictionary(); + this.ContentType = contentType; + this.Content = content; + } - public string ContentType { get; private set; } - public byte[] Content { get; private set; } + public string ContentType { get; private set; } + public byte[] Content { get; private set; } - public Dictionary Headers { get; private set; } - } + public Dictionary Headers { get; private set; } + } } diff --git a/PushSharp.Blackberry/Enums.cs b/PushSharp.Blackberry/Enums.cs index 2a6e43d2..18764f04 100644 --- a/PushSharp.Blackberry/Enums.cs +++ b/PushSharp.Blackberry/Enums.cs @@ -2,140 +2,140 @@ namespace PushSharp.Blackberry { - public class BlackberryMessageStatus - { - public BlackberryNotificationStatus NotificationStatus { get; set; } + public class BlackberryMessageStatus + { + public BlackberryNotificationStatus NotificationStatus { get; set; } - public BlackberryNotification Notification { get; set; } + public BlackberryNotification Notification { get; set; } - public System.Net.HttpStatusCode HttpStatus { get; set; } - } + public System.Net.HttpStatusCode HttpStatus { get; set; } + } - public enum BlackberryNotificationStatus - { - NotAvailable=0, - /// - /// The request was completed successfully - /// - RequestCompleted = 1000, - /// - /// The request was accepted for processing - /// - RequestAcceptedForProcessing = 1001, - /// - /// The request was accepted for processing, but the daily push count quota was exceeded - /// for the push application and future pushes to the application might start being rejected. - /// Future pushes should be delayed until the next day when more quota is available - /// - RequestAcceptedButPushQuotaExceeded = 1500, - /// - /// The request is invalid - /// - InvalidRequest = 2000, - /// - /// The requested action is forbidden - /// - ForbiddenRequestAction = 2001, - /// - /// The specified PIN or token is not recognized - /// - PinOrTokenNotRecognized = 2002, - /// - /// Could not find the specified Push ID - /// - PushIdNotFound = 2004, - /// - /// The supplied Push ID is not unique - /// - PushIdNotUnique = 2007, - /// - /// The Push ID is valid, but the push request could not be cancelled - /// - PushCantBeCancelled = 2008, - /// - /// The Push ID is valid, but the corresponding PINs or tokens are still being processed. - /// Status query is not possible at this time and should be tried again later - /// - StatusCodeNotPossible = 2009, - /// - /// The request was rejected because the daily push count quota was exceeded for the push application. - /// Future pushes should be delayed until the next day when more quota is available - /// - PushQuotaExceeded = 2500, - /// - /// The PPG could not complete the request due to an internal error - /// - InternalError = 3000, - /// - /// The server does not support the operation that was requested - /// - OperationNotSupported = 3001, - /// - /// The server does not support the PAP version specified in the request - /// - ProvidedPapVersionNotSupported = 3002, - /// - /// The PPG could not deliver the message using the specified method - /// - DeliveryFailed = 3007, - /// - /// The service failed - /// - ServiceFailed = 4000, - /// - /// The server is busy - /// - ServerBusy = 4001, - /// - /// The request expired - /// - RequestExpired = 4500, - /// - /// The request failed - /// - RequestFailed = 4501, - /// - /// The request failed because no application on the device is listening to receive the push - /// (either the application is closed and cannot be launched or it was removed from the device)" - /// - NoAppReceivePush = 4502, - /// - /// The device is unable to receive the push due to the push service being blocked - /// - PushServiceBlocked = 4503, - /// - /// Specific request was completed successfully - /// - SpecifiedRequestCompleted = 21000, - /// - /// Specific request is badly formed - /// - SpecifiedRequestMalformed = 22000, - /// - /// Could not find the specified application ID for the specific request - /// - SpecifiedRequestAppIdNotFound = 22001, - /// - /// The specified PIN or token in the specific request is invalid - /// - SpecifiedRequestInvalidPinOrToken = 22002, - /// - /// The specific request provides an incorrect status - /// - SpecifiedRequestIncorrectStatus = 22003, - /// - /// The specific request produces no results - /// - SpecifiedRequestNoResults = 22004, - /// - /// The specific request exceeds the number of calls allowed - /// within the specified time period - /// - SpecifiedRequestNumCallsExceeded = 22005, - /// - /// Internal error has prevented the request from being completed - /// - SpecifiedRequestInternalError = 23000 - } + public enum BlackberryNotificationStatus + { + NotAvailable = 0, + /// + /// The request was completed successfully + /// + RequestCompleted = 1000, + /// + /// The request was accepted for processing + /// + RequestAcceptedForProcessing = 1001, + /// + /// The request was accepted for processing, but the daily push count quota was exceeded + /// for the push application and future pushes to the application might start being rejected. + /// Future pushes should be delayed until the next day when more quota is available + /// + RequestAcceptedButPushQuotaExceeded = 1500, + /// + /// The request is invalid + /// + InvalidRequest = 2000, + /// + /// The requested action is forbidden + /// + ForbiddenRequestAction = 2001, + /// + /// The specified PIN or token is not recognized + /// + PinOrTokenNotRecognized = 2002, + /// + /// Could not find the specified Push ID + /// + PushIdNotFound = 2004, + /// + /// The supplied Push ID is not unique + /// + PushIdNotUnique = 2007, + /// + /// The Push ID is valid, but the push request could not be cancelled + /// + PushCantBeCancelled = 2008, + /// + /// The Push ID is valid, but the corresponding PINs or tokens are still being processed. + /// Status query is not possible at this time and should be tried again later + /// + StatusCodeNotPossible = 2009, + /// + /// The request was rejected because the daily push count quota was exceeded for the push application. + /// Future pushes should be delayed until the next day when more quota is available + /// + PushQuotaExceeded = 2500, + /// + /// The PPG could not complete the request due to an internal error + /// + InternalError = 3000, + /// + /// The server does not support the operation that was requested + /// + OperationNotSupported = 3001, + /// + /// The server does not support the PAP version specified in the request + /// + ProvidedPapVersionNotSupported = 3002, + /// + /// The PPG could not deliver the message using the specified method + /// + DeliveryFailed = 3007, + /// + /// The service failed + /// + ServiceFailed = 4000, + /// + /// The server is busy + /// + ServerBusy = 4001, + /// + /// The request expired + /// + RequestExpired = 4500, + /// + /// The request failed + /// + RequestFailed = 4501, + /// + /// The request failed because no application on the device is listening to receive the push + /// (either the application is closed and cannot be launched or it was removed from the device)" + /// + NoAppReceivePush = 4502, + /// + /// The device is unable to receive the push due to the push service being blocked + /// + PushServiceBlocked = 4503, + /// + /// Specific request was completed successfully + /// + SpecifiedRequestCompleted = 21000, + /// + /// Specific request is badly formed + /// + SpecifiedRequestMalformed = 22000, + /// + /// Could not find the specified application ID for the specific request + /// + SpecifiedRequestAppIdNotFound = 22001, + /// + /// The specified PIN or token in the specific request is invalid + /// + SpecifiedRequestInvalidPinOrToken = 22002, + /// + /// The specific request provides an incorrect status + /// + SpecifiedRequestIncorrectStatus = 22003, + /// + /// The specific request produces no results + /// + SpecifiedRequestNoResults = 22004, + /// + /// The specific request exceeds the number of calls allowed + /// within the specified time period + /// + SpecifiedRequestNumCallsExceeded = 22005, + /// + /// Internal error has prevented the request from being completed + /// + SpecifiedRequestInternalError = 23000 + } } diff --git a/PushSharp.Blackberry/Exceptions.cs b/PushSharp.Blackberry/Exceptions.cs index be990fce..8c1ade59 100644 --- a/PushSharp.Blackberry/Exceptions.cs +++ b/PushSharp.Blackberry/Exceptions.cs @@ -1,21 +1,19 @@ -using System; -using System.Collections.Generic; -using PushSharp.Core; +using PushSharp.Core; namespace PushSharp.Blackberry { - public class BlackberryNotificationException : NotificationException - { - public BlackberryNotificationException (BlackberryMessageStatus msgStatus, string desc, BlackberryNotification notification) - :base (desc + " - " + msgStatus, notification) - { - Notification = notification; - MessageStatus = msgStatus; - } + public class BlackberryNotificationException : NotificationException + { + public BlackberryNotificationException(BlackberryMessageStatus msgStatus, string desc, BlackberryNotification notification) + : base(desc + " - " + msgStatus, notification) + { + Notification = notification; + MessageStatus = msgStatus; + } - public new BlackberryNotification Notification { get; set; } + public new BlackberryNotification Notification { get; set; } - public BlackberryMessageStatus MessageStatus { get; private set; } - } + public BlackberryMessageStatus MessageStatus { get; private set; } + } } diff --git a/PushSharp.Blackberry/Properties/AssemblyInfo.cs b/PushSharp.Blackberry/Properties/AssemblyInfo.cs index ba2f5b78..da9d78b5 100644 --- a/PushSharp.Blackberry/Properties/AssemblyInfo.cs +++ b/PushSharp.Blackberry/Properties/AssemblyInfo.cs @@ -4,20 +4,20 @@ // Information about this assembly is defined by the following attributes. // Change them to the values specific to your project. -[assembly: AssemblyTitle ("PushSharp.Blackberry")] -[assembly: AssemblyDescription ("")] -[assembly: AssemblyConfiguration ("")] -[assembly: AssemblyCompany ("")] -[assembly: AssemblyProduct ("")] -[assembly: AssemblyCopyright ("redth")] -[assembly: AssemblyTrademark ("")] -[assembly: AssemblyCulture ("")] +[assembly: AssemblyTitle("PushSharp.Blackberry")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("redth")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion ("1.0.*")] +[assembly: AssemblyVersion("1.0.*")] // The following attributes are used to specify the signing key for the assembly, // if desired. See the Mono documentation for more information about signing. diff --git a/PushSharp.Blackberry/PushSharp.Blackberry.csproj b/PushSharp.Blackberry/PushSharp.Blackberry.csproj index 4e0e54c3..076b4f3e 100644 --- a/PushSharp.Blackberry/PushSharp.Blackberry.csproj +++ b/PushSharp.Blackberry/PushSharp.Blackberry.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -7,9 +7,10 @@ Library PushSharp.Blackberry PushSharp.Blackberry - v4.5 + v4.6.1 true ..\PushSharp-Signing.snk + true diff --git a/PushSharp.Core/Exceptions.cs b/PushSharp.Core/Exceptions.cs index 4cfc29fb..970dbbbf 100644 --- a/PushSharp.Core/Exceptions.cs +++ b/PushSharp.Core/Exceptions.cs @@ -1,51 +1,50 @@ using System; -using System.Collections.Generic; namespace PushSharp.Core { - public class DeviceSubscriptionExpiredException : DeviceSubscriptonExpiredException - { - public DeviceSubscriptionExpiredException (INotification notification) : base (notification) - { - } - } - - [Obsolete ("Do not use this class directly, it has a typo in it, instead use DeviceSubscriptionExpiredException")] - public class DeviceSubscriptonExpiredException : NotificationException - { - public DeviceSubscriptonExpiredException (INotification notification) : base ("Device Subscription has Expired", notification) - { - ExpiredAt = DateTime.UtcNow; - } - - public string OldSubscriptionId { get; set; } - public string NewSubscriptionId { get; set; } - public DateTime ExpiredAt { get; set; } - } - - public class NotificationException : Exception - { - public NotificationException (string message, INotification notification) : base (message) - { - Notification = notification; - } - - public NotificationException (string message, INotification notification, Exception innerException) - : base (message, innerException) - { - Notification = notification; - } - - public INotification Notification { get; set; } - } - - public class RetryAfterException : NotificationException - { - public RetryAfterException (INotification notification, string message, DateTime retryAfterUtc) : base (message, notification) - { - RetryAfterUtc = retryAfterUtc; - } - - public DateTime RetryAfterUtc { get; set; } - } + public class DeviceSubscriptionExpiredException : DeviceSubscriptonExpiredException + { + public DeviceSubscriptionExpiredException(INotification notification) : base(notification) + { + } + } + + [Obsolete("Do not use this class directly, it has a typo in it, instead use DeviceSubscriptionExpiredException")] + public class DeviceSubscriptonExpiredException : NotificationException + { + public DeviceSubscriptonExpiredException(INotification notification) : base("Device Subscription has Expired", notification) + { + ExpiredAt = DateTime.UtcNow; + } + + public string OldSubscriptionId { get; set; } + public string NewSubscriptionId { get; set; } + public DateTime ExpiredAt { get; set; } + } + + public class NotificationException : Exception + { + public NotificationException(string message, INotification notification) : base(message) + { + Notification = notification; + } + + public NotificationException(string message, INotification notification, Exception innerException) + : base(message, innerException) + { + Notification = notification; + } + + public INotification Notification { get; set; } + } + + public class RetryAfterException : NotificationException + { + public RetryAfterException(INotification notification, string message, DateTime retryAfterUtc) : base(message, notification) + { + RetryAfterUtc = retryAfterUtc; + } + + public DateTime RetryAfterUtc { get; set; } + } } diff --git a/PushSharp.Core/INotification.cs b/PushSharp.Core/INotification.cs index 23e26d0f..e02e92cf 100644 --- a/PushSharp.Core/INotification.cs +++ b/PushSharp.Core/INotification.cs @@ -2,9 +2,9 @@ namespace PushSharp.Core { - public interface INotification - { - bool IsDeviceRegistrationIdValid (); - object Tag { get; set; } - } + public interface INotification + { + bool IsDeviceRegistrationIdValid(); + object Tag { get; set; } + } } diff --git a/PushSharp.Core/IServiceBroker.cs b/PushSharp.Core/IServiceBroker.cs index e18b9d60..f49ced6f 100644 --- a/PushSharp.Core/IServiceBroker.cs +++ b/PushSharp.Core/IServiceBroker.cs @@ -2,16 +2,16 @@ namespace PushSharp.Core { - public interface IServiceBroker where TNotification : INotification - { - event NotificationSuccessDelegate OnNotificationSucceeded; - event NotificationFailureDelegate OnNotificationFailed; + public interface IServiceBroker where TNotification : INotification + { + event NotificationSuccessDelegate OnNotificationSucceeded; + event NotificationFailureDelegate OnNotificationFailed; - System.Collections.Generic.IEnumerable TakeMany (); - bool IsCompleted { get; } + System.Collections.Generic.IEnumerable TakeMany(); + bool IsCompleted { get; } - void RaiseNotificationSucceeded (TNotification notification); - void RaiseNotificationFailed (TNotification notification, AggregateException ex); - } + void RaiseNotificationSucceeded(TNotification notification); + void RaiseNotificationFailed(TNotification notification, AggregateException ex); + } } diff --git a/PushSharp.Core/IServiceConnection.cs b/PushSharp.Core/IServiceConnection.cs index cfc1812c..163d85be 100644 --- a/PushSharp.Core/IServiceConnection.cs +++ b/PushSharp.Core/IServiceConnection.cs @@ -3,12 +3,12 @@ namespace PushSharp.Core { - public delegate void NotificationSuccessDelegate (TNotification notification) where TNotification : INotification; - public delegate void NotificationFailureDelegate (TNotification notification, AggregateException exception) where TNotification : INotification; + public delegate void NotificationSuccessDelegate(TNotification notification) where TNotification : INotification; + public delegate void NotificationFailureDelegate(TNotification notification, AggregateException exception) where TNotification : INotification; - public interface IServiceConnection where TNotification : INotification - { - Task Send (TNotification notification); - } + public interface IServiceConnection where TNotification : INotification + { + Task Send(TNotification notification); + } } diff --git a/PushSharp.Core/IServiceConnectionFactory.cs b/PushSharp.Core/IServiceConnectionFactory.cs index bb30597a..f81d2ecb 100644 --- a/PushSharp.Core/IServiceConnectionFactory.cs +++ b/PushSharp.Core/IServiceConnectionFactory.cs @@ -1,10 +1,8 @@ -using System; - -namespace PushSharp.Core +namespace PushSharp.Core { - public interface IServiceConnectionFactory where TNotification : INotification - { - IServiceConnection Create (); - } + public interface IServiceConnectionFactory where TNotification : INotification + { + IServiceConnection Create(); + } } diff --git a/PushSharp.Core/Log.cs b/PushSharp.Core/Log.cs index fd749d2d..f997552a 100644 --- a/PushSharp.Core/Log.cs +++ b/PushSharp.Core/Log.cs @@ -4,154 +4,158 @@ namespace PushSharp.Core { - [Flags] - public enum LogLevel - { - Info = 0, - Debug = 2, - Error = 8 - } - - public interface ILogger - { - void Write (LogLevel level, string msg, params object[] args); - } - - public static class Log - { - static readonly object loggerLock = new object (); - - static List loggers { get; set; } - static Dictionary counters; - - static Log () - { - counters = new Dictionary (); - loggers = new List (); - - AddLogger (new ConsoleLogger ()); - } - - public static void AddLogger (ILogger logger) - { - lock (loggerLock) - loggers.Add (logger); - } - - public static void ClearLoggers () - { - lock (loggerLock) - loggers.Clear (); - } - - public static IEnumerable Loggers { - get { return loggers; } - } - - public static void Write (LogLevel level, string msg, params object[] args) - { - lock (loggers) { - foreach (var l in loggers) - l.Write (level, msg, args); - } - } - - public static void Info (string msg, params object[] args) - { - Write (LogLevel.Info, msg, args); - } - - public static void Debug (string msg, params object[] args) - { - Write (LogLevel.Debug, msg, args); - } - - public static void Error (string msg, params object[] args) - { - Write (LogLevel.Error, msg, args); - } - - public static CounterToken StartCounter () - { - var t = new CounterToken { - Id = Guid.NewGuid ().ToString () - }; - - var sw = new Stopwatch (); - - counters.Add (t, sw); - - sw.Start (); - - return t; - } - - public static TimeSpan StopCounter (CounterToken counterToken) - { - if (!counters.ContainsKey (counterToken)) - return TimeSpan.Zero; - - var sw = counters [counterToken]; - - sw.Stop (); - - counters.Remove (counterToken); - - return sw.Elapsed; - } - - public static void StopCounterAndLog (CounterToken counterToken, string msg, LogLevel level = LogLevel.Info) - { - var elapsed = StopCounter (counterToken); - - if (!msg.Contains ("{0}")) - msg += " {0}"; - - Log.Write (level, msg, elapsed.TotalMilliseconds); - } - } - - public static class CounterExtensions - { - public static void StopAndLog (this CounterToken counterToken, string msg, LogLevel level = LogLevel.Info) - { - Log.StopCounterAndLog (counterToken, msg, level); - } - - public static TimeSpan Stop (this CounterToken counterToken) - { - return Log.StopCounter (counterToken); - } - } - - public class CounterToken - { - public string Id { get;set; } - } - - public class ConsoleLogger : ILogger - { - public void Write (LogLevel level, string msg, params object[] args) - { - var s = msg; - - if (args != null && args.Length > 0) - s = string.Format (msg, args); - - var d = DateTime.Now.ToString ("yyyy-MM-dd HH:mm:ss.ttt"); - - switch (level) { - case LogLevel.Info: - Console.Out.WriteLine (d + " [INFO] " + s); - break; - case LogLevel.Debug: - Console.Out.WriteLine (d + " [DEBUG] " + s); - break; - case LogLevel.Error: - Console.Error.WriteLine (d + " [ERROR] " + s); - break; - } - } - } + [Flags] + public enum LogLevel + { + Info = 0, + Debug = 2, + Error = 8 + } + + public interface ILogger + { + void Write(LogLevel level, string msg, params object[] args); + } + + public static class Log + { + static readonly object loggerLock = new object(); + + static List loggers { get; set; } + static Dictionary counters; + + static Log() + { + counters = new Dictionary(); + loggers = new List(); + + AddLogger(new ConsoleLogger()); + } + + public static void AddLogger(ILogger logger) + { + lock (loggerLock) + loggers.Add(logger); + } + + public static void ClearLoggers() + { + lock (loggerLock) + loggers.Clear(); + } + + public static IEnumerable Loggers + { + get { return loggers; } + } + + public static void Write(LogLevel level, string msg, params object[] args) + { + lock (loggers) + { + foreach (var l in loggers) + l.Write(level, msg, args); + } + } + + public static void Info(string msg, params object[] args) + { + Write(LogLevel.Info, msg, args); + } + + public static void Debug(string msg, params object[] args) + { + Write(LogLevel.Debug, msg, args); + } + + public static void Error(string msg, params object[] args) + { + Write(LogLevel.Error, msg, args); + } + + public static CounterToken StartCounter() + { + var t = new CounterToken + { + Id = Guid.NewGuid().ToString() + }; + + var sw = new Stopwatch(); + + counters.Add(t, sw); + + sw.Start(); + + return t; + } + + public static TimeSpan StopCounter(CounterToken counterToken) + { + if (!counters.ContainsKey(counterToken)) + return TimeSpan.Zero; + + var sw = counters[counterToken]; + + sw.Stop(); + + counters.Remove(counterToken); + + return sw.Elapsed; + } + + public static void StopCounterAndLog(CounterToken counterToken, string msg, LogLevel level = LogLevel.Info) + { + var elapsed = StopCounter(counterToken); + + if (!msg.Contains("{0}")) + msg += " {0}"; + + Log.Write(level, msg, elapsed.TotalMilliseconds); + } + } + + public static class CounterExtensions + { + public static void StopAndLog(this CounterToken counterToken, string msg, LogLevel level = LogLevel.Info) + { + Log.StopCounterAndLog(counterToken, msg, level); + } + + public static TimeSpan Stop(this CounterToken counterToken) + { + return Log.StopCounter(counterToken); + } + } + + public class CounterToken + { + public string Id { get; set; } + } + + public class ConsoleLogger : ILogger + { + public void Write(LogLevel level, string msg, params object[] args) + { + var s = msg; + + if (args != null && args.Length > 0) + s = string.Format(msg, args); + + var d = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ttt"); + + switch (level) + { + case LogLevel.Info: + Console.Out.WriteLine(d + " [INFO] " + s); + break; + case LogLevel.Debug: + Console.Out.WriteLine(d + " [DEBUG] " + s); + break; + case LogLevel.Error: + Console.Error.WriteLine(d + " [ERROR] " + s); + break; + } + } + } } diff --git a/PushSharp.Core/NotificationBlockingCollection.cs b/PushSharp.Core/NotificationBlockingCollection.cs index 7cd4352b..92e9b4a9 100644 --- a/PushSharp.Core/NotificationBlockingCollection.cs +++ b/PushSharp.Core/NotificationBlockingCollection.cs @@ -4,7 +4,7 @@ namespace PushSharp.Core { public class NotificationBlockingCollection { - public NotificationBlockingCollection () + public NotificationBlockingCollection() { } } diff --git a/PushSharp.Core/Properties/AssemblyInfo.cs b/PushSharp.Core/Properties/AssemblyInfo.cs index 3b9c03a1..880bf1d5 100644 --- a/PushSharp.Core/Properties/AssemblyInfo.cs +++ b/PushSharp.Core/Properties/AssemblyInfo.cs @@ -4,20 +4,20 @@ // Information about this assembly is defined by the following attributes. // Change them to the values specific to your project. -[assembly: AssemblyTitle ("PushSharp.Core")] -[assembly: AssemblyDescription ("")] -[assembly: AssemblyConfiguration ("")] -[assembly: AssemblyCompany ("")] -[assembly: AssemblyProduct ("")] -[assembly: AssemblyCopyright ("redth")] -[assembly: AssemblyTrademark ("")] -[assembly: AssemblyCulture ("")] +[assembly: AssemblyTitle("PushSharp.Core")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("redth")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion ("1.0.*")] +[assembly: AssemblyVersion("1.0.*")] // The following attributes are used to specify the signing key for the assembly, // if desired. See the Mono documentation for more information about signing. diff --git a/PushSharp.Core/PushHttpClient.cs b/PushSharp.Core/PushHttpClient.cs index cbc6fcfd..db0fb06d 100644 --- a/PushSharp.Core/PushHttpClient.cs +++ b/PushSharp.Core/PushHttpClient.cs @@ -6,95 +6,103 @@ namespace PushSharp.Core { - public static class PushHttpClient - { - static PushHttpClient () - { - ServicePointManager.DefaultConnectionLimit = 100; - ServicePointManager.Expect100Continue = false; - } - - public static async Task RequestAsync (PushHttpRequest request) - { - var httpRequest = HttpWebRequest.CreateHttp (request.Url); - httpRequest.Proxy = null; - - httpRequest.Headers = request.Headers; - - if (!string.IsNullOrEmpty (request.Body)) { - var requestStream = await httpRequest.GetRequestStreamAsync (); - - var requestBody = request.Encoding.GetBytes (request.Body); - - await requestStream.WriteAsync (requestBody, 0, requestBody.Length); - } - - HttpWebResponse httpResponse; - Stream responseStream; - - try { - httpResponse = await httpRequest.GetResponseAsync () as HttpWebResponse; - - responseStream = httpResponse.GetResponseStream (); - } catch (WebException webEx) { - httpResponse = webEx.Response as HttpWebResponse; - - responseStream = httpResponse.GetResponseStream (); - } - - var body = string.Empty; - - using (var sr = new StreamReader (responseStream)) - body = await sr.ReadToEndAsync (); - - var responseEncoding = Encoding.ASCII; - try { - responseEncoding = Encoding.GetEncoding (httpResponse.ContentEncoding); - } catch { - } - - var response = new PushHttpResponse { - Body = body, - Headers = httpResponse.Headers, - Uri = httpResponse.ResponseUri, - Encoding = responseEncoding, - LastModified = httpResponse.LastModified, - StatusCode = httpResponse.StatusCode - }; - - httpResponse.Close (); - httpResponse.Dispose (); - - return response; - } - - } - - public class PushHttpRequest - { - public PushHttpRequest () - { - Encoding = Encoding.ASCII; - Headers = new WebHeaderCollection (); - Method = "GET"; - Body = string.Empty; - } - - public string Url { get;set; } - public string Method { get;set; } - public string Body { get;set; } - public WebHeaderCollection Headers { get;set; } - public Encoding Encoding { get;set; } - } - - public class PushHttpResponse - { - public HttpStatusCode StatusCode { get;set; } - public string Body { get;set; } - public WebHeaderCollection Headers { get;set; } - public Uri Uri { get;set; } - public Encoding Encoding { get;set; } - public DateTime LastModified { get;set; } - } + public static class PushHttpClient + { + static PushHttpClient() + { + ServicePointManager.DefaultConnectionLimit = 100; + ServicePointManager.Expect100Continue = false; + } + + public static async Task RequestAsync(PushHttpRequest request) + { + var httpRequest = HttpWebRequest.CreateHttp(request.Url); + httpRequest.Proxy = null; + + httpRequest.Headers = request.Headers; + + if (!string.IsNullOrEmpty(request.Body)) + { + var requestStream = await httpRequest.GetRequestStreamAsync(); + + var requestBody = request.Encoding.GetBytes(request.Body); + + await requestStream.WriteAsync(requestBody, 0, requestBody.Length); + } + + HttpWebResponse httpResponse; + Stream responseStream; + + try + { + httpResponse = await httpRequest.GetResponseAsync() as HttpWebResponse; + + responseStream = httpResponse.GetResponseStream(); + } + catch (WebException webEx) + { + httpResponse = webEx.Response as HttpWebResponse; + + responseStream = httpResponse.GetResponseStream(); + } + + var body = string.Empty; + + using (var sr = new StreamReader(responseStream)) + body = await sr.ReadToEndAsync(); + + var responseEncoding = Encoding.ASCII; + try + { + responseEncoding = Encoding.GetEncoding(httpResponse.ContentEncoding); + } + catch + { + } + + var response = new PushHttpResponse + { + Body = body, + Headers = httpResponse.Headers, + Uri = httpResponse.ResponseUri, + Encoding = responseEncoding, + LastModified = httpResponse.LastModified, + StatusCode = httpResponse.StatusCode + }; + + httpResponse.Close(); + httpResponse.Dispose(); + + return response; + } + + } + + public class PushHttpRequest + { + public PushHttpRequest() + { + Encoding = Encoding.ASCII; + Headers = new WebHeaderCollection(); + Method = "GET"; + Body = string.Empty; + } + + public string Url { get; set; } + public string Method { get; set; } + public string Body { get; set; } + public WebHeaderCollection Headers { get; set; } + public Encoding Encoding { get; set; } + } + + public class PushHttpResponse + { + public HttpStatusCode StatusCode { get; set; } + public string Body { get; set; } + public WebHeaderCollection Headers { get; set; } + public Uri Uri { get; set; } + public Encoding Encoding { get; set; } + public DateTime LastModified { get; set; } + } } diff --git a/PushSharp.Core/PushSharp.Core.csproj b/PushSharp.Core/PushSharp.Core.csproj index e46f9756..c5b4d8d0 100644 --- a/PushSharp.Core/PushSharp.Core.csproj +++ b/PushSharp.Core/PushSharp.Core.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -7,9 +7,10 @@ Library PushSharp.Core PushSharp.Core - v4.5 + v4.6.1 true ..\PushSharp-Signing.snk + true diff --git a/PushSharp.Core/ServiceBroker.cs b/PushSharp.Core/ServiceBroker.cs index 4b87f7b1..e9b8dfcd 100644 --- a/PushSharp.Core/ServiceBroker.cs +++ b/PushSharp.Core/ServiceBroker.cs @@ -8,216 +8,232 @@ namespace PushSharp.Core { - public class ServiceBroker : IServiceBroker where TNotification : INotification - { - static ServiceBroker () - { - ServicePointManager.DefaultConnectionLimit = 100; - ServicePointManager.Expect100Continue = false; - } - - public ServiceBroker (IServiceConnectionFactory connectionFactory) - { - ServiceConnectionFactory = connectionFactory; - - lockWorkers = new object (); - workers = new List> (); - running = false; - - notifications = new BlockingCollection (); - ScaleSize = 1; - //AutoScale = true; - //AutoScaleMaxSize = 20; - } - - public event NotificationSuccessDelegate OnNotificationSucceeded; - public event NotificationFailureDelegate OnNotificationFailed; - - //public bool AutoScale { get; set; } - //public int AutoScaleMaxSize { get; set; } - public int ScaleSize { get; private set; } - - public IServiceConnectionFactory ServiceConnectionFactory { get; set; } - - BlockingCollection notifications; - List> workers; - object lockWorkers; - bool running; - - public virtual void QueueNotification (TNotification notification) - { - notifications.Add (notification); - } - - public IEnumerable TakeMany () - { - return notifications.GetConsumingEnumerable (); - } - - public bool IsCompleted { - get { return notifications.IsCompleted; } - } - - public void Start () - { - if (running) - return; - - running = true; - ChangeScale (ScaleSize); - } - - public void Stop (bool immediately = false) - { - if (!running) - throw new OperationCanceledException ("ServiceBroker has already been signaled to Stop"); - - running = false; - - notifications.CompleteAdding (); - - lock (lockWorkers) { - // Kill all workers right away - if (immediately) - workers.ForEach (sw => sw.Cancel ()); - - var all = (from sw in workers - select sw.WorkerTask).ToArray (); - - Log.Info ("Stopping: Waiting on Tasks"); - - Task.WaitAll (all); - - Log.Info ("Stopping: Done Waiting on Tasks"); - - workers.Clear (); - } - } - - public void ChangeScale (int newScaleSize) - { - if (newScaleSize <= 0) - throw new ArgumentOutOfRangeException ("newScaleSize", "Must be Greater than Zero"); - - ScaleSize = newScaleSize; - - if (!running) - return; - - lock (lockWorkers) { - - // Scale down - while (workers.Count > ScaleSize) { - workers [0].Cancel (); - workers.RemoveAt (0); - } - - // Scale up - while (workers.Count < ScaleSize) { - var worker = new ServiceWorker (this, ServiceConnectionFactory.Create ()); - workers.Add (worker); - worker.Start (); - } - - Log.Debug ("Scaled Changed to: " + workers.Count); - } - } - - public void RaiseNotificationSucceeded (TNotification notification) - { - var evt = OnNotificationSucceeded; - if (evt != null) - evt (notification); - } - - public void RaiseNotificationFailed (TNotification notification, AggregateException exception) - { - var evt = OnNotificationFailed; - if (evt != null) - evt (notification, exception); - } - } - - class ServiceWorker where TNotification : INotification - { - public ServiceWorker (IServiceBroker broker, IServiceConnection connection) - { - Broker = broker; - Connection = connection; - - CancelTokenSource = new CancellationTokenSource (); - } - - public IServiceBroker Broker { get; private set; } - - public IServiceConnection Connection { get; private set; } - - public CancellationTokenSource CancelTokenSource { get; private set; } - - public Task WorkerTask { get; private set; } - - public void Start () - { - WorkerTask = Task.Factory.StartNew (async delegate { - while (!CancelTokenSource.IsCancellationRequested && !Broker.IsCompleted) { - - try { - - var toSend = new List (); - foreach (var n in Broker.TakeMany ()) { - var t = Connection.Send (n); - // Keep the continuation - var cont = t.ContinueWith (ct => { - var cn = n; - var ex = t.Exception; - - if (ex == null) - Broker.RaiseNotificationSucceeded (cn); - else - Broker.RaiseNotificationFailed (cn, ex); - }); - - // Let's wait for the continuation not the task itself - toSend.Add (cont); - } - - if (toSend.Count <= 0) - continue; - - try { - Log.Info ("Waiting on all tasks {0}", toSend.Count ()); - await Task.WhenAll (toSend).ConfigureAwait (false); - Log.Info ("All Tasks Finished"); - } catch (Exception ex) { - Log.Error ("Waiting on all tasks Failed: {0}", ex); - - } - Log.Info ("Passed WhenAll"); - - } catch (Exception ex) { - Log.Error ("Broker.Take: {0}", ex); - } - } - - if (CancelTokenSource.IsCancellationRequested) - Log.Info ("Cancellation was requested"); - if (Broker.IsCompleted) - Log.Info ("Broker IsCompleted"); - - Log.Debug ("Broker Task Ended"); - }, CancelTokenSource.Token, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap (); - - WorkerTask.ContinueWith (t => { - var ex = t.Exception; - if (ex != null) - Log.Error ("ServiceWorker.WorkerTask Error: {0}", ex); - }, TaskContinuationOptions.OnlyOnFaulted); - } - - public void Cancel () - { - CancelTokenSource.Cancel (); - } - } + public class ServiceBroker : IServiceBroker where TNotification : INotification + { + static ServiceBroker() + { + ServicePointManager.DefaultConnectionLimit = 100; + ServicePointManager.Expect100Continue = false; + } + + public ServiceBroker(IServiceConnectionFactory connectionFactory) + { + ServiceConnectionFactory = connectionFactory; + + lockWorkers = new object(); + workers = new List>(); + running = false; + + notifications = new BlockingCollection(); + ScaleSize = 1; + //AutoScale = true; + //AutoScaleMaxSize = 20; + } + + public event NotificationSuccessDelegate OnNotificationSucceeded; + public event NotificationFailureDelegate OnNotificationFailed; + + //public bool AutoScale { get; set; } + //public int AutoScaleMaxSize { get; set; } + public int ScaleSize { get; private set; } + + public IServiceConnectionFactory ServiceConnectionFactory { get; set; } + + BlockingCollection notifications; + List> workers; + object lockWorkers; + bool running; + + public virtual void QueueNotification(TNotification notification) + { + notifications.Add(notification); + } + + public IEnumerable TakeMany() + { + return notifications.GetConsumingEnumerable(); + } + + public bool IsCompleted + { + get { return notifications.IsCompleted; } + } + + public void Start() + { + if (running) + return; + + running = true; + ChangeScale(ScaleSize); + } + + public void Stop(bool immediately = false) + { + if (!running) + throw new OperationCanceledException("ServiceBroker has already been signaled to Stop"); + + running = false; + + notifications.CompleteAdding(); + + lock (lockWorkers) + { + // Kill all workers right away + if (immediately) + workers.ForEach(sw => sw.Cancel()); + + var all = (from sw in workers + select sw.WorkerTask).ToArray(); + + Log.Info("Stopping: Waiting on Tasks"); + + Task.WaitAll(all); + + Log.Info("Stopping: Done Waiting on Tasks"); + + workers.Clear(); + } + } + + public void ChangeScale(int newScaleSize) + { + if (newScaleSize <= 0) + throw new ArgumentOutOfRangeException("newScaleSize", "Must be Greater than Zero"); + + ScaleSize = newScaleSize; + + if (!running) + return; + + lock (lockWorkers) + { + + // Scale down + while (workers.Count > ScaleSize) + { + workers[0].Cancel(); + workers.RemoveAt(0); + } + + // Scale up + while (workers.Count < ScaleSize) + { + var worker = new ServiceWorker(this, ServiceConnectionFactory.Create()); + workers.Add(worker); + worker.Start(); + } + + Log.Debug("Scaled Changed to: " + workers.Count); + } + } + + public void RaiseNotificationSucceeded(TNotification notification) + { + var evt = OnNotificationSucceeded; + if (evt != null) + evt(notification); + } + + public void RaiseNotificationFailed(TNotification notification, AggregateException exception) + { + var evt = OnNotificationFailed; + if (evt != null) + evt(notification, exception); + } + } + + class ServiceWorker where TNotification : INotification + { + public ServiceWorker(IServiceBroker broker, IServiceConnection connection) + { + Broker = broker; + Connection = connection; + + CancelTokenSource = new CancellationTokenSource(); + } + + public IServiceBroker Broker { get; private set; } + + public IServiceConnection Connection { get; private set; } + + public CancellationTokenSource CancelTokenSource { get; private set; } + + public Task WorkerTask { get; private set; } + + public void Start() + { + WorkerTask = Task.Factory.StartNew(async delegate + { + while (!CancelTokenSource.IsCancellationRequested && !Broker.IsCompleted) + { + + try + { + + var toSend = new List(); + foreach (var n in Broker.TakeMany()) + { + var t = Connection.Send(n); + // Keep the continuation + var cont = t.ContinueWith(ct => + { + var cn = n; + var ex = t.Exception; + + if (ex == null) + Broker.RaiseNotificationSucceeded(cn); + else + Broker.RaiseNotificationFailed(cn, ex); + }); + + // Let's wait for the continuation not the task itself + toSend.Add(cont); + } + + if (toSend.Count <= 0) + continue; + + try + { + Log.Info("Waiting on all tasks {0}", toSend.Count()); + await Task.WhenAll(toSend).ConfigureAwait(false); + Log.Info("All Tasks Finished"); + } + catch (Exception ex) + { + Log.Error("Waiting on all tasks Failed: {0}", ex); + + } + Log.Info("Passed WhenAll"); + + } + catch (Exception ex) + { + Log.Error("Broker.Take: {0}", ex); + } + } + + if (CancelTokenSource.IsCancellationRequested) + Log.Info("Cancellation was requested"); + if (Broker.IsCompleted) + Log.Info("Broker IsCompleted"); + + Log.Debug("Broker Task Ended"); + }, CancelTokenSource.Token, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap(); + + WorkerTask.ContinueWith(t => + { + var ex = t.Exception; + if (ex != null) + Log.Error("ServiceWorker.WorkerTask Error: {0}", ex); + }, TaskContinuationOptions.OnlyOnFaulted); + } + + public void Cancel() + { + CancelTokenSource.Cancel(); + } + } } diff --git a/PushSharp.Firefox/Exceptions.cs b/PushSharp.Firefox/Exceptions.cs index 406fb1fd..218ffec6 100644 --- a/PushSharp.Firefox/Exceptions.cs +++ b/PushSharp.Firefox/Exceptions.cs @@ -3,14 +3,14 @@ namespace PushSharp.Firefox { - public class FirefoxNotificationException : NotificationException - { - public FirefoxNotificationException (FirefoxNotification notification, string msg) - : base (msg, notification) - { - Notification = notification; - } + public class FirefoxNotificationException : NotificationException + { + public FirefoxNotificationException(FirefoxNotification notification, string msg) + : base(msg, notification) + { + Notification = notification; + } - public new FirefoxNotification Notification { get; private set; } - } + public new FirefoxNotification Notification { get; private set; } + } } diff --git a/PushSharp.Firefox/FirefoxConfiguration.cs b/PushSharp.Firefox/FirefoxConfiguration.cs index 086a0ce0..67aef929 100644 --- a/PushSharp.Firefox/FirefoxConfiguration.cs +++ b/PushSharp.Firefox/FirefoxConfiguration.cs @@ -2,11 +2,11 @@ namespace PushSharp.Firefox { - public class FirefoxConfiguration - { - public FirefoxConfiguration () - { - } - } + public class FirefoxConfiguration + { + public FirefoxConfiguration() + { + } + } } diff --git a/PushSharp.Firefox/FirefoxConnection.cs b/PushSharp.Firefox/FirefoxConnection.cs index acdd8afe..62a83945 100644 --- a/PushSharp.Firefox/FirefoxConnection.cs +++ b/PushSharp.Firefox/FirefoxConnection.cs @@ -7,44 +7,45 @@ namespace PushSharp.Firefox { - public class FirefoxServiceConnectionFactory : IServiceConnectionFactory - { - public FirefoxServiceConnectionFactory (FirefoxConfiguration configuration) - { - Configuration = configuration; - } - - public FirefoxConfiguration Configuration { get; private set; } - - public IServiceConnection Create() - { - return new FirefoxServiceConnection (); - } - } - - public class FirefoxServiceBroker : ServiceBroker - { - public FirefoxServiceBroker (FirefoxConfiguration configuration) : base (new FirefoxServiceConnectionFactory (configuration)) - { - } - } - - public class FirefoxServiceConnection : IServiceConnection - { - HttpClient http = new HttpClient (); - - public async Task Send (FirefoxNotification notification) - { - var data = notification.ToString (); - - http.DefaultRequestHeaders.UserAgent.Clear (); - http.DefaultRequestHeaders.UserAgent.Add (new ProductInfoHeaderValue ("PushSharp", "3.0")); - - var result = await http.PutAsync (notification.EndPointUrl, new StringContent (data)); - - if (result.StatusCode != HttpStatusCode.OK && result.StatusCode != HttpStatusCode.NoContent) { - throw new FirefoxNotificationException (notification, "HTTP Status: " + result.StatusCode); - } - } - } + public class FirefoxServiceConnectionFactory : IServiceConnectionFactory + { + public FirefoxServiceConnectionFactory(FirefoxConfiguration configuration) + { + Configuration = configuration; + } + + public FirefoxConfiguration Configuration { get; private set; } + + public IServiceConnection Create() + { + return new FirefoxServiceConnection(); + } + } + + public class FirefoxServiceBroker : ServiceBroker + { + public FirefoxServiceBroker(FirefoxConfiguration configuration) : base(new FirefoxServiceConnectionFactory(configuration)) + { + } + } + + public class FirefoxServiceConnection : IServiceConnection + { + HttpClient http = new HttpClient(); + + public async Task Send(FirefoxNotification notification) + { + var data = notification.ToString(); + + http.DefaultRequestHeaders.UserAgent.Clear(); + http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("PushSharp", "3.0")); + + var result = await http.PutAsync(notification.EndPointUrl, new StringContent(data)); + + if (result.StatusCode != HttpStatusCode.OK && result.StatusCode != HttpStatusCode.NoContent) + { + throw new FirefoxNotificationException(notification, "HTTP Status: " + result.StatusCode); + } + } + } } diff --git a/PushSharp.Firefox/FirefoxNotification.cs b/PushSharp.Firefox/FirefoxNotification.cs index cbf1fd0c..49660f2b 100644 --- a/PushSharp.Firefox/FirefoxNotification.cs +++ b/PushSharp.Firefox/FirefoxNotification.cs @@ -3,42 +3,42 @@ namespace PushSharp.Firefox { - public class FirefoxNotification : INotification - { - /// - /// Gets or sets Unique URL to be used by the AppServer to initiate a response from the App. - /// - /// - /// - /// This is generated by SimplePush and sent to the App. - /// The App will need to relay this to the AppServer. - /// - public Uri EndPointUrl { get; set; } + public class FirefoxNotification : INotification + { + /// + /// Gets or sets Unique URL to be used by the AppServer to initiate a response from the App. + /// + /// + /// + /// This is generated by SimplePush and sent to the App. + /// The App will need to relay this to the AppServer. + /// + public Uri EndPointUrl { get; set; } - /// - /// Gets or sets notification version. - /// - /// - /// - /// SimplePush does not carry information, only versions. - /// A version is a number that keeps increasing. - /// The AppServer tells the Endpoint about a new version whenever it wants an App to be notified. - /// - public string Version { get; set; } + /// + /// Gets or sets notification version. + /// + /// + /// + /// SimplePush does not carry information, only versions. + /// A version is a number that keeps increasing. + /// The AppServer tells the Endpoint about a new version whenever it wants an App to be notified. + /// + public string Version { get; set; } - public override string ToString() - { - return string.Format("version={0}", Version); - } + public override string ToString() + { + return string.Format("version={0}", Version); + } - #region INotification implementation - public bool IsDeviceRegistrationIdValid () - { - return true; - } + #region INotification implementation + public bool IsDeviceRegistrationIdValid() + { + return true; + } - public object Tag { get; set; } - #endregion - } + public object Tag { get; set; } + #endregion + } } diff --git a/PushSharp.Firefox/Properties/AssemblyInfo.cs b/PushSharp.Firefox/Properties/AssemblyInfo.cs index ccdecdce..6ce1c41f 100644 --- a/PushSharp.Firefox/Properties/AssemblyInfo.cs +++ b/PushSharp.Firefox/Properties/AssemblyInfo.cs @@ -4,20 +4,20 @@ // Information about this assembly is defined by the following attributes. // Change them to the values specific to your project. -[assembly: AssemblyTitle ("PushSharp.Firefox")] -[assembly: AssemblyDescription ("")] -[assembly: AssemblyConfiguration ("")] -[assembly: AssemblyCompany ("")] -[assembly: AssemblyProduct ("")] -[assembly: AssemblyCopyright ("redth")] -[assembly: AssemblyTrademark ("")] -[assembly: AssemblyCulture ("")] +[assembly: AssemblyTitle("PushSharp.Firefox")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("redth")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion ("1.0.*")] +[assembly: AssemblyVersion("1.0.*")] // The following attributes are used to specify the signing key for the assembly, // if desired. See the Mono documentation for more information about signing. diff --git a/PushSharp.Firefox/PushSharp.Firefox.csproj b/PushSharp.Firefox/PushSharp.Firefox.csproj index 4fb18fb7..2fc1085e 100644 --- a/PushSharp.Firefox/PushSharp.Firefox.csproj +++ b/PushSharp.Firefox/PushSharp.Firefox.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -7,9 +7,10 @@ Library PushSharp.Firefox PushSharp.Firefox - v4.5 + v4.6.1 true ..\PushSharp-Signing.snk + true diff --git a/PushSharp.Google/Exceptions.cs b/PushSharp.Google/Exceptions.cs index b27da6ec..0f859498 100644 --- a/PushSharp.Google/Exceptions.cs +++ b/PushSharp.Google/Exceptions.cs @@ -4,34 +4,34 @@ namespace PushSharp.Google { - public class GcmNotificationException : NotificationException - { - public GcmNotificationException (GcmNotification notification, string msg) : base (msg, notification) - { - Notification = notification; - } + public class GcmNotificationException : NotificationException + { + public GcmNotificationException(GcmNotification notification, string msg) : base(msg, notification) + { + Notification = notification; + } - public GcmNotificationException (GcmNotification notification, string msg, string description) : base (msg, notification) - { - Notification = notification; - Description = description; - } + public GcmNotificationException(GcmNotification notification, string msg, string description) : base(msg, notification) + { + Notification = notification; + Description = description; + } - public new GcmNotification Notification { get; private set; } - public string Description { get; private set; } - } + public new GcmNotification Notification { get; private set; } + public string Description { get; private set; } + } - public class GcmMulticastResultException : Exception - { - public GcmMulticastResultException () : base ("One or more Registration Id's failed in the multicast notification") - { - Succeeded = new List (); - Failed = new Dictionary (); - } + public class GcmMulticastResultException : Exception + { + public GcmMulticastResultException() : base("One or more Registration Id's failed in the multicast notification") + { + Succeeded = new List(); + Failed = new Dictionary(); + } - public List Succeeded { get;set; } + public List Succeeded { get; set; } - public Dictionary Failed { get;set; } - } + public Dictionary Failed { get; set; } + } } diff --git a/PushSharp.Google/GcmConfiguration.cs b/PushSharp.Google/GcmConfiguration.cs index 91f320ff..4c3c4402 100644 --- a/PushSharp.Google/GcmConfiguration.cs +++ b/PushSharp.Google/GcmConfiguration.cs @@ -2,42 +2,41 @@ namespace PushSharp.Google { - public class GcmConfiguration - { - private const string GCM_SEND_URL = "https://gcm-http.googleapis.com/gcm/send"; + public class GcmConfiguration + { + private const string GCM_SEND_URL = "https://gcm-http.googleapis.com/gcm/send"; - public GcmConfiguration (string senderAuthToken) - { - this.SenderAuthToken = senderAuthToken; - this.GcmUrl = GCM_SEND_URL; + public GcmConfiguration(string senderAuthToken) + { + this.SenderAuthToken = senderAuthToken; + this.GcmUrl = GCM_SEND_URL; - this.ValidateServerCertificate = false; - } + this.ValidateServerCertificate = false; + } - public GcmConfiguration (string optionalSenderID, string senderAuthToken, string optionalApplicationIdPackageName) - { - this.SenderID = optionalSenderID; - this.SenderAuthToken = senderAuthToken; - this.ApplicationIdPackageName = optionalApplicationIdPackageName; - this.GcmUrl = GCM_SEND_URL; + public GcmConfiguration(string optionalSenderID, string senderAuthToken, string optionalApplicationIdPackageName) + { + this.SenderID = optionalSenderID; + this.SenderAuthToken = senderAuthToken; + this.ApplicationIdPackageName = optionalApplicationIdPackageName; + this.GcmUrl = GCM_SEND_URL; - this.ValidateServerCertificate = false; - } + this.ValidateServerCertificate = false; + } - public string SenderID { get; private set; } + public string SenderID { get; private set; } - public string SenderAuthToken { get; private set; } + public string SenderAuthToken { get; private set; } - public string ApplicationIdPackageName { get; private set; } + public string ApplicationIdPackageName { get; private set; } - public bool ValidateServerCertificate { get; set; } + public bool ValidateServerCertificate { get; set; } - public string GcmUrl { get; set; } - - public void OverrideUrl (string url) - { - GcmUrl = url; - } - } -} + public string GcmUrl { get; set; } + public void OverrideUrl(string url) + { + GcmUrl = url; + } + } +} \ No newline at end of file diff --git a/PushSharp.Google/GcmMessageResult.cs b/PushSharp.Google/GcmMessageResult.cs index e467505c..57735dad 100644 --- a/PushSharp.Google/GcmMessageResult.cs +++ b/PushSharp.Google/GcmMessageResult.cs @@ -6,58 +6,58 @@ namespace PushSharp.Google { - public class GcmMessageResult - { - [JsonProperty("message_id", NullValueHandling = NullValueHandling.Ignore)] - public string MessageId { get; set; } + public class GcmMessageResult + { + [JsonProperty("message_id", NullValueHandling = NullValueHandling.Ignore)] + public string MessageId { get; set; } - [JsonProperty("registration_id", NullValueHandling = NullValueHandling.Ignore)] - public string CanonicalRegistrationId { get; set; } + [JsonProperty("registration_id", NullValueHandling = NullValueHandling.Ignore)] + public string CanonicalRegistrationId { get; set; } - [JsonIgnore] - public GcmResponseStatus ResponseStatus { get; set; } + [JsonIgnore] + public GcmResponseStatus ResponseStatus { get; set; } - [JsonProperty("error", NullValueHandling = NullValueHandling.Ignore)] - public string Error - { - get - { - switch (ResponseStatus) - { - case GcmResponseStatus.Ok: - return null; - case GcmResponseStatus.Unavailable: - return "Unavailable"; - case GcmResponseStatus.QuotaExceeded: - return "QuotaExceeded"; - case GcmResponseStatus.NotRegistered: - return "NotRegistered"; - case GcmResponseStatus.MissingRegistrationId: - return "MissingRegistration"; - case GcmResponseStatus.MissingCollapseKey: - return "MissingCollapseKey"; - case GcmResponseStatus.MismatchSenderId: - return "MismatchSenderId"; - case GcmResponseStatus.MessageTooBig: - return "MessageTooBig"; - case GcmResponseStatus.InvalidTtl: - return "InvalidTtl"; - case GcmResponseStatus.InvalidRegistration: - return "InvalidRegistration"; - case GcmResponseStatus.InvalidDataKey: - return "InvalidDataKey"; - case GcmResponseStatus.InternalServerError: - return "InternalServerError"; - case GcmResponseStatus.DeviceQuotaExceeded: - return null; - case GcmResponseStatus.CanonicalRegistrationId: - return null; - case GcmResponseStatus.Error: - return "Error"; - default: - return null; - } - } - } - } -} + [JsonProperty("error", NullValueHandling = NullValueHandling.Ignore)] + public string Error + { + get + { + switch (ResponseStatus) + { + case GcmResponseStatus.Ok: + return null; + case GcmResponseStatus.Unavailable: + return "Unavailable"; + case GcmResponseStatus.QuotaExceeded: + return "QuotaExceeded"; + case GcmResponseStatus.NotRegistered: + return "NotRegistered"; + case GcmResponseStatus.MissingRegistrationId: + return "MissingRegistration"; + case GcmResponseStatus.MissingCollapseKey: + return "MissingCollapseKey"; + case GcmResponseStatus.MismatchSenderId: + return "MismatchSenderId"; + case GcmResponseStatus.MessageTooBig: + return "MessageTooBig"; + case GcmResponseStatus.InvalidTtl: + return "InvalidTtl"; + case GcmResponseStatus.InvalidRegistration: + return "InvalidRegistration"; + case GcmResponseStatus.InvalidDataKey: + return "InvalidDataKey"; + case GcmResponseStatus.InternalServerError: + return "InternalServerError"; + case GcmResponseStatus.DeviceQuotaExceeded: + return null; + case GcmResponseStatus.CanonicalRegistrationId: + return null; + case GcmResponseStatus.Error: + return "Error"; + default: + return null; + } + } + } + } +} \ No newline at end of file diff --git a/PushSharp.Google/GcmNotification.cs b/PushSharp.Google/GcmNotification.cs index 28ba0599..5035ce3b 100644 --- a/PushSharp.Google/GcmNotification.cs +++ b/PushSharp.Google/GcmNotification.cs @@ -8,166 +8,166 @@ namespace PushSharp.Google { - public class GcmNotification : INotification - { - public static GcmNotification ForSingleResult (GcmResponse response, int resultIndex) - { - var result = new GcmNotification (); - result.Tag = response.OriginalNotification.Tag; - result.MessageId = response.OriginalNotification.MessageId; - - if (response.OriginalNotification.RegistrationIds != null && response.OriginalNotification.RegistrationIds.Count >= (resultIndex + 1)) - result.RegistrationIds.Add (response.OriginalNotification.RegistrationIds [resultIndex]); - - result.CollapseKey = response.OriginalNotification.CollapseKey; - result.Data = response.OriginalNotification.Data; - result.DelayWhileIdle = response.OriginalNotification.DelayWhileIdle; - result.ContentAvailable = response.OriginalNotification.ContentAvailable; - result.DryRun = response.OriginalNotification.DryRun; - result.Priority = response.OriginalNotification.Priority; - result.To = response.OriginalNotification.To; - result.NotificationKey = response.OriginalNotification.NotificationKey; - - return result; - } - - public static GcmNotification ForSingleRegistrationId (GcmNotification msg, string registrationId) - { - var result = new GcmNotification (); - result.Tag = msg.Tag; - result.MessageId = msg.MessageId; - result.RegistrationIds.Add (registrationId); - result.To = null; - result.CollapseKey = msg.CollapseKey; - result.Data = msg.Data; - result.DelayWhileIdle = msg.DelayWhileIdle; - result.ContentAvailable = msg.ContentAvailable; - result.DryRun = msg.DryRun; - result.Priority = msg.Priority; - result.NotificationKey = msg.NotificationKey; - - return result; - } - - public GcmNotification () - { - RegistrationIds = new List (); - CollapseKey = string.Empty; - Data = null; - DelayWhileIdle = null; - } - - public bool IsDeviceRegistrationIdValid () - { - return RegistrationIds != null && RegistrationIds.Any (); - } - - [JsonIgnore] - public object Tag { get;set; } - - [JsonProperty ("message_id")] - public string MessageId { get; internal set; } - - /// - /// Registration ID of the Device(s). Maximum of 1000 registration Id's per notification. - /// - [JsonProperty ("registration_ids")] - public List RegistrationIds { get; set; } - - /// - /// Registration ID or Group/Topic to send notification to. Overrides RegsitrationIds. - /// - /// To. - [JsonProperty ("to")] - public string To { get;set; } - - /// - /// Only the latest message with the same collapse key will be delivered - /// - [JsonProperty ("collapse_key")] - public string CollapseKey { get; set; } - - /// - /// JSON Payload to be sent in the message - /// - [JsonProperty ("data")] - public JObject Data { get; set; } - - /// - /// Notification JSON payload - /// - /// The notification payload. - [JsonProperty ("notification")] - public JObject Notification { get; set; } - - /// - /// If true, GCM will only be delivered once the device's screen is on - /// - [JsonProperty ("delay_while_idle")] - public bool? DelayWhileIdle { get; set; } - - /// - /// Time in seconds that a message should be kept on the server if the device is offline. Default (and maximum) is 4 weeks. - /// - [JsonProperty ("time_to_live")] - public int? TimeToLive { get; set; } - - /// - /// If true, dry_run attribute will be sent in payload causing the notification not to be actually sent, but the result returned simulating the message - /// - [JsonProperty ("dry_run")] - public bool? DryRun { get; set; } - - /// - /// A string that maps a single user to multiple registration IDs associated with that user. This allows a 3rd-party server to send a single message to multiple app instances (typically on multiple devices) owned by a single user. - /// - [Obsolete ("Deprecated on GCM Server API. Use Device Group Messaging to send to multiple devices.")] - public string NotificationKey { get; set; } - - /// - /// A string containing the package name of your application. When set, messages will only be sent to registration IDs that match the package name - /// - [JsonProperty ("restricted_package_name")] - public string RestrictedPackageName { get; set; } - - /// - /// On iOS, use this field to represent content-available in the APNS payload. When a notification or message is sent and this is set to true, an inactive client app is awoken. On Android, data messages wake the app by default. On Chrome, currently not supported. - /// - /// The content available. - [JsonProperty ("content_available")] - public bool? ContentAvailable { get; set; } - - /// - /// Corresponds to iOS APNS priorities (Normal is 5 and high is 10). Default is Normal. - /// - /// The priority. - [JsonProperty ("priority"), JsonConverter (typeof (Newtonsoft.Json.Converters.StringEnumConverter))] - public GcmNotificationPriority? Priority { get; set; } - - internal string GetJson () - { - // If 'To' was used instead of RegistrationIds, let's make RegistrationId's null - // so we don't serialize an empty array for this property - // otherwise, google will complain that we specified both instead - if (RegistrationIds != null && RegistrationIds.Count <= 0 && !string.IsNullOrEmpty (To)) - RegistrationIds = null; - - // Ignore null values - return JsonConvert.SerializeObject (this, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); - } - - public override string ToString () - { - return GetJson (); - } - } - - public enum GcmNotificationPriority - { - [EnumMember (Value="normal")] - Normal = 5, - [EnumMember (Value="high")] - High = 10 - } -} + public class GcmNotification : INotification + { + public static GcmNotification ForSingleResult(GcmResponse response, int resultIndex) + { + var result = new GcmNotification(); + result.Tag = response.OriginalNotification.Tag; + result.MessageId = response.OriginalNotification.MessageId; + + if (response.OriginalNotification.RegistrationIds != null && response.OriginalNotification.RegistrationIds.Count >= (resultIndex + 1)) + result.RegistrationIds.Add(response.OriginalNotification.RegistrationIds[resultIndex]); + + result.CollapseKey = response.OriginalNotification.CollapseKey; + result.Data = response.OriginalNotification.Data; + result.DelayWhileIdle = response.OriginalNotification.DelayWhileIdle; + result.ContentAvailable = response.OriginalNotification.ContentAvailable; + result.DryRun = response.OriginalNotification.DryRun; + result.Priority = response.OriginalNotification.Priority; + result.To = response.OriginalNotification.To; + result.NotificationKey = response.OriginalNotification.NotificationKey; + + return result; + } + + public static GcmNotification ForSingleRegistrationId(GcmNotification msg, string registrationId) + { + var result = new GcmNotification(); + result.Tag = msg.Tag; + result.MessageId = msg.MessageId; + result.RegistrationIds.Add(registrationId); + result.To = null; + result.CollapseKey = msg.CollapseKey; + result.Data = msg.Data; + result.DelayWhileIdle = msg.DelayWhileIdle; + result.ContentAvailable = msg.ContentAvailable; + result.DryRun = msg.DryRun; + result.Priority = msg.Priority; + result.NotificationKey = msg.NotificationKey; + + return result; + } + + public GcmNotification() + { + RegistrationIds = new List(); + CollapseKey = string.Empty; + Data = null; + DelayWhileIdle = null; + } + + public bool IsDeviceRegistrationIdValid() + { + return RegistrationIds != null && RegistrationIds.Any(); + } + + [JsonIgnore] + public object Tag { get; set; } + + [JsonProperty("message_id")] + public string MessageId { get; internal set; } + + /// + /// Registration ID of the Device(s). Maximum of 1000 registration Id's per notification. + /// + [JsonProperty("registration_ids")] + public List RegistrationIds { get; set; } + + /// + /// Registration ID or Group/Topic to send notification to. Overrides RegsitrationIds. + /// + /// To. + [JsonProperty("to")] + public string To { get; set; } + + /// + /// Only the latest message with the same collapse key will be delivered + /// + [JsonProperty("collapse_key")] + public string CollapseKey { get; set; } + + /// + /// JSON Payload to be sent in the message + /// + [JsonProperty("data")] + public JObject Data { get; set; } + + /// + /// Notification JSON payload + /// + /// The notification payload. + [JsonProperty("notification")] + public JObject Notification { get; set; } + + /// + /// If true, GCM will only be delivered once the device's screen is on + /// + [JsonProperty("delay_while_idle")] + public bool? DelayWhileIdle { get; set; } + + /// + /// Time in seconds that a message should be kept on the server if the device is offline. Default (and maximum) is 4 weeks. + /// + [JsonProperty("time_to_live")] + public int? TimeToLive { get; set; } + + /// + /// If true, dry_run attribute will be sent in payload causing the notification not to be actually sent, but the result returned simulating the message + /// + [JsonProperty("dry_run")] + public bool? DryRun { get; set; } + + /// + /// A string that maps a single user to multiple registration IDs associated with that user. This allows a 3rd-party server to send a single message to multiple app instances (typically on multiple devices) owned by a single user. + /// + [Obsolete("Deprecated on GCM Server API. Use Device Group Messaging to send to multiple devices.")] + public string NotificationKey { get; set; } + + /// + /// A string containing the package name of your application. When set, messages will only be sent to registration IDs that match the package name + /// + [JsonProperty("restricted_package_name")] + public string RestrictedPackageName { get; set; } + + /// + /// On iOS, use this field to represent content-available in the APNS payload. When a notification or message is sent and this is set to true, an inactive client app is awoken. On Android, data messages wake the app by default. On Chrome, currently not supported. + /// + /// The content available. + [JsonProperty("content_available")] + public bool? ContentAvailable { get; set; } + + /// + /// Corresponds to iOS APNS priorities (Normal is 5 and high is 10). Default is Normal. + /// + /// The priority. + [JsonProperty("priority"), JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public GcmNotificationPriority? Priority { get; set; } + + internal string GetJson() + { + // If 'To' was used instead of RegistrationIds, let's make RegistrationId's null + // so we don't serialize an empty array for this property + // otherwise, google will complain that we specified both instead + if (RegistrationIds != null && RegistrationIds.Count <= 0 && !string.IsNullOrEmpty(To)) + RegistrationIds = null; + + // Ignore null values + return JsonConvert.SerializeObject(this, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + } + + public override string ToString() + { + return GetJson(); + } + } + + public enum GcmNotificationPriority + { + [EnumMember(Value = "normal")] + Normal = 5, + [EnumMember(Value = "high")] + High = 10 + } +} \ No newline at end of file diff --git a/PushSharp.Google/GcmResponse.cs b/PushSharp.Google/GcmResponse.cs index f580d014..2e710f64 100644 --- a/PushSharp.Google/GcmResponse.cs +++ b/PushSharp.Google/GcmResponse.cs @@ -5,100 +5,99 @@ namespace PushSharp.Google { - public class GcmResponse - { - public GcmResponse() - { - MulticastId = -1; - NumberOfSuccesses = 0; - NumberOfFailures = 0; - NumberOfCanonicalIds = 0; - OriginalNotification = null; - Results = new List(); - ResponseCode = GcmResponseCode.Ok; - } + public class GcmResponse + { + public GcmResponse() + { + MulticastId = -1; + NumberOfSuccesses = 0; + NumberOfFailures = 0; + NumberOfCanonicalIds = 0; + OriginalNotification = null; + Results = new List(); + ResponseCode = GcmResponseCode.Ok; + } - [JsonProperty("multicast_id")] - public long MulticastId { get; set; } + [JsonProperty("multicast_id")] + public long MulticastId { get; set; } - [JsonProperty("success")] - public long NumberOfSuccesses { get; set; } + [JsonProperty("success")] + public long NumberOfSuccesses { get; set; } - [JsonProperty("failure")] - public long NumberOfFailures { get; set; } + [JsonProperty("failure")] + public long NumberOfFailures { get; set; } - [JsonProperty("canonical_ids")] - public long NumberOfCanonicalIds { get; set; } + [JsonProperty("canonical_ids")] + public long NumberOfCanonicalIds { get; set; } - [JsonIgnore] - public GcmNotification OriginalNotification { get; set; } + [JsonIgnore] + public GcmNotification OriginalNotification { get; set; } - [JsonProperty("results")] - public List Results { get; set; } + [JsonProperty("results")] + public List Results { get; set; } - [JsonIgnore] - public GcmResponseCode ResponseCode { get; set; } - } + [JsonIgnore] + public GcmResponseCode ResponseCode { get; set; } + } - public enum GcmResponseCode - { - Ok, - Error, - BadRequest, - ServiceUnavailable, - InvalidAuthToken, - InternalServiceError - } + public enum GcmResponseCode + { + Ok, + Error, + BadRequest, + ServiceUnavailable, + InvalidAuthToken, + InternalServiceError + } - public enum GcmResponseStatus - { - [EnumMember (Value="Ok")] - Ok, + public enum GcmResponseStatus + { + [EnumMember(Value = "Ok")] + Ok, - [EnumMember (Value="Error")] - Error, + [EnumMember(Value = "Error")] + Error, - [EnumMember (Value="QuotaExceeded")] - QuotaExceeded, + [EnumMember(Value = "QuotaExceeded")] + QuotaExceeded, - [EnumMember (Value="DeviceQuotaExceeded")] - DeviceQuotaExceeded, + [EnumMember(Value = "DeviceQuotaExceeded")] + DeviceQuotaExceeded, - [EnumMember (Value="InvalidRegistration")] - InvalidRegistration, + [EnumMember(Value = "InvalidRegistration")] + InvalidRegistration, - [EnumMember (Value="NotRegistered")] - NotRegistered, + [EnumMember(Value = "NotRegistered")] + NotRegistered, - [EnumMember (Value="MessageTooBig")] - MessageTooBig, + [EnumMember(Value = "MessageTooBig")] + MessageTooBig, - [EnumMember (Value="MissingCollapseKey")] - MissingCollapseKey, + [EnumMember(Value = "MissingCollapseKey")] + MissingCollapseKey, - [EnumMember (Value="MissingRegistration")] - MissingRegistrationId, + [EnumMember(Value = "MissingRegistration")] + MissingRegistrationId, - [EnumMember (Value="Unavailable")] - Unavailable, + [EnumMember(Value = "Unavailable")] + Unavailable, - [EnumMember (Value="MismatchSenderId")] - MismatchSenderId, + [EnumMember(Value = "MismatchSenderId")] + MismatchSenderId, - [EnumMember (Value="CanonicalRegistrationId")] - CanonicalRegistrationId, + [EnumMember(Value = "CanonicalRegistrationId")] + CanonicalRegistrationId, - [EnumMember (Value="InvalidDataKey")] - InvalidDataKey, + [EnumMember(Value = "InvalidDataKey")] + InvalidDataKey, - [EnumMember (Value="InvalidTtl")] - InvalidTtl, + [EnumMember(Value = "InvalidTtl")] + InvalidTtl, - [EnumMember (Value="InternalServerError")] - InternalServerError, - - [EnumMember (Value="InvalidPackageName")] - InvalidPackageName - } -} + [EnumMember(Value = "InternalServerError")] + InternalServerError, + [EnumMember(Value = "InvalidPackageName")] + InvalidPackageName + } +} \ No newline at end of file diff --git a/PushSharp.Google/GcmServiceConnection.cs b/PushSharp.Google/GcmServiceConnection.cs index b3b539c5..d3533895 100644 --- a/PushSharp.Google/GcmServiceConnection.cs +++ b/PushSharp.Google/GcmServiceConnection.cs @@ -1,216 +1,240 @@ using System; -using System.Threading.Tasks; +using System.Linq; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Runtime.Serialization; +using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using System.Net; using PushSharp.Core; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; namespace PushSharp.Google { - public class GcmServiceConnectionFactory : IServiceConnectionFactory - { - public GcmServiceConnectionFactory (GcmConfiguration configuration) - { - Configuration = configuration; - } - - public GcmConfiguration Configuration { get; private set; } - - public IServiceConnection Create() - { - return new GcmServiceConnection (Configuration); - } - } - - public class GcmServiceBroker : ServiceBroker - { - public GcmServiceBroker (GcmConfiguration configuration) : base (new GcmServiceConnectionFactory (configuration)) - { - } - } - - public class GcmServiceConnection : IServiceConnection - { - public GcmServiceConnection (GcmConfiguration configuration) - { - Configuration = configuration; - http = new HttpClient (); - - http.DefaultRequestHeaders.UserAgent.Clear (); - http.DefaultRequestHeaders.UserAgent.Add (new ProductInfoHeaderValue ("PushSharp", "3.0")); - http.DefaultRequestHeaders.TryAddWithoutValidation ("Authorization", "key=" + Configuration.SenderAuthToken); - } - - public GcmConfiguration Configuration { get; private set; } - - readonly HttpClient http; - - public async Task Send (GcmNotification notification) - { - var json = notification.GetJson (); - - var content = new StringContent (json, System.Text.Encoding.UTF8, "application/json"); - - var response = await http.PostAsync (Configuration.GcmUrl, content); - - if (response.IsSuccessStatusCode) { - await processResponseOk (response, notification).ConfigureAwait (false); - } else { - await processResponseError (response, notification).ConfigureAwait (false); - } - } - - async Task processResponseOk (HttpResponseMessage httpResponse, GcmNotification notification) - { - var multicastResult = new GcmMulticastResultException (); - - var result = new GcmResponse () { - ResponseCode = GcmResponseCode.Ok, - OriginalNotification = notification - }; - - var str = await httpResponse.Content.ReadAsStringAsync (); - var json = JObject.Parse (str); - - result.NumberOfCanonicalIds = json.Value ("canonical_ids"); - result.NumberOfFailures = json.Value ("failure"); - result.NumberOfSuccesses = json.Value ("success"); - - var jsonResults = json ["results"] as JArray ?? new JArray (); - - foreach (var r in jsonResults) { - var msgResult = new GcmMessageResult (); - - msgResult.MessageId = r.Value ("message_id"); - msgResult.CanonicalRegistrationId = r.Value ("registration_id"); - msgResult.ResponseStatus = GcmResponseStatus.Ok; - - if (!string.IsNullOrEmpty (msgResult.CanonicalRegistrationId)) - msgResult.ResponseStatus = GcmResponseStatus.CanonicalRegistrationId; - else if (r ["error"] != null) { - var err = r.Value ("error") ?? ""; - - msgResult.ResponseStatus = GetGcmResponseStatus (err); - } - - result.Results.Add (msgResult); - } - - int index = 0; - - //Loop through every result in the response - // We will raise events for each individual result so that the consumer of the library - // can deal with individual registrationid's for the notification - foreach (var r in result.Results) { - var singleResultNotification = GcmNotification.ForSingleResult (result, index); - - singleResultNotification.MessageId = r.MessageId; - - if (r.ResponseStatus == GcmResponseStatus.Ok) { // Success - multicastResult.Succeeded.Add (singleResultNotification); - } else if (r.ResponseStatus == GcmResponseStatus.CanonicalRegistrationId) { //Need to swap reg id's - //Swap Registrations Id's - var newRegistrationId = r.CanonicalRegistrationId; - var oldRegistrationId = string.Empty; - - if (singleResultNotification.RegistrationIds != null && singleResultNotification.RegistrationIds.Count > 0) - { - oldRegistrationId = singleResultNotification.RegistrationIds[0]; - } - else if (!string.IsNullOrEmpty(singleResultNotification.To)) - { - oldRegistrationId = singleResultNotification.To; - } - - multicastResult.Failed.Add (singleResultNotification, - new DeviceSubscriptionExpiredException (singleResultNotification) { - OldSubscriptionId = oldRegistrationId, - NewSubscriptionId = newRegistrationId - }); - } else if (r.ResponseStatus == GcmResponseStatus.Unavailable) { // Unavailable - multicastResult.Failed.Add (singleResultNotification, new GcmNotificationException (singleResultNotification, "Unavailable Response Status")); - } else if (r.ResponseStatus == GcmResponseStatus.NotRegistered) { //Bad registration Id - var oldRegistrationId = string.Empty; - - if (singleResultNotification.RegistrationIds != null && singleResultNotification.RegistrationIds.Count > 0) - { - oldRegistrationId = singleResultNotification.RegistrationIds[0]; - } - else if (!string.IsNullOrEmpty(singleResultNotification.To)) - { - oldRegistrationId = singleResultNotification.To; - } - - multicastResult.Failed.Add (singleResultNotification, - new DeviceSubscriptionExpiredException (singleResultNotification) { - OldSubscriptionId = oldRegistrationId }); - } else { - multicastResult.Failed.Add (singleResultNotification, new GcmNotificationException (singleResultNotification, "Unknown Failure: " + r.ResponseStatus)); - } - - index++; - } - - // If we only have 1 total result, it is not *multicast*, - if (multicastResult.Succeeded.Count + multicastResult.Failed.Count == 1) { - // If not multicast, and succeeded, don't throw any errors! - if (multicastResult.Succeeded.Count == 1) - return; - // Otherwise, throw the one single failure we must have - throw multicastResult.Failed.First ().Value; - } - - // If we get here, we must have had a multicast message - // throw if we had any failures at all (otherwise all must be successful, so throw no error - if (multicastResult.Failed.Count > 0) - throw multicastResult; - } - - async Task processResponseError (HttpResponseMessage httpResponse, GcmNotification notification) - { - string responseBody = null; - - try { - responseBody = await httpResponse.Content.ReadAsStringAsync ().ConfigureAwait (false); - } catch { } - - //401 bad auth token - if (httpResponse.StatusCode == HttpStatusCode.Unauthorized) - throw new UnauthorizedAccessException ("GCM Authorization Failed"); - - if (httpResponse.StatusCode == HttpStatusCode.BadRequest) - throw new GcmNotificationException (notification, "HTTP 400 Bad Request", responseBody); - - if ((int)httpResponse.StatusCode >= 500 && (int)httpResponse.StatusCode < 600) { - //First try grabbing the retry-after header and parsing it. - var retryAfterHeader = httpResponse.Headers.RetryAfter; - - if (retryAfterHeader != null && retryAfterHeader.Delta.HasValue) { - var retryAfter = retryAfterHeader.Delta.Value; - throw new RetryAfterException (notification, "GCM Requested Backoff", DateTime.UtcNow + retryAfter); - } - } - - throw new GcmNotificationException (notification, "GCM HTTP Error: " + httpResponse.StatusCode, responseBody); - } - - static GcmResponseStatus GetGcmResponseStatus (string str) - { - var enumType = typeof(GcmResponseStatus); - - foreach (var name in Enum.GetNames (enumType)) { - var enumMemberAttribute = ((EnumMemberAttribute[])enumType.GetField (name).GetCustomAttributes (typeof(EnumMemberAttribute), true)).Single (); - - if (enumMemberAttribute.Value.Equals (str, StringComparison.InvariantCultureIgnoreCase)) - return (GcmResponseStatus)Enum.Parse (enumType, name); - } - - //Default - return GcmResponseStatus.Error; - } - } + public class GcmServiceConnectionFactory : IServiceConnectionFactory + { + public GcmServiceConnectionFactory(GcmConfiguration configuration) + { + Configuration = configuration; + } + + public GcmConfiguration Configuration { get; private set; } + + public IServiceConnection Create() + { + return new GcmServiceConnection(Configuration); + } + } + + public class GcmServiceBroker : ServiceBroker + { + public GcmServiceBroker(GcmConfiguration configuration) : base(new GcmServiceConnectionFactory(configuration)) + { + } + } + + public class GcmServiceConnection : IServiceConnection + { + public GcmServiceConnection(GcmConfiguration configuration) + { + Configuration = configuration; + http = new HttpClient(); + + http.DefaultRequestHeaders.UserAgent.Clear(); + http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("PushSharp", "3.0")); + http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "key=" + Configuration.SenderAuthToken); + } + + public GcmConfiguration Configuration { get; private set; } + + readonly HttpClient http; + + public async Task Send(GcmNotification notification) + { + var json = notification.GetJson(); + + var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); + + var response = await http.PostAsync(Configuration.GcmUrl, content); + + if (response.IsSuccessStatusCode) + { + await processResponseOk(response, notification).ConfigureAwait(false); + } + else + { + await processResponseError(response, notification).ConfigureAwait(false); + } + } + + async Task processResponseOk(HttpResponseMessage httpResponse, GcmNotification notification) + { + var multicastResult = new GcmMulticastResultException(); + + var result = new GcmResponse() + { + ResponseCode = GcmResponseCode.Ok, + OriginalNotification = notification + }; + + var str = await httpResponse.Content.ReadAsStringAsync(); + var json = JObject.Parse(str); + + result.NumberOfCanonicalIds = json.Value("canonical_ids"); + result.NumberOfFailures = json.Value("failure"); + result.NumberOfSuccesses = json.Value("success"); + + var jsonResults = json["results"] as JArray ?? new JArray(); + + foreach (var r in jsonResults) + { + var msgResult = new GcmMessageResult(); + + msgResult.MessageId = r.Value("message_id"); + msgResult.CanonicalRegistrationId = r.Value("registration_id"); + msgResult.ResponseStatus = GcmResponseStatus.Ok; + + if (!string.IsNullOrEmpty(msgResult.CanonicalRegistrationId)) + msgResult.ResponseStatus = GcmResponseStatus.CanonicalRegistrationId; + else if (r["error"] != null) + { + var err = r.Value("error") ?? ""; + + msgResult.ResponseStatus = GetGcmResponseStatus(err); + } + + result.Results.Add(msgResult); + } + + int index = 0; + + //Loop through every result in the response + // We will raise events for each individual result so that the consumer of the library + // can deal with individual registrationid's for the notification + foreach (var r in result.Results) + { + var singleResultNotification = GcmNotification.ForSingleResult(result, index); + + singleResultNotification.MessageId = r.MessageId; + + if (r.ResponseStatus == GcmResponseStatus.Ok) + { // Success + multicastResult.Succeeded.Add(singleResultNotification); + } + else if (r.ResponseStatus == GcmResponseStatus.CanonicalRegistrationId) + { //Need to swap reg id's + //Swap Registrations Id's + var newRegistrationId = r.CanonicalRegistrationId; + var oldRegistrationId = string.Empty; + + if (singleResultNotification.RegistrationIds != null && singleResultNotification.RegistrationIds.Count > 0) + { + oldRegistrationId = singleResultNotification.RegistrationIds[0]; + } + else if (!string.IsNullOrEmpty(singleResultNotification.To)) + { + oldRegistrationId = singleResultNotification.To; + } + + multicastResult.Failed.Add(singleResultNotification, + new DeviceSubscriptionExpiredException(singleResultNotification) + { + OldSubscriptionId = oldRegistrationId, + NewSubscriptionId = newRegistrationId + }); + } + else if (r.ResponseStatus == GcmResponseStatus.Unavailable) + { // Unavailable + multicastResult.Failed.Add(singleResultNotification, new GcmNotificationException(singleResultNotification, "Unavailable Response Status")); + } + else if (r.ResponseStatus == GcmResponseStatus.NotRegistered) + { //Bad registration Id + var oldRegistrationId = string.Empty; + + if (singleResultNotification.RegistrationIds != null && singleResultNotification.RegistrationIds.Count > 0) + { + oldRegistrationId = singleResultNotification.RegistrationIds[0]; + } + else if (!string.IsNullOrEmpty(singleResultNotification.To)) + { + oldRegistrationId = singleResultNotification.To; + } + + multicastResult.Failed.Add(singleResultNotification, + new DeviceSubscriptionExpiredException(singleResultNotification) + { + OldSubscriptionId = oldRegistrationId + }); + } + else + { + multicastResult.Failed.Add(singleResultNotification, new GcmNotificationException(singleResultNotification, "Unknown Failure: " + r.ResponseStatus)); + } + + index++; + } + + // If we only have 1 total result, it is not *multicast*, + if (multicastResult.Succeeded.Count + multicastResult.Failed.Count == 1) + { + // If not multicast, and succeeded, don't throw any errors! + if (multicastResult.Succeeded.Count == 1) + return; + // Otherwise, throw the one single failure we must have + throw multicastResult.Failed.First().Value; + } + + // If we get here, we must have had a multicast message + // throw if we had any failures at all (otherwise all must be successful, so throw no error + if (multicastResult.Failed.Count > 0) + throw multicastResult; + } + + async Task processResponseError(HttpResponseMessage httpResponse, GcmNotification notification) + { + string responseBody = null; + + try + { + responseBody = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false); + } + catch { } + + //401 bad auth token + if (httpResponse.StatusCode == HttpStatusCode.Unauthorized) + throw new UnauthorizedAccessException("GCM Authorization Failed"); + + if (httpResponse.StatusCode == HttpStatusCode.BadRequest) + throw new GcmNotificationException(notification, "HTTP 400 Bad Request", responseBody); + + if ((int)httpResponse.StatusCode >= 500 && (int)httpResponse.StatusCode < 600) + { + //First try grabbing the retry-after header and parsing it. + var retryAfterHeader = httpResponse.Headers.RetryAfter; + + if (retryAfterHeader != null && retryAfterHeader.Delta.HasValue) + { + var retryAfter = retryAfterHeader.Delta.Value; + throw new RetryAfterException(notification, "GCM Requested Backoff", DateTime.UtcNow + retryAfter); + } + } + + throw new GcmNotificationException(notification, "GCM HTTP Error: " + httpResponse.StatusCode, responseBody); + } + + static GcmResponseStatus GetGcmResponseStatus(string str) + { + var enumType = typeof(GcmResponseStatus); + + foreach (var name in Enum.GetNames(enumType)) + { + var enumMemberAttribute = ((EnumMemberAttribute[])enumType.GetField(name).GetCustomAttributes(typeof(EnumMemberAttribute), true)).Single(); + + if (enumMemberAttribute.Value.Equals(str, StringComparison.InvariantCultureIgnoreCase)) + return (GcmResponseStatus)Enum.Parse(enumType, name); + } + + //Default + return GcmResponseStatus.Error; + } + } } diff --git a/PushSharp.Google/Properties/AssemblyInfo.cs b/PushSharp.Google/Properties/AssemblyInfo.cs index 2afce1ae..394e9cd5 100644 --- a/PushSharp.Google/Properties/AssemblyInfo.cs +++ b/PushSharp.Google/Properties/AssemblyInfo.cs @@ -4,20 +4,20 @@ // Information about this assembly is defined by the following attributes. // Change them to the values specific to your project. -[assembly: AssemblyTitle ("PushSharp.Google")] -[assembly: AssemblyDescription ("")] -[assembly: AssemblyConfiguration ("")] -[assembly: AssemblyCompany ("")] -[assembly: AssemblyProduct ("")] -[assembly: AssemblyCopyright ("redth")] -[assembly: AssemblyTrademark ("")] -[assembly: AssemblyCulture ("")] +[assembly: AssemblyTitle("PushSharp.Google")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("redth")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion ("1.0.*")] +[assembly: AssemblyVersion("1.0.*")] // The following attributes are used to specify the signing key for the assembly, // if desired. See the Mono documentation for more information about signing. diff --git a/PushSharp.Google/PushSharp.Google.csproj b/PushSharp.Google/PushSharp.Google.csproj index 92d72f31..5a423ed6 100644 --- a/PushSharp.Google/PushSharp.Google.csproj +++ b/PushSharp.Google/PushSharp.Google.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -7,9 +7,10 @@ Library PushSharp.Google PushSharp.Google - v4.5 + v4.6.1 true ..\PushSharp-Signing.snk + true @@ -30,14 +31,15 @@ false + + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + True + - - ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll - diff --git a/PushSharp.Google/packages.config b/PushSharp.Google/packages.config index 505e5883..e1fae9c6 100644 --- a/PushSharp.Google/packages.config +++ b/PushSharp.Google/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/PushSharp.Tests/AdmRealTests.cs b/PushSharp.Tests/AdmRealTests.cs index 4c17e4c8..90492be3 100644 --- a/PushSharp.Tests/AdmRealTests.cs +++ b/PushSharp.Tests/AdmRealTests.cs @@ -1,46 +1,50 @@ -using System; -using NUnit.Framework; +using System.Collections.Generic; +using System.ComponentModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; using PushSharp.Amazon; -using System.Collections.Generic; namespace PushSharp.Tests { - [TestFixture] - public class AdmRealTests - { - [Test] - [Category ("Disabled")] - public void ADM_Send_Single () - { - var succeeded = 0; - var failed = 0; - var attempted = 0; + [TestClass] + public class AdmRealTests + { + [TestMethod] + [Category("Disabled")] + public void ADM_Send_Single() + { + var succeeded = 0; + var failed = 0; + var attempted = 0; - var config = new AdmConfiguration (Settings.Instance.AdmClientId, Settings.Instance.AdmClientSecret); - var broker = new AdmServiceBroker (config); - broker.OnNotificationFailed += (notification, exception) => { - failed++; - }; - broker.OnNotificationSucceeded += (notification) => { - succeeded++; - }; - broker.Start (); + var config = new AdmConfiguration(Settings.Instance.AdmClientId, Settings.Instance.AdmClientSecret); + var broker = new AdmServiceBroker(config); + broker.OnNotificationFailed += (notification, exception) => + { + failed++; + }; + broker.OnNotificationSucceeded += (notification) => + { + succeeded++; + }; + broker.Start(); - foreach (var regId in Settings.Instance.AdmRegistrationIds) { - attempted++; - broker.QueueNotification (new AdmNotification { - RegistrationId = regId, - Data = new Dictionary { - { "somekey", "somevalue" } - } - }); - } + foreach (var regId in Settings.Instance.AdmRegistrationIds) + { + attempted++; + broker.QueueNotification(new AdmNotification + { + RegistrationId = regId, + Data = new Dictionary { + { "somekey", "somevalue" } + } + }); + } - broker.Stop (); + broker.Stop(); - Assert.AreEqual (attempted, succeeded); - Assert.AreEqual (0, failed); - } - } + Assert.AreEqual(attempted, succeeded); + Assert.AreEqual(0, failed); + } + } } diff --git a/PushSharp.Tests/ApnsRealTest.cs b/PushSharp.Tests/ApnsRealTest.cs index 40e24294..dce78b44 100644 --- a/PushSharp.Tests/ApnsRealTest.cs +++ b/PushSharp.Tests/ApnsRealTest.cs @@ -1,60 +1,66 @@ using System; -using NUnit.Framework; -using PushSharp.Apple; +using System.ComponentModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json.Linq; +using PushSharp.Apple; namespace PushSharp.Tests { - [TestFixture] - [Category ("Disabled")] - public class ApnsRealTest - { - [Test] - public void APNS_Send_Single () - { - var succeeded = 0; - var failed = 0; - var attempted = 0; - - var config = new ApnsConfiguration (ApnsConfiguration.ApnsServerEnvironment.Sandbox, Settings.Instance.ApnsCertificateFile, Settings.Instance.ApnsCertificatePassword); - var broker = new ApnsServiceBroker (config); - broker.OnNotificationFailed += (notification, exception) => { - failed++; - }; - broker.OnNotificationSucceeded += (notification) => { - succeeded++; - }; - broker.Start (); - - foreach (var dt in Settings.Instance.ApnsDeviceTokens) { - attempted++; - broker.QueueNotification (new ApnsNotification { - DeviceToken = dt, - Payload = JObject.Parse ("{ \"aps\" : { \"alert\" : \"Hello PushSharp!\" } }") - }); - } - - broker.Stop (); - - 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 (); - } - } + [TestClass] + [Category("Disabled")] + public class ApnsRealTest + { + [TestMethod] + public void APNS_Send_Single() + { + var succeeded = 0; + var failed = 0; + var attempted = 0; + + var config = new ApnsConfiguration(ApnsConfiguration.ApnsServerEnvironment.Sandbox, Settings.Instance.ApnsCertificateFile, Settings.Instance.ApnsCertificatePassword); + var broker = new ApnsServiceBroker(config); + broker.OnNotificationFailed += (notification, exception) => + { + failed++; + }; + broker.OnNotificationSucceeded += (notification) => + { + succeeded++; + }; + broker.Start(); + + foreach (var dt in Settings.Instance.ApnsDeviceTokens) + { + attempted++; + broker.QueueNotification(new ApnsNotification + { + DeviceToken = dt, + Payload = JObject.Parse("{ \"aps\" : { \"alert\" : \"Hello PushSharp!\" } }") + }); + } + + broker.Stop(); + + Assert.AreEqual(attempted, succeeded); + Assert.AreEqual(0, failed); + } + + [TestMethod] + 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 index b7e5ab3f..fbb7cdb1 100644 --- a/PushSharp.Tests/ApnsTests.cs +++ b/PushSharp.Tests/ApnsTests.cs @@ -1,169 +1,177 @@ using System; -using System.Threading.Tasks; using System.Collections.Generic; -using PushSharp.Apple; +using System.ComponentModel; using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json.Linq; -using NUnit.Framework; +using PushSharp.Apple; 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); + [TestClass] + [Category("Fake")] + public class ApnsTests + { + [TestMethod] + public async Task APNS_Single_Succeeds() + { + await Apns(0, 1, new List()); + } + + [TestMethod] + public async Task APNS_Multiple_Succeed() + { + await Apns(0, 3, new List()); + } + + //[TestMethod] + public async Task APNS_Send_Many() + { + await Apns(10, 1010, new List { + new ApnsResponseFilter ((id, deviceToken, payload) => { + return id % 100 == 0; + }) + }); + } + + [TestMethod] + public async Task APNS_Send_Small() + { + await Apns(2, 256, new List() { + new ApnsResponseFilter ((id, deviceToken, payload) => { + return id % 100 == 0; + }) + }); + } + + //[TestMethod] + public async Task APNS_Scale_Brokers() + { + await Apns(10, 10100, new List { + new ApnsResponseFilter ((id, deviceToken, payload) => { + return id % 1000 == 0; + }) + }, scale: 2); + } + + [TestMethod] + 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"); - var expectedSuccess = notifications.Count - expectFailed; + await server.Stop().ConfigureAwait(false); + + var expectedSuccess = notifications.Count - expectFailed; - var actualFailed = failed; - var actualSuccess = success; + 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); + 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(expectFailed, actualFailed); + Assert.AreEqual(expectedSuccess, actualSuccess); - Assert.AreEqual (server.Failed, actualFailed); - Assert.AreEqual (server.Successful, actualSuccess); - } - } + Assert.AreEqual(server.Failed, actualFailed); + Assert.AreEqual(server.Successful, actualSuccess); + } + } } diff --git a/PushSharp.Tests/BrokerTests.cs b/PushSharp.Tests/BrokerTests.cs index 92308707..9c35f9b7 100644 --- a/PushSharp.Tests/BrokerTests.cs +++ b/PushSharp.Tests/BrokerTests.cs @@ -1,92 +1,91 @@ -using NUnit.Framework; - -using System; -using PushSharp.Core; -using System.Threading.Tasks; +using System.ComponentModel; using System.Linq; -using System.Collections.Generic; -using PushSharp.Apple; -using Newtonsoft.Json.Linq; -using System.Threading; - +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PushSharp.Core; namespace PushSharp.Tests { - [TestFixture] - [Category ("Core")] + [TestClass] + [Category("Core")] public class BrokerTests { - [Test] - public void Broker_Send_Many () + [TestMethod] + public void Broker_Send_Many() { - var succeeded = 0; - var failed = 0; - var attempted = 0; - - var broker = new TestServiceBroker (); - broker.OnNotificationFailed += (notification, exception) => { - failed++; - }; - broker.OnNotificationSucceeded += (notification) => { - succeeded++; - }; - broker.Start (); - broker.ChangeScale (1); - - var c = Log.StartCounter (); - - for (int i = 1; i <= 1000; i++) { - attempted++; - broker.QueueNotification (new TestNotification { TestId = i }); - } - - broker.Stop (); - - c.StopAndLog ("Test Took {0} ms"); - - Assert.AreEqual (attempted, succeeded); - Assert.AreEqual (0, failed); + var succeeded = 0; + var failed = 0; + var attempted = 0; + + var broker = new TestServiceBroker(); + broker.OnNotificationFailed += (notification, exception) => + { + failed++; + }; + broker.OnNotificationSucceeded += (notification) => + { + succeeded++; + }; + broker.Start(); + broker.ChangeScale(1); + + var c = Log.StartCounter(); + + for (int i = 1; i <= 1000; i++) + { + attempted++; + broker.QueueNotification(new TestNotification { TestId = i }); + } + + broker.Stop(); + + c.StopAndLog("Test Took {0} ms"); + + Assert.AreEqual(attempted, succeeded); + Assert.AreEqual(0, failed); } - [Test] - #pragma warning disable 1998 - public async Task Broker_Some_Fail () - #pragma warning restore 1998 - { - var succeeded = 0; - var failed = 0; - var attempted = 0; + [TestMethod] +#pragma warning disable 1998 + public async Task Broker_Some_Fail() +#pragma warning restore 1998 + { + var succeeded = 0; + var failed = 0; + var attempted = 0; - const int count = 10; - var failIds = new [] { 3, 5, 7 }; + const int count = 10; + var failIds = new[] { 3, 5, 7 }; - var broker = new TestServiceBroker (); - broker.OnNotificationFailed += (notification, exception) => - failed++; - broker.OnNotificationSucceeded += (notification) => - succeeded++; + var broker = new TestServiceBroker(); + broker.OnNotificationFailed += (notification, exception) => + failed++; + broker.OnNotificationSucceeded += (notification) => + succeeded++; - broker.Start (); - broker.ChangeScale (1); + broker.Start(); + broker.ChangeScale(1); - var c = Log.StartCounter (); + var c = Log.StartCounter(); - for (int i = 1; i <= count; i++) { - attempted++; - broker.QueueNotification (new TestNotification { - TestId = i, - ShouldFail = failIds.Contains (i) - }); - } + for (int i = 1; i <= count; i++) + { + attempted++; + broker.QueueNotification(new TestNotification + { + TestId = i, + ShouldFail = failIds.Contains(i) + }); + } - broker.Stop (); + broker.Stop(); - c.StopAndLog ("Test Took {0} ms"); + c.StopAndLog("Test Took {0} ms"); - Assert.AreEqual (attempted - failIds.Length, succeeded); - Assert.AreEqual (failIds.Length, failed); - } - } + Assert.AreEqual(attempted - failIds.Length, succeeded); + Assert.AreEqual(failIds.Length, failed); + } + } } diff --git a/PushSharp.Tests/GcmRealTests.cs b/PushSharp.Tests/GcmRealTests.cs index 4e915644..97ccda04 100644 --- a/PushSharp.Tests/GcmRealTests.cs +++ b/PushSharp.Tests/GcmRealTests.cs @@ -1,49 +1,50 @@ -using System; -using NUnit.Framework; -using PushSharp.Google; -using System.Collections.Generic; +using System.Collections.Generic; +using System.ComponentModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json.Linq; +using PushSharp.Google; namespace PushSharp.Tests { - [TestFixture] - [Category ("Real")] - public class GcmRealTests - { - [Test] - public void Gcm_Send_Single () - { - var succeeded = 0; - var failed = 0; - var attempted = 0; - - var config = new GcmConfiguration (Settings.Instance.GcmSenderId, Settings.Instance.GcmAuthToken, null); - var broker = new GcmServiceBroker (config); - broker.OnNotificationFailed += (notification, exception) => { - failed++; - }; - broker.OnNotificationSucceeded += (notification) => { - succeeded++; - }; - - broker.Start (); - - foreach (var regId in Settings.Instance.GcmRegistrationIds) { - attempted++; - - broker.QueueNotification (new GcmNotification { - RegistrationIds = new List { - regId - }, - Data = JObject.Parse ("{ \"somekey\" : \"somevalue\" }") - }); - } - - broker.Stop (); - - Assert.AreEqual (attempted, succeeded); - Assert.AreEqual (0, failed); - } - } -} - + [TestClass] + [Category("Real")] + public class GcmRealTests + { + [TestMethod] + public void Gcm_Send_Single() + { + var succeeded = 0; + var failed = 0; + var attempted = 0; + + var config = new GcmConfiguration(Settings.Instance.GcmSenderId, Settings.Instance.GcmAuthToken, null); + var broker = new GcmServiceBroker(config); + broker.OnNotificationFailed += (notification, exception) => + { + failed++; + }; + broker.OnNotificationSucceeded += (notification) => + { + succeeded++; + }; + + broker.Start(); + + foreach (var regId in Settings.Instance.GcmRegistrationIds) + { + attempted++; + + broker.QueueNotification(new GcmNotification + { + RegistrationIds = new List { regId }, + Data = JObject.Parse("{ \"somekey1\" : \"somevalue 1\" }") + }); + } + + broker.Stop(); + + Assert.AreEqual(attempted, succeeded); + Assert.AreEqual(0, failed); + } + } +} \ No newline at end of file diff --git a/PushSharp.Tests/GcmTests.cs b/PushSharp.Tests/GcmTests.cs index 2590a207..a6e6fe9f 100644 --- a/PushSharp.Tests/GcmTests.cs +++ b/PushSharp.Tests/GcmTests.cs @@ -1,36 +1,34 @@ -using System; -using NUnit.Framework; +using System.ComponentModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; using PushSharp.Google; -using System.Collections.Generic; -using Newtonsoft.Json.Linq; namespace PushSharp.Tests { - [Category ("GCM")] - [TestFixture] - public class GcmTests - { - [Test] - public void GcmNotification_Priority_Should_Serialize_As_String_High () - { - var n = new GcmNotification (); - n.Priority = GcmNotificationPriority.High; + [Category("GCM")] + [TestClass] + public class GcmTests + { + [TestMethod] + public void GcmNotification_Priority_Should_Serialize_As_String_High() + { + var n = new GcmNotification(); + n.Priority = GcmNotificationPriority.High; - var str = n.ToString (); + var str = n.ToString(); - Assert.IsTrue (str.Contains ("high")); - } + Assert.IsTrue(str.Contains("high")); + } - [Test] - public void GcmNotification_Priority_Should_Serialize_As_String_Normal () - { - var n = new GcmNotification (); - n.Priority = GcmNotificationPriority.Normal; + [TestMethod] + public void GcmNotification_Priority_Should_Serialize_As_String_Normal() + { + var n = new GcmNotification(); + n.Priority = GcmNotificationPriority.Normal; - var str = n.ToString (); + var str = n.ToString(); - Assert.IsTrue (str.Contains ("normal")); - } - } + Assert.IsTrue(str.Contains("normal")); + } + } } diff --git a/PushSharp.Tests/PushSharp.Tests.csproj b/PushSharp.Tests/PushSharp.Tests.csproj index 6cdef5f4..6e64fd96 100644 --- a/PushSharp.Tests/PushSharp.Tests.csproj +++ b/PushSharp.Tests/PushSharp.Tests.csproj @@ -1,5 +1,5 @@  - + Debug AnyCPU @@ -7,7 +7,9 @@ Library PushSharp.Tests PushSharp.Tests - v4.5 + v4.6.1 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + true @@ -28,13 +30,12 @@ false - - - ..\packages\NUnit.2.6.4\lib\nunit.framework.dll - - - ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + + + False + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + diff --git a/PushSharp.Tests/Servers/TestApnsServer.cs b/PushSharp.Tests/Servers/TestApnsServer.cs index 1be38243..a044ad4d 100644 --- a/PushSharp.Tests/Servers/TestApnsServer.cs +++ b/PushSharp.Tests/Servers/TestApnsServer.cs @@ -7,337 +7,370 @@ namespace PushSharp.Tests { - public class TestApnsServer - { - const string TAG = "TestApnsServer"; + public class TestApnsServer + { + const string TAG = "TestApnsServer"; - public TestApnsServer () - { - ResponseFilters = new List (); - } + public TestApnsServer() + { + ResponseFilters = new List(); + } - Socket listener; - bool running = false; - long totalBytesRx = 0; + Socket listener; + bool running = false; + long totalBytesRx = 0; - public int Successful { get; set; } - public int Failed { get; set; } + public int Successful { get; set; } + public int Failed { get; set; } - ManualResetEvent waitStop = new ManualResetEvent (false); + ManualResetEvent waitStop = new ManualResetEvent(false); - public List ResponseFilters { get; set; } + public List ResponseFilters { get; set; } - #pragma warning disable 1998 - public async Task Stop () - #pragma warning restore 1998 - { - running = false; +#pragma warning disable 1998 + public async Task Stop() +#pragma warning restore 1998 + { + running = false; - try { listener.Shutdown (SocketShutdown.Both); } - catch { } + try { listener.Shutdown(SocketShutdown.Both); } + catch { } - try { listener.Close (); } - catch { } + try { listener.Close(); } + catch { } - try { listener.Dispose (); } - catch { } + try { listener.Dispose(); } + catch { } - waitStop.WaitOne (); - } + waitStop.WaitOne(); + } - public async Task Start () - { - Console.WriteLine (TAG + " -> Starting Mock APNS Server..."); - running = true; + public async Task Start() + { + Console.WriteLine(TAG + " -> Starting Mock APNS Server..."); + running = true; - listener = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - listener.Bind (new IPEndPoint (IPAddress.Any, 2195)); + listener.Bind(new IPEndPoint(IPAddress.Any, 2195)); - listener.Listen (100); + listener.Listen(100); - Console.WriteLine (TAG + " -> Started Mock APNS Server."); + Console.WriteLine(TAG + " -> Started Mock APNS Server."); - while (running) { + while (running) + { - Socket socket = null; + Socket socket = null; - try { - // Get a client connection - socket = await Task.Factory.FromAsync ( - listener.BeginAccept (null, null), - listener.EndAccept).ConfigureAwait (false); - } catch { - } + try + { + // Get a client connection + socket = await Task.Factory.FromAsync( + listener.BeginAccept(null, null), + listener.EndAccept).ConfigureAwait(false); + } + catch + { + } - if (socket == null) - break; + if (socket == null) + break; - Console.WriteLine (TAG + " -> Client Connected."); + 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 + // 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 sentErrorResponse = false; + var s = socket; + byte[] buffer = new byte[1024000]; // 1 MB - var data = new List (); + 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; + // Do processing, continually receiving from the socket + while (true) + { + var received = s.Receive(buffer); - totalBytesRx += received; + if (received <= 0 && data.Count <= 0) + break; - Console.WriteLine (TAG + " -> Received {0} bytes...", received); + totalBytesRx += received; - // Add the received data to our data list - for (int i = 0; i < received; i++) - data.Add (buffer [i]); - - ApnsServerNotification notification = null; + Console.WriteLine(TAG + " -> Received {0} bytes...", received); - try { - - while ((notification = Parse (data)) != null) { + // Add the received data to our data list + for (int i = 0; i < received; i++) + data.Add(buffer[i]); - if (!sentErrorResponse) - Successful++; + ApnsServerNotification notification = null; - // Console.WriteLine (TAG + " -> Rx'd ID: {0}, DeviceToken: {1}, Payload: {2}", notification.Identifier, notification.DeviceToken, notification.Payload); + try + { - 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; - } - } - } - + while ((notification = Parse(data)) != null) + { - } catch (ApnsNotificationException ex) { + if (!sentErrorResponse) + Successful++; - Console.WriteLine (TAG + " -> Notification Exception: {0}", ex); + // Console.WriteLine (TAG + " -> Rx'd ID: {0}, DeviceToken: {1}, Payload: {2}", notification.Identifier, notification.DeviceToken, notification.Payload); - if (!sentErrorResponse) - SendErrorResponse (s, ex.ErrorStatusCode, ex.NotificationId); - sentErrorResponse = true; + 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; + } + } + } - break; - } - - } - try { - s.Shutdown (SocketShutdown.Both); - } catch { - } - try { - s.Close (); - } catch { - } - try { - s.Dispose (); - } catch { - } + } + catch (ApnsNotificationException ex) + { - Console.WriteLine (TAG + " -> Client Disconnected..."); - }); - } + Console.WriteLine(TAG + " -> Notification Exception: {0}", ex); - waitStop.Set (); + if (!sentErrorResponse) + SendErrorResponse(s, ex.ErrorStatusCode, ex.NotificationId); + sentErrorResponse = true; - Console.WriteLine (TAG + " -> Stopped APNS Server."); - } + break; + } + } - void SendErrorResponse (Socket s, ApnsNotificationErrorStatusCode statusCode, int identifier) - { - Failed++; - Successful--; + try + { + s.Shutdown(SocketShutdown.Both); + } + catch + { + } + try + { + s.Close(); + } + catch + { + } + try + { + s.Dispose(); + } + catch + { + } - var errorResponseData = new byte[6]; - errorResponseData[0] = 0x01; - errorResponseData[1] = BitConverter.GetBytes ( (short)statusCode)[0]; + Console.WriteLine(TAG + " -> Client Disconnected..."); + }); + } - var id = BitConverter.GetBytes (IPAddress.HostToNetworkOrder (identifier)); - Buffer.BlockCopy (id, 0, errorResponseData, 2, 4); + waitStop.Set(); - var sent = Task.Factory.FromAsync ( - s.BeginSend (errorResponseData, 0, errorResponseData.Length, SocketFlags.None, null, null), - s.EndSend).Result; - } + Console.WriteLine(TAG + " -> Stopped APNS Server."); + } - 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; } - } + 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; } + } } diff --git a/PushSharp.Tests/Settings.cs b/PushSharp.Tests/Settings.cs index 108b7b2f..b045d6e0 100644 --- a/PushSharp.Tests/Settings.cs +++ b/PushSharp.Tests/Settings.cs @@ -5,73 +5,77 @@ namespace PushSharp.Tests { - public class Settings - { - static Settings instance = null; + public class Settings + { + static Settings instance = null; - public static Settings Instance { - get { - if (instance == null) { + public static Settings Instance + { + get + { + if (instance == null) + { - var envData = Environment.GetEnvironmentVariable ("TEST_CONFIG_JSON"); + var envData = Environment.GetEnvironmentVariable("TEST_CONFIG_JSON"); - if (!string.IsNullOrEmpty (envData)) { - instance = JsonConvert.DeserializeObject (envData); - return instance; - } + if (!string.IsNullOrEmpty(envData)) + { + instance = JsonConvert.DeserializeObject(envData); + return instance; + } - var baseDir = AppDomain.CurrentDomain.BaseDirectory; + var baseDir = AppDomain.CurrentDomain.BaseDirectory; - var settingsFile = Path.Combine (baseDir, "settings.json"); + var settingsFile = Path.Combine(baseDir, "settings.json"); - if (!File.Exists (settingsFile)) - settingsFile = Path.Combine (baseDir, "../", "settings.json"); - if (!File.Exists (settingsFile)) - settingsFile = Path.Combine (baseDir, "../../", "settings.json"); - if (!File.Exists (settingsFile)) - settingsFile = Path.Combine (baseDir, "../../../", "settings.json"); + if (!File.Exists(settingsFile)) + settingsFile = Path.Combine(baseDir, "../", "settings.json"); + if (!File.Exists(settingsFile)) + settingsFile = Path.Combine(baseDir, "../../", "settings.json"); + if (!File.Exists(settingsFile)) + settingsFile = Path.Combine(baseDir, "../../../", "settings.json"); - if (!File.Exists (settingsFile)) - throw new FileNotFoundException ("You must provide a settings.json file to run these tests. See the settings.json.sample file for more information."); - - instance = JsonConvert.DeserializeObject (File.ReadAllText (settingsFile)); - } - return instance; - } - } - public Settings () - { - } + if (!File.Exists(settingsFile)) + throw new FileNotFoundException("You must provide a settings.json file to run these tests. See the settings.json.sample file for more information."); - [JsonProperty ("apns_cert_file")] - public string ApnsCertificateFile { get; set; } - [JsonProperty ("apns_cert_pwd")] - public string ApnsCertificatePassword { get;set; } - [JsonProperty ("apns_device_tokens")] - public List ApnsDeviceTokens { get;set; } + instance = JsonConvert.DeserializeObject(File.ReadAllText(settingsFile)); + } + return instance; + } + } + public Settings() + { + } - [JsonProperty ("gcm_auth_token")] - public string GcmAuthToken { get;set; } - [JsonProperty ("gcm_sender_id")] - public string GcmSenderId { get;set; } - [JsonProperty ("gcm_registration_ids")] - public List GcmRegistrationIds { get;set; } + [JsonProperty("apns_cert_file")] + public string ApnsCertificateFile { get; set; } + [JsonProperty("apns_cert_pwd")] + public string ApnsCertificatePassword { get; set; } + [JsonProperty("apns_device_tokens")] + public List ApnsDeviceTokens { get; set; } - [JsonProperty ("adm_client_id")] - public string AdmClientId { get;set; } - [JsonProperty ("adm_client_secret")] - public string AdmClientSecret { get;set; } - [JsonProperty ("adm_registration_ids")] - public List AdmRegistrationIds { get;set; } + [JsonProperty("gcm_auth_token")] + public string GcmAuthToken { get; set; } + [JsonProperty("gcm_sender_id")] + public string GcmSenderId { get; set; } + [JsonProperty("gcm_registration_ids")] + public List GcmRegistrationIds { get; set; } - [JsonProperty ("wns_package_name")] - public string WnsPackageName { get;set; } - [JsonProperty ("wns_package_sid")] - public string WnsPackageSid { get;set; } - [JsonProperty ("wns_client_secret")] - public string WnsClientSecret { get;set; } - [JsonProperty ("wns_channel_uris")] - public List WnsChannelUris { get;set; } - } + [JsonProperty("adm_client_id")] + public string AdmClientId { get; set; } + [JsonProperty("adm_client_secret")] + public string AdmClientSecret { get; set; } + [JsonProperty("adm_registration_ids")] + public List AdmRegistrationIds { get; set; } + + [JsonProperty("wns_package_name")] + public string WnsPackageName { get; set; } + [JsonProperty("wns_package_sid")] + public string WnsPackageSid { get; set; } + [JsonProperty("wns_client_secret")] + public string WnsClientSecret { get; set; } + [JsonProperty("wns_channel_uris")] + public List WnsChannelUris { get; set; } + } } diff --git a/PushSharp.Tests/TestServiceConnection.cs b/PushSharp.Tests/TestServiceConnection.cs index 628dac9c..df9f4094 100644 --- a/PushSharp.Tests/TestServiceConnection.cs +++ b/PushSharp.Tests/TestServiceConnection.cs @@ -1,67 +1,67 @@ using System; -using System.Linq; -using PushSharp.Core; using System.Threading.Tasks; +using PushSharp.Core; namespace PushSharp.Tests { - public class TestNotification : INotification + public class TestNotification : INotification { public static int TESTID = 1; public TestNotification() { - TestId = TestNotification.TESTID++; + TestId = TestNotification.TESTID++; } - public object Tag { get;set; } - public int TestId { get;set; } + public object Tag { get; set; } + public int TestId { get; set; } - public bool ShouldFail { get;set; } + public bool ShouldFail { get; set; } - public override string ToString () + public override string ToString() { return TestId.ToString(); } - public bool IsDeviceRegistrationIdValid () - { - return true; - } + public bool IsDeviceRegistrationIdValid() + { + return true; + } } public class TestServiceConnectionFactory : IServiceConnectionFactory { - public IServiceConnection Create () + public IServiceConnection Create() { - return new TestServiceConnection (); + return new TestServiceConnection(); } } - public class TestServiceBroker : ServiceBroker - { - public TestServiceBroker (TestServiceConnectionFactory connectionFactory) : base (connectionFactory) - { - } + public class TestServiceBroker : ServiceBroker + { + public TestServiceBroker(TestServiceConnectionFactory connectionFactory) : base(connectionFactory) + { + } - public TestServiceBroker () : base (new TestServiceConnectionFactory ()) - { - } - } + public TestServiceBroker() : base(new TestServiceConnectionFactory()) + { + } + } - public class TestServiceConnection : IServiceConnection - { - public async Task Send (TestNotification notification) - { - var id = notification.TestId; + public class TestServiceConnection : IServiceConnection + { + public async Task Send(TestNotification notification) + { + var id = notification.TestId; - await Task.Delay (250).ConfigureAwait (false); + await Task.Delay(250).ConfigureAwait(false); - if (notification.ShouldFail) { - Console.WriteLine ("Fail {0}...", id); - throw new Exception ("Notification Should Fail: " + id); - } + if (notification.ShouldFail) + { + Console.WriteLine("Fail {0}...", id); + throw new Exception("Notification Should Fail: " + id); + } } } -} +} diff --git a/PushSharp.Tests/WnsRealTests.cs b/PushSharp.Tests/WnsRealTests.cs index 005af51e..e222644d 100644 --- a/PushSharp.Tests/WnsRealTests.cs +++ b/PushSharp.Tests/WnsRealTests.cs @@ -1,40 +1,44 @@ -using System; -using NUnit.Framework; -using PushSharp.Windows; +using System.ComponentModel; using System.Xml.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PushSharp.Windows; namespace PushSharp.Tests { - [TestFixture] - [Category ("Disabled")] - public class WnsRealTests - { - [Test] - public void WNS_Send_Single () - { - var succeeded = 0; - var failed = 0; - var attempted = 0; + [TestClass] + [Category("Disabled")] + public class WnsRealTests + { + [TestMethod] + public void WNS_Send_Single() + { + var succeeded = 0; + var failed = 0; + var attempted = 0; - var config = new WnsConfiguration (Settings.Instance.WnsPackageName, - Settings.Instance.WnsPackageSid, - Settings.Instance.WnsClientSecret); + var config = new WnsConfiguration(Settings.Instance.WnsPackageName, + Settings.Instance.WnsPackageSid, + Settings.Instance.WnsClientSecret); - var broker = new WnsServiceBroker (config); - broker.OnNotificationFailed += (notification, exception) => { - failed++; - }; - broker.OnNotificationSucceeded += (notification) => { - succeeded ++; - }; + var broker = new WnsServiceBroker(config); + broker.OnNotificationFailed += (notification, exception) => + { + failed++; + }; + broker.OnNotificationSucceeded += (notification) => + { + succeeded++; + }; - broker.Start (); + broker.Start(); - foreach (var uri in Settings.Instance.WnsChannelUris) { - attempted++; - broker.QueueNotification (new WnsToastNotification { - ChannelUri = uri, - Payload = XElement.Parse (@" + foreach (var uri in Settings.Instance.WnsChannelUris) + { + attempted++; + broker.QueueNotification(new WnsToastNotification + { + ChannelUri = uri, + Payload = XElement.Parse(@" @@ -43,42 +47,47 @@ public void WNS_Send_Single () ") - }); - } + }); + } - broker.Stop (); + broker.Stop(); - Assert.AreEqual (attempted, succeeded); - Assert.AreEqual (0, failed); - } + Assert.AreEqual(attempted, succeeded); + Assert.AreEqual(0, failed); + } - [Test] - public void WNS_Send_Mutiple () - { - var succeeded = 0; - var failed = 0; - var attempted = 0; + [TestMethod] + public void WNS_Send_Mutiple() + { + var succeeded = 0; + var failed = 0; + var attempted = 0; - var config = new WnsConfiguration (Settings.Instance.WnsPackageName, - Settings.Instance.WnsPackageSid, - Settings.Instance.WnsClientSecret); + var config = new WnsConfiguration(Settings.Instance.WnsPackageName, + Settings.Instance.WnsPackageSid, + Settings.Instance.WnsClientSecret); - var broker = new WnsServiceBroker (config); - broker.OnNotificationFailed += (notification, exception) => { - failed++; - }; - broker.OnNotificationSucceeded += (notification) => { - succeeded++; - }; + var broker = new WnsServiceBroker(config); + broker.OnNotificationFailed += (notification, exception) => + { + failed++; + }; + broker.OnNotificationSucceeded += (notification) => + { + succeeded++; + }; - broker.Start (); + broker.Start(); - foreach (var uri in Settings.Instance.WnsChannelUris) { - for (var i = 1; i <= 3; i++) { - attempted++; - broker.QueueNotification (new WnsToastNotification { - ChannelUri = uri, - Payload = XElement.Parse(@" + foreach (var uri in Settings.Instance.WnsChannelUris) + { + for (var i = 1; i <= 3; i++) + { + attempted++; + broker.QueueNotification(new WnsToastNotification + { + ChannelUri = uri, + Payload = XElement.Parse(@" @@ -87,15 +96,15 @@ public void WNS_Send_Mutiple () ") - }); - } - } + }); + } + } - broker.Stop (); + broker.Stop(); - Assert.AreEqual (attempted, succeeded); - Assert.AreEqual (0, failed); - } - } + Assert.AreEqual(attempted, succeeded); + Assert.AreEqual(0, failed); + } + } } diff --git a/PushSharp.Tests/packages.config b/PushSharp.Tests/packages.config index 3b829b9f..e1fae9c6 100644 --- a/PushSharp.Tests/packages.config +++ b/PushSharp.Tests/packages.config @@ -1,5 +1,4 @@  - - + \ No newline at end of file diff --git a/PushSharp.Tests/settings.sample.json b/PushSharp.Tests/settings.sample.json deleted file mode 100644 index b4994bbe..00000000 --- a/PushSharp.Tests/settings.sample.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "apns_cert_file" : "", - "apns_cert_pwd" : "", - "apns_device_tokens" : - [ - "", - ], - - "gcm_auth_token" : "", - "gcm_sender_id" : "", - "gcm_registration_ids" : - [ - "", - ], - - "adm_client_id" : "", - "adm_client_secret" : "", - "adm_registration_ids" : - [ - "", - ], -} diff --git a/PushSharp.Windows/Exceptions.cs b/PushSharp.Windows/Exceptions.cs index d3d39ded..3c11b2e3 100644 --- a/PushSharp.Windows/Exceptions.cs +++ b/PushSharp.Windows/Exceptions.cs @@ -3,21 +3,21 @@ namespace PushSharp.Windows { - public class WnsNotificationException : NotificationException - { - public WnsNotificationException (WnsNotificationStatus status) : base (status.ErrorDescription, status.Notification) - { - Notification = status.Notification; - Status = status; - } + public class WnsNotificationException : NotificationException + { + public WnsNotificationException(WnsNotificationStatus status) : base(status.ErrorDescription, status.Notification) + { + Notification = status.Notification; + Status = status; + } - public new WnsNotification Notification { get; set; } - public WnsNotificationStatus Status { get; private set; } + public new WnsNotification Notification { get; set; } + public WnsNotificationStatus Status { get; private set; } - public override string ToString () - { - return base.ToString() + " Status = " + Status.HttpStatus; - } - } + public override string ToString() + { + return base.ToString() + " Status = " + Status.HttpStatus; + } + } } diff --git a/PushSharp.Windows/Properties/AssemblyInfo.cs b/PushSharp.Windows/Properties/AssemblyInfo.cs index 61efcba9..74a59fac 100644 --- a/PushSharp.Windows/Properties/AssemblyInfo.cs +++ b/PushSharp.Windows/Properties/AssemblyInfo.cs @@ -4,20 +4,20 @@ // Information about this assembly is defined by the following attributes. // Change them to the values specific to your project. -[assembly: AssemblyTitle ("PushSharp.Windows")] -[assembly: AssemblyDescription ("")] -[assembly: AssemblyConfiguration ("")] -[assembly: AssemblyCompany ("")] -[assembly: AssemblyProduct ("")] -[assembly: AssemblyCopyright ("redth")] -[assembly: AssemblyTrademark ("")] -[assembly: AssemblyCulture ("")] +[assembly: AssemblyTitle("PushSharp.Windows")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("redth")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion ("1.0.*")] +[assembly: AssemblyVersion("1.0.*")] // The following attributes are used to specify the signing key for the assembly, // if desired. See the Mono documentation for more information about signing. diff --git a/PushSharp.Windows/PushSharp.Windows.csproj b/PushSharp.Windows/PushSharp.Windows.csproj index e85dc90b..0ebb42d6 100644 --- a/PushSharp.Windows/PushSharp.Windows.csproj +++ b/PushSharp.Windows/PushSharp.Windows.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -7,9 +7,10 @@ Library PushSharp.Windows PushSharp.Windows - v4.5 + v4.6.1 true ..\PushSharp-Signing.snk + true @@ -30,13 +31,14 @@ false + + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + True + - - ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll - diff --git a/PushSharp.Windows/WnsConfiguration.cs b/PushSharp.Windows/WnsConfiguration.cs index 9da7811f..61e02d13 100644 --- a/PushSharp.Windows/WnsConfiguration.cs +++ b/PushSharp.Windows/WnsConfiguration.cs @@ -2,18 +2,18 @@ namespace PushSharp.Windows { - public class WnsConfiguration - { - public WnsConfiguration (string packageName, string packageSecurityIdentifier, string clientSecret) - { - PackageName = packageName; - PackageSecurityIdentifier = packageSecurityIdentifier; - ClientSecret = clientSecret; - } + public class WnsConfiguration + { + public WnsConfiguration(string packageName, string packageSecurityIdentifier, string clientSecret) + { + PackageName = packageName; + PackageSecurityIdentifier = packageSecurityIdentifier; + ClientSecret = clientSecret; + } - public string PackageName { get; private set; } - public string PackageSecurityIdentifier { get; private set; } - public string ClientSecret { get; private set; } - } + public string PackageName { get; private set; } + public string PackageSecurityIdentifier { get; private set; } + public string ClientSecret { get; private set; } + } } diff --git a/PushSharp.Windows/WnsConnection.cs b/PushSharp.Windows/WnsConnection.cs index 323ebed6..89885231 100644 --- a/PushSharp.Windows/WnsConnection.cs +++ b/PushSharp.Windows/WnsConnection.cs @@ -1,197 +1,202 @@ using System; -using PushSharp.Core; -using System.Threading.Tasks; -using System.Net.Http; -using System.Net.Http.Headers; using System.Collections.Generic; -using Newtonsoft.Json.Linq; -using System.Xml; +using System.IO; using System.Linq; using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; using System.Text; -using System.IO; +using System.Threading.Tasks; +using PushSharp.Core; namespace PushSharp.Windows { - public class WnsServiceConnectionFactory : IServiceConnectionFactory - { - WnsAccessTokenManager wnsAccessTokenManager; - - public WnsServiceConnectionFactory (WnsConfiguration configuration) - { - wnsAccessTokenManager = new WnsAccessTokenManager (configuration); - Configuration = configuration; - } - - public WnsConfiguration Configuration { get; private set; } - - public IServiceConnection Create() - { - return new WnsServiceConnection (Configuration, wnsAccessTokenManager); - } - } - - public class WnsServiceBroker : ServiceBroker - { - public WnsServiceBroker (WnsConfiguration configuration) : base (new WnsServiceConnectionFactory (configuration)) - { - } - } - - public class WnsServiceConnection : IServiceConnection - { - public WnsServiceConnection (WnsConfiguration configuration, WnsAccessTokenManager accessTokenManager) - { - AccessTokenManager = accessTokenManager; - Configuration = configuration; - } - - public WnsAccessTokenManager AccessTokenManager { get; private set; } - public WnsConfiguration Configuration { get; private set; } - - public async Task Send (WnsNotification notification) - { - // Get or renew our access token - var accessToken = await AccessTokenManager.GetAccessToken (); - - //https://cloud.notify.windows.com/?token=..... - //Authorization: Bearer {AccessToken} - // - - //TODO: Microsoft recommends we disable expect-100 to improve latency - // Not sure how to do this in httpclient - var http = new HttpClient (); - - http.DefaultRequestHeaders.TryAddWithoutValidation ("X-WNS-Type", string.Format ("wns/{0}", notification.Type.ToString ().ToLower ())); - if(!http.DefaultRequestHeaders.Contains("Authorization")) //prevent double values - http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "Bearer " + accessToken); - - if (notification.RequestForStatus.HasValue) - http.DefaultRequestHeaders.TryAddWithoutValidation ("X-WNS-RequestForStatus", notification.RequestForStatus.Value.ToString().ToLower()); - - if (notification.TimeToLive.HasValue) - http.DefaultRequestHeaders.TryAddWithoutValidation ("X-WNS-TTL", notification.TimeToLive.Value.ToString()); //Time to live in seconds - - if (notification.Type == WnsNotificationType.Tile) - { - var winTileNot = notification as WnsTileNotification; - - if (winTileNot != null && winTileNot.CachePolicy.HasValue) - http.DefaultRequestHeaders.Add("X-WNS-Cache-Policy", winTileNot.CachePolicy == WnsNotificationCachePolicyType.Cache ? "cache" : "no-cache"); - - if (winTileNot != null && !string.IsNullOrEmpty(winTileNot.NotificationTag)) - http.DefaultRequestHeaders.Add("X-WNS-Tag", winTileNot.NotificationTag); // TILE only - } - else if (notification.Type == WnsNotificationType.Badge) - { - var winTileBadge = notification as WnsBadgeNotification; - - if (winTileBadge != null && winTileBadge.CachePolicy.HasValue) - http.DefaultRequestHeaders.Add("X-WNS-Cache-Policy", winTileBadge.CachePolicy == WnsNotificationCachePolicyType.Cache ? "cache" : "no-cache"); - } - - HttpContent content = null; - - if (notification.Type == WnsNotificationType.Raw) { - content = new StreamContent (new MemoryStream (Encoding.UTF8.GetBytes (notification.Payload.ToString()))); - } else { - content = new StringContent( - notification.Payload.ToString(), // Get XML payload - Encoding.UTF8, - "text/xml"); - } - - var result = await http.PostAsync (notification.ChannelUri, content); - - var status = ParseStatus (result, notification); - - //RESPONSE HEADERS - // X-WNS-Debug-Trace string - // X-WNS-DeviceConnectionStatus connected | disconnected | tempdisconnected (if RequestForStatus was set to true) - // X-WNS-Error-Description string - // X-WNS-Msg-ID string (max 16 char) - // X-WNS-NotificationStatus received | dropped | channelthrottled - // - - // 200 OK - // 400 One or more headers were specified incorrectly or conflict with another header. - // 401 The cloud service did not present a valid authentication ticket. The OAuth ticket may be invalid. - // 403 The cloud service is not authorized to send a notification to this URI even though they are authenticated. - // 404 The channel URI is not valid or is not recognized by WNS. - Raise Expiry - // 405 Invalid Method - never will get - // 406 The cloud service exceeded its throttle limit. - // 410 The channel expired. - Raise Expiry - // 413 The notification payload exceeds the 5000 byte size limit. - // 500 An internal failure caused notification delivery to fail. - // 503 The server is currently unavailable. - - // OK, everything worked! - if (status.HttpStatus == HttpStatusCode.OK - && status.NotificationStatus == WnsNotificationSendStatus.Received) { - return; - } - - //401 - if (status.HttpStatus == HttpStatusCode.Unauthorized) { - AccessTokenManager.InvalidateAccessToken (accessToken); - throw new RetryAfterException (notification, "Access token expired", DateTime.UtcNow.AddSeconds (5)); - } - - //404 or 410 - if (status.HttpStatus == HttpStatusCode.NotFound || status.HttpStatus == HttpStatusCode.Gone) { - throw new DeviceSubscriptionExpiredException (notification) { - OldSubscriptionId = notification.ChannelUri, - ExpiredAt = DateTime.UtcNow - }; - } - - - // Any other error - throw new WnsNotificationException (status); - } - - WnsNotificationStatus ParseStatus(HttpResponseMessage resp, WnsNotification notification) - { - var result = new WnsNotificationStatus(); - - result.Notification = notification; - result.HttpStatus = resp.StatusCode; - - var wnsDebugTrace = TryGetHeaderValue (resp.Headers, "X-WNS-DEBUG-TRACE") ?? ""; - var wnsDeviceConnectionStatus = TryGetHeaderValue (resp.Headers, "X-WNS-DEVICECONNECTIONSTATUS") ?? "connected"; - var wnsErrorDescription = TryGetHeaderValue (resp.Headers, "X-WNS-ERROR-DESCRIPTION") ?? ""; - var wnsMsgId = TryGetHeaderValue (resp.Headers, "X-WNS-MSG-ID"); - var wnsNotificationStatus = TryGetHeaderValue (resp.Headers, "X-WNS-NOTIFICATIONSTATUS") ?? ""; - - result.DebugTrace = wnsDebugTrace; - result.ErrorDescription = wnsErrorDescription; - result.MessageId = wnsMsgId; - - if (wnsNotificationStatus.Equals("received", StringComparison.InvariantCultureIgnoreCase)) - result.NotificationStatus = WnsNotificationSendStatus.Received; - else if (wnsNotificationStatus.Equals("dropped", StringComparison.InvariantCultureIgnoreCase)) - result.NotificationStatus = WnsNotificationSendStatus.Dropped; - else - result.NotificationStatus = WnsNotificationSendStatus.ChannelThrottled; - - if (wnsDeviceConnectionStatus.Equals("connected", StringComparison.InvariantCultureIgnoreCase)) - result.DeviceConnectionStatus = WnsDeviceConnectionStatus.Connected; - else if (wnsDeviceConnectionStatus.Equals("tempdisconnected", StringComparison.InvariantCultureIgnoreCase)) - result.DeviceConnectionStatus = WnsDeviceConnectionStatus.TempDisconnected; - else - result.DeviceConnectionStatus = WnsDeviceConnectionStatus.Disconnected; - - return result; - } - - string TryGetHeaderValue (HttpResponseHeaders headers, string headerName) - { - IEnumerable values; - if (headers.TryGetValues (headerName, out values)) - return values.FirstOrDefault (); - - return null; - } - } + public class WnsServiceConnectionFactory : IServiceConnectionFactory + { + WnsAccessTokenManager wnsAccessTokenManager; + + public WnsServiceConnectionFactory(WnsConfiguration configuration) + { + wnsAccessTokenManager = new WnsAccessTokenManager(configuration); + Configuration = configuration; + } + + public WnsConfiguration Configuration { get; private set; } + + public IServiceConnection Create() + { + return new WnsServiceConnection(Configuration, wnsAccessTokenManager); + } + } + + public class WnsServiceBroker : ServiceBroker + { + public WnsServiceBroker(WnsConfiguration configuration) : base(new WnsServiceConnectionFactory(configuration)) + { + } + } + + public class WnsServiceConnection : IServiceConnection + { + public WnsServiceConnection(WnsConfiguration configuration, WnsAccessTokenManager accessTokenManager) + { + AccessTokenManager = accessTokenManager; + Configuration = configuration; + } + + public WnsAccessTokenManager AccessTokenManager { get; private set; } + public WnsConfiguration Configuration { get; private set; } + + public async Task Send(WnsNotification notification) + { + // Get or renew our access token + var accessToken = await AccessTokenManager.GetAccessToken(); + + //https://cloud.notify.windows.com/?token=..... + //Authorization: Bearer {AccessToken} + // + + //TODO: Microsoft recommends we disable expect-100 to improve latency + // Not sure how to do this in httpclient + var http = new HttpClient(); + + http.DefaultRequestHeaders.TryAddWithoutValidation("X-WNS-Type", string.Format("wns/{0}", notification.Type.ToString().ToLower())); + if (!http.DefaultRequestHeaders.Contains("Authorization")) //prevent double values + http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "Bearer " + accessToken); + + if (notification.RequestForStatus.HasValue) + http.DefaultRequestHeaders.TryAddWithoutValidation("X-WNS-RequestForStatus", notification.RequestForStatus.Value.ToString().ToLower()); + + if (notification.TimeToLive.HasValue) + http.DefaultRequestHeaders.TryAddWithoutValidation("X-WNS-TTL", notification.TimeToLive.Value.ToString()); //Time to live in seconds + + if (notification.Type == WnsNotificationType.Tile) + { + var winTileNot = notification as WnsTileNotification; + + if (winTileNot != null && winTileNot.CachePolicy.HasValue) + http.DefaultRequestHeaders.Add("X-WNS-Cache-Policy", winTileNot.CachePolicy == WnsNotificationCachePolicyType.Cache ? "cache" : "no-cache"); + + if (winTileNot != null && !string.IsNullOrEmpty(winTileNot.NotificationTag)) + http.DefaultRequestHeaders.Add("X-WNS-Tag", winTileNot.NotificationTag); // TILE only + } + else if (notification.Type == WnsNotificationType.Badge) + { + var winTileBadge = notification as WnsBadgeNotification; + + if (winTileBadge != null && winTileBadge.CachePolicy.HasValue) + http.DefaultRequestHeaders.Add("X-WNS-Cache-Policy", winTileBadge.CachePolicy == WnsNotificationCachePolicyType.Cache ? "cache" : "no-cache"); + } + + HttpContent content = null; + + if (notification.Type == WnsNotificationType.Raw) + { + content = new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(notification.Payload.ToString()))); + } + else + { + content = new StringContent( + notification.Payload.ToString(), // Get XML payload + Encoding.UTF8, + "text/xml"); + } + + var result = await http.PostAsync(notification.ChannelUri, content); + + var status = ParseStatus(result, notification); + + //RESPONSE HEADERS + // X-WNS-Debug-Trace string + // X-WNS-DeviceConnectionStatus connected | disconnected | tempdisconnected (if RequestForStatus was set to true) + // X-WNS-Error-Description string + // X-WNS-Msg-ID string (max 16 char) + // X-WNS-NotificationStatus received | dropped | channelthrottled + // + + // 200 OK + // 400 One or more headers were specified incorrectly or conflict with another header. + // 401 The cloud service did not present a valid authentication ticket. The OAuth ticket may be invalid. + // 403 The cloud service is not authorized to send a notification to this URI even though they are authenticated. + // 404 The channel URI is not valid or is not recognized by WNS. - Raise Expiry + // 405 Invalid Method - never will get + // 406 The cloud service exceeded its throttle limit. + // 410 The channel expired. - Raise Expiry + // 413 The notification payload exceeds the 5000 byte size limit. + // 500 An internal failure caused notification delivery to fail. + // 503 The server is currently unavailable. + + // OK, everything worked! + if (status.HttpStatus == HttpStatusCode.OK + && status.NotificationStatus == WnsNotificationSendStatus.Received) + { + return; + } + + //401 + if (status.HttpStatus == HttpStatusCode.Unauthorized) + { + AccessTokenManager.InvalidateAccessToken(accessToken); + throw new RetryAfterException(notification, "Access token expired", DateTime.UtcNow.AddSeconds(5)); + } + + //404 or 410 + if (status.HttpStatus == HttpStatusCode.NotFound || status.HttpStatus == HttpStatusCode.Gone) + { + throw new DeviceSubscriptionExpiredException(notification) + { + OldSubscriptionId = notification.ChannelUri, + ExpiredAt = DateTime.UtcNow + }; + } + + + // Any other error + throw new WnsNotificationException(status); + } + + WnsNotificationStatus ParseStatus(HttpResponseMessage resp, WnsNotification notification) + { + var result = new WnsNotificationStatus(); + + result.Notification = notification; + result.HttpStatus = resp.StatusCode; + + var wnsDebugTrace = TryGetHeaderValue(resp.Headers, "X-WNS-DEBUG-TRACE") ?? ""; + var wnsDeviceConnectionStatus = TryGetHeaderValue(resp.Headers, "X-WNS-DEVICECONNECTIONSTATUS") ?? "connected"; + var wnsErrorDescription = TryGetHeaderValue(resp.Headers, "X-WNS-ERROR-DESCRIPTION") ?? ""; + var wnsMsgId = TryGetHeaderValue(resp.Headers, "X-WNS-MSG-ID"); + var wnsNotificationStatus = TryGetHeaderValue(resp.Headers, "X-WNS-NOTIFICATIONSTATUS") ?? ""; + + result.DebugTrace = wnsDebugTrace; + result.ErrorDescription = wnsErrorDescription; + result.MessageId = wnsMsgId; + + if (wnsNotificationStatus.Equals("received", StringComparison.InvariantCultureIgnoreCase)) + result.NotificationStatus = WnsNotificationSendStatus.Received; + else if (wnsNotificationStatus.Equals("dropped", StringComparison.InvariantCultureIgnoreCase)) + result.NotificationStatus = WnsNotificationSendStatus.Dropped; + else + result.NotificationStatus = WnsNotificationSendStatus.ChannelThrottled; + + if (wnsDeviceConnectionStatus.Equals("connected", StringComparison.InvariantCultureIgnoreCase)) + result.DeviceConnectionStatus = WnsDeviceConnectionStatus.Connected; + else if (wnsDeviceConnectionStatus.Equals("tempdisconnected", StringComparison.InvariantCultureIgnoreCase)) + result.DeviceConnectionStatus = WnsDeviceConnectionStatus.TempDisconnected; + else + result.DeviceConnectionStatus = WnsDeviceConnectionStatus.Disconnected; + + return result; + } + + string TryGetHeaderValue(HttpResponseHeaders headers, string headerName) + { + IEnumerable values; + if (headers.TryGetValues(headerName, out values)) + return values.FirstOrDefault(); + + return null; + } + } } diff --git a/PushSharp.Windows/WnsNotification.cs b/PushSharp.Windows/WnsNotification.cs index 15bdcb33..de024f14 100644 --- a/PushSharp.Windows/WnsNotification.cs +++ b/PushSharp.Windows/WnsNotification.cs @@ -3,54 +3,54 @@ namespace PushSharp.Windows { - public abstract class WnsNotification : INotification - { - public string ChannelUri { get; set; } - - public bool? RequestForStatus { get; set; } - public int? TimeToLive { get; set; } - - public XElement Payload { get; set; } - - public abstract WnsNotificationType Type { get; } - - public bool IsDeviceRegistrationIdValid () - { - return true; - } - - public object Tag { get; set; } - } - - public class WnsTileNotification : WnsNotification - { - public override WnsNotificationType Type - { - get { return WnsNotificationType.Tile; } - } - - public WnsNotificationCachePolicyType? CachePolicy { get; set; } - - public string NotificationTag { get; set; } - } - - public class WnsToastNotification : WnsNotification - { - public override WnsNotificationType Type - { - get { return WnsNotificationType.Toast; } - } - } - - public class WnsBadgeNotification : WnsNotification - { - public override WnsNotificationType Type - { - get { return WnsNotificationType.Badge; } - } - - public WnsNotificationCachePolicyType? CachePolicy { get; set; } - } + public abstract class WnsNotification : INotification + { + public string ChannelUri { get; set; } + + public bool? RequestForStatus { get; set; } + public int? TimeToLive { get; set; } + + public XElement Payload { get; set; } + + public abstract WnsNotificationType Type { get; } + + public bool IsDeviceRegistrationIdValid() + { + return true; + } + + public object Tag { get; set; } + } + + public class WnsTileNotification : WnsNotification + { + public override WnsNotificationType Type + { + get { return WnsNotificationType.Tile; } + } + + public WnsNotificationCachePolicyType? CachePolicy { get; set; } + + public string NotificationTag { get; set; } + } + + public class WnsToastNotification : WnsNotification + { + public override WnsNotificationType Type + { + get { return WnsNotificationType.Toast; } + } + } + + public class WnsBadgeNotification : WnsNotification + { + public override WnsNotificationType Type + { + get { return WnsNotificationType.Badge; } + } + + public WnsNotificationCachePolicyType? CachePolicy { get; set; } + } } diff --git a/PushSharp.Windows/WnsNotificationStatus.cs b/PushSharp.Windows/WnsNotificationStatus.cs index c32b62f3..573d4177 100644 --- a/PushSharp.Windows/WnsNotificationStatus.cs +++ b/PushSharp.Windows/WnsNotificationStatus.cs @@ -2,46 +2,46 @@ namespace PushSharp.Windows { - public class WnsNotificationStatus - { - public string MessageId { get; set; } - public string DebugTrace { get; set; } - public string ErrorDescription { get; set; } - - public WnsNotificationSendStatus NotificationStatus { get; set; } - public WnsDeviceConnectionStatus DeviceConnectionStatus { get; set; } - - public WnsNotification Notification { get; set; } - - public System.Net.HttpStatusCode HttpStatus { get; set; } - } - - public enum WnsNotificationSendStatus - { - Received, - Dropped, - ChannelThrottled - } - - public enum WnsDeviceConnectionStatus - { - Connected, - Disconnected, - TempDisconnected - } - - public enum WnsNotificationCachePolicyType - { - Cache, - NoCache - } - - public enum WnsNotificationType - { - Badge, - Tile, - Toast, - Raw - } + public class WnsNotificationStatus + { + public string MessageId { get; set; } + public string DebugTrace { get; set; } + public string ErrorDescription { get; set; } + + public WnsNotificationSendStatus NotificationStatus { get; set; } + public WnsDeviceConnectionStatus DeviceConnectionStatus { get; set; } + + public WnsNotification Notification { get; set; } + + public System.Net.HttpStatusCode HttpStatus { get; set; } + } + + public enum WnsNotificationSendStatus + { + Received, + Dropped, + ChannelThrottled + } + + public enum WnsDeviceConnectionStatus + { + Connected, + Disconnected, + TempDisconnected + } + + public enum WnsNotificationCachePolicyType + { + Cache, + NoCache + } + + public enum WnsNotificationType + { + Badge, + Tile, + Toast, + Raw + } } diff --git a/PushSharp.Windows/WnsTokenAccessManager.cs b/PushSharp.Windows/WnsTokenAccessManager.cs index 8e2c9908..6d19243f 100644 --- a/PushSharp.Windows/WnsTokenAccessManager.cs +++ b/PushSharp.Windows/WnsTokenAccessManager.cs @@ -7,72 +7,82 @@ namespace PushSharp.Windows { - public class WnsAccessTokenManager - { - Task renewAccessTokenTask = null; - string accessToken = null; - HttpClient http; + public class WnsAccessTokenManager + { + Task renewAccessTokenTask = null; + string accessToken = null; + HttpClient http; - public WnsAccessTokenManager (WnsConfiguration configuration) - { - http = new HttpClient (); - Configuration = configuration; - } + public WnsAccessTokenManager(WnsConfiguration configuration) + { + http = new HttpClient(); + Configuration = configuration; + } - public WnsConfiguration Configuration { get; private set; } + public WnsConfiguration Configuration { get; private set; } - public async Task GetAccessToken () - { - if (accessToken == null) { - if (renewAccessTokenTask == null) { - Log.Info ("Renewing Access Token"); - renewAccessTokenTask = RenewAccessToken (); - await renewAccessTokenTask; - } else { - Log.Info ("Waiting for access token"); - await renewAccessTokenTask; - } - } + public async Task GetAccessToken() + { + if (accessToken == null) + { + if (renewAccessTokenTask == null) + { + Log.Info("Renewing Access Token"); + renewAccessTokenTask = RenewAccessToken(); + await renewAccessTokenTask; + } + else + { + Log.Info("Waiting for access token"); + await renewAccessTokenTask; + } + } - return accessToken; - } + return accessToken; + } - public void InvalidateAccessToken (string currentAccessToken) - { - if (accessToken == currentAccessToken) - accessToken = null; - } + public void InvalidateAccessToken(string currentAccessToken) + { + if (accessToken == currentAccessToken) + accessToken = null; + } - async Task RenewAccessToken () - { - var p = new Dictionary { - { "grant_type", "client_credentials" }, - { "client_id", Configuration.PackageSecurityIdentifier }, - { "client_secret", Configuration.ClientSecret }, - { "scope", "notify.windows.com" } - }; + async Task RenewAccessToken() + { + var p = new Dictionary { + { "grant_type", "client_credentials" }, + { "client_id", Configuration.PackageSecurityIdentifier }, + { "client_secret", Configuration.ClientSecret }, + { "scope", "notify.windows.com" } + }; - var result = await http.PostAsync ("https://login.live.com/accesstoken.srf", new FormUrlEncodedContent (p)); + var result = await http.PostAsync("https://login.live.com/accesstoken.srf", new FormUrlEncodedContent(p)); - var data = await result.Content.ReadAsStringAsync (); + var data = await result.Content.ReadAsStringAsync(); - var token = string.Empty; - var tokenType = string.Empty; + var token = string.Empty; + var tokenType = string.Empty; - try { - var json = JObject.Parse (data); - token = json.Value ("access_token"); - tokenType = json.Value ("token_type"); - } catch { - } + try + { + var json = JObject.Parse(data); + token = json.Value("access_token"); + tokenType = json.Value("token_type"); + } + catch + { + } - if (!string.IsNullOrEmpty (token) && !string.IsNullOrEmpty (tokenType)) { - accessToken = token; - } else { - accessToken = null; - throw new UnauthorizedAccessException ("Could not retrieve access token for the supplied Package Security Identifier (SID) and client secret"); - } - } - } + if (!string.IsNullOrEmpty(token) && !string.IsNullOrEmpty(tokenType)) + { + accessToken = token; + } + else + { + accessToken = null; + throw new UnauthorizedAccessException("Could not retrieve access token for the supplied Package Security Identifier (SID) and client secret"); + } + } + } } diff --git a/PushSharp.Windows/packages.config b/PushSharp.Windows/packages.config index 505e5883..e1fae9c6 100644 --- a/PushSharp.Windows/packages.config +++ b/PushSharp.Windows/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/PushSharp.sln b/PushSharp.sln index d96c467d..ec8c7689 100644 --- a/PushSharp.sln +++ b/PushSharp.sln @@ -1,6 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2012 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PushSharp.Core", "PushSharp.Core\PushSharp.Core.csproj", "{2B44A8DA-60BC-4577-A2D7-D9D53F164B2E}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PushSharp.Tests", "PushSharp.Tests\PushSharp.Tests.csproj", "{989B7357-800E-46B9-91AF-A4CE8A55F389}" @@ -19,38 +21,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PushSharp.Google", "PushSha EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PushSharp.Blackberry", "PushSharp.Blackberry\PushSharp.Blackberry.csproj", "{9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Support Libraries", "Support Libraries", "{6449DAB1-E76A-4354-B633-CC6AC53BB757}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2468C63B-C964-4FC3-9B16-13DC17CF7D11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2468C63B-C964-4FC3-9B16-13DC17CF7D11}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2468C63B-C964-4FC3-9B16-13DC17CF7D11}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2468C63B-C964-4FC3-9B16-13DC17CF7D11}.Release|Any CPU.Build.0 = Release|Any CPU {2B44A8DA-60BC-4577-A2D7-D9D53F164B2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2B44A8DA-60BC-4577-A2D7-D9D53F164B2E}.Debug|Any CPU.Build.0 = Debug|Any CPU {2B44A8DA-60BC-4577-A2D7-D9D53F164B2E}.Release|Any CPU.ActiveCfg = Release|Any CPU {2B44A8DA-60BC-4577-A2D7-D9D53F164B2E}.Release|Any CPU.Build.0 = Release|Any CPU - {54A4C1F9-6571-4086-BB4B-EC202138AF00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {54A4C1F9-6571-4086-BB4B-EC202138AF00}.Debug|Any CPU.Build.0 = Debug|Any CPU - {54A4C1F9-6571-4086-BB4B-EC202138AF00}.Release|Any CPU.ActiveCfg = Release|Any CPU - {54A4C1F9-6571-4086-BB4B-EC202138AF00}.Release|Any CPU.Build.0 = Release|Any CPU - {94F16497-471F-433F-A99E-C455FB2D7724}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {94F16497-471F-433F-A99E-C455FB2D7724}.Debug|Any CPU.Build.0 = Debug|Any CPU - {94F16497-471F-433F-A99E-C455FB2D7724}.Release|Any CPU.ActiveCfg = Release|Any CPU - {94F16497-471F-433F-A99E-C455FB2D7724}.Release|Any CPU.Build.0 = Release|Any CPU {989B7357-800E-46B9-91AF-A4CE8A55F389}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {989B7357-800E-46B9-91AF-A4CE8A55F389}.Debug|Any CPU.Build.0 = Debug|Any CPU {989B7357-800E-46B9-91AF-A4CE8A55F389}.Release|Any CPU.ActiveCfg = Release|Any CPU {989B7357-800E-46B9-91AF-A4CE8A55F389}.Release|Any CPU.Build.0 = Release|Any CPU - {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Release|Any CPU.Build.0 = Release|Any CPU {A9D99F80-FEEB-4E74-96C5-66F17103C773}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A9D99F80-FEEB-4E74-96C5-66F17103C773}.Debug|Any CPU.Build.0 = Debug|Any CPU {A9D99F80-FEEB-4E74-96C5-66F17103C773}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -59,6 +43,25 @@ Global {DC80552B-6730-44AA-9B74-1E036BD909C3}.Debug|Any CPU.Build.0 = Debug|Any CPU {DC80552B-6730-44AA-9B74-1E036BD909C3}.Release|Any CPU.ActiveCfg = Release|Any CPU {DC80552B-6730-44AA-9B74-1E036BD909C3}.Release|Any CPU.Build.0 = Release|Any CPU + {54A4C1F9-6571-4086-BB4B-EC202138AF00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54A4C1F9-6571-4086-BB4B-EC202138AF00}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54A4C1F9-6571-4086-BB4B-EC202138AF00}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54A4C1F9-6571-4086-BB4B-EC202138AF00}.Release|Any CPU.Build.0 = Release|Any CPU + {2468C63B-C964-4FC3-9B16-13DC17CF7D11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2468C63B-C964-4FC3-9B16-13DC17CF7D11}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2468C63B-C964-4FC3-9B16-13DC17CF7D11}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2468C63B-C964-4FC3-9B16-13DC17CF7D11}.Release|Any CPU.Build.0 = Release|Any CPU + {94F16497-471F-433F-A99E-C455FB2D7724}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {94F16497-471F-433F-A99E-C455FB2D7724}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94F16497-471F-433F-A99E-C455FB2D7724}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94F16497-471F-433F-A99E-C455FB2D7724}.Release|Any CPU.Build.0 = Release|Any CPU + {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {A9D99F80-FEEB-4E74-96C5-66F17103C773} = {2B7243CB-60A5-4682-802B-B7EC3DEBCF9A} From c7f8f984efb45771487700e6311280217ba5e0f4 Mon Sep 17 00:00:00 2001 From: Karimi Date: Mon, 24 Oct 2016 13:25:06 +0330 Subject: [PATCH 2/5] Reformat all cods and update packages and upgrate to .net 4.6.1 --- .gitignore | 1 + PushSharp.Amazon/AdmConfiguration.cs | 51 +- PushSharp.Amazon/AdmConnection.cs | 291 +++--- PushSharp.Amazon/AdmNotification.cs | 80 +- PushSharp.Amazon/Exceptions.cs | 43 +- PushSharp.Amazon/Properties/AssemblyInfo.cs | 18 +- PushSharp.Amazon/PushSharp.Amazon.csproj | 14 +- PushSharp.Amazon/packages.config | 2 +- PushSharp.Apple/ApnsConfiguration.cs | 296 +++--- PushSharp.Apple/ApnsConnection.cs | 946 +++++++++--------- PushSharp.Apple/ApnsFeedbackService.cs | 227 +++-- PushSharp.Apple/ApnsNotification.cs | 329 +++--- PushSharp.Apple/ApnsServiceConnection.cs | 96 +- PushSharp.Apple/Exceptions.cs | 114 +-- PushSharp.Apple/Properties/AssemblyInfo.cs | 18 +- PushSharp.Apple/PushSharp.Apple.csproj | 14 +- PushSharp.Apple/packages.config | 2 +- .../BlackberryConfiguration.cs | 92 +- PushSharp.Blackberry/BlackberryConnection.cs | 150 +-- PushSharp.Blackberry/BlackberryHttpClient.cs | 54 +- .../BlackberryNotification.cs | 276 ++--- PushSharp.Blackberry/Enums.cs | 264 ++--- PushSharp.Blackberry/Exceptions.cs | 26 +- .../Properties/AssemblyInfo.cs | 18 +- .../PushSharp.Blackberry.csproj | 7 +- PushSharp.Core/Exceptions.cs | 91 +- PushSharp.Core/INotification.cs | 10 +- PushSharp.Core/IServiceBroker.cs | 18 +- PushSharp.Core/IServiceConnection.cs | 12 +- PushSharp.Core/IServiceConnectionFactory.cs | 12 +- PushSharp.Core/Log.cs | 302 +++--- .../NotificationBlockingCollection.cs | 2 +- PushSharp.Core/Properties/AssemblyInfo.cs | 18 +- PushSharp.Core/PushHttpClient.cs | 188 ++-- PushSharp.Core/PushSharp.Core.csproj | 7 +- PushSharp.Core/ServiceBroker.cs | 438 ++++---- PushSharp.Firefox/Exceptions.cs | 18 +- PushSharp.Firefox/FirefoxConfiguration.cs | 12 +- PushSharp.Firefox/FirefoxConnection.cs | 81 +- PushSharp.Firefox/FirefoxNotification.cs | 66 +- PushSharp.Firefox/Properties/AssemblyInfo.cs | 18 +- PushSharp.Firefox/PushSharp.Firefox.csproj | 7 +- PushSharp.Google/Exceptions.cs | 48 +- PushSharp.Google/GcmConfiguration.cs | 57 +- PushSharp.Google/GcmMessageResult.cs | 104 +- PushSharp.Google/GcmNotification.cs | 326 +++--- PushSharp.Google/GcmResponse.cs | 145 ++- PushSharp.Google/GcmServiceConnection.cs | 438 ++++---- PushSharp.Google/Properties/AssemblyInfo.cs | 18 +- PushSharp.Google/PushSharp.Google.csproj | 14 +- PushSharp.Google/packages.config | 2 +- PushSharp.Tests/AdmRealTests.cs | 76 +- PushSharp.Tests/ApnsRealTest.cs | 112 ++- PushSharp.Tests/ApnsTests.cs | 316 +++--- PushSharp.Tests/BrokerTests.cs | 143 ++- PushSharp.Tests/GcmRealTests.cs | 91 +- PushSharp.Tests/GcmTests.cs | 48 +- PushSharp.Tests/PushSharp.Tests.csproj | 17 +- PushSharp.Tests/Servers/TestApnsServer.cs | 603 +++++------ PushSharp.Tests/Settings.cs | 118 +-- PushSharp.Tests/TestServiceConnection.cs | 68 +- PushSharp.Tests/WnsRealTests.cs | 141 +-- PushSharp.Tests/packages.config | 3 +- PushSharp.Tests/settings.sample.json | 22 - PushSharp.Windows/Exceptions.cs | 28 +- PushSharp.Windows/Properties/AssemblyInfo.cs | 18 +- PushSharp.Windows/PushSharp.Windows.csproj | 14 +- PushSharp.Windows/WnsConfiguration.cs | 24 +- PushSharp.Windows/WnsConnection.cs | 379 +++---- PushSharp.Windows/WnsNotification.cs | 96 +- PushSharp.Windows/WnsNotificationStatus.cs | 82 +- PushSharp.Windows/WnsTokenAccessManager.cs | 122 +-- PushSharp.Windows/packages.config | 2 +- PushSharp.sln | 41 +- 74 files changed, 4308 insertions(+), 4137 deletions(-) delete mode 100644 PushSharp.Tests/settings.sample.json diff --git a/.gitignore b/.gitignore index b8dbc72c..8d1474ce 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ node_modules/ .vs/ settings.json +/UnitTestProject1 diff --git a/PushSharp.Amazon/AdmConfiguration.cs b/PushSharp.Amazon/AdmConfiguration.cs index 529fbbc1..2ed9034e 100644 --- a/PushSharp.Amazon/AdmConfiguration.cs +++ b/PushSharp.Amazon/AdmConfiguration.cs @@ -2,34 +2,33 @@ namespace PushSharp.Amazon { - public class AdmConfiguration - { - const string ADM_SEND_URL = "https://api.amazon.com/messaging/registrations/{0}/messages"; - const string ADM_AUTH_URL = "https://api.amazon.com/auth/O2/token"; + public class AdmConfiguration + { + const string ADM_SEND_URL = "https://api.amazon.com/messaging/registrations/{0}/messages"; + const string ADM_AUTH_URL = "https://api.amazon.com/auth/O2/token"; - public AdmConfiguration (string clientId, string clientSecret) - { - ClientId = clientId; - ClientSecret = clientSecret; - AdmSendUrl = ADM_SEND_URL; - AdmAuthUrl = ADM_AUTH_URL; - } + public AdmConfiguration(string clientId, string clientSecret) + { + ClientId = clientId; + ClientSecret = clientSecret; + AdmSendUrl = ADM_SEND_URL; + AdmAuthUrl = ADM_AUTH_URL; + } - public string ClientId { get; private set; } - public string ClientSecret { get; private set; } + public string ClientId { get; private set; } + public string ClientSecret { get; private set; } - public string AdmSendUrl { get; private set; } - public string AdmAuthUrl { get; private set; } + public string AdmSendUrl { get; private set; } + public string AdmAuthUrl { get; private set; } - public void OverrideSendUrl(string url) - { - AdmSendUrl = url; - } - - public void OverrideAuthUrl(string url) - { - AdmAuthUrl = url; - } - } -} + public void OverrideSendUrl(string url) + { + AdmSendUrl = url; + } + public void OverrideAuthUrl(string url) + { + AdmAuthUrl = url; + } + } +} \ No newline at end of file diff --git a/PushSharp.Amazon/AdmConnection.cs b/PushSharp.Amazon/AdmConnection.cs index 3ce9d346..56be8292 100644 --- a/PushSharp.Amazon/AdmConnection.cs +++ b/PushSharp.Amazon/AdmConnection.cs @@ -1,154 +1,155 @@ using System; -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using System.Net; using PushSharp.Core; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Threading; namespace PushSharp.Amazon { - public class AdmServiceConnectionFactory : IServiceConnectionFactory - { - public AdmServiceConnectionFactory (AdmConfiguration configuration) - { - Configuration = configuration; - } - - public AdmConfiguration Configuration { get; private set; } - - public IServiceConnection Create() - { - return new AdmServiceConnection (Configuration); - } - } - - public class AdmServiceBroker : ServiceBroker - { - public AdmServiceBroker (AdmConfiguration configuration) : base (new AdmServiceConnectionFactory (configuration)) - { - } - } - - public class AdmServiceConnection : IServiceConnection - { - public AdmServiceConnection (AdmConfiguration configuration) - { - Configuration = configuration; - - Expires = DateTime.UtcNow.AddYears(-1); - - http.DefaultRequestHeaders.Add ("X-Amzn-Type-Version", "com.amazon.device.messaging.ADMMessage@1.0"); - http.DefaultRequestHeaders.Add ("X-Amzn-Accept-Type", "com.amazon.device.messaging.ADMSendResult@1.0"); - http.DefaultRequestHeaders.Add ("Accept", "application/json"); - http.DefaultRequestHeaders.ConnectionClose = true; - - http.DefaultRequestHeaders.Remove("connection"); - } - - public AdmConfiguration Configuration { get; private set; } - - public DateTime Expires { get; set; } - public DateTime LastRequest { get; private set; } - public string LastAmazonRequestId { get; private set; } - public string AccessToken { get; private set; } - - readonly HttpClient http = new HttpClient (); - - public async Task Send (AdmNotification notification) - { - try - { - if (string.IsNullOrEmpty(AccessToken) || Expires <= DateTime.UtcNow) { - await UpdateAccessToken (); - http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "Bearer " + AccessToken); - //http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken); - } - - var sc = new StringContent(notification.ToJson ()); - sc.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - - var response = await http.PostAsync (string.Format(Configuration.AdmSendUrl, notification.RegistrationId), sc); - - // We're done here if it was a success - if (response.IsSuccessStatusCode) { - return; - } - - var data = await response.Content.ReadAsStringAsync(); - - var json = JObject.Parse (data); - - var reason = json ["reason"].ToString (); - - var regId = notification.RegistrationId; - - if (json["registrationID"] != null) - regId = json["registrationID"].ToString(); - - switch (response.StatusCode) - { - case HttpStatusCode.BadGateway: //400 - case HttpStatusCode.BadRequest: // - if ("InvalidRegistrationId".Equals (reason, StringComparison.InvariantCultureIgnoreCase)) { - throw new DeviceSubscriptionExpiredException (notification) { - OldSubscriptionId = regId, - ExpiredAt = DateTime.UtcNow - }; - } - throw new NotificationException ("Notification Failed: " + reason, notification); - case HttpStatusCode.Unauthorized: //401 - //Access token expired - AccessToken = null; - throw new UnauthorizedAccessException ("Access token failed authorization"); - case HttpStatusCode.Forbidden: //403 - throw new AdmRateLimitExceededException (reason, notification); - case HttpStatusCode.RequestEntityTooLarge: //413 - throw new AdmMessageTooLargeException (notification); - default: - throw new NotificationException ("Unknown ADM Failure", notification); - } - } - catch (Exception ex) - { - throw new NotificationException ("Unknown ADM Failure", notification, ex); - } - } - - - async Task UpdateAccessToken() - { - var http = new HttpClient (); - - var param = new Dictionary (); - param.Add ("grant_type", "client_credentials"); - param.Add ("scope", "messaging:push"); - param.Add ("client_id", Configuration.ClientId); - param.Add ("client_secret", Configuration.ClientSecret); - - - var result = await http.PostAsync (Configuration.AdmAuthUrl, new FormUrlEncodedContent (param)); - var data = await result.Content.ReadAsStringAsync(); - - var json = JObject.Parse (data); - - AccessToken = json ["access_token"].ToString (); - - JToken expiresJson = new JValue(3540); - if (json.TryGetValue("expires_in", out expiresJson)) - Expires = DateTime.UtcNow.AddSeconds(expiresJson.ToObject() - 60); - else - Expires = DateTime.UtcNow.AddSeconds(3540); - - if (result.Headers.Contains ("X-Amzn-RequestId")) - this.LastAmazonRequestId = string.Join("; ", result.Headers.GetValues("X-Amzn-RequestId")); - - LastRequest = DateTime.UtcNow; - } - - } + public class AdmServiceConnectionFactory : IServiceConnectionFactory + { + public AdmServiceConnectionFactory(AdmConfiguration configuration) + { + Configuration = configuration; + } + + public AdmConfiguration Configuration { get; private set; } + + public IServiceConnection Create() + { + return new AdmServiceConnection(Configuration); + } + } + + public class AdmServiceBroker : ServiceBroker + { + public AdmServiceBroker(AdmConfiguration configuration) : base(new AdmServiceConnectionFactory(configuration)) + { + } + } + + public class AdmServiceConnection : IServiceConnection + { + public AdmServiceConnection(AdmConfiguration configuration) + { + Configuration = configuration; + + Expires = DateTime.UtcNow.AddYears(-1); + + http.DefaultRequestHeaders.Add("X-Amzn-Type-Version", "com.amazon.device.messaging.ADMMessage@1.0"); + http.DefaultRequestHeaders.Add("X-Amzn-Accept-Type", "com.amazon.device.messaging.ADMSendResult@1.0"); + http.DefaultRequestHeaders.Add("Accept", "application/json"); + http.DefaultRequestHeaders.ConnectionClose = true; + + http.DefaultRequestHeaders.Remove("connection"); + } + + public AdmConfiguration Configuration { get; private set; } + + public DateTime Expires { get; set; } + public DateTime LastRequest { get; private set; } + public string LastAmazonRequestId { get; private set; } + public string AccessToken { get; private set; } + + readonly HttpClient http = new HttpClient(); + + public async Task Send(AdmNotification notification) + { + try + { + if (string.IsNullOrEmpty(AccessToken) || Expires <= DateTime.UtcNow) + { + await UpdateAccessToken(); + http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "Bearer " + AccessToken); + //http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken); + } + + var sc = new StringContent(notification.ToJson()); + sc.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + var response = await http.PostAsync(string.Format(Configuration.AdmSendUrl, notification.RegistrationId), sc); + + // We're done here if it was a success + if (response.IsSuccessStatusCode) + { + return; + } + + var data = await response.Content.ReadAsStringAsync(); + + var json = JObject.Parse(data); + + var reason = json["reason"].ToString(); + + var regId = notification.RegistrationId; + + if (json["registrationID"] != null) + regId = json["registrationID"].ToString(); + + switch (response.StatusCode) + { + case HttpStatusCode.BadGateway: //400 + case HttpStatusCode.BadRequest: // + if ("InvalidRegistrationId".Equals(reason, StringComparison.InvariantCultureIgnoreCase)) + { + throw new DeviceSubscriptionExpiredException(notification) + { + OldSubscriptionId = regId, + ExpiredAt = DateTime.UtcNow + }; + } + throw new NotificationException("Notification Failed: " + reason, notification); + case HttpStatusCode.Unauthorized: //401 + //Access token expired + AccessToken = null; + throw new UnauthorizedAccessException("Access token failed authorization"); + case HttpStatusCode.Forbidden: //403 + throw new AdmRateLimitExceededException(reason, notification); + case HttpStatusCode.RequestEntityTooLarge: //413 + throw new AdmMessageTooLargeException(notification); + default: + throw new NotificationException("Unknown ADM Failure", notification); + } + } + catch (Exception ex) + { + throw new NotificationException("Unknown ADM Failure", notification, ex); + } + } + + + async Task UpdateAccessToken() + { + var http = new HttpClient(); + + var param = new Dictionary(); + param.Add("grant_type", "client_credentials"); + param.Add("scope", "messaging:push"); + param.Add("client_id", Configuration.ClientId); + param.Add("client_secret", Configuration.ClientSecret); + + + var result = await http.PostAsync(Configuration.AdmAuthUrl, new FormUrlEncodedContent(param)); + var data = await result.Content.ReadAsStringAsync(); + + var json = JObject.Parse(data); + + AccessToken = json["access_token"].ToString(); + + JToken expiresJson = new JValue(3540); + if (json.TryGetValue("expires_in", out expiresJson)) + Expires = DateTime.UtcNow.AddSeconds(expiresJson.ToObject() - 60); + else + Expires = DateTime.UtcNow.AddSeconds(3540); + + if (result.Headers.Contains("X-Amzn-RequestId")) + this.LastAmazonRequestId = string.Join("; ", result.Headers.GetValues("X-Amzn-RequestId")); + + LastRequest = DateTime.UtcNow; + } + + } } diff --git a/PushSharp.Amazon/AdmNotification.cs b/PushSharp.Amazon/AdmNotification.cs index 74dad5b4..657f031e 100644 --- a/PushSharp.Amazon/AdmNotification.cs +++ b/PushSharp.Amazon/AdmNotification.cs @@ -1,59 +1,59 @@ -using System; -using System.Collections.Generic; -using PushSharp.Core; +using System.Collections.Generic; using Newtonsoft.Json.Linq; +using PushSharp.Core; namespace PushSharp.Amazon { - public class AdmNotification : INotification - { - public AdmNotification () - { - Data = new Dictionary (); - } + public class AdmNotification : INotification + { + public AdmNotification() + { + Data = new Dictionary(); + } - public Dictionary Data { get; set; } + public Dictionary Data { get; set; } - public string ConsolidationKey { get;set; } + public string ConsolidationKey { get; set; } - public int? ExpiresAfter { get;set; } + public int? ExpiresAfter { get; set; } - public string RegistrationId { get;set; } + public string RegistrationId { get; set; } - public string ToJson() - { - var json = new JObject (); - var data = new JObject (); + public string ToJson() + { + var json = new JObject(); + var data = new JObject(); - if (Data != null && Data.Count > 0) { - foreach (var key in Data.Keys) - data [key] = Data [key]; - } + if (Data != null && Data.Count > 0) + { + foreach (var key in Data.Keys) + data[key] = Data[key]; + } - json ["data"] = data; + json["data"] = data; - if (!string.IsNullOrEmpty (ConsolidationKey)) - json ["consolidationKey"] = ConsolidationKey; + if (!string.IsNullOrEmpty(ConsolidationKey)) + json["consolidationKey"] = ConsolidationKey; - if (ExpiresAfter.HasValue && ExpiresAfter.Value >= 0) - json ["expiresAfter"] = ExpiresAfter.Value; + if (ExpiresAfter.HasValue && ExpiresAfter.Value >= 0) + json["expiresAfter"] = ExpiresAfter.Value; - return json.ToString(); - } + return json.ToString(); + } - public override string ToString () - { - return ToJson (); - } + public override string ToString() + { + return ToJson(); + } - #region INotification implementation - public bool IsDeviceRegistrationIdValid () - { - return !string.IsNullOrWhiteSpace (RegistrationId); - } + #region INotification implementation + public bool IsDeviceRegistrationIdValid() + { + return !string.IsNullOrWhiteSpace(RegistrationId); + } - public object Tag { get; set; } - #endregion - } + public object Tag { get; set; } + #endregion + } } diff --git a/PushSharp.Amazon/Exceptions.cs b/PushSharp.Amazon/Exceptions.cs index b3f145b9..fc125729 100644 --- a/PushSharp.Amazon/Exceptions.cs +++ b/PushSharp.Amazon/Exceptions.cs @@ -1,29 +1,28 @@ -using System; -using PushSharp.Core; +using PushSharp.Core; namespace PushSharp.Amazon { - public class AdmRateLimitExceededException : NotificationException - { - public AdmRateLimitExceededException (string reason, AdmNotification notification) - : base ("Rate Limit Exceeded (" + reason + ")", notification) - { - Notification = notification; - Reason = reason; - } + public class AdmRateLimitExceededException : NotificationException + { + public AdmRateLimitExceededException(string reason, AdmNotification notification) + : base("Rate Limit Exceeded (" + reason + ")", notification) + { + Notification = notification; + Reason = reason; + } - public new AdmNotification Notification { get; set; } - public string Reason { get; private set; } - } + public new AdmNotification Notification { get; set; } + public string Reason { get; private set; } + } - public class AdmMessageTooLargeException : NotificationException - { - public AdmMessageTooLargeException (AdmNotification notification) - : base ("ADM Message too Large, must be <= 6kb", notification) - { - Notification = notification; - } + public class AdmMessageTooLargeException : NotificationException + { + public AdmMessageTooLargeException(AdmNotification notification) + : base("ADM Message too Large, must be <= 6kb", notification) + { + Notification = notification; + } - public new AdmNotification Notification { get; set; } - } + public new AdmNotification Notification { get; set; } + } } diff --git a/PushSharp.Amazon/Properties/AssemblyInfo.cs b/PushSharp.Amazon/Properties/AssemblyInfo.cs index a2f8a614..2a42711f 100644 --- a/PushSharp.Amazon/Properties/AssemblyInfo.cs +++ b/PushSharp.Amazon/Properties/AssemblyInfo.cs @@ -4,20 +4,20 @@ // Information about this assembly is defined by the following attributes. // Change them to the values specific to your project. -[assembly: AssemblyTitle ("PushSharp.Amazon")] -[assembly: AssemblyDescription ("")] -[assembly: AssemblyConfiguration ("")] -[assembly: AssemblyCompany ("")] -[assembly: AssemblyProduct ("")] -[assembly: AssemblyCopyright ("redth")] -[assembly: AssemblyTrademark ("")] -[assembly: AssemblyCulture ("")] +[assembly: AssemblyTitle("PushSharp.Amazon")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("redth")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion ("1.0.*")] +[assembly: AssemblyVersion("1.0.*")] // The following attributes are used to specify the signing key for the assembly, // if desired. See the Mono documentation for more information about signing. diff --git a/PushSharp.Amazon/PushSharp.Amazon.csproj b/PushSharp.Amazon/PushSharp.Amazon.csproj index 594d22c3..b6b858a8 100644 --- a/PushSharp.Amazon/PushSharp.Amazon.csproj +++ b/PushSharp.Amazon/PushSharp.Amazon.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -7,9 +7,10 @@ Library PushSharp.Amazon PushSharp.Amazon - v4.5 + v4.6.1 true ..\PushSharp-Signing.snk + true @@ -30,11 +31,12 @@ false + + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + True + - - ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll - diff --git a/PushSharp.Amazon/packages.config b/PushSharp.Amazon/packages.config index 505e5883..e1fae9c6 100644 --- a/PushSharp.Amazon/packages.config +++ b/PushSharp.Amazon/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/PushSharp.Apple/ApnsConfiguration.cs b/PushSharp.Apple/ApnsConfiguration.cs index 464a5e62..0a28cbe7 100644 --- a/PushSharp.Apple/ApnsConfiguration.cs +++ b/PushSharp.Apple/ApnsConfiguration.cs @@ -5,204 +5,208 @@ 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"; + 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 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_PORT = 2195; + const int APNS_PRODUCTION_PORT = 2195; - const int APNS_SANDBOX_FEEDBACK_PORT = 2196; - const int APNS_PRODUCTION_FEEDBACK_PORT = 2196; + const int APNS_SANDBOX_FEEDBACK_PORT = 2196; + const int APNS_PRODUCTION_FEEDBACK_PORT = 2196; - #endregion + #endregion - public ApnsConfiguration (ApnsServerEnvironment serverEnvironment, string certificateFile, string certificateFilePwd, bool validateIsApnsCertificate) - : this (serverEnvironment, System.IO.File.ReadAllBytes (certificateFile), certificateFilePwd, validateIsApnsCertificate) - { - } + public ApnsConfiguration(ApnsServerEnvironment serverEnvironment, string certificateFile, string certificateFilePwd, bool validateIsApnsCertificate) + : this(serverEnvironment, System.IO.File.ReadAllBytes(certificateFile), certificateFilePwd, validateIsApnsCertificate) + { + } - public ApnsConfiguration (ApnsServerEnvironment serverEnvironment, string certificateFile, string certificateFilePwd) - : this (serverEnvironment, System.IO.File.ReadAllBytes (certificateFile), certificateFilePwd) - { - } + public ApnsConfiguration(ApnsServerEnvironment serverEnvironment, string certificateFile, string certificateFilePwd) + : this(serverEnvironment, System.IO.File.ReadAllBytes(certificateFile), certificateFilePwd) + { + } - public ApnsConfiguration (ApnsServerEnvironment serverEnvironment, byte[] certificateData, string certificateFilePwd, bool validateIsApnsCertificate) - : this (serverEnvironment, new X509Certificate2 (certificateData, certificateFilePwd, - X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable), validateIsApnsCertificate) - { - } + public ApnsConfiguration(ApnsServerEnvironment serverEnvironment, byte[] certificateData, string certificateFilePwd, bool validateIsApnsCertificate) + : this(serverEnvironment, new X509Certificate2(certificateData, certificateFilePwd, + X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable), validateIsApnsCertificate) + { + } - public ApnsConfiguration (ApnsServerEnvironment serverEnvironment, byte[] certificateData, string certificateFilePwd) - : this (serverEnvironment, new X509Certificate2 (certificateData, certificateFilePwd, - X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable)) - { - } + public ApnsConfiguration(ApnsServerEnvironment serverEnvironment, byte[] certificateData, string certificateFilePwd) + : this(serverEnvironment, new X509Certificate2(certificateData, certificateFilePwd, + X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable)) + { + } - public ApnsConfiguration (string overrideHost, int overridePort, bool skipSsl = true) - { - SkipSsl = skipSsl; + public ApnsConfiguration(string overrideHost, int overridePort, bool skipSsl = true) + { + SkipSsl = skipSsl; - Initialize (ApnsServerEnvironment.Sandbox, null, false); + Initialize(ApnsServerEnvironment.Sandbox, null, false); - OverrideServer (overrideHost, overridePort); - } + OverrideServer(overrideHost, overridePort); + } - public ApnsConfiguration (ApnsServerEnvironment serverEnvironment, X509Certificate2 certificate) - { - Initialize (serverEnvironment, certificate, true); - } + public ApnsConfiguration(ApnsServerEnvironment serverEnvironment, X509Certificate2 certificate) + { + Initialize(serverEnvironment, certificate, true); + } - public ApnsConfiguration (ApnsServerEnvironment serverEnvironment, X509Certificate2 certificate, bool validateIsApnsCertificate) - { - Initialize (serverEnvironment, certificate, validateIsApnsCertificate); - } + public ApnsConfiguration(ApnsServerEnvironment serverEnvironment, X509Certificate2 certificate, bool validateIsApnsCertificate) + { + Initialize(serverEnvironment, certificate, validateIsApnsCertificate); + } - void Initialize (ApnsServerEnvironment serverEnvironment, X509Certificate2 certificate, bool validateIsApnsCertificate) - { - ServerEnvironment = serverEnvironment; + void Initialize(ApnsServerEnvironment serverEnvironment, X509Certificate2 certificate, bool validateIsApnsCertificate) + { + ServerEnvironment = serverEnvironment; - var production = serverEnvironment == ApnsServerEnvironment.Production; + 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; + 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; + Certificate = certificate; - MillisecondsToWaitBeforeMessageDeclaredSuccess = 3000; - ConnectionTimeout = 10000; - MaxConnectionAttempts = 3; + MillisecondsToWaitBeforeMessageDeclaredSuccess = 3000; + ConnectionTimeout = 10000; + MaxConnectionAttempts = 3; - FeedbackIntervalMinutes = 10; - FeedbackTimeIsUTC = false; + FeedbackIntervalMinutes = 10; + FeedbackTimeIsUTC = false; - AdditionalCertificates = new List (); - AddLocalAndMachineCertificateStores = false; + AdditionalCertificates = new List(); + AddLocalAndMachineCertificateStores = false; - if (validateIsApnsCertificate) - CheckIsApnsCertificate (); + if (validateIsApnsCertificate) + CheckIsApnsCertificate(); - ValidateServerCertificate = false; + ValidateServerCertificate = false; - KeepAlivePeriod = TimeSpan.FromMinutes (20); - KeepAliveRetryPeriod = TimeSpan.FromSeconds (30); + KeepAlivePeriod = TimeSpan.FromMinutes(20); + KeepAliveRetryPeriod = TimeSpan.FromSeconds(30); - InternalBatchSize = 1000; - InternalBatchingWaitPeriod = TimeSpan.FromMilliseconds (750); + InternalBatchSize = 1000; + InternalBatchingWaitPeriod = TimeSpan.FromMilliseconds(750); - InternalBatchFailureRetryCount = 1; - } + InternalBatchFailureRetryCount = 1; + } - void CheckIsApnsCertificate () - { - if (Certificate != null) { - var issuerName = Certificate.IssuerName.Name; - var commonName = Certificate.SubjectName.Name; + void CheckIsApnsCertificate() + { + if (Certificate != null) + { + var issuerName = Certificate.IssuerName.Name; + var commonName = Certificate.SubjectName.Name; - if (!issuerName.Contains ("Apple")) - throw new ArgumentOutOfRangeException ("Your Certificate does not appear to be issued by Apple! Please check to ensure you have the correct certificate!"); + if (!issuerName.Contains("Apple")) + throw new ArgumentOutOfRangeException("Your Certificate does not appear to be issued by Apple! Please check to ensure you have the correct certificate!"); - if (!Regex.IsMatch (commonName, "Apple.*?Push Services") - && !commonName.Contains ("Website Push ID:")) - throw new ArgumentOutOfRangeException ("Your Certificate is not a valid certificate for connecting to Apple's APNS servers"); + if (!Regex.IsMatch(commonName, "Apple.*?Push Services") + && !commonName.Contains("Website Push ID:")) + throw new ArgumentOutOfRangeException("Your Certificate is not a valid certificate for connecting to Apple's APNS servers"); - if (commonName.Contains ("Development") && ServerEnvironment != ApnsServerEnvironment.Sandbox) - throw new ArgumentOutOfRangeException ("You are using a certificate created for connecting only to the Sandbox APNS server but have selected a different server environment to connect to."); + if (commonName.Contains("Development") && ServerEnvironment != ApnsServerEnvironment.Sandbox) + throw new ArgumentOutOfRangeException("You are using a certificate created for connecting only to the Sandbox APNS server but have selected a different server environment to connect to."); - if (commonName.Contains ("Production") && ServerEnvironment != ApnsServerEnvironment.Production) - throw new ArgumentOutOfRangeException ("You are using a certificate created for connecting only to the Production APNS server but have selected a different server environment to connect to."); - } else { - throw new ArgumentOutOfRangeException ("You must provide a Certificate to connect to APNS with!"); - } - } + if (commonName.Contains("Production") && ServerEnvironment != ApnsServerEnvironment.Production) + throw new ArgumentOutOfRangeException("You are using a certificate created for connecting only to the Production APNS server but have selected a different server environment to connect to."); + } + else + { + throw new ArgumentOutOfRangeException("You must provide a Certificate to connect to APNS with!"); + } + } - public void OverrideServer (string host, int port) - { - Host = host; - Port = port; - } + public void OverrideServer(string host, int port) + { + Host = host; + Port = port; + } - public void OverrideFeedbackServer (string host, int port) - { - FeedbackHost = host; - FeedbackPort = port; - } + public void OverrideFeedbackServer(string host, int port) + { + FeedbackHost = host; + FeedbackPort = port; + } - public string Host { get; private set; } + public string Host { get; private set; } - public int Port { get; private set; } + public int Port { get; private set; } - public string FeedbackHost { get; private set; } + public string FeedbackHost { get; private set; } - public int FeedbackPort { get; private set; } + public int FeedbackPort { get; private set; } - public X509Certificate2 Certificate { get; private set; } + public X509Certificate2 Certificate { get; private set; } - public List AdditionalCertificates { get; private set; } + public List AdditionalCertificates { get; private set; } - public bool AddLocalAndMachineCertificateStores { get; set; } + public bool AddLocalAndMachineCertificateStores { get; set; } - public bool SkipSsl { get; set; } + public bool SkipSsl { get; set; } - public int MillisecondsToWaitBeforeMessageDeclaredSuccess { get; set; } + public int MillisecondsToWaitBeforeMessageDeclaredSuccess { get; set; } - public int FeedbackIntervalMinutes { get; set; } + public int FeedbackIntervalMinutes { get; set; } - public bool FeedbackTimeIsUTC { get; set; } + public bool FeedbackTimeIsUTC { get; set; } - public bool ValidateServerCertificate { get; set; } + public bool ValidateServerCertificate { get; set; } - public int ConnectionTimeout { get; set; } + public int ConnectionTimeout { get; set; } - public int MaxConnectionAttempts { 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; } + /// + /// 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 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; } + /// + /// 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 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 or sets the keep alive retry period to set on the APNS socket + /// + public TimeSpan KeepAliveRetryPeriod { get; set; } - /// - /// Gets the configured APNS server environment - /// - /// The server environment. - public ApnsServerEnvironment ServerEnvironment { get; private set; } + /// + /// Gets the configured APNS server environment + /// + /// The server environment. + public ApnsServerEnvironment ServerEnvironment { get; private set; } - public enum ApnsServerEnvironment { - Sandbox, - Production - } - } + public enum ApnsServerEnvironment + { + Sandbox, + Production + } + } } \ No newline at end of file diff --git a/PushSharp.Apple/ApnsConnection.cs b/PushSharp.Apple/ApnsConnection.cs index 0908b166..a465dafb 100644 --- a/PushSharp.Apple/ApnsConnection.cs +++ b/PushSharp.Apple/ApnsConnection.cs @@ -1,477 +1,521 @@ using System; +using System.Collections.Generic; using System.IO; -using System.Net.Sockets; +using System.Net; using System.Net.Security; -using System.Threading.Tasks; +using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; -using System.Collections.Generic; using System.Threading; -using System.Net; +using System.Threading.Tasks; using PushSharp.Core; 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 (); - } - - }), null, Timeout.Infinite, Timeout.Infinite); - } - - 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 (); - - 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) - { - 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; - - // 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); + 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); + } - // 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); + // Add optionally specified additional certs into our collection + if (Configuration.AdditionalCertificates != null) + { + foreach (var addlCert in Configuration.AdditionalCertificates) + certificates.Add(addlCert); + } - // Clear our sent list - sent.Clear (); + // Finally, add the main private cert for authenticating to our collection + if (certificate != null) + certificates.Add(certificate); - // Apple will close our connection after this anyway - disconnect (); - } + timerBatchWait = new Timer(new TimerCallback(async state => + { - bool socketCanWrite () - { - if (client == null) - return false; + await batchSendSemaphore.WaitAsync(); + try + { + await SendBatch().ConfigureAwait(false); + } + finally + { + batchSendSemaphore.Release(); + } - if (networkStream == null || !networkStream.CanWrite) - return false; + }), null, Timeout.Infinite, Timeout.Infinite); + } + + 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(); + + try + { + // See if we need to connect + if (!socketCanWrite() || i > 0) + await connect(); + } + finally + { + connectingSemaphore.Release(); + } - if (!client.Client.Connected) - return false; + 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); + } + } - var p = client.Client.Poll (1000, SelectMode.SelectWrite); + foreach (var n in toSend) + sent.Add(new SentNotification(n)); + } - Log.Info ("APNS-Client[{0}]: Can Write? {1}", id, p); + } + 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)); + } - return p; - } + Log.Info("APNS-Client[{0}]: Sent Batch, waiting for possible response...", id); - async Task connect () - { - if (client != null) - disconnect (); - - Log.Info ("APNS-Client[{0}]: Connecting (Batch ID={1})", id, batchId); + 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) + { + 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(); - client = new TcpClient (); + // 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! - try { - await client.ConnectAsync (Configuration.Host, Configuration.Port).ConfigureAwait (false); + Log.Info("APNS-Client[{0}]: Batch (ID={1}) completed with no error response...", id, batchId); - //Set keep alive on the socket may help maintain our APNS connection - try { - client.Client.SetSocketOption (SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); - } catch { - } + //Everything was ok, let's assume all 'sent' succeeded + foreach (var s in sent) + s.Notification.CompleteSuccessfully(); + + sent.Clear(); + return; + } - //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); - } + // If we make it here, we did get data back, so we have errors - // We can configure skipping ssl all together, ie: if we want to hit a test server - if (Configuration.SkipSsl) { - networkStream = client.GetStream (); - } else { + 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; + + // 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); - // Create our ssl stream - stream = new SslStream (client.GetStream (), - false, - ValidateRemoteCertificate, - (sender, targetHost, localCerts, remoteCert, acceptableIssuers) => certificate); - - try { - stream.AuthenticateAsClient (Configuration.Host, certificates, System.Security.Authentication.SslProtocols.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 () - { - completionSource.SetResult (null); - } - - public void CompleteFailed (Exception ex) - { - completionSource.SetResult (ex); - } - } - - /// - /// 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 + 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 + { + stream.AuthenticateAsClient(Configuration.Host, certificates, System.Security.Authentication.SslProtocols.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() + { + completionSource.SetResult(null); + } + + public void CompleteFailed(Exception ex) + { + completionSource.SetResult(ex); + } + } + + /// + /// 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); - } - } -} + 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); + } + } +} \ No newline at end of file diff --git a/PushSharp.Apple/ApnsFeedbackService.cs b/PushSharp.Apple/ApnsFeedbackService.cs index 57fb887a..832c2335 100644 --- a/PushSharp.Apple/ApnsFeedbackService.cs +++ b/PushSharp.Apple/ApnsFeedbackService.cs @@ -1,126 +1,125 @@ 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; }); - - stream.AuthenticateAsClient(Configuration.FeedbackHost, certificates, System.Security.Authentication.SslProtocols.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 { } + public class FeedbackService + { + public FeedbackService(ApnsConfiguration configuration) + { + Configuration = configuration; + } - try { client.Close (); } catch { } + 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; }); + + stream.AuthenticateAsClient(Configuration.FeedbackHost, certificates, System.Security.Authentication.SslProtocols.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/ApnsNotification.cs b/PushSharp.Apple/ApnsNotification.cs index b9a4ea9e..6fe73902 100644 --- a/PushSharp.Apple/ApnsNotification.cs +++ b/PushSharp.Apple/ApnsNotification.cs @@ -1,168 +1,177 @@ using System; -using PushSharp.Core; -using Newtonsoft.Json.Linq; +using System.Collections.Generic; using System.Net; using System.Text; -using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using PushSharp.Core; namespace PushSharp.Apple { - public class ApnsNotification : INotification - { - static readonly object nextIdentifierLock = new object (); - static int nextIdentifier = 1; - - static int GetNextIdentifier () - { - lock (nextIdentifierLock) { - if (nextIdentifier >= int.MaxValue - 10) - nextIdentifier = 1; - - return nextIdentifier++; - } - } - - /// - /// DO NOT Call this unless you know what you are doing! - /// - public static void ResetIdentifier () - { - lock (nextIdentifierLock) - nextIdentifier = 0; - } - - public object Tag { get; set; } - - public int Identifier { get; private set; } - - public string DeviceToken { get; set; } - - 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 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); - - public ApnsNotification () : this (string.Empty, new JObject ()) - { - } - - public ApnsNotification (string deviceToken) : this (deviceToken, new JObject ()) - { - } - - public ApnsNotification (string deviceToken, JObject payload) - { - if (!string.IsNullOrEmpty (deviceToken) && deviceToken.Length < DEVICE_TOKEN_STRING_MIN_SIZE) - throw new NotificationException ("Invalid DeviceToken Length", this); - - DeviceToken = deviceToken; - Payload = payload; - - Identifier = GetNextIdentifier (); - } - - public bool IsDeviceRegistrationIdValid () - { - var r = new System.Text.RegularExpressions.Regex (@"^[0-9A-F]+$", System.Text.RegularExpressions.RegexOptions.IgnoreCase); - return r.Match (this.DeviceToken).Success; - } - - public override string ToString () - { - try { - if (Payload != null) - return Payload.ToString(Newtonsoft.Json.Formatting.None); - } catch { - } - - return "{}"; - } - - 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 (); - } - - } + public class ApnsNotification : INotification + { + static readonly object nextIdentifierLock = new object(); + static int nextIdentifier = 1; + + static int GetNextIdentifier() + { + lock (nextIdentifierLock) + { + if (nextIdentifier >= int.MaxValue - 10) + nextIdentifier = 1; + + return nextIdentifier++; + } + } + + /// + /// DO NOT Call this unless you know what you are doing! + /// + public static void ResetIdentifier() + { + lock (nextIdentifierLock) + nextIdentifier = 0; + } + + public object Tag { get; set; } + + public int Identifier { get; private set; } + + public string DeviceToken { get; set; } + + 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 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); + + public ApnsNotification() : this(string.Empty, new JObject()) + { + } + + public ApnsNotification(string deviceToken) : this(deviceToken, new JObject()) + { + } + + public ApnsNotification(string deviceToken, JObject payload) + { + if (!string.IsNullOrEmpty(deviceToken) && deviceToken.Length < DEVICE_TOKEN_STRING_MIN_SIZE) + throw new NotificationException("Invalid DeviceToken Length", this); + + DeviceToken = deviceToken; + Payload = payload; + + Identifier = GetNextIdentifier(); + } + + public bool IsDeviceRegistrationIdValid() + { + var r = new System.Text.RegularExpressions.Regex(@"^[0-9A-F]+$", System.Text.RegularExpressions.RegexOptions.IgnoreCase); + return r.Match(this.DeviceToken).Success; + } + + public override string ToString() + { + try + { + if (Payload != null) + return Payload.ToString(Newtonsoft.Json.Formatting.None); + } + catch + { + } + + return "{}"; + } + + 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(); + } + + } } diff --git a/PushSharp.Apple/ApnsServiceConnection.cs b/PushSharp.Apple/ApnsServiceConnection.cs index 2dc760cb..30f2acbb 100644 --- a/PushSharp.Apple/ApnsServiceConnection.cs +++ b/PushSharp.Apple/ApnsServiceConnection.cs @@ -1,53 +1,53 @@ -using System; +using System.Threading.Tasks; using PushSharp.Core; -using System.Threading.Tasks; namespace PushSharp.Apple { - public class ApnsServiceConnectionFactory : IServiceConnectionFactory - { - public ApnsServiceConnectionFactory (ApnsConfiguration configuration) - { - Configuration = configuration; - } - - public ApnsConfiguration Configuration { get; private set; } - - public IServiceConnection Create() - { - return new ApnsServiceConnection (Configuration); - } - } - - public class ApnsServiceBroker : ServiceBroker - { - public ApnsServiceBroker (ApnsConfiguration configuration) : base (new ApnsServiceConnectionFactory (configuration)) - { - } - } - - public class ApnsServiceConnection : IServiceConnection - { - readonly ApnsConnection connection; - - public ApnsServiceConnection (ApnsConfiguration configuration) - { - connection = new ApnsConnection (configuration); - } - - public async Task Send (ApnsNotification notification) - { - var completableNotification = new ApnsConnection.CompletableApnsNotification (notification); - - connection.Send (completableNotification); - - var ex = await completableNotification.WaitForComplete ().ConfigureAwait (false); - - //Log.Info ("Finished Waiting for Notification: {0} (Had Exception? {1})", notification.Identifier, ex != null); - - if (ex != null) { - throw ex; - } - } - } + public class ApnsServiceConnectionFactory : IServiceConnectionFactory + { + public ApnsServiceConnectionFactory(ApnsConfiguration configuration) + { + Configuration = configuration; + } + + public ApnsConfiguration Configuration { get; private set; } + + public IServiceConnection Create() + { + return new ApnsServiceConnection(Configuration); + } + } + + public class ApnsServiceBroker : ServiceBroker + { + public ApnsServiceBroker(ApnsConfiguration configuration) : base(new ApnsServiceConnectionFactory(configuration)) + { + } + } + + public class ApnsServiceConnection : IServiceConnection + { + readonly ApnsConnection connection; + + public ApnsServiceConnection(ApnsConfiguration configuration) + { + connection = new ApnsConnection(configuration); + } + + public async Task Send(ApnsNotification notification) + { + var completableNotification = new ApnsConnection.CompletableApnsNotification(notification); + + connection.Send(completableNotification); + + var ex = await completableNotification.WaitForComplete().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 index 33d28192..97eb3176 100644 --- a/PushSharp.Apple/Exceptions.cs +++ b/PushSharp.Apple/Exceptions.cs @@ -3,61 +3,61 @@ 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) - { - } - } + 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/Properties/AssemblyInfo.cs b/PushSharp.Apple/Properties/AssemblyInfo.cs index 3b074103..076fbe53 100644 --- a/PushSharp.Apple/Properties/AssemblyInfo.cs +++ b/PushSharp.Apple/Properties/AssemblyInfo.cs @@ -4,20 +4,20 @@ // Information about this assembly is defined by the following attributes. // Change them to the values specific to your project. -[assembly: AssemblyTitle ("PushSharp.Apple")] -[assembly: AssemblyDescription ("")] -[assembly: AssemblyConfiguration ("")] -[assembly: AssemblyCompany ("")] -[assembly: AssemblyProduct ("")] -[assembly: AssemblyCopyright ("redth")] -[assembly: AssemblyTrademark ("")] -[assembly: AssemblyCulture ("")] +[assembly: AssemblyTitle("PushSharp.Apple")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("redth")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion ("1.0.*")] +[assembly: AssemblyVersion("1.0.*")] // The following attributes are used to specify the signing key for the assembly, // if desired. See the Mono documentation for more information about signing. diff --git a/PushSharp.Apple/PushSharp.Apple.csproj b/PushSharp.Apple/PushSharp.Apple.csproj index 3c9344a0..bca698c4 100644 --- a/PushSharp.Apple/PushSharp.Apple.csproj +++ b/PushSharp.Apple/PushSharp.Apple.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -7,9 +7,10 @@ Library PushSharp.Apple PushSharp.Apple - v4.5 + v4.6.1 true ..\PushSharp-Signing.snk + true @@ -30,10 +31,11 @@ false - - - ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + True + diff --git a/PushSharp.Apple/packages.config b/PushSharp.Apple/packages.config index 505e5883..e1fae9c6 100644 --- a/PushSharp.Apple/packages.config +++ b/PushSharp.Apple/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/PushSharp.Blackberry/BlackberryConfiguration.cs b/PushSharp.Blackberry/BlackberryConfiguration.cs index 80fbd376..294ce197 100644 --- a/PushSharp.Blackberry/BlackberryConfiguration.cs +++ b/PushSharp.Blackberry/BlackberryConfiguration.cs @@ -2,51 +2,51 @@ namespace PushSharp.Blackberry { - public class BlackberryConfiguration - { - const string SEND_URL = "https://pushapi.eval.blackberry.com/mss/PD_pushRequest"; - - public BlackberryConfiguration () - { - SendUrl = SEND_URL; - } - - public BlackberryConfiguration (string applicationId, string password) - { - ApplicationId = applicationId; - Password = password; - SendUrl = SEND_URL; - } - - public string ApplicationId { get; set; } - public string Password { get; set; } - public string Boundary { get { return "ASDFaslkdfjasfaSfdasfhpoiurwqrwm"; } } - - - /// - /// Push Proxy Gateway (PPG) Url is used for submitting push requests - /// Default value is BIS PPG evaluation url - /// https://pushapi.eval.blackberry.com/mss/PD_pushRequest - /// - public string SendUrl { get; private set; } - - /// - /// Overrides SendUrl with any PPG url: BIS or BES - /// - /// Push Proxy Gateway (PPG) Url, - /// For BIS,it's PPG production url is in format: http://cpxxx.pushapi.na.blackberry.com - /// where xxx should be replaced with CPID (Content Provider ID) - public void OverrideSendUrl(string url) - { - if (!string.IsNullOrWhiteSpace(url)) - { - if (url.EndsWith("pushapi.na.blackberry.com", StringComparison.InvariantCultureIgnoreCase) || - url.EndsWith("pushapi.eval.blackberry.com", StringComparison.InvariantCultureIgnoreCase)) - url = url + @"/mss/PD_pushRequest"; - } - SendUrl = url; - } - - } + public class BlackberryConfiguration + { + const string SEND_URL = "https://pushapi.eval.blackberry.com/mss/PD_pushRequest"; + + public BlackberryConfiguration() + { + SendUrl = SEND_URL; + } + + public BlackberryConfiguration(string applicationId, string password) + { + ApplicationId = applicationId; + Password = password; + SendUrl = SEND_URL; + } + + public string ApplicationId { get; set; } + public string Password { get; set; } + public string Boundary { get { return "ASDFaslkdfjasfaSfdasfhpoiurwqrwm"; } } + + + /// + /// Push Proxy Gateway (PPG) Url is used for submitting push requests + /// Default value is BIS PPG evaluation url + /// https://pushapi.eval.blackberry.com/mss/PD_pushRequest + /// + public string SendUrl { get; private set; } + + /// + /// Overrides SendUrl with any PPG url: BIS or BES + /// + /// Push Proxy Gateway (PPG) Url, + /// For BIS,it's PPG production url is in format: http://cpxxx.pushapi.na.blackberry.com + /// where xxx should be replaced with CPID (Content Provider ID) + public void OverrideSendUrl(string url) + { + if (!string.IsNullOrWhiteSpace(url)) + { + if (url.EndsWith("pushapi.na.blackberry.com", StringComparison.InvariantCultureIgnoreCase) || + url.EndsWith("pushapi.eval.blackberry.com", StringComparison.InvariantCultureIgnoreCase)) + url = url + @"/mss/PD_pushRequest"; + } + SendUrl = url; + } + + } } diff --git a/PushSharp.Blackberry/BlackberryConnection.cs b/PushSharp.Blackberry/BlackberryConnection.cs index 14f17573..993c3dbd 100644 --- a/PushSharp.Blackberry/BlackberryConnection.cs +++ b/PushSharp.Blackberry/BlackberryConnection.cs @@ -7,80 +7,80 @@ namespace PushSharp.Blackberry { - public class BlackberryServiceConnectionFactory : IServiceConnectionFactory - { - public BlackberryServiceConnectionFactory (BlackberryConfiguration configuration) - { - Configuration = configuration; - } - - public BlackberryConfiguration Configuration { get; private set; } - - public IServiceConnection Create() - { - return new BlackberryServiceConnection (Configuration); - } - } - - public class BlackberryServiceBroker : ServiceBroker - { - public BlackberryServiceBroker (BlackberryConfiguration configuration) : base (new BlackberryServiceConnectionFactory (configuration)) - { - } - } - - public class BlackberryServiceConnection : IServiceConnection - { - public BlackberryServiceConnection (BlackberryConfiguration configuration) - { - Configuration = configuration; - http = new BlackberryHttpClient (Configuration); - } - - public BlackberryConfiguration Configuration { get; private set; } - - readonly BlackberryHttpClient http; - - public async Task Send (BlackberryNotification notification) - { - var response = await http.PostNotification (notification); - var description = string.Empty; - - var status = new BlackberryMessageStatus - { - Notification = notification, - HttpStatus = HttpStatusCode.ServiceUnavailable - }; - - var bbNotStatus = string.Empty; - status.HttpStatus = response.StatusCode; - - var xmlContent = await response.Content.ReadAsStreamAsync (); - var doc = XDocument.Load (xmlContent); - - XElement result = doc.Descendants().FirstOrDefault(desc => - desc.Name == "response-result" || - desc.Name == "badmessage-response"); - if (result != null) - { - bbNotStatus = result.Attribute("code").Value; - description = result.Attribute("desc").Value; - } - - BlackberryNotificationStatus notStatus; - Enum.TryParse(bbNotStatus, true, out notStatus); - status.NotificationStatus = notStatus; - - if (status.NotificationStatus == BlackberryNotificationStatus.NoAppReceivePush) - throw new DeviceSubscriptionExpiredException (notification); - - if (status.HttpStatus == HttpStatusCode.OK - && status.NotificationStatus == BlackberryNotificationStatus.RequestAcceptedForProcessing) - return; - - throw new BlackberryNotificationException (status, description, notification); - - } - } + public class BlackberryServiceConnectionFactory : IServiceConnectionFactory + { + public BlackberryServiceConnectionFactory(BlackberryConfiguration configuration) + { + Configuration = configuration; + } + + public BlackberryConfiguration Configuration { get; private set; } + + public IServiceConnection Create() + { + return new BlackberryServiceConnection(Configuration); + } + } + + public class BlackberryServiceBroker : ServiceBroker + { + public BlackberryServiceBroker(BlackberryConfiguration configuration) : base(new BlackberryServiceConnectionFactory(configuration)) + { + } + } + + public class BlackberryServiceConnection : IServiceConnection + { + public BlackberryServiceConnection(BlackberryConfiguration configuration) + { + Configuration = configuration; + http = new BlackberryHttpClient(Configuration); + } + + public BlackberryConfiguration Configuration { get; private set; } + + readonly BlackberryHttpClient http; + + public async Task Send(BlackberryNotification notification) + { + var response = await http.PostNotification(notification); + var description = string.Empty; + + var status = new BlackberryMessageStatus + { + Notification = notification, + HttpStatus = HttpStatusCode.ServiceUnavailable + }; + + var bbNotStatus = string.Empty; + status.HttpStatus = response.StatusCode; + + var xmlContent = await response.Content.ReadAsStreamAsync(); + var doc = XDocument.Load(xmlContent); + + XElement result = doc.Descendants().FirstOrDefault(desc => + desc.Name == "response-result" || + desc.Name == "badmessage-response"); + if (result != null) + { + bbNotStatus = result.Attribute("code").Value; + description = result.Attribute("desc").Value; + } + + BlackberryNotificationStatus notStatus; + Enum.TryParse(bbNotStatus, true, out notStatus); + status.NotificationStatus = notStatus; + + if (status.NotificationStatus == BlackberryNotificationStatus.NoAppReceivePush) + throw new DeviceSubscriptionExpiredException(notification); + + if (status.HttpStatus == HttpStatusCode.OK + && status.NotificationStatus == BlackberryNotificationStatus.RequestAcceptedForProcessing) + return; + + throw new BlackberryNotificationException(status, description, notification); + + } + } } diff --git a/PushSharp.Blackberry/BlackberryHttpClient.cs b/PushSharp.Blackberry/BlackberryHttpClient.cs index c54c2878..d9415800 100644 --- a/PushSharp.Blackberry/BlackberryHttpClient.cs +++ b/PushSharp.Blackberry/BlackberryHttpClient.cs @@ -6,45 +6,45 @@ namespace PushSharp.Blackberry { - public class BlackberryHttpClient : HttpClient - { - public BlackberryConfiguration Configuration { get; private set; } + public class BlackberryHttpClient : HttpClient + { + public BlackberryConfiguration Configuration { get; private set; } - public BlackberryHttpClient (BlackberryConfiguration configuration) : base() - { - Configuration = configuration; + public BlackberryHttpClient(BlackberryConfiguration configuration) : base() + { + Configuration = configuration; - var authInfo = Configuration.ApplicationId + ":" + Configuration.Password; - authInfo = Convert.ToBase64String (Encoding.Default.GetBytes(authInfo)); + var authInfo = Configuration.ApplicationId + ":" + Configuration.Password; + authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); - this.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue ("Basic", authInfo); - this.DefaultRequestHeaders.ConnectionClose = true; + this.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authInfo); + this.DefaultRequestHeaders.ConnectionClose = true; - this.DefaultRequestHeaders.Remove("connection"); - } + this.DefaultRequestHeaders.Remove("connection"); + } - public Task PostNotification (BlackberryNotification notification) - { - var c = new MultipartContent ("related", Configuration.Boundary); - c.Headers.Remove("Content-Type"); - c.Headers.TryAddWithoutValidation("Content-Type", "multipart/related; boundary=" + Configuration.Boundary + "; type=application/xml"); + public Task PostNotification(BlackberryNotification notification) + { + var c = new MultipartContent("related", Configuration.Boundary); + c.Headers.Remove("Content-Type"); + c.Headers.TryAddWithoutValidation("Content-Type", "multipart/related; boundary=" + Configuration.Boundary + "; type=application/xml"); - var xml = notification.ToPapXml (); + var xml = notification.ToPapXml(); - c.Add (new StringContent (xml, Encoding.UTF8, "application/xml")); + c.Add(new StringContent(xml, Encoding.UTF8, "application/xml")); - var bc = new ByteArrayContent(notification.Content.Content); - bc.Headers.Add("Content-Type", notification.Content.ContentType); + var bc = new ByteArrayContent(notification.Content.Content); + bc.Headers.Add("Content-Type", notification.Content.ContentType); - foreach (var header in notification.Content.Headers) - bc.Headers.Add(header.Key, header.Value); + foreach (var header in notification.Content.Headers) + bc.Headers.Add(header.Key, header.Value); - c.Add(bc); + c.Add(bc); - return PostAsync (Configuration.SendUrl, c); - } - } + return PostAsync(Configuration.SendUrl, c); + } + } } diff --git a/PushSharp.Blackberry/BlackberryNotification.cs b/PushSharp.Blackberry/BlackberryNotification.cs index 64b1fa17..30122ee8 100644 --- a/PushSharp.Blackberry/BlackberryNotification.cs +++ b/PushSharp.Blackberry/BlackberryNotification.cs @@ -7,162 +7,162 @@ namespace PushSharp.Blackberry { - public enum QualityOfServiceLevel - { - NotSpecified, - Unconfirmed, - PreferConfirmed, - Confirmed - } + public enum QualityOfServiceLevel + { + NotSpecified, + Unconfirmed, + PreferConfirmed, + Confirmed + } - public class BlackberryNotification : INotification - { - public BlackberryNotification() - { - PushId = Guid.NewGuid ().ToString (); - Recipients = new List (); - DeliverBeforeTimestamp = DateTime.UtcNow.AddMinutes(5); - QualityOfService = QualityOfServiceLevel.Unconfirmed; - } + public class BlackberryNotification : INotification + { + public BlackberryNotification() + { + PushId = Guid.NewGuid().ToString(); + Recipients = new List(); + DeliverBeforeTimestamp = DateTime.UtcNow.AddMinutes(5); + QualityOfService = QualityOfServiceLevel.Unconfirmed; + } - public bool IsDeviceRegistrationIdValid () - { - return true; - } + public bool IsDeviceRegistrationIdValid() + { + return true; + } - public object Tag { get;set; } + public object Tag { get; set; } - public string PushId { get; private set; } + public string PushId { get; private set; } - public QualityOfServiceLevel QualityOfService { get;set; } + public QualityOfServiceLevel QualityOfService { get; set; } - /// - /// Address (e.g. URL) that Blackberry push service could use for notification - /// of results related to the message - /// - public string PpgNotifyRequestedTo { get; set; } + /// + /// Address (e.g. URL) that Blackberry push service could use for notification + /// of results related to the message + /// + public string PpgNotifyRequestedTo { get; set; } - /// - /// Date and time by which the content must be delivered,expressed as UTC - /// Message that has aged beyond this date will not be transmitted - /// - public DateTime? DeliverBeforeTimestamp { get; set; } + /// + /// Date and time by which the content must be delivered,expressed as UTC + /// Message that has aged beyond this date will not be transmitted + /// + public DateTime? DeliverBeforeTimestamp { get; set; } - /// - /// Date and time after which the content should be delivered,expressed as UTC - /// Message will not be transmitted before this date - /// - public DateTime? DeliverAfterTimestamp { get; set; } + /// + /// Date and time after which the content should be delivered,expressed as UTC + /// Message will not be transmitted before this date + /// + public DateTime? DeliverAfterTimestamp { get; set; } - public List Recipients { get;set; } - - public string SourceReference { get; set; } - - public BlackberryMessageContent Content { get; set; } - - public string ToPapXml() - { - var doc = new XDocument (); - - var docType = new XDocumentType("pap", "-//WAPFORUM//DTD PAP 2.1//EN", "http://www.openmobilealliance.org/tech/DTD/pap_2.1.dtd", ""); - - doc.AddFirst (docType); - - var pap = new XElement ("pap"); - - var pushMsg = new XElement ("push-message"); - - pushMsg.Add (new XAttribute ("push-id", this.PushId)); - pushMsg.Add(new XAttribute("source-reference", this.SourceReference)); - - if (!string.IsNullOrEmpty (this.PpgNotifyRequestedTo)) - pushMsg.Add(new XAttribute("ppg-notify-requested-to", this.PpgNotifyRequestedTo)); - - if (this.DeliverAfterTimestamp.HasValue) - pushMsg.Add (new XAttribute ("deliver-after-timestamp", this.DeliverAfterTimestamp.Value.ToUniversalTime ().ToString("s", CultureInfo.InvariantCulture) + "Z")); - if (this.DeliverBeforeTimestamp.HasValue) - pushMsg.Add (new XAttribute ("deliver-before-timestamp", this.DeliverBeforeTimestamp.Value.ToUniversalTime ().ToString("s", CultureInfo.InvariantCulture) + "Z")); - - //Add all the recipients - foreach (var r in Recipients) - { - var address = new XElement("address"); - - var addrValue = r.Recipient; - - if (!string.IsNullOrEmpty(r.RecipientType)) - { - addrValue = string.Format("WAPPUSH={0}%3A{1}/TYPE={2}", System.Web.HttpUtility.UrlEncode(r.Recipient), - r.Port, r.RecipientType); - } - - address.Add(new XAttribute("address-value", addrValue)); - pushMsg.Add (address); - } - - pushMsg.Add (new XElement ("quality-of-service", new XAttribute ("delivery-method", this.QualityOfService.ToString ().ToLowerInvariant ()))); - - pap.Add(pushMsg); - doc.Add (pap); - - return "" + Environment.NewLine + doc.ToString (SaveOptions.None); - } - - - protected string XmlEncode(string text) - { - return System.Security.SecurityElement.Escape(text); - } + public List Recipients { get; set; } + + public string SourceReference { get; set; } + + public BlackberryMessageContent Content { get; set; } + + public string ToPapXml() + { + var doc = new XDocument(); + + var docType = new XDocumentType("pap", "-//WAPFORUM//DTD PAP 2.1//EN", "http://www.openmobilealliance.org/tech/DTD/pap_2.1.dtd", ""); + + doc.AddFirst(docType); + + var pap = new XElement("pap"); + + var pushMsg = new XElement("push-message"); + + pushMsg.Add(new XAttribute("push-id", this.PushId)); + pushMsg.Add(new XAttribute("source-reference", this.SourceReference)); + + if (!string.IsNullOrEmpty(this.PpgNotifyRequestedTo)) + pushMsg.Add(new XAttribute("ppg-notify-requested-to", this.PpgNotifyRequestedTo)); + + if (this.DeliverAfterTimestamp.HasValue) + pushMsg.Add(new XAttribute("deliver-after-timestamp", this.DeliverAfterTimestamp.Value.ToUniversalTime().ToString("s", CultureInfo.InvariantCulture) + "Z")); + if (this.DeliverBeforeTimestamp.HasValue) + pushMsg.Add(new XAttribute("deliver-before-timestamp", this.DeliverBeforeTimestamp.Value.ToUniversalTime().ToString("s", CultureInfo.InvariantCulture) + "Z")); + + //Add all the recipients + foreach (var r in Recipients) + { + var address = new XElement("address"); + + var addrValue = r.Recipient; + + if (!string.IsNullOrEmpty(r.RecipientType)) + { + addrValue = string.Format("WAPPUSH={0}%3A{1}/TYPE={2}", System.Web.HttpUtility.UrlEncode(r.Recipient), + r.Port, r.RecipientType); + } + + address.Add(new XAttribute("address-value", addrValue)); + pushMsg.Add(address); + } + + pushMsg.Add(new XElement("quality-of-service", new XAttribute("delivery-method", this.QualityOfService.ToString().ToLowerInvariant()))); + + pap.Add(pushMsg); + doc.Add(pap); + + return "" + Environment.NewLine + doc.ToString(SaveOptions.None); + } + + + protected string XmlEncode(string text) + { + return System.Security.SecurityElement.Escape(text); + } - } + } - public class BlackberryRecipient - { - public BlackberryRecipient(string recipient) - { - Recipient = recipient; - } + public class BlackberryRecipient + { + public BlackberryRecipient(string recipient) + { + Recipient = recipient; + } - public BlackberryRecipient(string recipient, int port, string recipientType) - { - Recipient = recipient; - Port = port; - RecipientType = recipientType; - } + public BlackberryRecipient(string recipient, int port, string recipientType) + { + Recipient = recipient; + Port = port; + RecipientType = recipientType; + } - public string Recipient { get;set; } - public int Port { get;set; } - public string RecipientType { get;set; } - } + public string Recipient { get; set; } + public int Port { get; set; } + public string RecipientType { get; set; } + } - public class BlackberryMessageContent - { + public class BlackberryMessageContent + { - public BlackberryMessageContent(string contentType, string content) - { - this.Headers = new Dictionary(); - this.ContentType = contentType; - this.Content = Encoding.UTF8.GetBytes(content); - } + public BlackberryMessageContent(string contentType, string content) + { + this.Headers = new Dictionary(); + this.ContentType = contentType; + this.Content = Encoding.UTF8.GetBytes(content); + } - public BlackberryMessageContent(string content) - { - this.Headers = new Dictionary(); - this.ContentType = "text/plain"; - this.Content = Encoding.UTF8.GetBytes(content); - } + public BlackberryMessageContent(string content) + { + this.Headers = new Dictionary(); + this.ContentType = "text/plain"; + this.Content = Encoding.UTF8.GetBytes(content); + } - public BlackberryMessageContent(string contentType, byte[] content) - { - this.Headers = new Dictionary(); - this.ContentType = contentType; - this.Content = content; - } + public BlackberryMessageContent(string contentType, byte[] content) + { + this.Headers = new Dictionary(); + this.ContentType = contentType; + this.Content = content; + } - public string ContentType { get; private set; } - public byte[] Content { get; private set; } + public string ContentType { get; private set; } + public byte[] Content { get; private set; } - public Dictionary Headers { get; private set; } - } + public Dictionary Headers { get; private set; } + } } diff --git a/PushSharp.Blackberry/Enums.cs b/PushSharp.Blackberry/Enums.cs index 2a6e43d2..18764f04 100644 --- a/PushSharp.Blackberry/Enums.cs +++ b/PushSharp.Blackberry/Enums.cs @@ -2,140 +2,140 @@ namespace PushSharp.Blackberry { - public class BlackberryMessageStatus - { - public BlackberryNotificationStatus NotificationStatus { get; set; } + public class BlackberryMessageStatus + { + public BlackberryNotificationStatus NotificationStatus { get; set; } - public BlackberryNotification Notification { get; set; } + public BlackberryNotification Notification { get; set; } - public System.Net.HttpStatusCode HttpStatus { get; set; } - } + public System.Net.HttpStatusCode HttpStatus { get; set; } + } - public enum BlackberryNotificationStatus - { - NotAvailable=0, - /// - /// The request was completed successfully - /// - RequestCompleted = 1000, - /// - /// The request was accepted for processing - /// - RequestAcceptedForProcessing = 1001, - /// - /// The request was accepted for processing, but the daily push count quota was exceeded - /// for the push application and future pushes to the application might start being rejected. - /// Future pushes should be delayed until the next day when more quota is available - /// - RequestAcceptedButPushQuotaExceeded = 1500, - /// - /// The request is invalid - /// - InvalidRequest = 2000, - /// - /// The requested action is forbidden - /// - ForbiddenRequestAction = 2001, - /// - /// The specified PIN or token is not recognized - /// - PinOrTokenNotRecognized = 2002, - /// - /// Could not find the specified Push ID - /// - PushIdNotFound = 2004, - /// - /// The supplied Push ID is not unique - /// - PushIdNotUnique = 2007, - /// - /// The Push ID is valid, but the push request could not be cancelled - /// - PushCantBeCancelled = 2008, - /// - /// The Push ID is valid, but the corresponding PINs or tokens are still being processed. - /// Status query is not possible at this time and should be tried again later - /// - StatusCodeNotPossible = 2009, - /// - /// The request was rejected because the daily push count quota was exceeded for the push application. - /// Future pushes should be delayed until the next day when more quota is available - /// - PushQuotaExceeded = 2500, - /// - /// The PPG could not complete the request due to an internal error - /// - InternalError = 3000, - /// - /// The server does not support the operation that was requested - /// - OperationNotSupported = 3001, - /// - /// The server does not support the PAP version specified in the request - /// - ProvidedPapVersionNotSupported = 3002, - /// - /// The PPG could not deliver the message using the specified method - /// - DeliveryFailed = 3007, - /// - /// The service failed - /// - ServiceFailed = 4000, - /// - /// The server is busy - /// - ServerBusy = 4001, - /// - /// The request expired - /// - RequestExpired = 4500, - /// - /// The request failed - /// - RequestFailed = 4501, - /// - /// The request failed because no application on the device is listening to receive the push - /// (either the application is closed and cannot be launched or it was removed from the device)" - /// - NoAppReceivePush = 4502, - /// - /// The device is unable to receive the push due to the push service being blocked - /// - PushServiceBlocked = 4503, - /// - /// Specific request was completed successfully - /// - SpecifiedRequestCompleted = 21000, - /// - /// Specific request is badly formed - /// - SpecifiedRequestMalformed = 22000, - /// - /// Could not find the specified application ID for the specific request - /// - SpecifiedRequestAppIdNotFound = 22001, - /// - /// The specified PIN or token in the specific request is invalid - /// - SpecifiedRequestInvalidPinOrToken = 22002, - /// - /// The specific request provides an incorrect status - /// - SpecifiedRequestIncorrectStatus = 22003, - /// - /// The specific request produces no results - /// - SpecifiedRequestNoResults = 22004, - /// - /// The specific request exceeds the number of calls allowed - /// within the specified time period - /// - SpecifiedRequestNumCallsExceeded = 22005, - /// - /// Internal error has prevented the request from being completed - /// - SpecifiedRequestInternalError = 23000 - } + public enum BlackberryNotificationStatus + { + NotAvailable = 0, + /// + /// The request was completed successfully + /// + RequestCompleted = 1000, + /// + /// The request was accepted for processing + /// + RequestAcceptedForProcessing = 1001, + /// + /// The request was accepted for processing, but the daily push count quota was exceeded + /// for the push application and future pushes to the application might start being rejected. + /// Future pushes should be delayed until the next day when more quota is available + /// + RequestAcceptedButPushQuotaExceeded = 1500, + /// + /// The request is invalid + /// + InvalidRequest = 2000, + /// + /// The requested action is forbidden + /// + ForbiddenRequestAction = 2001, + /// + /// The specified PIN or token is not recognized + /// + PinOrTokenNotRecognized = 2002, + /// + /// Could not find the specified Push ID + /// + PushIdNotFound = 2004, + /// + /// The supplied Push ID is not unique + /// + PushIdNotUnique = 2007, + /// + /// The Push ID is valid, but the push request could not be cancelled + /// + PushCantBeCancelled = 2008, + /// + /// The Push ID is valid, but the corresponding PINs or tokens are still being processed. + /// Status query is not possible at this time and should be tried again later + /// + StatusCodeNotPossible = 2009, + /// + /// The request was rejected because the daily push count quota was exceeded for the push application. + /// Future pushes should be delayed until the next day when more quota is available + /// + PushQuotaExceeded = 2500, + /// + /// The PPG could not complete the request due to an internal error + /// + InternalError = 3000, + /// + /// The server does not support the operation that was requested + /// + OperationNotSupported = 3001, + /// + /// The server does not support the PAP version specified in the request + /// + ProvidedPapVersionNotSupported = 3002, + /// + /// The PPG could not deliver the message using the specified method + /// + DeliveryFailed = 3007, + /// + /// The service failed + /// + ServiceFailed = 4000, + /// + /// The server is busy + /// + ServerBusy = 4001, + /// + /// The request expired + /// + RequestExpired = 4500, + /// + /// The request failed + /// + RequestFailed = 4501, + /// + /// The request failed because no application on the device is listening to receive the push + /// (either the application is closed and cannot be launched or it was removed from the device)" + /// + NoAppReceivePush = 4502, + /// + /// The device is unable to receive the push due to the push service being blocked + /// + PushServiceBlocked = 4503, + /// + /// Specific request was completed successfully + /// + SpecifiedRequestCompleted = 21000, + /// + /// Specific request is badly formed + /// + SpecifiedRequestMalformed = 22000, + /// + /// Could not find the specified application ID for the specific request + /// + SpecifiedRequestAppIdNotFound = 22001, + /// + /// The specified PIN or token in the specific request is invalid + /// + SpecifiedRequestInvalidPinOrToken = 22002, + /// + /// The specific request provides an incorrect status + /// + SpecifiedRequestIncorrectStatus = 22003, + /// + /// The specific request produces no results + /// + SpecifiedRequestNoResults = 22004, + /// + /// The specific request exceeds the number of calls allowed + /// within the specified time period + /// + SpecifiedRequestNumCallsExceeded = 22005, + /// + /// Internal error has prevented the request from being completed + /// + SpecifiedRequestInternalError = 23000 + } } diff --git a/PushSharp.Blackberry/Exceptions.cs b/PushSharp.Blackberry/Exceptions.cs index be990fce..8c1ade59 100644 --- a/PushSharp.Blackberry/Exceptions.cs +++ b/PushSharp.Blackberry/Exceptions.cs @@ -1,21 +1,19 @@ -using System; -using System.Collections.Generic; -using PushSharp.Core; +using PushSharp.Core; namespace PushSharp.Blackberry { - public class BlackberryNotificationException : NotificationException - { - public BlackberryNotificationException (BlackberryMessageStatus msgStatus, string desc, BlackberryNotification notification) - :base (desc + " - " + msgStatus, notification) - { - Notification = notification; - MessageStatus = msgStatus; - } + public class BlackberryNotificationException : NotificationException + { + public BlackberryNotificationException(BlackberryMessageStatus msgStatus, string desc, BlackberryNotification notification) + : base(desc + " - " + msgStatus, notification) + { + Notification = notification; + MessageStatus = msgStatus; + } - public new BlackberryNotification Notification { get; set; } + public new BlackberryNotification Notification { get; set; } - public BlackberryMessageStatus MessageStatus { get; private set; } - } + public BlackberryMessageStatus MessageStatus { get; private set; } + } } diff --git a/PushSharp.Blackberry/Properties/AssemblyInfo.cs b/PushSharp.Blackberry/Properties/AssemblyInfo.cs index ba2f5b78..da9d78b5 100644 --- a/PushSharp.Blackberry/Properties/AssemblyInfo.cs +++ b/PushSharp.Blackberry/Properties/AssemblyInfo.cs @@ -4,20 +4,20 @@ // Information about this assembly is defined by the following attributes. // Change them to the values specific to your project. -[assembly: AssemblyTitle ("PushSharp.Blackberry")] -[assembly: AssemblyDescription ("")] -[assembly: AssemblyConfiguration ("")] -[assembly: AssemblyCompany ("")] -[assembly: AssemblyProduct ("")] -[assembly: AssemblyCopyright ("redth")] -[assembly: AssemblyTrademark ("")] -[assembly: AssemblyCulture ("")] +[assembly: AssemblyTitle("PushSharp.Blackberry")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("redth")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion ("1.0.*")] +[assembly: AssemblyVersion("1.0.*")] // The following attributes are used to specify the signing key for the assembly, // if desired. See the Mono documentation for more information about signing. diff --git a/PushSharp.Blackberry/PushSharp.Blackberry.csproj b/PushSharp.Blackberry/PushSharp.Blackberry.csproj index 4e0e54c3..076b4f3e 100644 --- a/PushSharp.Blackberry/PushSharp.Blackberry.csproj +++ b/PushSharp.Blackberry/PushSharp.Blackberry.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -7,9 +7,10 @@ Library PushSharp.Blackberry PushSharp.Blackberry - v4.5 + v4.6.1 true ..\PushSharp-Signing.snk + true diff --git a/PushSharp.Core/Exceptions.cs b/PushSharp.Core/Exceptions.cs index 4cfc29fb..970dbbbf 100644 --- a/PushSharp.Core/Exceptions.cs +++ b/PushSharp.Core/Exceptions.cs @@ -1,51 +1,50 @@ using System; -using System.Collections.Generic; namespace PushSharp.Core { - public class DeviceSubscriptionExpiredException : DeviceSubscriptonExpiredException - { - public DeviceSubscriptionExpiredException (INotification notification) : base (notification) - { - } - } - - [Obsolete ("Do not use this class directly, it has a typo in it, instead use DeviceSubscriptionExpiredException")] - public class DeviceSubscriptonExpiredException : NotificationException - { - public DeviceSubscriptonExpiredException (INotification notification) : base ("Device Subscription has Expired", notification) - { - ExpiredAt = DateTime.UtcNow; - } - - public string OldSubscriptionId { get; set; } - public string NewSubscriptionId { get; set; } - public DateTime ExpiredAt { get; set; } - } - - public class NotificationException : Exception - { - public NotificationException (string message, INotification notification) : base (message) - { - Notification = notification; - } - - public NotificationException (string message, INotification notification, Exception innerException) - : base (message, innerException) - { - Notification = notification; - } - - public INotification Notification { get; set; } - } - - public class RetryAfterException : NotificationException - { - public RetryAfterException (INotification notification, string message, DateTime retryAfterUtc) : base (message, notification) - { - RetryAfterUtc = retryAfterUtc; - } - - public DateTime RetryAfterUtc { get; set; } - } + public class DeviceSubscriptionExpiredException : DeviceSubscriptonExpiredException + { + public DeviceSubscriptionExpiredException(INotification notification) : base(notification) + { + } + } + + [Obsolete("Do not use this class directly, it has a typo in it, instead use DeviceSubscriptionExpiredException")] + public class DeviceSubscriptonExpiredException : NotificationException + { + public DeviceSubscriptonExpiredException(INotification notification) : base("Device Subscription has Expired", notification) + { + ExpiredAt = DateTime.UtcNow; + } + + public string OldSubscriptionId { get; set; } + public string NewSubscriptionId { get; set; } + public DateTime ExpiredAt { get; set; } + } + + public class NotificationException : Exception + { + public NotificationException(string message, INotification notification) : base(message) + { + Notification = notification; + } + + public NotificationException(string message, INotification notification, Exception innerException) + : base(message, innerException) + { + Notification = notification; + } + + public INotification Notification { get; set; } + } + + public class RetryAfterException : NotificationException + { + public RetryAfterException(INotification notification, string message, DateTime retryAfterUtc) : base(message, notification) + { + RetryAfterUtc = retryAfterUtc; + } + + public DateTime RetryAfterUtc { get; set; } + } } diff --git a/PushSharp.Core/INotification.cs b/PushSharp.Core/INotification.cs index 23e26d0f..e02e92cf 100644 --- a/PushSharp.Core/INotification.cs +++ b/PushSharp.Core/INotification.cs @@ -2,9 +2,9 @@ namespace PushSharp.Core { - public interface INotification - { - bool IsDeviceRegistrationIdValid (); - object Tag { get; set; } - } + public interface INotification + { + bool IsDeviceRegistrationIdValid(); + object Tag { get; set; } + } } diff --git a/PushSharp.Core/IServiceBroker.cs b/PushSharp.Core/IServiceBroker.cs index e18b9d60..f49ced6f 100644 --- a/PushSharp.Core/IServiceBroker.cs +++ b/PushSharp.Core/IServiceBroker.cs @@ -2,16 +2,16 @@ namespace PushSharp.Core { - public interface IServiceBroker where TNotification : INotification - { - event NotificationSuccessDelegate OnNotificationSucceeded; - event NotificationFailureDelegate OnNotificationFailed; + public interface IServiceBroker where TNotification : INotification + { + event NotificationSuccessDelegate OnNotificationSucceeded; + event NotificationFailureDelegate OnNotificationFailed; - System.Collections.Generic.IEnumerable TakeMany (); - bool IsCompleted { get; } + System.Collections.Generic.IEnumerable TakeMany(); + bool IsCompleted { get; } - void RaiseNotificationSucceeded (TNotification notification); - void RaiseNotificationFailed (TNotification notification, AggregateException ex); - } + void RaiseNotificationSucceeded(TNotification notification); + void RaiseNotificationFailed(TNotification notification, AggregateException ex); + } } diff --git a/PushSharp.Core/IServiceConnection.cs b/PushSharp.Core/IServiceConnection.cs index cfc1812c..163d85be 100644 --- a/PushSharp.Core/IServiceConnection.cs +++ b/PushSharp.Core/IServiceConnection.cs @@ -3,12 +3,12 @@ namespace PushSharp.Core { - public delegate void NotificationSuccessDelegate (TNotification notification) where TNotification : INotification; - public delegate void NotificationFailureDelegate (TNotification notification, AggregateException exception) where TNotification : INotification; + public delegate void NotificationSuccessDelegate(TNotification notification) where TNotification : INotification; + public delegate void NotificationFailureDelegate(TNotification notification, AggregateException exception) where TNotification : INotification; - public interface IServiceConnection where TNotification : INotification - { - Task Send (TNotification notification); - } + public interface IServiceConnection where TNotification : INotification + { + Task Send(TNotification notification); + } } diff --git a/PushSharp.Core/IServiceConnectionFactory.cs b/PushSharp.Core/IServiceConnectionFactory.cs index bb30597a..f81d2ecb 100644 --- a/PushSharp.Core/IServiceConnectionFactory.cs +++ b/PushSharp.Core/IServiceConnectionFactory.cs @@ -1,10 +1,8 @@ -using System; - -namespace PushSharp.Core +namespace PushSharp.Core { - public interface IServiceConnectionFactory where TNotification : INotification - { - IServiceConnection Create (); - } + public interface IServiceConnectionFactory where TNotification : INotification + { + IServiceConnection Create(); + } } diff --git a/PushSharp.Core/Log.cs b/PushSharp.Core/Log.cs index fd749d2d..f997552a 100644 --- a/PushSharp.Core/Log.cs +++ b/PushSharp.Core/Log.cs @@ -4,154 +4,158 @@ namespace PushSharp.Core { - [Flags] - public enum LogLevel - { - Info = 0, - Debug = 2, - Error = 8 - } - - public interface ILogger - { - void Write (LogLevel level, string msg, params object[] args); - } - - public static class Log - { - static readonly object loggerLock = new object (); - - static List loggers { get; set; } - static Dictionary counters; - - static Log () - { - counters = new Dictionary (); - loggers = new List (); - - AddLogger (new ConsoleLogger ()); - } - - public static void AddLogger (ILogger logger) - { - lock (loggerLock) - loggers.Add (logger); - } - - public static void ClearLoggers () - { - lock (loggerLock) - loggers.Clear (); - } - - public static IEnumerable Loggers { - get { return loggers; } - } - - public static void Write (LogLevel level, string msg, params object[] args) - { - lock (loggers) { - foreach (var l in loggers) - l.Write (level, msg, args); - } - } - - public static void Info (string msg, params object[] args) - { - Write (LogLevel.Info, msg, args); - } - - public static void Debug (string msg, params object[] args) - { - Write (LogLevel.Debug, msg, args); - } - - public static void Error (string msg, params object[] args) - { - Write (LogLevel.Error, msg, args); - } - - public static CounterToken StartCounter () - { - var t = new CounterToken { - Id = Guid.NewGuid ().ToString () - }; - - var sw = new Stopwatch (); - - counters.Add (t, sw); - - sw.Start (); - - return t; - } - - public static TimeSpan StopCounter (CounterToken counterToken) - { - if (!counters.ContainsKey (counterToken)) - return TimeSpan.Zero; - - var sw = counters [counterToken]; - - sw.Stop (); - - counters.Remove (counterToken); - - return sw.Elapsed; - } - - public static void StopCounterAndLog (CounterToken counterToken, string msg, LogLevel level = LogLevel.Info) - { - var elapsed = StopCounter (counterToken); - - if (!msg.Contains ("{0}")) - msg += " {0}"; - - Log.Write (level, msg, elapsed.TotalMilliseconds); - } - } - - public static class CounterExtensions - { - public static void StopAndLog (this CounterToken counterToken, string msg, LogLevel level = LogLevel.Info) - { - Log.StopCounterAndLog (counterToken, msg, level); - } - - public static TimeSpan Stop (this CounterToken counterToken) - { - return Log.StopCounter (counterToken); - } - } - - public class CounterToken - { - public string Id { get;set; } - } - - public class ConsoleLogger : ILogger - { - public void Write (LogLevel level, string msg, params object[] args) - { - var s = msg; - - if (args != null && args.Length > 0) - s = string.Format (msg, args); - - var d = DateTime.Now.ToString ("yyyy-MM-dd HH:mm:ss.ttt"); - - switch (level) { - case LogLevel.Info: - Console.Out.WriteLine (d + " [INFO] " + s); - break; - case LogLevel.Debug: - Console.Out.WriteLine (d + " [DEBUG] " + s); - break; - case LogLevel.Error: - Console.Error.WriteLine (d + " [ERROR] " + s); - break; - } - } - } + [Flags] + public enum LogLevel + { + Info = 0, + Debug = 2, + Error = 8 + } + + public interface ILogger + { + void Write(LogLevel level, string msg, params object[] args); + } + + public static class Log + { + static readonly object loggerLock = new object(); + + static List loggers { get; set; } + static Dictionary counters; + + static Log() + { + counters = new Dictionary(); + loggers = new List(); + + AddLogger(new ConsoleLogger()); + } + + public static void AddLogger(ILogger logger) + { + lock (loggerLock) + loggers.Add(logger); + } + + public static void ClearLoggers() + { + lock (loggerLock) + loggers.Clear(); + } + + public static IEnumerable Loggers + { + get { return loggers; } + } + + public static void Write(LogLevel level, string msg, params object[] args) + { + lock (loggers) + { + foreach (var l in loggers) + l.Write(level, msg, args); + } + } + + public static void Info(string msg, params object[] args) + { + Write(LogLevel.Info, msg, args); + } + + public static void Debug(string msg, params object[] args) + { + Write(LogLevel.Debug, msg, args); + } + + public static void Error(string msg, params object[] args) + { + Write(LogLevel.Error, msg, args); + } + + public static CounterToken StartCounter() + { + var t = new CounterToken + { + Id = Guid.NewGuid().ToString() + }; + + var sw = new Stopwatch(); + + counters.Add(t, sw); + + sw.Start(); + + return t; + } + + public static TimeSpan StopCounter(CounterToken counterToken) + { + if (!counters.ContainsKey(counterToken)) + return TimeSpan.Zero; + + var sw = counters[counterToken]; + + sw.Stop(); + + counters.Remove(counterToken); + + return sw.Elapsed; + } + + public static void StopCounterAndLog(CounterToken counterToken, string msg, LogLevel level = LogLevel.Info) + { + var elapsed = StopCounter(counterToken); + + if (!msg.Contains("{0}")) + msg += " {0}"; + + Log.Write(level, msg, elapsed.TotalMilliseconds); + } + } + + public static class CounterExtensions + { + public static void StopAndLog(this CounterToken counterToken, string msg, LogLevel level = LogLevel.Info) + { + Log.StopCounterAndLog(counterToken, msg, level); + } + + public static TimeSpan Stop(this CounterToken counterToken) + { + return Log.StopCounter(counterToken); + } + } + + public class CounterToken + { + public string Id { get; set; } + } + + public class ConsoleLogger : ILogger + { + public void Write(LogLevel level, string msg, params object[] args) + { + var s = msg; + + if (args != null && args.Length > 0) + s = string.Format(msg, args); + + var d = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ttt"); + + switch (level) + { + case LogLevel.Info: + Console.Out.WriteLine(d + " [INFO] " + s); + break; + case LogLevel.Debug: + Console.Out.WriteLine(d + " [DEBUG] " + s); + break; + case LogLevel.Error: + Console.Error.WriteLine(d + " [ERROR] " + s); + break; + } + } + } } diff --git a/PushSharp.Core/NotificationBlockingCollection.cs b/PushSharp.Core/NotificationBlockingCollection.cs index 7cd4352b..92e9b4a9 100644 --- a/PushSharp.Core/NotificationBlockingCollection.cs +++ b/PushSharp.Core/NotificationBlockingCollection.cs @@ -4,7 +4,7 @@ namespace PushSharp.Core { public class NotificationBlockingCollection { - public NotificationBlockingCollection () + public NotificationBlockingCollection() { } } diff --git a/PushSharp.Core/Properties/AssemblyInfo.cs b/PushSharp.Core/Properties/AssemblyInfo.cs index 3b9c03a1..880bf1d5 100644 --- a/PushSharp.Core/Properties/AssemblyInfo.cs +++ b/PushSharp.Core/Properties/AssemblyInfo.cs @@ -4,20 +4,20 @@ // Information about this assembly is defined by the following attributes. // Change them to the values specific to your project. -[assembly: AssemblyTitle ("PushSharp.Core")] -[assembly: AssemblyDescription ("")] -[assembly: AssemblyConfiguration ("")] -[assembly: AssemblyCompany ("")] -[assembly: AssemblyProduct ("")] -[assembly: AssemblyCopyright ("redth")] -[assembly: AssemblyTrademark ("")] -[assembly: AssemblyCulture ("")] +[assembly: AssemblyTitle("PushSharp.Core")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("redth")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion ("1.0.*")] +[assembly: AssemblyVersion("1.0.*")] // The following attributes are used to specify the signing key for the assembly, // if desired. See the Mono documentation for more information about signing. diff --git a/PushSharp.Core/PushHttpClient.cs b/PushSharp.Core/PushHttpClient.cs index cbc6fcfd..db0fb06d 100644 --- a/PushSharp.Core/PushHttpClient.cs +++ b/PushSharp.Core/PushHttpClient.cs @@ -6,95 +6,103 @@ namespace PushSharp.Core { - public static class PushHttpClient - { - static PushHttpClient () - { - ServicePointManager.DefaultConnectionLimit = 100; - ServicePointManager.Expect100Continue = false; - } - - public static async Task RequestAsync (PushHttpRequest request) - { - var httpRequest = HttpWebRequest.CreateHttp (request.Url); - httpRequest.Proxy = null; - - httpRequest.Headers = request.Headers; - - if (!string.IsNullOrEmpty (request.Body)) { - var requestStream = await httpRequest.GetRequestStreamAsync (); - - var requestBody = request.Encoding.GetBytes (request.Body); - - await requestStream.WriteAsync (requestBody, 0, requestBody.Length); - } - - HttpWebResponse httpResponse; - Stream responseStream; - - try { - httpResponse = await httpRequest.GetResponseAsync () as HttpWebResponse; - - responseStream = httpResponse.GetResponseStream (); - } catch (WebException webEx) { - httpResponse = webEx.Response as HttpWebResponse; - - responseStream = httpResponse.GetResponseStream (); - } - - var body = string.Empty; - - using (var sr = new StreamReader (responseStream)) - body = await sr.ReadToEndAsync (); - - var responseEncoding = Encoding.ASCII; - try { - responseEncoding = Encoding.GetEncoding (httpResponse.ContentEncoding); - } catch { - } - - var response = new PushHttpResponse { - Body = body, - Headers = httpResponse.Headers, - Uri = httpResponse.ResponseUri, - Encoding = responseEncoding, - LastModified = httpResponse.LastModified, - StatusCode = httpResponse.StatusCode - }; - - httpResponse.Close (); - httpResponse.Dispose (); - - return response; - } - - } - - public class PushHttpRequest - { - public PushHttpRequest () - { - Encoding = Encoding.ASCII; - Headers = new WebHeaderCollection (); - Method = "GET"; - Body = string.Empty; - } - - public string Url { get;set; } - public string Method { get;set; } - public string Body { get;set; } - public WebHeaderCollection Headers { get;set; } - public Encoding Encoding { get;set; } - } - - public class PushHttpResponse - { - public HttpStatusCode StatusCode { get;set; } - public string Body { get;set; } - public WebHeaderCollection Headers { get;set; } - public Uri Uri { get;set; } - public Encoding Encoding { get;set; } - public DateTime LastModified { get;set; } - } + public static class PushHttpClient + { + static PushHttpClient() + { + ServicePointManager.DefaultConnectionLimit = 100; + ServicePointManager.Expect100Continue = false; + } + + public static async Task RequestAsync(PushHttpRequest request) + { + var httpRequest = HttpWebRequest.CreateHttp(request.Url); + httpRequest.Proxy = null; + + httpRequest.Headers = request.Headers; + + if (!string.IsNullOrEmpty(request.Body)) + { + var requestStream = await httpRequest.GetRequestStreamAsync(); + + var requestBody = request.Encoding.GetBytes(request.Body); + + await requestStream.WriteAsync(requestBody, 0, requestBody.Length); + } + + HttpWebResponse httpResponse; + Stream responseStream; + + try + { + httpResponse = await httpRequest.GetResponseAsync() as HttpWebResponse; + + responseStream = httpResponse.GetResponseStream(); + } + catch (WebException webEx) + { + httpResponse = webEx.Response as HttpWebResponse; + + responseStream = httpResponse.GetResponseStream(); + } + + var body = string.Empty; + + using (var sr = new StreamReader(responseStream)) + body = await sr.ReadToEndAsync(); + + var responseEncoding = Encoding.ASCII; + try + { + responseEncoding = Encoding.GetEncoding(httpResponse.ContentEncoding); + } + catch + { + } + + var response = new PushHttpResponse + { + Body = body, + Headers = httpResponse.Headers, + Uri = httpResponse.ResponseUri, + Encoding = responseEncoding, + LastModified = httpResponse.LastModified, + StatusCode = httpResponse.StatusCode + }; + + httpResponse.Close(); + httpResponse.Dispose(); + + return response; + } + + } + + public class PushHttpRequest + { + public PushHttpRequest() + { + Encoding = Encoding.ASCII; + Headers = new WebHeaderCollection(); + Method = "GET"; + Body = string.Empty; + } + + public string Url { get; set; } + public string Method { get; set; } + public string Body { get; set; } + public WebHeaderCollection Headers { get; set; } + public Encoding Encoding { get; set; } + } + + public class PushHttpResponse + { + public HttpStatusCode StatusCode { get; set; } + public string Body { get; set; } + public WebHeaderCollection Headers { get; set; } + public Uri Uri { get; set; } + public Encoding Encoding { get; set; } + public DateTime LastModified { get; set; } + } } diff --git a/PushSharp.Core/PushSharp.Core.csproj b/PushSharp.Core/PushSharp.Core.csproj index e46f9756..c5b4d8d0 100644 --- a/PushSharp.Core/PushSharp.Core.csproj +++ b/PushSharp.Core/PushSharp.Core.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -7,9 +7,10 @@ Library PushSharp.Core PushSharp.Core - v4.5 + v4.6.1 true ..\PushSharp-Signing.snk + true diff --git a/PushSharp.Core/ServiceBroker.cs b/PushSharp.Core/ServiceBroker.cs index 4b87f7b1..e9b8dfcd 100644 --- a/PushSharp.Core/ServiceBroker.cs +++ b/PushSharp.Core/ServiceBroker.cs @@ -8,216 +8,232 @@ namespace PushSharp.Core { - public class ServiceBroker : IServiceBroker where TNotification : INotification - { - static ServiceBroker () - { - ServicePointManager.DefaultConnectionLimit = 100; - ServicePointManager.Expect100Continue = false; - } - - public ServiceBroker (IServiceConnectionFactory connectionFactory) - { - ServiceConnectionFactory = connectionFactory; - - lockWorkers = new object (); - workers = new List> (); - running = false; - - notifications = new BlockingCollection (); - ScaleSize = 1; - //AutoScale = true; - //AutoScaleMaxSize = 20; - } - - public event NotificationSuccessDelegate OnNotificationSucceeded; - public event NotificationFailureDelegate OnNotificationFailed; - - //public bool AutoScale { get; set; } - //public int AutoScaleMaxSize { get; set; } - public int ScaleSize { get; private set; } - - public IServiceConnectionFactory ServiceConnectionFactory { get; set; } - - BlockingCollection notifications; - List> workers; - object lockWorkers; - bool running; - - public virtual void QueueNotification (TNotification notification) - { - notifications.Add (notification); - } - - public IEnumerable TakeMany () - { - return notifications.GetConsumingEnumerable (); - } - - public bool IsCompleted { - get { return notifications.IsCompleted; } - } - - public void Start () - { - if (running) - return; - - running = true; - ChangeScale (ScaleSize); - } - - public void Stop (bool immediately = false) - { - if (!running) - throw new OperationCanceledException ("ServiceBroker has already been signaled to Stop"); - - running = false; - - notifications.CompleteAdding (); - - lock (lockWorkers) { - // Kill all workers right away - if (immediately) - workers.ForEach (sw => sw.Cancel ()); - - var all = (from sw in workers - select sw.WorkerTask).ToArray (); - - Log.Info ("Stopping: Waiting on Tasks"); - - Task.WaitAll (all); - - Log.Info ("Stopping: Done Waiting on Tasks"); - - workers.Clear (); - } - } - - public void ChangeScale (int newScaleSize) - { - if (newScaleSize <= 0) - throw new ArgumentOutOfRangeException ("newScaleSize", "Must be Greater than Zero"); - - ScaleSize = newScaleSize; - - if (!running) - return; - - lock (lockWorkers) { - - // Scale down - while (workers.Count > ScaleSize) { - workers [0].Cancel (); - workers.RemoveAt (0); - } - - // Scale up - while (workers.Count < ScaleSize) { - var worker = new ServiceWorker (this, ServiceConnectionFactory.Create ()); - workers.Add (worker); - worker.Start (); - } - - Log.Debug ("Scaled Changed to: " + workers.Count); - } - } - - public void RaiseNotificationSucceeded (TNotification notification) - { - var evt = OnNotificationSucceeded; - if (evt != null) - evt (notification); - } - - public void RaiseNotificationFailed (TNotification notification, AggregateException exception) - { - var evt = OnNotificationFailed; - if (evt != null) - evt (notification, exception); - } - } - - class ServiceWorker where TNotification : INotification - { - public ServiceWorker (IServiceBroker broker, IServiceConnection connection) - { - Broker = broker; - Connection = connection; - - CancelTokenSource = new CancellationTokenSource (); - } - - public IServiceBroker Broker { get; private set; } - - public IServiceConnection Connection { get; private set; } - - public CancellationTokenSource CancelTokenSource { get; private set; } - - public Task WorkerTask { get; private set; } - - public void Start () - { - WorkerTask = Task.Factory.StartNew (async delegate { - while (!CancelTokenSource.IsCancellationRequested && !Broker.IsCompleted) { - - try { - - var toSend = new List (); - foreach (var n in Broker.TakeMany ()) { - var t = Connection.Send (n); - // Keep the continuation - var cont = t.ContinueWith (ct => { - var cn = n; - var ex = t.Exception; - - if (ex == null) - Broker.RaiseNotificationSucceeded (cn); - else - Broker.RaiseNotificationFailed (cn, ex); - }); - - // Let's wait for the continuation not the task itself - toSend.Add (cont); - } - - if (toSend.Count <= 0) - continue; - - try { - Log.Info ("Waiting on all tasks {0}", toSend.Count ()); - await Task.WhenAll (toSend).ConfigureAwait (false); - Log.Info ("All Tasks Finished"); - } catch (Exception ex) { - Log.Error ("Waiting on all tasks Failed: {0}", ex); - - } - Log.Info ("Passed WhenAll"); - - } catch (Exception ex) { - Log.Error ("Broker.Take: {0}", ex); - } - } - - if (CancelTokenSource.IsCancellationRequested) - Log.Info ("Cancellation was requested"); - if (Broker.IsCompleted) - Log.Info ("Broker IsCompleted"); - - Log.Debug ("Broker Task Ended"); - }, CancelTokenSource.Token, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap (); - - WorkerTask.ContinueWith (t => { - var ex = t.Exception; - if (ex != null) - Log.Error ("ServiceWorker.WorkerTask Error: {0}", ex); - }, TaskContinuationOptions.OnlyOnFaulted); - } - - public void Cancel () - { - CancelTokenSource.Cancel (); - } - } + public class ServiceBroker : IServiceBroker where TNotification : INotification + { + static ServiceBroker() + { + ServicePointManager.DefaultConnectionLimit = 100; + ServicePointManager.Expect100Continue = false; + } + + public ServiceBroker(IServiceConnectionFactory connectionFactory) + { + ServiceConnectionFactory = connectionFactory; + + lockWorkers = new object(); + workers = new List>(); + running = false; + + notifications = new BlockingCollection(); + ScaleSize = 1; + //AutoScale = true; + //AutoScaleMaxSize = 20; + } + + public event NotificationSuccessDelegate OnNotificationSucceeded; + public event NotificationFailureDelegate OnNotificationFailed; + + //public bool AutoScale { get; set; } + //public int AutoScaleMaxSize { get; set; } + public int ScaleSize { get; private set; } + + public IServiceConnectionFactory ServiceConnectionFactory { get; set; } + + BlockingCollection notifications; + List> workers; + object lockWorkers; + bool running; + + public virtual void QueueNotification(TNotification notification) + { + notifications.Add(notification); + } + + public IEnumerable TakeMany() + { + return notifications.GetConsumingEnumerable(); + } + + public bool IsCompleted + { + get { return notifications.IsCompleted; } + } + + public void Start() + { + if (running) + return; + + running = true; + ChangeScale(ScaleSize); + } + + public void Stop(bool immediately = false) + { + if (!running) + throw new OperationCanceledException("ServiceBroker has already been signaled to Stop"); + + running = false; + + notifications.CompleteAdding(); + + lock (lockWorkers) + { + // Kill all workers right away + if (immediately) + workers.ForEach(sw => sw.Cancel()); + + var all = (from sw in workers + select sw.WorkerTask).ToArray(); + + Log.Info("Stopping: Waiting on Tasks"); + + Task.WaitAll(all); + + Log.Info("Stopping: Done Waiting on Tasks"); + + workers.Clear(); + } + } + + public void ChangeScale(int newScaleSize) + { + if (newScaleSize <= 0) + throw new ArgumentOutOfRangeException("newScaleSize", "Must be Greater than Zero"); + + ScaleSize = newScaleSize; + + if (!running) + return; + + lock (lockWorkers) + { + + // Scale down + while (workers.Count > ScaleSize) + { + workers[0].Cancel(); + workers.RemoveAt(0); + } + + // Scale up + while (workers.Count < ScaleSize) + { + var worker = new ServiceWorker(this, ServiceConnectionFactory.Create()); + workers.Add(worker); + worker.Start(); + } + + Log.Debug("Scaled Changed to: " + workers.Count); + } + } + + public void RaiseNotificationSucceeded(TNotification notification) + { + var evt = OnNotificationSucceeded; + if (evt != null) + evt(notification); + } + + public void RaiseNotificationFailed(TNotification notification, AggregateException exception) + { + var evt = OnNotificationFailed; + if (evt != null) + evt(notification, exception); + } + } + + class ServiceWorker where TNotification : INotification + { + public ServiceWorker(IServiceBroker broker, IServiceConnection connection) + { + Broker = broker; + Connection = connection; + + CancelTokenSource = new CancellationTokenSource(); + } + + public IServiceBroker Broker { get; private set; } + + public IServiceConnection Connection { get; private set; } + + public CancellationTokenSource CancelTokenSource { get; private set; } + + public Task WorkerTask { get; private set; } + + public void Start() + { + WorkerTask = Task.Factory.StartNew(async delegate + { + while (!CancelTokenSource.IsCancellationRequested && !Broker.IsCompleted) + { + + try + { + + var toSend = new List(); + foreach (var n in Broker.TakeMany()) + { + var t = Connection.Send(n); + // Keep the continuation + var cont = t.ContinueWith(ct => + { + var cn = n; + var ex = t.Exception; + + if (ex == null) + Broker.RaiseNotificationSucceeded(cn); + else + Broker.RaiseNotificationFailed(cn, ex); + }); + + // Let's wait for the continuation not the task itself + toSend.Add(cont); + } + + if (toSend.Count <= 0) + continue; + + try + { + Log.Info("Waiting on all tasks {0}", toSend.Count()); + await Task.WhenAll(toSend).ConfigureAwait(false); + Log.Info("All Tasks Finished"); + } + catch (Exception ex) + { + Log.Error("Waiting on all tasks Failed: {0}", ex); + + } + Log.Info("Passed WhenAll"); + + } + catch (Exception ex) + { + Log.Error("Broker.Take: {0}", ex); + } + } + + if (CancelTokenSource.IsCancellationRequested) + Log.Info("Cancellation was requested"); + if (Broker.IsCompleted) + Log.Info("Broker IsCompleted"); + + Log.Debug("Broker Task Ended"); + }, CancelTokenSource.Token, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap(); + + WorkerTask.ContinueWith(t => + { + var ex = t.Exception; + if (ex != null) + Log.Error("ServiceWorker.WorkerTask Error: {0}", ex); + }, TaskContinuationOptions.OnlyOnFaulted); + } + + public void Cancel() + { + CancelTokenSource.Cancel(); + } + } } diff --git a/PushSharp.Firefox/Exceptions.cs b/PushSharp.Firefox/Exceptions.cs index 406fb1fd..218ffec6 100644 --- a/PushSharp.Firefox/Exceptions.cs +++ b/PushSharp.Firefox/Exceptions.cs @@ -3,14 +3,14 @@ namespace PushSharp.Firefox { - public class FirefoxNotificationException : NotificationException - { - public FirefoxNotificationException (FirefoxNotification notification, string msg) - : base (msg, notification) - { - Notification = notification; - } + public class FirefoxNotificationException : NotificationException + { + public FirefoxNotificationException(FirefoxNotification notification, string msg) + : base(msg, notification) + { + Notification = notification; + } - public new FirefoxNotification Notification { get; private set; } - } + public new FirefoxNotification Notification { get; private set; } + } } diff --git a/PushSharp.Firefox/FirefoxConfiguration.cs b/PushSharp.Firefox/FirefoxConfiguration.cs index 086a0ce0..67aef929 100644 --- a/PushSharp.Firefox/FirefoxConfiguration.cs +++ b/PushSharp.Firefox/FirefoxConfiguration.cs @@ -2,11 +2,11 @@ namespace PushSharp.Firefox { - public class FirefoxConfiguration - { - public FirefoxConfiguration () - { - } - } + public class FirefoxConfiguration + { + public FirefoxConfiguration() + { + } + } } diff --git a/PushSharp.Firefox/FirefoxConnection.cs b/PushSharp.Firefox/FirefoxConnection.cs index acdd8afe..62a83945 100644 --- a/PushSharp.Firefox/FirefoxConnection.cs +++ b/PushSharp.Firefox/FirefoxConnection.cs @@ -7,44 +7,45 @@ namespace PushSharp.Firefox { - public class FirefoxServiceConnectionFactory : IServiceConnectionFactory - { - public FirefoxServiceConnectionFactory (FirefoxConfiguration configuration) - { - Configuration = configuration; - } - - public FirefoxConfiguration Configuration { get; private set; } - - public IServiceConnection Create() - { - return new FirefoxServiceConnection (); - } - } - - public class FirefoxServiceBroker : ServiceBroker - { - public FirefoxServiceBroker (FirefoxConfiguration configuration) : base (new FirefoxServiceConnectionFactory (configuration)) - { - } - } - - public class FirefoxServiceConnection : IServiceConnection - { - HttpClient http = new HttpClient (); - - public async Task Send (FirefoxNotification notification) - { - var data = notification.ToString (); - - http.DefaultRequestHeaders.UserAgent.Clear (); - http.DefaultRequestHeaders.UserAgent.Add (new ProductInfoHeaderValue ("PushSharp", "3.0")); - - var result = await http.PutAsync (notification.EndPointUrl, new StringContent (data)); - - if (result.StatusCode != HttpStatusCode.OK && result.StatusCode != HttpStatusCode.NoContent) { - throw new FirefoxNotificationException (notification, "HTTP Status: " + result.StatusCode); - } - } - } + public class FirefoxServiceConnectionFactory : IServiceConnectionFactory + { + public FirefoxServiceConnectionFactory(FirefoxConfiguration configuration) + { + Configuration = configuration; + } + + public FirefoxConfiguration Configuration { get; private set; } + + public IServiceConnection Create() + { + return new FirefoxServiceConnection(); + } + } + + public class FirefoxServiceBroker : ServiceBroker + { + public FirefoxServiceBroker(FirefoxConfiguration configuration) : base(new FirefoxServiceConnectionFactory(configuration)) + { + } + } + + public class FirefoxServiceConnection : IServiceConnection + { + HttpClient http = new HttpClient(); + + public async Task Send(FirefoxNotification notification) + { + var data = notification.ToString(); + + http.DefaultRequestHeaders.UserAgent.Clear(); + http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("PushSharp", "3.0")); + + var result = await http.PutAsync(notification.EndPointUrl, new StringContent(data)); + + if (result.StatusCode != HttpStatusCode.OK && result.StatusCode != HttpStatusCode.NoContent) + { + throw new FirefoxNotificationException(notification, "HTTP Status: " + result.StatusCode); + } + } + } } diff --git a/PushSharp.Firefox/FirefoxNotification.cs b/PushSharp.Firefox/FirefoxNotification.cs index cbf1fd0c..49660f2b 100644 --- a/PushSharp.Firefox/FirefoxNotification.cs +++ b/PushSharp.Firefox/FirefoxNotification.cs @@ -3,42 +3,42 @@ namespace PushSharp.Firefox { - public class FirefoxNotification : INotification - { - /// - /// Gets or sets Unique URL to be used by the AppServer to initiate a response from the App. - /// - /// - /// - /// This is generated by SimplePush and sent to the App. - /// The App will need to relay this to the AppServer. - /// - public Uri EndPointUrl { get; set; } + public class FirefoxNotification : INotification + { + /// + /// Gets or sets Unique URL to be used by the AppServer to initiate a response from the App. + /// + /// + /// + /// This is generated by SimplePush and sent to the App. + /// The App will need to relay this to the AppServer. + /// + public Uri EndPointUrl { get; set; } - /// - /// Gets or sets notification version. - /// - /// - /// - /// SimplePush does not carry information, only versions. - /// A version is a number that keeps increasing. - /// The AppServer tells the Endpoint about a new version whenever it wants an App to be notified. - /// - public string Version { get; set; } + /// + /// Gets or sets notification version. + /// + /// + /// + /// SimplePush does not carry information, only versions. + /// A version is a number that keeps increasing. + /// The AppServer tells the Endpoint about a new version whenever it wants an App to be notified. + /// + public string Version { get; set; } - public override string ToString() - { - return string.Format("version={0}", Version); - } + public override string ToString() + { + return string.Format("version={0}", Version); + } - #region INotification implementation - public bool IsDeviceRegistrationIdValid () - { - return true; - } + #region INotification implementation + public bool IsDeviceRegistrationIdValid() + { + return true; + } - public object Tag { get; set; } - #endregion - } + public object Tag { get; set; } + #endregion + } } diff --git a/PushSharp.Firefox/Properties/AssemblyInfo.cs b/PushSharp.Firefox/Properties/AssemblyInfo.cs index ccdecdce..6ce1c41f 100644 --- a/PushSharp.Firefox/Properties/AssemblyInfo.cs +++ b/PushSharp.Firefox/Properties/AssemblyInfo.cs @@ -4,20 +4,20 @@ // Information about this assembly is defined by the following attributes. // Change them to the values specific to your project. -[assembly: AssemblyTitle ("PushSharp.Firefox")] -[assembly: AssemblyDescription ("")] -[assembly: AssemblyConfiguration ("")] -[assembly: AssemblyCompany ("")] -[assembly: AssemblyProduct ("")] -[assembly: AssemblyCopyright ("redth")] -[assembly: AssemblyTrademark ("")] -[assembly: AssemblyCulture ("")] +[assembly: AssemblyTitle("PushSharp.Firefox")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("redth")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion ("1.0.*")] +[assembly: AssemblyVersion("1.0.*")] // The following attributes are used to specify the signing key for the assembly, // if desired. See the Mono documentation for more information about signing. diff --git a/PushSharp.Firefox/PushSharp.Firefox.csproj b/PushSharp.Firefox/PushSharp.Firefox.csproj index 4fb18fb7..2fc1085e 100644 --- a/PushSharp.Firefox/PushSharp.Firefox.csproj +++ b/PushSharp.Firefox/PushSharp.Firefox.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -7,9 +7,10 @@ Library PushSharp.Firefox PushSharp.Firefox - v4.5 + v4.6.1 true ..\PushSharp-Signing.snk + true diff --git a/PushSharp.Google/Exceptions.cs b/PushSharp.Google/Exceptions.cs index b27da6ec..0f859498 100644 --- a/PushSharp.Google/Exceptions.cs +++ b/PushSharp.Google/Exceptions.cs @@ -4,34 +4,34 @@ namespace PushSharp.Google { - public class GcmNotificationException : NotificationException - { - public GcmNotificationException (GcmNotification notification, string msg) : base (msg, notification) - { - Notification = notification; - } + public class GcmNotificationException : NotificationException + { + public GcmNotificationException(GcmNotification notification, string msg) : base(msg, notification) + { + Notification = notification; + } - public GcmNotificationException (GcmNotification notification, string msg, string description) : base (msg, notification) - { - Notification = notification; - Description = description; - } + public GcmNotificationException(GcmNotification notification, string msg, string description) : base(msg, notification) + { + Notification = notification; + Description = description; + } - public new GcmNotification Notification { get; private set; } - public string Description { get; private set; } - } + public new GcmNotification Notification { get; private set; } + public string Description { get; private set; } + } - public class GcmMulticastResultException : Exception - { - public GcmMulticastResultException () : base ("One or more Registration Id's failed in the multicast notification") - { - Succeeded = new List (); - Failed = new Dictionary (); - } + public class GcmMulticastResultException : Exception + { + public GcmMulticastResultException() : base("One or more Registration Id's failed in the multicast notification") + { + Succeeded = new List(); + Failed = new Dictionary(); + } - public List Succeeded { get;set; } + public List Succeeded { get; set; } - public Dictionary Failed { get;set; } - } + public Dictionary Failed { get; set; } + } } diff --git a/PushSharp.Google/GcmConfiguration.cs b/PushSharp.Google/GcmConfiguration.cs index 91f320ff..4c3c4402 100644 --- a/PushSharp.Google/GcmConfiguration.cs +++ b/PushSharp.Google/GcmConfiguration.cs @@ -2,42 +2,41 @@ namespace PushSharp.Google { - public class GcmConfiguration - { - private const string GCM_SEND_URL = "https://gcm-http.googleapis.com/gcm/send"; + public class GcmConfiguration + { + private const string GCM_SEND_URL = "https://gcm-http.googleapis.com/gcm/send"; - public GcmConfiguration (string senderAuthToken) - { - this.SenderAuthToken = senderAuthToken; - this.GcmUrl = GCM_SEND_URL; + public GcmConfiguration(string senderAuthToken) + { + this.SenderAuthToken = senderAuthToken; + this.GcmUrl = GCM_SEND_URL; - this.ValidateServerCertificate = false; - } + this.ValidateServerCertificate = false; + } - public GcmConfiguration (string optionalSenderID, string senderAuthToken, string optionalApplicationIdPackageName) - { - this.SenderID = optionalSenderID; - this.SenderAuthToken = senderAuthToken; - this.ApplicationIdPackageName = optionalApplicationIdPackageName; - this.GcmUrl = GCM_SEND_URL; + public GcmConfiguration(string optionalSenderID, string senderAuthToken, string optionalApplicationIdPackageName) + { + this.SenderID = optionalSenderID; + this.SenderAuthToken = senderAuthToken; + this.ApplicationIdPackageName = optionalApplicationIdPackageName; + this.GcmUrl = GCM_SEND_URL; - this.ValidateServerCertificate = false; - } + this.ValidateServerCertificate = false; + } - public string SenderID { get; private set; } + public string SenderID { get; private set; } - public string SenderAuthToken { get; private set; } + public string SenderAuthToken { get; private set; } - public string ApplicationIdPackageName { get; private set; } + public string ApplicationIdPackageName { get; private set; } - public bool ValidateServerCertificate { get; set; } + public bool ValidateServerCertificate { get; set; } - public string GcmUrl { get; set; } - - public void OverrideUrl (string url) - { - GcmUrl = url; - } - } -} + public string GcmUrl { get; set; } + public void OverrideUrl(string url) + { + GcmUrl = url; + } + } +} \ No newline at end of file diff --git a/PushSharp.Google/GcmMessageResult.cs b/PushSharp.Google/GcmMessageResult.cs index e467505c..57735dad 100644 --- a/PushSharp.Google/GcmMessageResult.cs +++ b/PushSharp.Google/GcmMessageResult.cs @@ -6,58 +6,58 @@ namespace PushSharp.Google { - public class GcmMessageResult - { - [JsonProperty("message_id", NullValueHandling = NullValueHandling.Ignore)] - public string MessageId { get; set; } + public class GcmMessageResult + { + [JsonProperty("message_id", NullValueHandling = NullValueHandling.Ignore)] + public string MessageId { get; set; } - [JsonProperty("registration_id", NullValueHandling = NullValueHandling.Ignore)] - public string CanonicalRegistrationId { get; set; } + [JsonProperty("registration_id", NullValueHandling = NullValueHandling.Ignore)] + public string CanonicalRegistrationId { get; set; } - [JsonIgnore] - public GcmResponseStatus ResponseStatus { get; set; } + [JsonIgnore] + public GcmResponseStatus ResponseStatus { get; set; } - [JsonProperty("error", NullValueHandling = NullValueHandling.Ignore)] - public string Error - { - get - { - switch (ResponseStatus) - { - case GcmResponseStatus.Ok: - return null; - case GcmResponseStatus.Unavailable: - return "Unavailable"; - case GcmResponseStatus.QuotaExceeded: - return "QuotaExceeded"; - case GcmResponseStatus.NotRegistered: - return "NotRegistered"; - case GcmResponseStatus.MissingRegistrationId: - return "MissingRegistration"; - case GcmResponseStatus.MissingCollapseKey: - return "MissingCollapseKey"; - case GcmResponseStatus.MismatchSenderId: - return "MismatchSenderId"; - case GcmResponseStatus.MessageTooBig: - return "MessageTooBig"; - case GcmResponseStatus.InvalidTtl: - return "InvalidTtl"; - case GcmResponseStatus.InvalidRegistration: - return "InvalidRegistration"; - case GcmResponseStatus.InvalidDataKey: - return "InvalidDataKey"; - case GcmResponseStatus.InternalServerError: - return "InternalServerError"; - case GcmResponseStatus.DeviceQuotaExceeded: - return null; - case GcmResponseStatus.CanonicalRegistrationId: - return null; - case GcmResponseStatus.Error: - return "Error"; - default: - return null; - } - } - } - } -} + [JsonProperty("error", NullValueHandling = NullValueHandling.Ignore)] + public string Error + { + get + { + switch (ResponseStatus) + { + case GcmResponseStatus.Ok: + return null; + case GcmResponseStatus.Unavailable: + return "Unavailable"; + case GcmResponseStatus.QuotaExceeded: + return "QuotaExceeded"; + case GcmResponseStatus.NotRegistered: + return "NotRegistered"; + case GcmResponseStatus.MissingRegistrationId: + return "MissingRegistration"; + case GcmResponseStatus.MissingCollapseKey: + return "MissingCollapseKey"; + case GcmResponseStatus.MismatchSenderId: + return "MismatchSenderId"; + case GcmResponseStatus.MessageTooBig: + return "MessageTooBig"; + case GcmResponseStatus.InvalidTtl: + return "InvalidTtl"; + case GcmResponseStatus.InvalidRegistration: + return "InvalidRegistration"; + case GcmResponseStatus.InvalidDataKey: + return "InvalidDataKey"; + case GcmResponseStatus.InternalServerError: + return "InternalServerError"; + case GcmResponseStatus.DeviceQuotaExceeded: + return null; + case GcmResponseStatus.CanonicalRegistrationId: + return null; + case GcmResponseStatus.Error: + return "Error"; + default: + return null; + } + } + } + } +} \ No newline at end of file diff --git a/PushSharp.Google/GcmNotification.cs b/PushSharp.Google/GcmNotification.cs index 28ba0599..5035ce3b 100644 --- a/PushSharp.Google/GcmNotification.cs +++ b/PushSharp.Google/GcmNotification.cs @@ -8,166 +8,166 @@ namespace PushSharp.Google { - public class GcmNotification : INotification - { - public static GcmNotification ForSingleResult (GcmResponse response, int resultIndex) - { - var result = new GcmNotification (); - result.Tag = response.OriginalNotification.Tag; - result.MessageId = response.OriginalNotification.MessageId; - - if (response.OriginalNotification.RegistrationIds != null && response.OriginalNotification.RegistrationIds.Count >= (resultIndex + 1)) - result.RegistrationIds.Add (response.OriginalNotification.RegistrationIds [resultIndex]); - - result.CollapseKey = response.OriginalNotification.CollapseKey; - result.Data = response.OriginalNotification.Data; - result.DelayWhileIdle = response.OriginalNotification.DelayWhileIdle; - result.ContentAvailable = response.OriginalNotification.ContentAvailable; - result.DryRun = response.OriginalNotification.DryRun; - result.Priority = response.OriginalNotification.Priority; - result.To = response.OriginalNotification.To; - result.NotificationKey = response.OriginalNotification.NotificationKey; - - return result; - } - - public static GcmNotification ForSingleRegistrationId (GcmNotification msg, string registrationId) - { - var result = new GcmNotification (); - result.Tag = msg.Tag; - result.MessageId = msg.MessageId; - result.RegistrationIds.Add (registrationId); - result.To = null; - result.CollapseKey = msg.CollapseKey; - result.Data = msg.Data; - result.DelayWhileIdle = msg.DelayWhileIdle; - result.ContentAvailable = msg.ContentAvailable; - result.DryRun = msg.DryRun; - result.Priority = msg.Priority; - result.NotificationKey = msg.NotificationKey; - - return result; - } - - public GcmNotification () - { - RegistrationIds = new List (); - CollapseKey = string.Empty; - Data = null; - DelayWhileIdle = null; - } - - public bool IsDeviceRegistrationIdValid () - { - return RegistrationIds != null && RegistrationIds.Any (); - } - - [JsonIgnore] - public object Tag { get;set; } - - [JsonProperty ("message_id")] - public string MessageId { get; internal set; } - - /// - /// Registration ID of the Device(s). Maximum of 1000 registration Id's per notification. - /// - [JsonProperty ("registration_ids")] - public List RegistrationIds { get; set; } - - /// - /// Registration ID or Group/Topic to send notification to. Overrides RegsitrationIds. - /// - /// To. - [JsonProperty ("to")] - public string To { get;set; } - - /// - /// Only the latest message with the same collapse key will be delivered - /// - [JsonProperty ("collapse_key")] - public string CollapseKey { get; set; } - - /// - /// JSON Payload to be sent in the message - /// - [JsonProperty ("data")] - public JObject Data { get; set; } - - /// - /// Notification JSON payload - /// - /// The notification payload. - [JsonProperty ("notification")] - public JObject Notification { get; set; } - - /// - /// If true, GCM will only be delivered once the device's screen is on - /// - [JsonProperty ("delay_while_idle")] - public bool? DelayWhileIdle { get; set; } - - /// - /// Time in seconds that a message should be kept on the server if the device is offline. Default (and maximum) is 4 weeks. - /// - [JsonProperty ("time_to_live")] - public int? TimeToLive { get; set; } - - /// - /// If true, dry_run attribute will be sent in payload causing the notification not to be actually sent, but the result returned simulating the message - /// - [JsonProperty ("dry_run")] - public bool? DryRun { get; set; } - - /// - /// A string that maps a single user to multiple registration IDs associated with that user. This allows a 3rd-party server to send a single message to multiple app instances (typically on multiple devices) owned by a single user. - /// - [Obsolete ("Deprecated on GCM Server API. Use Device Group Messaging to send to multiple devices.")] - public string NotificationKey { get; set; } - - /// - /// A string containing the package name of your application. When set, messages will only be sent to registration IDs that match the package name - /// - [JsonProperty ("restricted_package_name")] - public string RestrictedPackageName { get; set; } - - /// - /// On iOS, use this field to represent content-available in the APNS payload. When a notification or message is sent and this is set to true, an inactive client app is awoken. On Android, data messages wake the app by default. On Chrome, currently not supported. - /// - /// The content available. - [JsonProperty ("content_available")] - public bool? ContentAvailable { get; set; } - - /// - /// Corresponds to iOS APNS priorities (Normal is 5 and high is 10). Default is Normal. - /// - /// The priority. - [JsonProperty ("priority"), JsonConverter (typeof (Newtonsoft.Json.Converters.StringEnumConverter))] - public GcmNotificationPriority? Priority { get; set; } - - internal string GetJson () - { - // If 'To' was used instead of RegistrationIds, let's make RegistrationId's null - // so we don't serialize an empty array for this property - // otherwise, google will complain that we specified both instead - if (RegistrationIds != null && RegistrationIds.Count <= 0 && !string.IsNullOrEmpty (To)) - RegistrationIds = null; - - // Ignore null values - return JsonConvert.SerializeObject (this, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); - } - - public override string ToString () - { - return GetJson (); - } - } - - public enum GcmNotificationPriority - { - [EnumMember (Value="normal")] - Normal = 5, - [EnumMember (Value="high")] - High = 10 - } -} + public class GcmNotification : INotification + { + public static GcmNotification ForSingleResult(GcmResponse response, int resultIndex) + { + var result = new GcmNotification(); + result.Tag = response.OriginalNotification.Tag; + result.MessageId = response.OriginalNotification.MessageId; + + if (response.OriginalNotification.RegistrationIds != null && response.OriginalNotification.RegistrationIds.Count >= (resultIndex + 1)) + result.RegistrationIds.Add(response.OriginalNotification.RegistrationIds[resultIndex]); + + result.CollapseKey = response.OriginalNotification.CollapseKey; + result.Data = response.OriginalNotification.Data; + result.DelayWhileIdle = response.OriginalNotification.DelayWhileIdle; + result.ContentAvailable = response.OriginalNotification.ContentAvailable; + result.DryRun = response.OriginalNotification.DryRun; + result.Priority = response.OriginalNotification.Priority; + result.To = response.OriginalNotification.To; + result.NotificationKey = response.OriginalNotification.NotificationKey; + + return result; + } + + public static GcmNotification ForSingleRegistrationId(GcmNotification msg, string registrationId) + { + var result = new GcmNotification(); + result.Tag = msg.Tag; + result.MessageId = msg.MessageId; + result.RegistrationIds.Add(registrationId); + result.To = null; + result.CollapseKey = msg.CollapseKey; + result.Data = msg.Data; + result.DelayWhileIdle = msg.DelayWhileIdle; + result.ContentAvailable = msg.ContentAvailable; + result.DryRun = msg.DryRun; + result.Priority = msg.Priority; + result.NotificationKey = msg.NotificationKey; + + return result; + } + + public GcmNotification() + { + RegistrationIds = new List(); + CollapseKey = string.Empty; + Data = null; + DelayWhileIdle = null; + } + + public bool IsDeviceRegistrationIdValid() + { + return RegistrationIds != null && RegistrationIds.Any(); + } + + [JsonIgnore] + public object Tag { get; set; } + + [JsonProperty("message_id")] + public string MessageId { get; internal set; } + + /// + /// Registration ID of the Device(s). Maximum of 1000 registration Id's per notification. + /// + [JsonProperty("registration_ids")] + public List RegistrationIds { get; set; } + + /// + /// Registration ID or Group/Topic to send notification to. Overrides RegsitrationIds. + /// + /// To. + [JsonProperty("to")] + public string To { get; set; } + + /// + /// Only the latest message with the same collapse key will be delivered + /// + [JsonProperty("collapse_key")] + public string CollapseKey { get; set; } + + /// + /// JSON Payload to be sent in the message + /// + [JsonProperty("data")] + public JObject Data { get; set; } + + /// + /// Notification JSON payload + /// + /// The notification payload. + [JsonProperty("notification")] + public JObject Notification { get; set; } + + /// + /// If true, GCM will only be delivered once the device's screen is on + /// + [JsonProperty("delay_while_idle")] + public bool? DelayWhileIdle { get; set; } + + /// + /// Time in seconds that a message should be kept on the server if the device is offline. Default (and maximum) is 4 weeks. + /// + [JsonProperty("time_to_live")] + public int? TimeToLive { get; set; } + + /// + /// If true, dry_run attribute will be sent in payload causing the notification not to be actually sent, but the result returned simulating the message + /// + [JsonProperty("dry_run")] + public bool? DryRun { get; set; } + + /// + /// A string that maps a single user to multiple registration IDs associated with that user. This allows a 3rd-party server to send a single message to multiple app instances (typically on multiple devices) owned by a single user. + /// + [Obsolete("Deprecated on GCM Server API. Use Device Group Messaging to send to multiple devices.")] + public string NotificationKey { get; set; } + + /// + /// A string containing the package name of your application. When set, messages will only be sent to registration IDs that match the package name + /// + [JsonProperty("restricted_package_name")] + public string RestrictedPackageName { get; set; } + + /// + /// On iOS, use this field to represent content-available in the APNS payload. When a notification or message is sent and this is set to true, an inactive client app is awoken. On Android, data messages wake the app by default. On Chrome, currently not supported. + /// + /// The content available. + [JsonProperty("content_available")] + public bool? ContentAvailable { get; set; } + + /// + /// Corresponds to iOS APNS priorities (Normal is 5 and high is 10). Default is Normal. + /// + /// The priority. + [JsonProperty("priority"), JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public GcmNotificationPriority? Priority { get; set; } + + internal string GetJson() + { + // If 'To' was used instead of RegistrationIds, let's make RegistrationId's null + // so we don't serialize an empty array for this property + // otherwise, google will complain that we specified both instead + if (RegistrationIds != null && RegistrationIds.Count <= 0 && !string.IsNullOrEmpty(To)) + RegistrationIds = null; + + // Ignore null values + return JsonConvert.SerializeObject(this, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + } + + public override string ToString() + { + return GetJson(); + } + } + + public enum GcmNotificationPriority + { + [EnumMember(Value = "normal")] + Normal = 5, + [EnumMember(Value = "high")] + High = 10 + } +} \ No newline at end of file diff --git a/PushSharp.Google/GcmResponse.cs b/PushSharp.Google/GcmResponse.cs index f580d014..2e710f64 100644 --- a/PushSharp.Google/GcmResponse.cs +++ b/PushSharp.Google/GcmResponse.cs @@ -5,100 +5,99 @@ namespace PushSharp.Google { - public class GcmResponse - { - public GcmResponse() - { - MulticastId = -1; - NumberOfSuccesses = 0; - NumberOfFailures = 0; - NumberOfCanonicalIds = 0; - OriginalNotification = null; - Results = new List(); - ResponseCode = GcmResponseCode.Ok; - } + public class GcmResponse + { + public GcmResponse() + { + MulticastId = -1; + NumberOfSuccesses = 0; + NumberOfFailures = 0; + NumberOfCanonicalIds = 0; + OriginalNotification = null; + Results = new List(); + ResponseCode = GcmResponseCode.Ok; + } - [JsonProperty("multicast_id")] - public long MulticastId { get; set; } + [JsonProperty("multicast_id")] + public long MulticastId { get; set; } - [JsonProperty("success")] - public long NumberOfSuccesses { get; set; } + [JsonProperty("success")] + public long NumberOfSuccesses { get; set; } - [JsonProperty("failure")] - public long NumberOfFailures { get; set; } + [JsonProperty("failure")] + public long NumberOfFailures { get; set; } - [JsonProperty("canonical_ids")] - public long NumberOfCanonicalIds { get; set; } + [JsonProperty("canonical_ids")] + public long NumberOfCanonicalIds { get; set; } - [JsonIgnore] - public GcmNotification OriginalNotification { get; set; } + [JsonIgnore] + public GcmNotification OriginalNotification { get; set; } - [JsonProperty("results")] - public List Results { get; set; } + [JsonProperty("results")] + public List Results { get; set; } - [JsonIgnore] - public GcmResponseCode ResponseCode { get; set; } - } + [JsonIgnore] + public GcmResponseCode ResponseCode { get; set; } + } - public enum GcmResponseCode - { - Ok, - Error, - BadRequest, - ServiceUnavailable, - InvalidAuthToken, - InternalServiceError - } + public enum GcmResponseCode + { + Ok, + Error, + BadRequest, + ServiceUnavailable, + InvalidAuthToken, + InternalServiceError + } - public enum GcmResponseStatus - { - [EnumMember (Value="Ok")] - Ok, + public enum GcmResponseStatus + { + [EnumMember(Value = "Ok")] + Ok, - [EnumMember (Value="Error")] - Error, + [EnumMember(Value = "Error")] + Error, - [EnumMember (Value="QuotaExceeded")] - QuotaExceeded, + [EnumMember(Value = "QuotaExceeded")] + QuotaExceeded, - [EnumMember (Value="DeviceQuotaExceeded")] - DeviceQuotaExceeded, + [EnumMember(Value = "DeviceQuotaExceeded")] + DeviceQuotaExceeded, - [EnumMember (Value="InvalidRegistration")] - InvalidRegistration, + [EnumMember(Value = "InvalidRegistration")] + InvalidRegistration, - [EnumMember (Value="NotRegistered")] - NotRegistered, + [EnumMember(Value = "NotRegistered")] + NotRegistered, - [EnumMember (Value="MessageTooBig")] - MessageTooBig, + [EnumMember(Value = "MessageTooBig")] + MessageTooBig, - [EnumMember (Value="MissingCollapseKey")] - MissingCollapseKey, + [EnumMember(Value = "MissingCollapseKey")] + MissingCollapseKey, - [EnumMember (Value="MissingRegistration")] - MissingRegistrationId, + [EnumMember(Value = "MissingRegistration")] + MissingRegistrationId, - [EnumMember (Value="Unavailable")] - Unavailable, + [EnumMember(Value = "Unavailable")] + Unavailable, - [EnumMember (Value="MismatchSenderId")] - MismatchSenderId, + [EnumMember(Value = "MismatchSenderId")] + MismatchSenderId, - [EnumMember (Value="CanonicalRegistrationId")] - CanonicalRegistrationId, + [EnumMember(Value = "CanonicalRegistrationId")] + CanonicalRegistrationId, - [EnumMember (Value="InvalidDataKey")] - InvalidDataKey, + [EnumMember(Value = "InvalidDataKey")] + InvalidDataKey, - [EnumMember (Value="InvalidTtl")] - InvalidTtl, + [EnumMember(Value = "InvalidTtl")] + InvalidTtl, - [EnumMember (Value="InternalServerError")] - InternalServerError, - - [EnumMember (Value="InvalidPackageName")] - InvalidPackageName - } -} + [EnumMember(Value = "InternalServerError")] + InternalServerError, + [EnumMember(Value = "InvalidPackageName")] + InvalidPackageName + } +} \ No newline at end of file diff --git a/PushSharp.Google/GcmServiceConnection.cs b/PushSharp.Google/GcmServiceConnection.cs index b3b539c5..d3533895 100644 --- a/PushSharp.Google/GcmServiceConnection.cs +++ b/PushSharp.Google/GcmServiceConnection.cs @@ -1,216 +1,240 @@ using System; -using System.Threading.Tasks; +using System.Linq; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Runtime.Serialization; +using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using System.Net; using PushSharp.Core; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; namespace PushSharp.Google { - public class GcmServiceConnectionFactory : IServiceConnectionFactory - { - public GcmServiceConnectionFactory (GcmConfiguration configuration) - { - Configuration = configuration; - } - - public GcmConfiguration Configuration { get; private set; } - - public IServiceConnection Create() - { - return new GcmServiceConnection (Configuration); - } - } - - public class GcmServiceBroker : ServiceBroker - { - public GcmServiceBroker (GcmConfiguration configuration) : base (new GcmServiceConnectionFactory (configuration)) - { - } - } - - public class GcmServiceConnection : IServiceConnection - { - public GcmServiceConnection (GcmConfiguration configuration) - { - Configuration = configuration; - http = new HttpClient (); - - http.DefaultRequestHeaders.UserAgent.Clear (); - http.DefaultRequestHeaders.UserAgent.Add (new ProductInfoHeaderValue ("PushSharp", "3.0")); - http.DefaultRequestHeaders.TryAddWithoutValidation ("Authorization", "key=" + Configuration.SenderAuthToken); - } - - public GcmConfiguration Configuration { get; private set; } - - readonly HttpClient http; - - public async Task Send (GcmNotification notification) - { - var json = notification.GetJson (); - - var content = new StringContent (json, System.Text.Encoding.UTF8, "application/json"); - - var response = await http.PostAsync (Configuration.GcmUrl, content); - - if (response.IsSuccessStatusCode) { - await processResponseOk (response, notification).ConfigureAwait (false); - } else { - await processResponseError (response, notification).ConfigureAwait (false); - } - } - - async Task processResponseOk (HttpResponseMessage httpResponse, GcmNotification notification) - { - var multicastResult = new GcmMulticastResultException (); - - var result = new GcmResponse () { - ResponseCode = GcmResponseCode.Ok, - OriginalNotification = notification - }; - - var str = await httpResponse.Content.ReadAsStringAsync (); - var json = JObject.Parse (str); - - result.NumberOfCanonicalIds = json.Value ("canonical_ids"); - result.NumberOfFailures = json.Value ("failure"); - result.NumberOfSuccesses = json.Value ("success"); - - var jsonResults = json ["results"] as JArray ?? new JArray (); - - foreach (var r in jsonResults) { - var msgResult = new GcmMessageResult (); - - msgResult.MessageId = r.Value ("message_id"); - msgResult.CanonicalRegistrationId = r.Value ("registration_id"); - msgResult.ResponseStatus = GcmResponseStatus.Ok; - - if (!string.IsNullOrEmpty (msgResult.CanonicalRegistrationId)) - msgResult.ResponseStatus = GcmResponseStatus.CanonicalRegistrationId; - else if (r ["error"] != null) { - var err = r.Value ("error") ?? ""; - - msgResult.ResponseStatus = GetGcmResponseStatus (err); - } - - result.Results.Add (msgResult); - } - - int index = 0; - - //Loop through every result in the response - // We will raise events for each individual result so that the consumer of the library - // can deal with individual registrationid's for the notification - foreach (var r in result.Results) { - var singleResultNotification = GcmNotification.ForSingleResult (result, index); - - singleResultNotification.MessageId = r.MessageId; - - if (r.ResponseStatus == GcmResponseStatus.Ok) { // Success - multicastResult.Succeeded.Add (singleResultNotification); - } else if (r.ResponseStatus == GcmResponseStatus.CanonicalRegistrationId) { //Need to swap reg id's - //Swap Registrations Id's - var newRegistrationId = r.CanonicalRegistrationId; - var oldRegistrationId = string.Empty; - - if (singleResultNotification.RegistrationIds != null && singleResultNotification.RegistrationIds.Count > 0) - { - oldRegistrationId = singleResultNotification.RegistrationIds[0]; - } - else if (!string.IsNullOrEmpty(singleResultNotification.To)) - { - oldRegistrationId = singleResultNotification.To; - } - - multicastResult.Failed.Add (singleResultNotification, - new DeviceSubscriptionExpiredException (singleResultNotification) { - OldSubscriptionId = oldRegistrationId, - NewSubscriptionId = newRegistrationId - }); - } else if (r.ResponseStatus == GcmResponseStatus.Unavailable) { // Unavailable - multicastResult.Failed.Add (singleResultNotification, new GcmNotificationException (singleResultNotification, "Unavailable Response Status")); - } else if (r.ResponseStatus == GcmResponseStatus.NotRegistered) { //Bad registration Id - var oldRegistrationId = string.Empty; - - if (singleResultNotification.RegistrationIds != null && singleResultNotification.RegistrationIds.Count > 0) - { - oldRegistrationId = singleResultNotification.RegistrationIds[0]; - } - else if (!string.IsNullOrEmpty(singleResultNotification.To)) - { - oldRegistrationId = singleResultNotification.To; - } - - multicastResult.Failed.Add (singleResultNotification, - new DeviceSubscriptionExpiredException (singleResultNotification) { - OldSubscriptionId = oldRegistrationId }); - } else { - multicastResult.Failed.Add (singleResultNotification, new GcmNotificationException (singleResultNotification, "Unknown Failure: " + r.ResponseStatus)); - } - - index++; - } - - // If we only have 1 total result, it is not *multicast*, - if (multicastResult.Succeeded.Count + multicastResult.Failed.Count == 1) { - // If not multicast, and succeeded, don't throw any errors! - if (multicastResult.Succeeded.Count == 1) - return; - // Otherwise, throw the one single failure we must have - throw multicastResult.Failed.First ().Value; - } - - // If we get here, we must have had a multicast message - // throw if we had any failures at all (otherwise all must be successful, so throw no error - if (multicastResult.Failed.Count > 0) - throw multicastResult; - } - - async Task processResponseError (HttpResponseMessage httpResponse, GcmNotification notification) - { - string responseBody = null; - - try { - responseBody = await httpResponse.Content.ReadAsStringAsync ().ConfigureAwait (false); - } catch { } - - //401 bad auth token - if (httpResponse.StatusCode == HttpStatusCode.Unauthorized) - throw new UnauthorizedAccessException ("GCM Authorization Failed"); - - if (httpResponse.StatusCode == HttpStatusCode.BadRequest) - throw new GcmNotificationException (notification, "HTTP 400 Bad Request", responseBody); - - if ((int)httpResponse.StatusCode >= 500 && (int)httpResponse.StatusCode < 600) { - //First try grabbing the retry-after header and parsing it. - var retryAfterHeader = httpResponse.Headers.RetryAfter; - - if (retryAfterHeader != null && retryAfterHeader.Delta.HasValue) { - var retryAfter = retryAfterHeader.Delta.Value; - throw new RetryAfterException (notification, "GCM Requested Backoff", DateTime.UtcNow + retryAfter); - } - } - - throw new GcmNotificationException (notification, "GCM HTTP Error: " + httpResponse.StatusCode, responseBody); - } - - static GcmResponseStatus GetGcmResponseStatus (string str) - { - var enumType = typeof(GcmResponseStatus); - - foreach (var name in Enum.GetNames (enumType)) { - var enumMemberAttribute = ((EnumMemberAttribute[])enumType.GetField (name).GetCustomAttributes (typeof(EnumMemberAttribute), true)).Single (); - - if (enumMemberAttribute.Value.Equals (str, StringComparison.InvariantCultureIgnoreCase)) - return (GcmResponseStatus)Enum.Parse (enumType, name); - } - - //Default - return GcmResponseStatus.Error; - } - } + public class GcmServiceConnectionFactory : IServiceConnectionFactory + { + public GcmServiceConnectionFactory(GcmConfiguration configuration) + { + Configuration = configuration; + } + + public GcmConfiguration Configuration { get; private set; } + + public IServiceConnection Create() + { + return new GcmServiceConnection(Configuration); + } + } + + public class GcmServiceBroker : ServiceBroker + { + public GcmServiceBroker(GcmConfiguration configuration) : base(new GcmServiceConnectionFactory(configuration)) + { + } + } + + public class GcmServiceConnection : IServiceConnection + { + public GcmServiceConnection(GcmConfiguration configuration) + { + Configuration = configuration; + http = new HttpClient(); + + http.DefaultRequestHeaders.UserAgent.Clear(); + http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("PushSharp", "3.0")); + http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "key=" + Configuration.SenderAuthToken); + } + + public GcmConfiguration Configuration { get; private set; } + + readonly HttpClient http; + + public async Task Send(GcmNotification notification) + { + var json = notification.GetJson(); + + var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); + + var response = await http.PostAsync(Configuration.GcmUrl, content); + + if (response.IsSuccessStatusCode) + { + await processResponseOk(response, notification).ConfigureAwait(false); + } + else + { + await processResponseError(response, notification).ConfigureAwait(false); + } + } + + async Task processResponseOk(HttpResponseMessage httpResponse, GcmNotification notification) + { + var multicastResult = new GcmMulticastResultException(); + + var result = new GcmResponse() + { + ResponseCode = GcmResponseCode.Ok, + OriginalNotification = notification + }; + + var str = await httpResponse.Content.ReadAsStringAsync(); + var json = JObject.Parse(str); + + result.NumberOfCanonicalIds = json.Value("canonical_ids"); + result.NumberOfFailures = json.Value("failure"); + result.NumberOfSuccesses = json.Value("success"); + + var jsonResults = json["results"] as JArray ?? new JArray(); + + foreach (var r in jsonResults) + { + var msgResult = new GcmMessageResult(); + + msgResult.MessageId = r.Value("message_id"); + msgResult.CanonicalRegistrationId = r.Value("registration_id"); + msgResult.ResponseStatus = GcmResponseStatus.Ok; + + if (!string.IsNullOrEmpty(msgResult.CanonicalRegistrationId)) + msgResult.ResponseStatus = GcmResponseStatus.CanonicalRegistrationId; + else if (r["error"] != null) + { + var err = r.Value("error") ?? ""; + + msgResult.ResponseStatus = GetGcmResponseStatus(err); + } + + result.Results.Add(msgResult); + } + + int index = 0; + + //Loop through every result in the response + // We will raise events for each individual result so that the consumer of the library + // can deal with individual registrationid's for the notification + foreach (var r in result.Results) + { + var singleResultNotification = GcmNotification.ForSingleResult(result, index); + + singleResultNotification.MessageId = r.MessageId; + + if (r.ResponseStatus == GcmResponseStatus.Ok) + { // Success + multicastResult.Succeeded.Add(singleResultNotification); + } + else if (r.ResponseStatus == GcmResponseStatus.CanonicalRegistrationId) + { //Need to swap reg id's + //Swap Registrations Id's + var newRegistrationId = r.CanonicalRegistrationId; + var oldRegistrationId = string.Empty; + + if (singleResultNotification.RegistrationIds != null && singleResultNotification.RegistrationIds.Count > 0) + { + oldRegistrationId = singleResultNotification.RegistrationIds[0]; + } + else if (!string.IsNullOrEmpty(singleResultNotification.To)) + { + oldRegistrationId = singleResultNotification.To; + } + + multicastResult.Failed.Add(singleResultNotification, + new DeviceSubscriptionExpiredException(singleResultNotification) + { + OldSubscriptionId = oldRegistrationId, + NewSubscriptionId = newRegistrationId + }); + } + else if (r.ResponseStatus == GcmResponseStatus.Unavailable) + { // Unavailable + multicastResult.Failed.Add(singleResultNotification, new GcmNotificationException(singleResultNotification, "Unavailable Response Status")); + } + else if (r.ResponseStatus == GcmResponseStatus.NotRegistered) + { //Bad registration Id + var oldRegistrationId = string.Empty; + + if (singleResultNotification.RegistrationIds != null && singleResultNotification.RegistrationIds.Count > 0) + { + oldRegistrationId = singleResultNotification.RegistrationIds[0]; + } + else if (!string.IsNullOrEmpty(singleResultNotification.To)) + { + oldRegistrationId = singleResultNotification.To; + } + + multicastResult.Failed.Add(singleResultNotification, + new DeviceSubscriptionExpiredException(singleResultNotification) + { + OldSubscriptionId = oldRegistrationId + }); + } + else + { + multicastResult.Failed.Add(singleResultNotification, new GcmNotificationException(singleResultNotification, "Unknown Failure: " + r.ResponseStatus)); + } + + index++; + } + + // If we only have 1 total result, it is not *multicast*, + if (multicastResult.Succeeded.Count + multicastResult.Failed.Count == 1) + { + // If not multicast, and succeeded, don't throw any errors! + if (multicastResult.Succeeded.Count == 1) + return; + // Otherwise, throw the one single failure we must have + throw multicastResult.Failed.First().Value; + } + + // If we get here, we must have had a multicast message + // throw if we had any failures at all (otherwise all must be successful, so throw no error + if (multicastResult.Failed.Count > 0) + throw multicastResult; + } + + async Task processResponseError(HttpResponseMessage httpResponse, GcmNotification notification) + { + string responseBody = null; + + try + { + responseBody = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false); + } + catch { } + + //401 bad auth token + if (httpResponse.StatusCode == HttpStatusCode.Unauthorized) + throw new UnauthorizedAccessException("GCM Authorization Failed"); + + if (httpResponse.StatusCode == HttpStatusCode.BadRequest) + throw new GcmNotificationException(notification, "HTTP 400 Bad Request", responseBody); + + if ((int)httpResponse.StatusCode >= 500 && (int)httpResponse.StatusCode < 600) + { + //First try grabbing the retry-after header and parsing it. + var retryAfterHeader = httpResponse.Headers.RetryAfter; + + if (retryAfterHeader != null && retryAfterHeader.Delta.HasValue) + { + var retryAfter = retryAfterHeader.Delta.Value; + throw new RetryAfterException(notification, "GCM Requested Backoff", DateTime.UtcNow + retryAfter); + } + } + + throw new GcmNotificationException(notification, "GCM HTTP Error: " + httpResponse.StatusCode, responseBody); + } + + static GcmResponseStatus GetGcmResponseStatus(string str) + { + var enumType = typeof(GcmResponseStatus); + + foreach (var name in Enum.GetNames(enumType)) + { + var enumMemberAttribute = ((EnumMemberAttribute[])enumType.GetField(name).GetCustomAttributes(typeof(EnumMemberAttribute), true)).Single(); + + if (enumMemberAttribute.Value.Equals(str, StringComparison.InvariantCultureIgnoreCase)) + return (GcmResponseStatus)Enum.Parse(enumType, name); + } + + //Default + return GcmResponseStatus.Error; + } + } } diff --git a/PushSharp.Google/Properties/AssemblyInfo.cs b/PushSharp.Google/Properties/AssemblyInfo.cs index 2afce1ae..394e9cd5 100644 --- a/PushSharp.Google/Properties/AssemblyInfo.cs +++ b/PushSharp.Google/Properties/AssemblyInfo.cs @@ -4,20 +4,20 @@ // Information about this assembly is defined by the following attributes. // Change them to the values specific to your project. -[assembly: AssemblyTitle ("PushSharp.Google")] -[assembly: AssemblyDescription ("")] -[assembly: AssemblyConfiguration ("")] -[assembly: AssemblyCompany ("")] -[assembly: AssemblyProduct ("")] -[assembly: AssemblyCopyright ("redth")] -[assembly: AssemblyTrademark ("")] -[assembly: AssemblyCulture ("")] +[assembly: AssemblyTitle("PushSharp.Google")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("redth")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion ("1.0.*")] +[assembly: AssemblyVersion("1.0.*")] // The following attributes are used to specify the signing key for the assembly, // if desired. See the Mono documentation for more information about signing. diff --git a/PushSharp.Google/PushSharp.Google.csproj b/PushSharp.Google/PushSharp.Google.csproj index 92d72f31..5a423ed6 100644 --- a/PushSharp.Google/PushSharp.Google.csproj +++ b/PushSharp.Google/PushSharp.Google.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -7,9 +7,10 @@ Library PushSharp.Google PushSharp.Google - v4.5 + v4.6.1 true ..\PushSharp-Signing.snk + true @@ -30,14 +31,15 @@ false + + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + True + - - ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll - diff --git a/PushSharp.Google/packages.config b/PushSharp.Google/packages.config index 505e5883..e1fae9c6 100644 --- a/PushSharp.Google/packages.config +++ b/PushSharp.Google/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/PushSharp.Tests/AdmRealTests.cs b/PushSharp.Tests/AdmRealTests.cs index 4c17e4c8..90492be3 100644 --- a/PushSharp.Tests/AdmRealTests.cs +++ b/PushSharp.Tests/AdmRealTests.cs @@ -1,46 +1,50 @@ -using System; -using NUnit.Framework; +using System.Collections.Generic; +using System.ComponentModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; using PushSharp.Amazon; -using System.Collections.Generic; namespace PushSharp.Tests { - [TestFixture] - public class AdmRealTests - { - [Test] - [Category ("Disabled")] - public void ADM_Send_Single () - { - var succeeded = 0; - var failed = 0; - var attempted = 0; + [TestClass] + public class AdmRealTests + { + [TestMethod] + [Category("Disabled")] + public void ADM_Send_Single() + { + var succeeded = 0; + var failed = 0; + var attempted = 0; - var config = new AdmConfiguration (Settings.Instance.AdmClientId, Settings.Instance.AdmClientSecret); - var broker = new AdmServiceBroker (config); - broker.OnNotificationFailed += (notification, exception) => { - failed++; - }; - broker.OnNotificationSucceeded += (notification) => { - succeeded++; - }; - broker.Start (); + var config = new AdmConfiguration(Settings.Instance.AdmClientId, Settings.Instance.AdmClientSecret); + var broker = new AdmServiceBroker(config); + broker.OnNotificationFailed += (notification, exception) => + { + failed++; + }; + broker.OnNotificationSucceeded += (notification) => + { + succeeded++; + }; + broker.Start(); - foreach (var regId in Settings.Instance.AdmRegistrationIds) { - attempted++; - broker.QueueNotification (new AdmNotification { - RegistrationId = regId, - Data = new Dictionary { - { "somekey", "somevalue" } - } - }); - } + foreach (var regId in Settings.Instance.AdmRegistrationIds) + { + attempted++; + broker.QueueNotification(new AdmNotification + { + RegistrationId = regId, + Data = new Dictionary { + { "somekey", "somevalue" } + } + }); + } - broker.Stop (); + broker.Stop(); - Assert.AreEqual (attempted, succeeded); - Assert.AreEqual (0, failed); - } - } + Assert.AreEqual(attempted, succeeded); + Assert.AreEqual(0, failed); + } + } } diff --git a/PushSharp.Tests/ApnsRealTest.cs b/PushSharp.Tests/ApnsRealTest.cs index 40e24294..dce78b44 100644 --- a/PushSharp.Tests/ApnsRealTest.cs +++ b/PushSharp.Tests/ApnsRealTest.cs @@ -1,60 +1,66 @@ using System; -using NUnit.Framework; -using PushSharp.Apple; +using System.ComponentModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json.Linq; +using PushSharp.Apple; namespace PushSharp.Tests { - [TestFixture] - [Category ("Disabled")] - public class ApnsRealTest - { - [Test] - public void APNS_Send_Single () - { - var succeeded = 0; - var failed = 0; - var attempted = 0; - - var config = new ApnsConfiguration (ApnsConfiguration.ApnsServerEnvironment.Sandbox, Settings.Instance.ApnsCertificateFile, Settings.Instance.ApnsCertificatePassword); - var broker = new ApnsServiceBroker (config); - broker.OnNotificationFailed += (notification, exception) => { - failed++; - }; - broker.OnNotificationSucceeded += (notification) => { - succeeded++; - }; - broker.Start (); - - foreach (var dt in Settings.Instance.ApnsDeviceTokens) { - attempted++; - broker.QueueNotification (new ApnsNotification { - DeviceToken = dt, - Payload = JObject.Parse ("{ \"aps\" : { \"alert\" : \"Hello PushSharp!\" } }") - }); - } - - broker.Stop (); - - 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 (); - } - } + [TestClass] + [Category("Disabled")] + public class ApnsRealTest + { + [TestMethod] + public void APNS_Send_Single() + { + var succeeded = 0; + var failed = 0; + var attempted = 0; + + var config = new ApnsConfiguration(ApnsConfiguration.ApnsServerEnvironment.Sandbox, Settings.Instance.ApnsCertificateFile, Settings.Instance.ApnsCertificatePassword); + var broker = new ApnsServiceBroker(config); + broker.OnNotificationFailed += (notification, exception) => + { + failed++; + }; + broker.OnNotificationSucceeded += (notification) => + { + succeeded++; + }; + broker.Start(); + + foreach (var dt in Settings.Instance.ApnsDeviceTokens) + { + attempted++; + broker.QueueNotification(new ApnsNotification + { + DeviceToken = dt, + Payload = JObject.Parse("{ \"aps\" : { \"alert\" : \"Hello PushSharp!\" } }") + }); + } + + broker.Stop(); + + Assert.AreEqual(attempted, succeeded); + Assert.AreEqual(0, failed); + } + + [TestMethod] + 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 index b7e5ab3f..fbb7cdb1 100644 --- a/PushSharp.Tests/ApnsTests.cs +++ b/PushSharp.Tests/ApnsTests.cs @@ -1,169 +1,177 @@ using System; -using System.Threading.Tasks; using System.Collections.Generic; -using PushSharp.Apple; +using System.ComponentModel; using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json.Linq; -using NUnit.Framework; +using PushSharp.Apple; 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); + [TestClass] + [Category("Fake")] + public class ApnsTests + { + [TestMethod] + public async Task APNS_Single_Succeeds() + { + await Apns(0, 1, new List()); + } + + [TestMethod] + public async Task APNS_Multiple_Succeed() + { + await Apns(0, 3, new List()); + } + + //[TestMethod] + public async Task APNS_Send_Many() + { + await Apns(10, 1010, new List { + new ApnsResponseFilter ((id, deviceToken, payload) => { + return id % 100 == 0; + }) + }); + } + + [TestMethod] + public async Task APNS_Send_Small() + { + await Apns(2, 256, new List() { + new ApnsResponseFilter ((id, deviceToken, payload) => { + return id % 100 == 0; + }) + }); + } + + //[TestMethod] + public async Task APNS_Scale_Brokers() + { + await Apns(10, 10100, new List { + new ApnsResponseFilter ((id, deviceToken, payload) => { + return id % 1000 == 0; + }) + }, scale: 2); + } + + [TestMethod] + 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"); - var expectedSuccess = notifications.Count - expectFailed; + await server.Stop().ConfigureAwait(false); + + var expectedSuccess = notifications.Count - expectFailed; - var actualFailed = failed; - var actualSuccess = success; + 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); + 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(expectFailed, actualFailed); + Assert.AreEqual(expectedSuccess, actualSuccess); - Assert.AreEqual (server.Failed, actualFailed); - Assert.AreEqual (server.Successful, actualSuccess); - } - } + Assert.AreEqual(server.Failed, actualFailed); + Assert.AreEqual(server.Successful, actualSuccess); + } + } } diff --git a/PushSharp.Tests/BrokerTests.cs b/PushSharp.Tests/BrokerTests.cs index 92308707..9c35f9b7 100644 --- a/PushSharp.Tests/BrokerTests.cs +++ b/PushSharp.Tests/BrokerTests.cs @@ -1,92 +1,91 @@ -using NUnit.Framework; - -using System; -using PushSharp.Core; -using System.Threading.Tasks; +using System.ComponentModel; using System.Linq; -using System.Collections.Generic; -using PushSharp.Apple; -using Newtonsoft.Json.Linq; -using System.Threading; - +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PushSharp.Core; namespace PushSharp.Tests { - [TestFixture] - [Category ("Core")] + [TestClass] + [Category("Core")] public class BrokerTests { - [Test] - public void Broker_Send_Many () + [TestMethod] + public void Broker_Send_Many() { - var succeeded = 0; - var failed = 0; - var attempted = 0; - - var broker = new TestServiceBroker (); - broker.OnNotificationFailed += (notification, exception) => { - failed++; - }; - broker.OnNotificationSucceeded += (notification) => { - succeeded++; - }; - broker.Start (); - broker.ChangeScale (1); - - var c = Log.StartCounter (); - - for (int i = 1; i <= 1000; i++) { - attempted++; - broker.QueueNotification (new TestNotification { TestId = i }); - } - - broker.Stop (); - - c.StopAndLog ("Test Took {0} ms"); - - Assert.AreEqual (attempted, succeeded); - Assert.AreEqual (0, failed); + var succeeded = 0; + var failed = 0; + var attempted = 0; + + var broker = new TestServiceBroker(); + broker.OnNotificationFailed += (notification, exception) => + { + failed++; + }; + broker.OnNotificationSucceeded += (notification) => + { + succeeded++; + }; + broker.Start(); + broker.ChangeScale(1); + + var c = Log.StartCounter(); + + for (int i = 1; i <= 1000; i++) + { + attempted++; + broker.QueueNotification(new TestNotification { TestId = i }); + } + + broker.Stop(); + + c.StopAndLog("Test Took {0} ms"); + + Assert.AreEqual(attempted, succeeded); + Assert.AreEqual(0, failed); } - [Test] - #pragma warning disable 1998 - public async Task Broker_Some_Fail () - #pragma warning restore 1998 - { - var succeeded = 0; - var failed = 0; - var attempted = 0; + [TestMethod] +#pragma warning disable 1998 + public async Task Broker_Some_Fail() +#pragma warning restore 1998 + { + var succeeded = 0; + var failed = 0; + var attempted = 0; - const int count = 10; - var failIds = new [] { 3, 5, 7 }; + const int count = 10; + var failIds = new[] { 3, 5, 7 }; - var broker = new TestServiceBroker (); - broker.OnNotificationFailed += (notification, exception) => - failed++; - broker.OnNotificationSucceeded += (notification) => - succeeded++; + var broker = new TestServiceBroker(); + broker.OnNotificationFailed += (notification, exception) => + failed++; + broker.OnNotificationSucceeded += (notification) => + succeeded++; - broker.Start (); - broker.ChangeScale (1); + broker.Start(); + broker.ChangeScale(1); - var c = Log.StartCounter (); + var c = Log.StartCounter(); - for (int i = 1; i <= count; i++) { - attempted++; - broker.QueueNotification (new TestNotification { - TestId = i, - ShouldFail = failIds.Contains (i) - }); - } + for (int i = 1; i <= count; i++) + { + attempted++; + broker.QueueNotification(new TestNotification + { + TestId = i, + ShouldFail = failIds.Contains(i) + }); + } - broker.Stop (); + broker.Stop(); - c.StopAndLog ("Test Took {0} ms"); + c.StopAndLog("Test Took {0} ms"); - Assert.AreEqual (attempted - failIds.Length, succeeded); - Assert.AreEqual (failIds.Length, failed); - } - } + Assert.AreEqual(attempted - failIds.Length, succeeded); + Assert.AreEqual(failIds.Length, failed); + } + } } diff --git a/PushSharp.Tests/GcmRealTests.cs b/PushSharp.Tests/GcmRealTests.cs index 4e915644..97ccda04 100644 --- a/PushSharp.Tests/GcmRealTests.cs +++ b/PushSharp.Tests/GcmRealTests.cs @@ -1,49 +1,50 @@ -using System; -using NUnit.Framework; -using PushSharp.Google; -using System.Collections.Generic; +using System.Collections.Generic; +using System.ComponentModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json.Linq; +using PushSharp.Google; namespace PushSharp.Tests { - [TestFixture] - [Category ("Real")] - public class GcmRealTests - { - [Test] - public void Gcm_Send_Single () - { - var succeeded = 0; - var failed = 0; - var attempted = 0; - - var config = new GcmConfiguration (Settings.Instance.GcmSenderId, Settings.Instance.GcmAuthToken, null); - var broker = new GcmServiceBroker (config); - broker.OnNotificationFailed += (notification, exception) => { - failed++; - }; - broker.OnNotificationSucceeded += (notification) => { - succeeded++; - }; - - broker.Start (); - - foreach (var regId in Settings.Instance.GcmRegistrationIds) { - attempted++; - - broker.QueueNotification (new GcmNotification { - RegistrationIds = new List { - regId - }, - Data = JObject.Parse ("{ \"somekey\" : \"somevalue\" }") - }); - } - - broker.Stop (); - - Assert.AreEqual (attempted, succeeded); - Assert.AreEqual (0, failed); - } - } -} - + [TestClass] + [Category("Real")] + public class GcmRealTests + { + [TestMethod] + public void Gcm_Send_Single() + { + var succeeded = 0; + var failed = 0; + var attempted = 0; + + var config = new GcmConfiguration(Settings.Instance.GcmSenderId, Settings.Instance.GcmAuthToken, null); + var broker = new GcmServiceBroker(config); + broker.OnNotificationFailed += (notification, exception) => + { + failed++; + }; + broker.OnNotificationSucceeded += (notification) => + { + succeeded++; + }; + + broker.Start(); + + foreach (var regId in Settings.Instance.GcmRegistrationIds) + { + attempted++; + + broker.QueueNotification(new GcmNotification + { + RegistrationIds = new List { regId }, + Data = JObject.Parse("{ \"somekey1\" : \"somevalue 1\" }") + }); + } + + broker.Stop(); + + Assert.AreEqual(attempted, succeeded); + Assert.AreEqual(0, failed); + } + } +} \ No newline at end of file diff --git a/PushSharp.Tests/GcmTests.cs b/PushSharp.Tests/GcmTests.cs index 2590a207..a6e6fe9f 100644 --- a/PushSharp.Tests/GcmTests.cs +++ b/PushSharp.Tests/GcmTests.cs @@ -1,36 +1,34 @@ -using System; -using NUnit.Framework; +using System.ComponentModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; using PushSharp.Google; -using System.Collections.Generic; -using Newtonsoft.Json.Linq; namespace PushSharp.Tests { - [Category ("GCM")] - [TestFixture] - public class GcmTests - { - [Test] - public void GcmNotification_Priority_Should_Serialize_As_String_High () - { - var n = new GcmNotification (); - n.Priority = GcmNotificationPriority.High; + [Category("GCM")] + [TestClass] + public class GcmTests + { + [TestMethod] + public void GcmNotification_Priority_Should_Serialize_As_String_High() + { + var n = new GcmNotification(); + n.Priority = GcmNotificationPriority.High; - var str = n.ToString (); + var str = n.ToString(); - Assert.IsTrue (str.Contains ("high")); - } + Assert.IsTrue(str.Contains("high")); + } - [Test] - public void GcmNotification_Priority_Should_Serialize_As_String_Normal () - { - var n = new GcmNotification (); - n.Priority = GcmNotificationPriority.Normal; + [TestMethod] + public void GcmNotification_Priority_Should_Serialize_As_String_Normal() + { + var n = new GcmNotification(); + n.Priority = GcmNotificationPriority.Normal; - var str = n.ToString (); + var str = n.ToString(); - Assert.IsTrue (str.Contains ("normal")); - } - } + Assert.IsTrue(str.Contains("normal")); + } + } } diff --git a/PushSharp.Tests/PushSharp.Tests.csproj b/PushSharp.Tests/PushSharp.Tests.csproj index 6cdef5f4..6e64fd96 100644 --- a/PushSharp.Tests/PushSharp.Tests.csproj +++ b/PushSharp.Tests/PushSharp.Tests.csproj @@ -1,5 +1,5 @@  - + Debug AnyCPU @@ -7,7 +7,9 @@ Library PushSharp.Tests PushSharp.Tests - v4.5 + v4.6.1 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + true @@ -28,13 +30,12 @@ false - - - ..\packages\NUnit.2.6.4\lib\nunit.framework.dll - - - ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + + + False + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + diff --git a/PushSharp.Tests/Servers/TestApnsServer.cs b/PushSharp.Tests/Servers/TestApnsServer.cs index 1be38243..a044ad4d 100644 --- a/PushSharp.Tests/Servers/TestApnsServer.cs +++ b/PushSharp.Tests/Servers/TestApnsServer.cs @@ -7,337 +7,370 @@ namespace PushSharp.Tests { - public class TestApnsServer - { - const string TAG = "TestApnsServer"; + public class TestApnsServer + { + const string TAG = "TestApnsServer"; - public TestApnsServer () - { - ResponseFilters = new List (); - } + public TestApnsServer() + { + ResponseFilters = new List(); + } - Socket listener; - bool running = false; - long totalBytesRx = 0; + Socket listener; + bool running = false; + long totalBytesRx = 0; - public int Successful { get; set; } - public int Failed { get; set; } + public int Successful { get; set; } + public int Failed { get; set; } - ManualResetEvent waitStop = new ManualResetEvent (false); + ManualResetEvent waitStop = new ManualResetEvent(false); - public List ResponseFilters { get; set; } + public List ResponseFilters { get; set; } - #pragma warning disable 1998 - public async Task Stop () - #pragma warning restore 1998 - { - running = false; +#pragma warning disable 1998 + public async Task Stop() +#pragma warning restore 1998 + { + running = false; - try { listener.Shutdown (SocketShutdown.Both); } - catch { } + try { listener.Shutdown(SocketShutdown.Both); } + catch { } - try { listener.Close (); } - catch { } + try { listener.Close(); } + catch { } - try { listener.Dispose (); } - catch { } + try { listener.Dispose(); } + catch { } - waitStop.WaitOne (); - } + waitStop.WaitOne(); + } - public async Task Start () - { - Console.WriteLine (TAG + " -> Starting Mock APNS Server..."); - running = true; + public async Task Start() + { + Console.WriteLine(TAG + " -> Starting Mock APNS Server..."); + running = true; - listener = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - listener.Bind (new IPEndPoint (IPAddress.Any, 2195)); + listener.Bind(new IPEndPoint(IPAddress.Any, 2195)); - listener.Listen (100); + listener.Listen(100); - Console.WriteLine (TAG + " -> Started Mock APNS Server."); + Console.WriteLine(TAG + " -> Started Mock APNS Server."); - while (running) { + while (running) + { - Socket socket = null; + Socket socket = null; - try { - // Get a client connection - socket = await Task.Factory.FromAsync ( - listener.BeginAccept (null, null), - listener.EndAccept).ConfigureAwait (false); - } catch { - } + try + { + // Get a client connection + socket = await Task.Factory.FromAsync( + listener.BeginAccept(null, null), + listener.EndAccept).ConfigureAwait(false); + } + catch + { + } - if (socket == null) - break; + if (socket == null) + break; - Console.WriteLine (TAG + " -> Client Connected."); + 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 + // 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 sentErrorResponse = false; + var s = socket; + byte[] buffer = new byte[1024000]; // 1 MB - var data = new List (); + 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; + // Do processing, continually receiving from the socket + while (true) + { + var received = s.Receive(buffer); - totalBytesRx += received; + if (received <= 0 && data.Count <= 0) + break; - Console.WriteLine (TAG + " -> Received {0} bytes...", received); + totalBytesRx += received; - // Add the received data to our data list - for (int i = 0; i < received; i++) - data.Add (buffer [i]); - - ApnsServerNotification notification = null; + Console.WriteLine(TAG + " -> Received {0} bytes...", received); - try { - - while ((notification = Parse (data)) != null) { + // Add the received data to our data list + for (int i = 0; i < received; i++) + data.Add(buffer[i]); - if (!sentErrorResponse) - Successful++; + ApnsServerNotification notification = null; - // Console.WriteLine (TAG + " -> Rx'd ID: {0}, DeviceToken: {1}, Payload: {2}", notification.Identifier, notification.DeviceToken, notification.Payload); + try + { - 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; - } - } - } - + while ((notification = Parse(data)) != null) + { - } catch (ApnsNotificationException ex) { + if (!sentErrorResponse) + Successful++; - Console.WriteLine (TAG + " -> Notification Exception: {0}", ex); + // Console.WriteLine (TAG + " -> Rx'd ID: {0}, DeviceToken: {1}, Payload: {2}", notification.Identifier, notification.DeviceToken, notification.Payload); - if (!sentErrorResponse) - SendErrorResponse (s, ex.ErrorStatusCode, ex.NotificationId); - sentErrorResponse = true; + 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; + } + } + } - break; - } - - } - try { - s.Shutdown (SocketShutdown.Both); - } catch { - } - try { - s.Close (); - } catch { - } - try { - s.Dispose (); - } catch { - } + } + catch (ApnsNotificationException ex) + { - Console.WriteLine (TAG + " -> Client Disconnected..."); - }); - } + Console.WriteLine(TAG + " -> Notification Exception: {0}", ex); - waitStop.Set (); + if (!sentErrorResponse) + SendErrorResponse(s, ex.ErrorStatusCode, ex.NotificationId); + sentErrorResponse = true; - Console.WriteLine (TAG + " -> Stopped APNS Server."); - } + break; + } + } - void SendErrorResponse (Socket s, ApnsNotificationErrorStatusCode statusCode, int identifier) - { - Failed++; - Successful--; + try + { + s.Shutdown(SocketShutdown.Both); + } + catch + { + } + try + { + s.Close(); + } + catch + { + } + try + { + s.Dispose(); + } + catch + { + } - var errorResponseData = new byte[6]; - errorResponseData[0] = 0x01; - errorResponseData[1] = BitConverter.GetBytes ( (short)statusCode)[0]; + Console.WriteLine(TAG + " -> Client Disconnected..."); + }); + } - var id = BitConverter.GetBytes (IPAddress.HostToNetworkOrder (identifier)); - Buffer.BlockCopy (id, 0, errorResponseData, 2, 4); + waitStop.Set(); - var sent = Task.Factory.FromAsync ( - s.BeginSend (errorResponseData, 0, errorResponseData.Length, SocketFlags.None, null, null), - s.EndSend).Result; - } + Console.WriteLine(TAG + " -> Stopped APNS Server."); + } - 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; } - } + 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; } + } } diff --git a/PushSharp.Tests/Settings.cs b/PushSharp.Tests/Settings.cs index 108b7b2f..b045d6e0 100644 --- a/PushSharp.Tests/Settings.cs +++ b/PushSharp.Tests/Settings.cs @@ -5,73 +5,77 @@ namespace PushSharp.Tests { - public class Settings - { - static Settings instance = null; + public class Settings + { + static Settings instance = null; - public static Settings Instance { - get { - if (instance == null) { + public static Settings Instance + { + get + { + if (instance == null) + { - var envData = Environment.GetEnvironmentVariable ("TEST_CONFIG_JSON"); + var envData = Environment.GetEnvironmentVariable("TEST_CONFIG_JSON"); - if (!string.IsNullOrEmpty (envData)) { - instance = JsonConvert.DeserializeObject (envData); - return instance; - } + if (!string.IsNullOrEmpty(envData)) + { + instance = JsonConvert.DeserializeObject(envData); + return instance; + } - var baseDir = AppDomain.CurrentDomain.BaseDirectory; + var baseDir = AppDomain.CurrentDomain.BaseDirectory; - var settingsFile = Path.Combine (baseDir, "settings.json"); + var settingsFile = Path.Combine(baseDir, "settings.json"); - if (!File.Exists (settingsFile)) - settingsFile = Path.Combine (baseDir, "../", "settings.json"); - if (!File.Exists (settingsFile)) - settingsFile = Path.Combine (baseDir, "../../", "settings.json"); - if (!File.Exists (settingsFile)) - settingsFile = Path.Combine (baseDir, "../../../", "settings.json"); + if (!File.Exists(settingsFile)) + settingsFile = Path.Combine(baseDir, "../", "settings.json"); + if (!File.Exists(settingsFile)) + settingsFile = Path.Combine(baseDir, "../../", "settings.json"); + if (!File.Exists(settingsFile)) + settingsFile = Path.Combine(baseDir, "../../../", "settings.json"); - if (!File.Exists (settingsFile)) - throw new FileNotFoundException ("You must provide a settings.json file to run these tests. See the settings.json.sample file for more information."); - - instance = JsonConvert.DeserializeObject (File.ReadAllText (settingsFile)); - } - return instance; - } - } - public Settings () - { - } + if (!File.Exists(settingsFile)) + throw new FileNotFoundException("You must provide a settings.json file to run these tests. See the settings.json.sample file for more information."); - [JsonProperty ("apns_cert_file")] - public string ApnsCertificateFile { get; set; } - [JsonProperty ("apns_cert_pwd")] - public string ApnsCertificatePassword { get;set; } - [JsonProperty ("apns_device_tokens")] - public List ApnsDeviceTokens { get;set; } + instance = JsonConvert.DeserializeObject(File.ReadAllText(settingsFile)); + } + return instance; + } + } + public Settings() + { + } - [JsonProperty ("gcm_auth_token")] - public string GcmAuthToken { get;set; } - [JsonProperty ("gcm_sender_id")] - public string GcmSenderId { get;set; } - [JsonProperty ("gcm_registration_ids")] - public List GcmRegistrationIds { get;set; } + [JsonProperty("apns_cert_file")] + public string ApnsCertificateFile { get; set; } + [JsonProperty("apns_cert_pwd")] + public string ApnsCertificatePassword { get; set; } + [JsonProperty("apns_device_tokens")] + public List ApnsDeviceTokens { get; set; } - [JsonProperty ("adm_client_id")] - public string AdmClientId { get;set; } - [JsonProperty ("adm_client_secret")] - public string AdmClientSecret { get;set; } - [JsonProperty ("adm_registration_ids")] - public List AdmRegistrationIds { get;set; } + [JsonProperty("gcm_auth_token")] + public string GcmAuthToken { get; set; } + [JsonProperty("gcm_sender_id")] + public string GcmSenderId { get; set; } + [JsonProperty("gcm_registration_ids")] + public List GcmRegistrationIds { get; set; } - [JsonProperty ("wns_package_name")] - public string WnsPackageName { get;set; } - [JsonProperty ("wns_package_sid")] - public string WnsPackageSid { get;set; } - [JsonProperty ("wns_client_secret")] - public string WnsClientSecret { get;set; } - [JsonProperty ("wns_channel_uris")] - public List WnsChannelUris { get;set; } - } + [JsonProperty("adm_client_id")] + public string AdmClientId { get; set; } + [JsonProperty("adm_client_secret")] + public string AdmClientSecret { get; set; } + [JsonProperty("adm_registration_ids")] + public List AdmRegistrationIds { get; set; } + + [JsonProperty("wns_package_name")] + public string WnsPackageName { get; set; } + [JsonProperty("wns_package_sid")] + public string WnsPackageSid { get; set; } + [JsonProperty("wns_client_secret")] + public string WnsClientSecret { get; set; } + [JsonProperty("wns_channel_uris")] + public List WnsChannelUris { get; set; } + } } diff --git a/PushSharp.Tests/TestServiceConnection.cs b/PushSharp.Tests/TestServiceConnection.cs index 628dac9c..df9f4094 100644 --- a/PushSharp.Tests/TestServiceConnection.cs +++ b/PushSharp.Tests/TestServiceConnection.cs @@ -1,67 +1,67 @@ using System; -using System.Linq; -using PushSharp.Core; using System.Threading.Tasks; +using PushSharp.Core; namespace PushSharp.Tests { - public class TestNotification : INotification + public class TestNotification : INotification { public static int TESTID = 1; public TestNotification() { - TestId = TestNotification.TESTID++; + TestId = TestNotification.TESTID++; } - public object Tag { get;set; } - public int TestId { get;set; } + public object Tag { get; set; } + public int TestId { get; set; } - public bool ShouldFail { get;set; } + public bool ShouldFail { get; set; } - public override string ToString () + public override string ToString() { return TestId.ToString(); } - public bool IsDeviceRegistrationIdValid () - { - return true; - } + public bool IsDeviceRegistrationIdValid() + { + return true; + } } public class TestServiceConnectionFactory : IServiceConnectionFactory { - public IServiceConnection Create () + public IServiceConnection Create() { - return new TestServiceConnection (); + return new TestServiceConnection(); } } - public class TestServiceBroker : ServiceBroker - { - public TestServiceBroker (TestServiceConnectionFactory connectionFactory) : base (connectionFactory) - { - } + public class TestServiceBroker : ServiceBroker + { + public TestServiceBroker(TestServiceConnectionFactory connectionFactory) : base(connectionFactory) + { + } - public TestServiceBroker () : base (new TestServiceConnectionFactory ()) - { - } - } + public TestServiceBroker() : base(new TestServiceConnectionFactory()) + { + } + } - public class TestServiceConnection : IServiceConnection - { - public async Task Send (TestNotification notification) - { - var id = notification.TestId; + public class TestServiceConnection : IServiceConnection + { + public async Task Send(TestNotification notification) + { + var id = notification.TestId; - await Task.Delay (250).ConfigureAwait (false); + await Task.Delay(250).ConfigureAwait(false); - if (notification.ShouldFail) { - Console.WriteLine ("Fail {0}...", id); - throw new Exception ("Notification Should Fail: " + id); - } + if (notification.ShouldFail) + { + Console.WriteLine("Fail {0}...", id); + throw new Exception("Notification Should Fail: " + id); + } } } -} +} diff --git a/PushSharp.Tests/WnsRealTests.cs b/PushSharp.Tests/WnsRealTests.cs index 005af51e..e222644d 100644 --- a/PushSharp.Tests/WnsRealTests.cs +++ b/PushSharp.Tests/WnsRealTests.cs @@ -1,40 +1,44 @@ -using System; -using NUnit.Framework; -using PushSharp.Windows; +using System.ComponentModel; using System.Xml.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PushSharp.Windows; namespace PushSharp.Tests { - [TestFixture] - [Category ("Disabled")] - public class WnsRealTests - { - [Test] - public void WNS_Send_Single () - { - var succeeded = 0; - var failed = 0; - var attempted = 0; + [TestClass] + [Category("Disabled")] + public class WnsRealTests + { + [TestMethod] + public void WNS_Send_Single() + { + var succeeded = 0; + var failed = 0; + var attempted = 0; - var config = new WnsConfiguration (Settings.Instance.WnsPackageName, - Settings.Instance.WnsPackageSid, - Settings.Instance.WnsClientSecret); + var config = new WnsConfiguration(Settings.Instance.WnsPackageName, + Settings.Instance.WnsPackageSid, + Settings.Instance.WnsClientSecret); - var broker = new WnsServiceBroker (config); - broker.OnNotificationFailed += (notification, exception) => { - failed++; - }; - broker.OnNotificationSucceeded += (notification) => { - succeeded ++; - }; + var broker = new WnsServiceBroker(config); + broker.OnNotificationFailed += (notification, exception) => + { + failed++; + }; + broker.OnNotificationSucceeded += (notification) => + { + succeeded++; + }; - broker.Start (); + broker.Start(); - foreach (var uri in Settings.Instance.WnsChannelUris) { - attempted++; - broker.QueueNotification (new WnsToastNotification { - ChannelUri = uri, - Payload = XElement.Parse (@" + foreach (var uri in Settings.Instance.WnsChannelUris) + { + attempted++; + broker.QueueNotification(new WnsToastNotification + { + ChannelUri = uri, + Payload = XElement.Parse(@" @@ -43,42 +47,47 @@ public void WNS_Send_Single () ") - }); - } + }); + } - broker.Stop (); + broker.Stop(); - Assert.AreEqual (attempted, succeeded); - Assert.AreEqual (0, failed); - } + Assert.AreEqual(attempted, succeeded); + Assert.AreEqual(0, failed); + } - [Test] - public void WNS_Send_Mutiple () - { - var succeeded = 0; - var failed = 0; - var attempted = 0; + [TestMethod] + public void WNS_Send_Mutiple() + { + var succeeded = 0; + var failed = 0; + var attempted = 0; - var config = new WnsConfiguration (Settings.Instance.WnsPackageName, - Settings.Instance.WnsPackageSid, - Settings.Instance.WnsClientSecret); + var config = new WnsConfiguration(Settings.Instance.WnsPackageName, + Settings.Instance.WnsPackageSid, + Settings.Instance.WnsClientSecret); - var broker = new WnsServiceBroker (config); - broker.OnNotificationFailed += (notification, exception) => { - failed++; - }; - broker.OnNotificationSucceeded += (notification) => { - succeeded++; - }; + var broker = new WnsServiceBroker(config); + broker.OnNotificationFailed += (notification, exception) => + { + failed++; + }; + broker.OnNotificationSucceeded += (notification) => + { + succeeded++; + }; - broker.Start (); + broker.Start(); - foreach (var uri in Settings.Instance.WnsChannelUris) { - for (var i = 1; i <= 3; i++) { - attempted++; - broker.QueueNotification (new WnsToastNotification { - ChannelUri = uri, - Payload = XElement.Parse(@" + foreach (var uri in Settings.Instance.WnsChannelUris) + { + for (var i = 1; i <= 3; i++) + { + attempted++; + broker.QueueNotification(new WnsToastNotification + { + ChannelUri = uri, + Payload = XElement.Parse(@" @@ -87,15 +96,15 @@ public void WNS_Send_Mutiple () ") - }); - } - } + }); + } + } - broker.Stop (); + broker.Stop(); - Assert.AreEqual (attempted, succeeded); - Assert.AreEqual (0, failed); - } - } + Assert.AreEqual(attempted, succeeded); + Assert.AreEqual(0, failed); + } + } } diff --git a/PushSharp.Tests/packages.config b/PushSharp.Tests/packages.config index 3b829b9f..e1fae9c6 100644 --- a/PushSharp.Tests/packages.config +++ b/PushSharp.Tests/packages.config @@ -1,5 +1,4 @@  - - + \ No newline at end of file diff --git a/PushSharp.Tests/settings.sample.json b/PushSharp.Tests/settings.sample.json deleted file mode 100644 index b4994bbe..00000000 --- a/PushSharp.Tests/settings.sample.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "apns_cert_file" : "", - "apns_cert_pwd" : "", - "apns_device_tokens" : - [ - "", - ], - - "gcm_auth_token" : "", - "gcm_sender_id" : "", - "gcm_registration_ids" : - [ - "", - ], - - "adm_client_id" : "", - "adm_client_secret" : "", - "adm_registration_ids" : - [ - "", - ], -} diff --git a/PushSharp.Windows/Exceptions.cs b/PushSharp.Windows/Exceptions.cs index d3d39ded..3c11b2e3 100644 --- a/PushSharp.Windows/Exceptions.cs +++ b/PushSharp.Windows/Exceptions.cs @@ -3,21 +3,21 @@ namespace PushSharp.Windows { - public class WnsNotificationException : NotificationException - { - public WnsNotificationException (WnsNotificationStatus status) : base (status.ErrorDescription, status.Notification) - { - Notification = status.Notification; - Status = status; - } + public class WnsNotificationException : NotificationException + { + public WnsNotificationException(WnsNotificationStatus status) : base(status.ErrorDescription, status.Notification) + { + Notification = status.Notification; + Status = status; + } - public new WnsNotification Notification { get; set; } - public WnsNotificationStatus Status { get; private set; } + public new WnsNotification Notification { get; set; } + public WnsNotificationStatus Status { get; private set; } - public override string ToString () - { - return base.ToString() + " Status = " + Status.HttpStatus; - } - } + public override string ToString() + { + return base.ToString() + " Status = " + Status.HttpStatus; + } + } } diff --git a/PushSharp.Windows/Properties/AssemblyInfo.cs b/PushSharp.Windows/Properties/AssemblyInfo.cs index 61efcba9..74a59fac 100644 --- a/PushSharp.Windows/Properties/AssemblyInfo.cs +++ b/PushSharp.Windows/Properties/AssemblyInfo.cs @@ -4,20 +4,20 @@ // Information about this assembly is defined by the following attributes. // Change them to the values specific to your project. -[assembly: AssemblyTitle ("PushSharp.Windows")] -[assembly: AssemblyDescription ("")] -[assembly: AssemblyConfiguration ("")] -[assembly: AssemblyCompany ("")] -[assembly: AssemblyProduct ("")] -[assembly: AssemblyCopyright ("redth")] -[assembly: AssemblyTrademark ("")] -[assembly: AssemblyCulture ("")] +[assembly: AssemblyTitle("PushSharp.Windows")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("redth")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion ("1.0.*")] +[assembly: AssemblyVersion("1.0.*")] // The following attributes are used to specify the signing key for the assembly, // if desired. See the Mono documentation for more information about signing. diff --git a/PushSharp.Windows/PushSharp.Windows.csproj b/PushSharp.Windows/PushSharp.Windows.csproj index e85dc90b..0ebb42d6 100644 --- a/PushSharp.Windows/PushSharp.Windows.csproj +++ b/PushSharp.Windows/PushSharp.Windows.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -7,9 +7,10 @@ Library PushSharp.Windows PushSharp.Windows - v4.5 + v4.6.1 true ..\PushSharp-Signing.snk + true @@ -30,13 +31,14 @@ false + + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + True + - - ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll - diff --git a/PushSharp.Windows/WnsConfiguration.cs b/PushSharp.Windows/WnsConfiguration.cs index 9da7811f..61e02d13 100644 --- a/PushSharp.Windows/WnsConfiguration.cs +++ b/PushSharp.Windows/WnsConfiguration.cs @@ -2,18 +2,18 @@ namespace PushSharp.Windows { - public class WnsConfiguration - { - public WnsConfiguration (string packageName, string packageSecurityIdentifier, string clientSecret) - { - PackageName = packageName; - PackageSecurityIdentifier = packageSecurityIdentifier; - ClientSecret = clientSecret; - } + public class WnsConfiguration + { + public WnsConfiguration(string packageName, string packageSecurityIdentifier, string clientSecret) + { + PackageName = packageName; + PackageSecurityIdentifier = packageSecurityIdentifier; + ClientSecret = clientSecret; + } - public string PackageName { get; private set; } - public string PackageSecurityIdentifier { get; private set; } - public string ClientSecret { get; private set; } - } + public string PackageName { get; private set; } + public string PackageSecurityIdentifier { get; private set; } + public string ClientSecret { get; private set; } + } } diff --git a/PushSharp.Windows/WnsConnection.cs b/PushSharp.Windows/WnsConnection.cs index 323ebed6..89885231 100644 --- a/PushSharp.Windows/WnsConnection.cs +++ b/PushSharp.Windows/WnsConnection.cs @@ -1,197 +1,202 @@ using System; -using PushSharp.Core; -using System.Threading.Tasks; -using System.Net.Http; -using System.Net.Http.Headers; using System.Collections.Generic; -using Newtonsoft.Json.Linq; -using System.Xml; +using System.IO; using System.Linq; using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; using System.Text; -using System.IO; +using System.Threading.Tasks; +using PushSharp.Core; namespace PushSharp.Windows { - public class WnsServiceConnectionFactory : IServiceConnectionFactory - { - WnsAccessTokenManager wnsAccessTokenManager; - - public WnsServiceConnectionFactory (WnsConfiguration configuration) - { - wnsAccessTokenManager = new WnsAccessTokenManager (configuration); - Configuration = configuration; - } - - public WnsConfiguration Configuration { get; private set; } - - public IServiceConnection Create() - { - return new WnsServiceConnection (Configuration, wnsAccessTokenManager); - } - } - - public class WnsServiceBroker : ServiceBroker - { - public WnsServiceBroker (WnsConfiguration configuration) : base (new WnsServiceConnectionFactory (configuration)) - { - } - } - - public class WnsServiceConnection : IServiceConnection - { - public WnsServiceConnection (WnsConfiguration configuration, WnsAccessTokenManager accessTokenManager) - { - AccessTokenManager = accessTokenManager; - Configuration = configuration; - } - - public WnsAccessTokenManager AccessTokenManager { get; private set; } - public WnsConfiguration Configuration { get; private set; } - - public async Task Send (WnsNotification notification) - { - // Get or renew our access token - var accessToken = await AccessTokenManager.GetAccessToken (); - - //https://cloud.notify.windows.com/?token=..... - //Authorization: Bearer {AccessToken} - // - - //TODO: Microsoft recommends we disable expect-100 to improve latency - // Not sure how to do this in httpclient - var http = new HttpClient (); - - http.DefaultRequestHeaders.TryAddWithoutValidation ("X-WNS-Type", string.Format ("wns/{0}", notification.Type.ToString ().ToLower ())); - if(!http.DefaultRequestHeaders.Contains("Authorization")) //prevent double values - http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "Bearer " + accessToken); - - if (notification.RequestForStatus.HasValue) - http.DefaultRequestHeaders.TryAddWithoutValidation ("X-WNS-RequestForStatus", notification.RequestForStatus.Value.ToString().ToLower()); - - if (notification.TimeToLive.HasValue) - http.DefaultRequestHeaders.TryAddWithoutValidation ("X-WNS-TTL", notification.TimeToLive.Value.ToString()); //Time to live in seconds - - if (notification.Type == WnsNotificationType.Tile) - { - var winTileNot = notification as WnsTileNotification; - - if (winTileNot != null && winTileNot.CachePolicy.HasValue) - http.DefaultRequestHeaders.Add("X-WNS-Cache-Policy", winTileNot.CachePolicy == WnsNotificationCachePolicyType.Cache ? "cache" : "no-cache"); - - if (winTileNot != null && !string.IsNullOrEmpty(winTileNot.NotificationTag)) - http.DefaultRequestHeaders.Add("X-WNS-Tag", winTileNot.NotificationTag); // TILE only - } - else if (notification.Type == WnsNotificationType.Badge) - { - var winTileBadge = notification as WnsBadgeNotification; - - if (winTileBadge != null && winTileBadge.CachePolicy.HasValue) - http.DefaultRequestHeaders.Add("X-WNS-Cache-Policy", winTileBadge.CachePolicy == WnsNotificationCachePolicyType.Cache ? "cache" : "no-cache"); - } - - HttpContent content = null; - - if (notification.Type == WnsNotificationType.Raw) { - content = new StreamContent (new MemoryStream (Encoding.UTF8.GetBytes (notification.Payload.ToString()))); - } else { - content = new StringContent( - notification.Payload.ToString(), // Get XML payload - Encoding.UTF8, - "text/xml"); - } - - var result = await http.PostAsync (notification.ChannelUri, content); - - var status = ParseStatus (result, notification); - - //RESPONSE HEADERS - // X-WNS-Debug-Trace string - // X-WNS-DeviceConnectionStatus connected | disconnected | tempdisconnected (if RequestForStatus was set to true) - // X-WNS-Error-Description string - // X-WNS-Msg-ID string (max 16 char) - // X-WNS-NotificationStatus received | dropped | channelthrottled - // - - // 200 OK - // 400 One or more headers were specified incorrectly or conflict with another header. - // 401 The cloud service did not present a valid authentication ticket. The OAuth ticket may be invalid. - // 403 The cloud service is not authorized to send a notification to this URI even though they are authenticated. - // 404 The channel URI is not valid or is not recognized by WNS. - Raise Expiry - // 405 Invalid Method - never will get - // 406 The cloud service exceeded its throttle limit. - // 410 The channel expired. - Raise Expiry - // 413 The notification payload exceeds the 5000 byte size limit. - // 500 An internal failure caused notification delivery to fail. - // 503 The server is currently unavailable. - - // OK, everything worked! - if (status.HttpStatus == HttpStatusCode.OK - && status.NotificationStatus == WnsNotificationSendStatus.Received) { - return; - } - - //401 - if (status.HttpStatus == HttpStatusCode.Unauthorized) { - AccessTokenManager.InvalidateAccessToken (accessToken); - throw new RetryAfterException (notification, "Access token expired", DateTime.UtcNow.AddSeconds (5)); - } - - //404 or 410 - if (status.HttpStatus == HttpStatusCode.NotFound || status.HttpStatus == HttpStatusCode.Gone) { - throw new DeviceSubscriptionExpiredException (notification) { - OldSubscriptionId = notification.ChannelUri, - ExpiredAt = DateTime.UtcNow - }; - } - - - // Any other error - throw new WnsNotificationException (status); - } - - WnsNotificationStatus ParseStatus(HttpResponseMessage resp, WnsNotification notification) - { - var result = new WnsNotificationStatus(); - - result.Notification = notification; - result.HttpStatus = resp.StatusCode; - - var wnsDebugTrace = TryGetHeaderValue (resp.Headers, "X-WNS-DEBUG-TRACE") ?? ""; - var wnsDeviceConnectionStatus = TryGetHeaderValue (resp.Headers, "X-WNS-DEVICECONNECTIONSTATUS") ?? "connected"; - var wnsErrorDescription = TryGetHeaderValue (resp.Headers, "X-WNS-ERROR-DESCRIPTION") ?? ""; - var wnsMsgId = TryGetHeaderValue (resp.Headers, "X-WNS-MSG-ID"); - var wnsNotificationStatus = TryGetHeaderValue (resp.Headers, "X-WNS-NOTIFICATIONSTATUS") ?? ""; - - result.DebugTrace = wnsDebugTrace; - result.ErrorDescription = wnsErrorDescription; - result.MessageId = wnsMsgId; - - if (wnsNotificationStatus.Equals("received", StringComparison.InvariantCultureIgnoreCase)) - result.NotificationStatus = WnsNotificationSendStatus.Received; - else if (wnsNotificationStatus.Equals("dropped", StringComparison.InvariantCultureIgnoreCase)) - result.NotificationStatus = WnsNotificationSendStatus.Dropped; - else - result.NotificationStatus = WnsNotificationSendStatus.ChannelThrottled; - - if (wnsDeviceConnectionStatus.Equals("connected", StringComparison.InvariantCultureIgnoreCase)) - result.DeviceConnectionStatus = WnsDeviceConnectionStatus.Connected; - else if (wnsDeviceConnectionStatus.Equals("tempdisconnected", StringComparison.InvariantCultureIgnoreCase)) - result.DeviceConnectionStatus = WnsDeviceConnectionStatus.TempDisconnected; - else - result.DeviceConnectionStatus = WnsDeviceConnectionStatus.Disconnected; - - return result; - } - - string TryGetHeaderValue (HttpResponseHeaders headers, string headerName) - { - IEnumerable values; - if (headers.TryGetValues (headerName, out values)) - return values.FirstOrDefault (); - - return null; - } - } + public class WnsServiceConnectionFactory : IServiceConnectionFactory + { + WnsAccessTokenManager wnsAccessTokenManager; + + public WnsServiceConnectionFactory(WnsConfiguration configuration) + { + wnsAccessTokenManager = new WnsAccessTokenManager(configuration); + Configuration = configuration; + } + + public WnsConfiguration Configuration { get; private set; } + + public IServiceConnection Create() + { + return new WnsServiceConnection(Configuration, wnsAccessTokenManager); + } + } + + public class WnsServiceBroker : ServiceBroker + { + public WnsServiceBroker(WnsConfiguration configuration) : base(new WnsServiceConnectionFactory(configuration)) + { + } + } + + public class WnsServiceConnection : IServiceConnection + { + public WnsServiceConnection(WnsConfiguration configuration, WnsAccessTokenManager accessTokenManager) + { + AccessTokenManager = accessTokenManager; + Configuration = configuration; + } + + public WnsAccessTokenManager AccessTokenManager { get; private set; } + public WnsConfiguration Configuration { get; private set; } + + public async Task Send(WnsNotification notification) + { + // Get or renew our access token + var accessToken = await AccessTokenManager.GetAccessToken(); + + //https://cloud.notify.windows.com/?token=..... + //Authorization: Bearer {AccessToken} + // + + //TODO: Microsoft recommends we disable expect-100 to improve latency + // Not sure how to do this in httpclient + var http = new HttpClient(); + + http.DefaultRequestHeaders.TryAddWithoutValidation("X-WNS-Type", string.Format("wns/{0}", notification.Type.ToString().ToLower())); + if (!http.DefaultRequestHeaders.Contains("Authorization")) //prevent double values + http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "Bearer " + accessToken); + + if (notification.RequestForStatus.HasValue) + http.DefaultRequestHeaders.TryAddWithoutValidation("X-WNS-RequestForStatus", notification.RequestForStatus.Value.ToString().ToLower()); + + if (notification.TimeToLive.HasValue) + http.DefaultRequestHeaders.TryAddWithoutValidation("X-WNS-TTL", notification.TimeToLive.Value.ToString()); //Time to live in seconds + + if (notification.Type == WnsNotificationType.Tile) + { + var winTileNot = notification as WnsTileNotification; + + if (winTileNot != null && winTileNot.CachePolicy.HasValue) + http.DefaultRequestHeaders.Add("X-WNS-Cache-Policy", winTileNot.CachePolicy == WnsNotificationCachePolicyType.Cache ? "cache" : "no-cache"); + + if (winTileNot != null && !string.IsNullOrEmpty(winTileNot.NotificationTag)) + http.DefaultRequestHeaders.Add("X-WNS-Tag", winTileNot.NotificationTag); // TILE only + } + else if (notification.Type == WnsNotificationType.Badge) + { + var winTileBadge = notification as WnsBadgeNotification; + + if (winTileBadge != null && winTileBadge.CachePolicy.HasValue) + http.DefaultRequestHeaders.Add("X-WNS-Cache-Policy", winTileBadge.CachePolicy == WnsNotificationCachePolicyType.Cache ? "cache" : "no-cache"); + } + + HttpContent content = null; + + if (notification.Type == WnsNotificationType.Raw) + { + content = new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(notification.Payload.ToString()))); + } + else + { + content = new StringContent( + notification.Payload.ToString(), // Get XML payload + Encoding.UTF8, + "text/xml"); + } + + var result = await http.PostAsync(notification.ChannelUri, content); + + var status = ParseStatus(result, notification); + + //RESPONSE HEADERS + // X-WNS-Debug-Trace string + // X-WNS-DeviceConnectionStatus connected | disconnected | tempdisconnected (if RequestForStatus was set to true) + // X-WNS-Error-Description string + // X-WNS-Msg-ID string (max 16 char) + // X-WNS-NotificationStatus received | dropped | channelthrottled + // + + // 200 OK + // 400 One or more headers were specified incorrectly or conflict with another header. + // 401 The cloud service did not present a valid authentication ticket. The OAuth ticket may be invalid. + // 403 The cloud service is not authorized to send a notification to this URI even though they are authenticated. + // 404 The channel URI is not valid or is not recognized by WNS. - Raise Expiry + // 405 Invalid Method - never will get + // 406 The cloud service exceeded its throttle limit. + // 410 The channel expired. - Raise Expiry + // 413 The notification payload exceeds the 5000 byte size limit. + // 500 An internal failure caused notification delivery to fail. + // 503 The server is currently unavailable. + + // OK, everything worked! + if (status.HttpStatus == HttpStatusCode.OK + && status.NotificationStatus == WnsNotificationSendStatus.Received) + { + return; + } + + //401 + if (status.HttpStatus == HttpStatusCode.Unauthorized) + { + AccessTokenManager.InvalidateAccessToken(accessToken); + throw new RetryAfterException(notification, "Access token expired", DateTime.UtcNow.AddSeconds(5)); + } + + //404 or 410 + if (status.HttpStatus == HttpStatusCode.NotFound || status.HttpStatus == HttpStatusCode.Gone) + { + throw new DeviceSubscriptionExpiredException(notification) + { + OldSubscriptionId = notification.ChannelUri, + ExpiredAt = DateTime.UtcNow + }; + } + + + // Any other error + throw new WnsNotificationException(status); + } + + WnsNotificationStatus ParseStatus(HttpResponseMessage resp, WnsNotification notification) + { + var result = new WnsNotificationStatus(); + + result.Notification = notification; + result.HttpStatus = resp.StatusCode; + + var wnsDebugTrace = TryGetHeaderValue(resp.Headers, "X-WNS-DEBUG-TRACE") ?? ""; + var wnsDeviceConnectionStatus = TryGetHeaderValue(resp.Headers, "X-WNS-DEVICECONNECTIONSTATUS") ?? "connected"; + var wnsErrorDescription = TryGetHeaderValue(resp.Headers, "X-WNS-ERROR-DESCRIPTION") ?? ""; + var wnsMsgId = TryGetHeaderValue(resp.Headers, "X-WNS-MSG-ID"); + var wnsNotificationStatus = TryGetHeaderValue(resp.Headers, "X-WNS-NOTIFICATIONSTATUS") ?? ""; + + result.DebugTrace = wnsDebugTrace; + result.ErrorDescription = wnsErrorDescription; + result.MessageId = wnsMsgId; + + if (wnsNotificationStatus.Equals("received", StringComparison.InvariantCultureIgnoreCase)) + result.NotificationStatus = WnsNotificationSendStatus.Received; + else if (wnsNotificationStatus.Equals("dropped", StringComparison.InvariantCultureIgnoreCase)) + result.NotificationStatus = WnsNotificationSendStatus.Dropped; + else + result.NotificationStatus = WnsNotificationSendStatus.ChannelThrottled; + + if (wnsDeviceConnectionStatus.Equals("connected", StringComparison.InvariantCultureIgnoreCase)) + result.DeviceConnectionStatus = WnsDeviceConnectionStatus.Connected; + else if (wnsDeviceConnectionStatus.Equals("tempdisconnected", StringComparison.InvariantCultureIgnoreCase)) + result.DeviceConnectionStatus = WnsDeviceConnectionStatus.TempDisconnected; + else + result.DeviceConnectionStatus = WnsDeviceConnectionStatus.Disconnected; + + return result; + } + + string TryGetHeaderValue(HttpResponseHeaders headers, string headerName) + { + IEnumerable values; + if (headers.TryGetValues(headerName, out values)) + return values.FirstOrDefault(); + + return null; + } + } } diff --git a/PushSharp.Windows/WnsNotification.cs b/PushSharp.Windows/WnsNotification.cs index 15bdcb33..de024f14 100644 --- a/PushSharp.Windows/WnsNotification.cs +++ b/PushSharp.Windows/WnsNotification.cs @@ -3,54 +3,54 @@ namespace PushSharp.Windows { - public abstract class WnsNotification : INotification - { - public string ChannelUri { get; set; } - - public bool? RequestForStatus { get; set; } - public int? TimeToLive { get; set; } - - public XElement Payload { get; set; } - - public abstract WnsNotificationType Type { get; } - - public bool IsDeviceRegistrationIdValid () - { - return true; - } - - public object Tag { get; set; } - } - - public class WnsTileNotification : WnsNotification - { - public override WnsNotificationType Type - { - get { return WnsNotificationType.Tile; } - } - - public WnsNotificationCachePolicyType? CachePolicy { get; set; } - - public string NotificationTag { get; set; } - } - - public class WnsToastNotification : WnsNotification - { - public override WnsNotificationType Type - { - get { return WnsNotificationType.Toast; } - } - } - - public class WnsBadgeNotification : WnsNotification - { - public override WnsNotificationType Type - { - get { return WnsNotificationType.Badge; } - } - - public WnsNotificationCachePolicyType? CachePolicy { get; set; } - } + public abstract class WnsNotification : INotification + { + public string ChannelUri { get; set; } + + public bool? RequestForStatus { get; set; } + public int? TimeToLive { get; set; } + + public XElement Payload { get; set; } + + public abstract WnsNotificationType Type { get; } + + public bool IsDeviceRegistrationIdValid() + { + return true; + } + + public object Tag { get; set; } + } + + public class WnsTileNotification : WnsNotification + { + public override WnsNotificationType Type + { + get { return WnsNotificationType.Tile; } + } + + public WnsNotificationCachePolicyType? CachePolicy { get; set; } + + public string NotificationTag { get; set; } + } + + public class WnsToastNotification : WnsNotification + { + public override WnsNotificationType Type + { + get { return WnsNotificationType.Toast; } + } + } + + public class WnsBadgeNotification : WnsNotification + { + public override WnsNotificationType Type + { + get { return WnsNotificationType.Badge; } + } + + public WnsNotificationCachePolicyType? CachePolicy { get; set; } + } } diff --git a/PushSharp.Windows/WnsNotificationStatus.cs b/PushSharp.Windows/WnsNotificationStatus.cs index c32b62f3..573d4177 100644 --- a/PushSharp.Windows/WnsNotificationStatus.cs +++ b/PushSharp.Windows/WnsNotificationStatus.cs @@ -2,46 +2,46 @@ namespace PushSharp.Windows { - public class WnsNotificationStatus - { - public string MessageId { get; set; } - public string DebugTrace { get; set; } - public string ErrorDescription { get; set; } - - public WnsNotificationSendStatus NotificationStatus { get; set; } - public WnsDeviceConnectionStatus DeviceConnectionStatus { get; set; } - - public WnsNotification Notification { get; set; } - - public System.Net.HttpStatusCode HttpStatus { get; set; } - } - - public enum WnsNotificationSendStatus - { - Received, - Dropped, - ChannelThrottled - } - - public enum WnsDeviceConnectionStatus - { - Connected, - Disconnected, - TempDisconnected - } - - public enum WnsNotificationCachePolicyType - { - Cache, - NoCache - } - - public enum WnsNotificationType - { - Badge, - Tile, - Toast, - Raw - } + public class WnsNotificationStatus + { + public string MessageId { get; set; } + public string DebugTrace { get; set; } + public string ErrorDescription { get; set; } + + public WnsNotificationSendStatus NotificationStatus { get; set; } + public WnsDeviceConnectionStatus DeviceConnectionStatus { get; set; } + + public WnsNotification Notification { get; set; } + + public System.Net.HttpStatusCode HttpStatus { get; set; } + } + + public enum WnsNotificationSendStatus + { + Received, + Dropped, + ChannelThrottled + } + + public enum WnsDeviceConnectionStatus + { + Connected, + Disconnected, + TempDisconnected + } + + public enum WnsNotificationCachePolicyType + { + Cache, + NoCache + } + + public enum WnsNotificationType + { + Badge, + Tile, + Toast, + Raw + } } diff --git a/PushSharp.Windows/WnsTokenAccessManager.cs b/PushSharp.Windows/WnsTokenAccessManager.cs index 8e2c9908..6d19243f 100644 --- a/PushSharp.Windows/WnsTokenAccessManager.cs +++ b/PushSharp.Windows/WnsTokenAccessManager.cs @@ -7,72 +7,82 @@ namespace PushSharp.Windows { - public class WnsAccessTokenManager - { - Task renewAccessTokenTask = null; - string accessToken = null; - HttpClient http; + public class WnsAccessTokenManager + { + Task renewAccessTokenTask = null; + string accessToken = null; + HttpClient http; - public WnsAccessTokenManager (WnsConfiguration configuration) - { - http = new HttpClient (); - Configuration = configuration; - } + public WnsAccessTokenManager(WnsConfiguration configuration) + { + http = new HttpClient(); + Configuration = configuration; + } - public WnsConfiguration Configuration { get; private set; } + public WnsConfiguration Configuration { get; private set; } - public async Task GetAccessToken () - { - if (accessToken == null) { - if (renewAccessTokenTask == null) { - Log.Info ("Renewing Access Token"); - renewAccessTokenTask = RenewAccessToken (); - await renewAccessTokenTask; - } else { - Log.Info ("Waiting for access token"); - await renewAccessTokenTask; - } - } + public async Task GetAccessToken() + { + if (accessToken == null) + { + if (renewAccessTokenTask == null) + { + Log.Info("Renewing Access Token"); + renewAccessTokenTask = RenewAccessToken(); + await renewAccessTokenTask; + } + else + { + Log.Info("Waiting for access token"); + await renewAccessTokenTask; + } + } - return accessToken; - } + return accessToken; + } - public void InvalidateAccessToken (string currentAccessToken) - { - if (accessToken == currentAccessToken) - accessToken = null; - } + public void InvalidateAccessToken(string currentAccessToken) + { + if (accessToken == currentAccessToken) + accessToken = null; + } - async Task RenewAccessToken () - { - var p = new Dictionary { - { "grant_type", "client_credentials" }, - { "client_id", Configuration.PackageSecurityIdentifier }, - { "client_secret", Configuration.ClientSecret }, - { "scope", "notify.windows.com" } - }; + async Task RenewAccessToken() + { + var p = new Dictionary { + { "grant_type", "client_credentials" }, + { "client_id", Configuration.PackageSecurityIdentifier }, + { "client_secret", Configuration.ClientSecret }, + { "scope", "notify.windows.com" } + }; - var result = await http.PostAsync ("https://login.live.com/accesstoken.srf", new FormUrlEncodedContent (p)); + var result = await http.PostAsync("https://login.live.com/accesstoken.srf", new FormUrlEncodedContent(p)); - var data = await result.Content.ReadAsStringAsync (); + var data = await result.Content.ReadAsStringAsync(); - var token = string.Empty; - var tokenType = string.Empty; + var token = string.Empty; + var tokenType = string.Empty; - try { - var json = JObject.Parse (data); - token = json.Value ("access_token"); - tokenType = json.Value ("token_type"); - } catch { - } + try + { + var json = JObject.Parse(data); + token = json.Value("access_token"); + tokenType = json.Value("token_type"); + } + catch + { + } - if (!string.IsNullOrEmpty (token) && !string.IsNullOrEmpty (tokenType)) { - accessToken = token; - } else { - accessToken = null; - throw new UnauthorizedAccessException ("Could not retrieve access token for the supplied Package Security Identifier (SID) and client secret"); - } - } - } + if (!string.IsNullOrEmpty(token) && !string.IsNullOrEmpty(tokenType)) + { + accessToken = token; + } + else + { + accessToken = null; + throw new UnauthorizedAccessException("Could not retrieve access token for the supplied Package Security Identifier (SID) and client secret"); + } + } + } } diff --git a/PushSharp.Windows/packages.config b/PushSharp.Windows/packages.config index 505e5883..e1fae9c6 100644 --- a/PushSharp.Windows/packages.config +++ b/PushSharp.Windows/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/PushSharp.sln b/PushSharp.sln index d96c467d..ec8c7689 100644 --- a/PushSharp.sln +++ b/PushSharp.sln @@ -1,6 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2012 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PushSharp.Core", "PushSharp.Core\PushSharp.Core.csproj", "{2B44A8DA-60BC-4577-A2D7-D9D53F164B2E}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PushSharp.Tests", "PushSharp.Tests\PushSharp.Tests.csproj", "{989B7357-800E-46B9-91AF-A4CE8A55F389}" @@ -19,38 +21,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PushSharp.Google", "PushSha EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PushSharp.Blackberry", "PushSharp.Blackberry\PushSharp.Blackberry.csproj", "{9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Support Libraries", "Support Libraries", "{6449DAB1-E76A-4354-B633-CC6AC53BB757}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2468C63B-C964-4FC3-9B16-13DC17CF7D11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2468C63B-C964-4FC3-9B16-13DC17CF7D11}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2468C63B-C964-4FC3-9B16-13DC17CF7D11}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2468C63B-C964-4FC3-9B16-13DC17CF7D11}.Release|Any CPU.Build.0 = Release|Any CPU {2B44A8DA-60BC-4577-A2D7-D9D53F164B2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2B44A8DA-60BC-4577-A2D7-D9D53F164B2E}.Debug|Any CPU.Build.0 = Debug|Any CPU {2B44A8DA-60BC-4577-A2D7-D9D53F164B2E}.Release|Any CPU.ActiveCfg = Release|Any CPU {2B44A8DA-60BC-4577-A2D7-D9D53F164B2E}.Release|Any CPU.Build.0 = Release|Any CPU - {54A4C1F9-6571-4086-BB4B-EC202138AF00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {54A4C1F9-6571-4086-BB4B-EC202138AF00}.Debug|Any CPU.Build.0 = Debug|Any CPU - {54A4C1F9-6571-4086-BB4B-EC202138AF00}.Release|Any CPU.ActiveCfg = Release|Any CPU - {54A4C1F9-6571-4086-BB4B-EC202138AF00}.Release|Any CPU.Build.0 = Release|Any CPU - {94F16497-471F-433F-A99E-C455FB2D7724}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {94F16497-471F-433F-A99E-C455FB2D7724}.Debug|Any CPU.Build.0 = Debug|Any CPU - {94F16497-471F-433F-A99E-C455FB2D7724}.Release|Any CPU.ActiveCfg = Release|Any CPU - {94F16497-471F-433F-A99E-C455FB2D7724}.Release|Any CPU.Build.0 = Release|Any CPU {989B7357-800E-46B9-91AF-A4CE8A55F389}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {989B7357-800E-46B9-91AF-A4CE8A55F389}.Debug|Any CPU.Build.0 = Debug|Any CPU {989B7357-800E-46B9-91AF-A4CE8A55F389}.Release|Any CPU.ActiveCfg = Release|Any CPU {989B7357-800E-46B9-91AF-A4CE8A55F389}.Release|Any CPU.Build.0 = Release|Any CPU - {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Release|Any CPU.Build.0 = Release|Any CPU {A9D99F80-FEEB-4E74-96C5-66F17103C773}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A9D99F80-FEEB-4E74-96C5-66F17103C773}.Debug|Any CPU.Build.0 = Debug|Any CPU {A9D99F80-FEEB-4E74-96C5-66F17103C773}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -59,6 +43,25 @@ Global {DC80552B-6730-44AA-9B74-1E036BD909C3}.Debug|Any CPU.Build.0 = Debug|Any CPU {DC80552B-6730-44AA-9B74-1E036BD909C3}.Release|Any CPU.ActiveCfg = Release|Any CPU {DC80552B-6730-44AA-9B74-1E036BD909C3}.Release|Any CPU.Build.0 = Release|Any CPU + {54A4C1F9-6571-4086-BB4B-EC202138AF00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54A4C1F9-6571-4086-BB4B-EC202138AF00}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54A4C1F9-6571-4086-BB4B-EC202138AF00}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54A4C1F9-6571-4086-BB4B-EC202138AF00}.Release|Any CPU.Build.0 = Release|Any CPU + {2468C63B-C964-4FC3-9B16-13DC17CF7D11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2468C63B-C964-4FC3-9B16-13DC17CF7D11}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2468C63B-C964-4FC3-9B16-13DC17CF7D11}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2468C63B-C964-4FC3-9B16-13DC17CF7D11}.Release|Any CPU.Build.0 = Release|Any CPU + {94F16497-471F-433F-A99E-C455FB2D7724}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {94F16497-471F-433F-A99E-C455FB2D7724}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94F16497-471F-433F-A99E-C455FB2D7724}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94F16497-471F-433F-A99E-C455FB2D7724}.Release|Any CPU.Build.0 = Release|Any CPU + {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {A9D99F80-FEEB-4E74-96C5-66F17103C773} = {2B7243CB-60A5-4682-802B-B7EC3DEBCF9A} From 6481ab1f75b4a51e9164cbc7388cd5cef5df0693 Mon Sep 17 00:00:00 2001 From: Karimi Date: Sat, 29 Oct 2016 17:03:35 +0330 Subject: [PATCH 3/5] Add Firebase Messaging --- PushSharp.Firebase/Exceptions.cs | 37 +++ PushSharp.Firebase/FcmConfiguration.cs | 42 +++ PushSharp.Firebase/FcmMessageResult.cs | 71 +++++ PushSharp.Firebase/FcmNotification.cs | 148 +++++++++++ PushSharp.Firebase/FcmResponse.cs | 175 +++++++++++++ PushSharp.Firebase/FcmServiceConnection.cs | 243 ++++++++++++++++++ PushSharp.Firebase/Properties/AssemblyInfo.cs | 36 +++ PushSharp.Firebase/PushSharp.Firebase.csproj | 73 ++++++ PushSharp.Firebase/packages.config | 4 + PushSharp.Tests/FcmRealTests.cs | 50 ++++ PushSharp.Tests/FcmTests.cs | 34 +++ PushSharp.Tests/GcmRealTests.cs | 2 +- PushSharp.Tests/PushSharp.Tests.csproj | 6 + PushSharp.Tests/Settings.cs | 7 + PushSharp.sln | 7 + 15 files changed, 934 insertions(+), 1 deletion(-) create mode 100644 PushSharp.Firebase/Exceptions.cs create mode 100644 PushSharp.Firebase/FcmConfiguration.cs create mode 100644 PushSharp.Firebase/FcmMessageResult.cs create mode 100644 PushSharp.Firebase/FcmNotification.cs create mode 100644 PushSharp.Firebase/FcmResponse.cs create mode 100644 PushSharp.Firebase/FcmServiceConnection.cs create mode 100644 PushSharp.Firebase/Properties/AssemblyInfo.cs create mode 100644 PushSharp.Firebase/PushSharp.Firebase.csproj create mode 100644 PushSharp.Firebase/packages.config create mode 100644 PushSharp.Tests/FcmRealTests.cs create mode 100644 PushSharp.Tests/FcmTests.cs diff --git a/PushSharp.Firebase/Exceptions.cs b/PushSharp.Firebase/Exceptions.cs new file mode 100644 index 00000000..89f83491 --- /dev/null +++ b/PushSharp.Firebase/Exceptions.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using PushSharp.Core; + +namespace PushSharp.Firebase +{ + public class FcmNotificationException : NotificationException + { + public FcmNotificationException(FcmNotification notification, string msg) : base(msg, notification) + { + Notification = notification; + } + + public FcmNotificationException(FcmNotification notification, string msg, string description) : base(msg, notification) + { + Notification = notification; + Description = description; + } + + public new FcmNotification Notification { get; private set; } + public string Description { get; private set; } + } + + public class FcmMulticastResultException : Exception + { + public FcmMulticastResultException() : base("One or more Registration Id's failed in the multicast notification") + { + Succeeded = new List(); + Failed = new Dictionary(); + } + + public List Succeeded { get; set; } + + public Dictionary Failed { get; set; } + } +} + diff --git a/PushSharp.Firebase/FcmConfiguration.cs b/PushSharp.Firebase/FcmConfiguration.cs new file mode 100644 index 00000000..b27a567a --- /dev/null +++ b/PushSharp.Firebase/FcmConfiguration.cs @@ -0,0 +1,42 @@ +using System; + +namespace PushSharp.Firebase +{ + public class FcmConfiguration + { + private const string FCM_SEND_URL = "https://fcm.googleapis.com/fcm/send"; + + public FcmConfiguration(string senderAuthToken) + { + this.SenderAuthToken = senderAuthToken; + this.FcmUrl = FCM_SEND_URL; + + this.ValidateServerCertificate = false; + } + + public FcmConfiguration(string optionalSenderID, string senderAuthToken, string optionalApplicationIdPackageName) + { + this.SenderID = optionalSenderID; + this.SenderAuthToken = senderAuthToken; + this.ApplicationIdPackageName = optionalApplicationIdPackageName; + this.FcmUrl = FCM_SEND_URL; + + this.ValidateServerCertificate = false; + } + + public string SenderID { get; private set; } + + public string SenderAuthToken { get; private set; } + + public string ApplicationIdPackageName { get; private set; } + + public bool ValidateServerCertificate { get; set; } + + public string FcmUrl { get; set; } + + public void OverrideUrl(string url) + { + FcmUrl = url; + } + } +} \ No newline at end of file diff --git a/PushSharp.Firebase/FcmMessageResult.cs b/PushSharp.Firebase/FcmMessageResult.cs new file mode 100644 index 00000000..80e71a70 --- /dev/null +++ b/PushSharp.Firebase/FcmMessageResult.cs @@ -0,0 +1,71 @@ +using Newtonsoft.Json; + +namespace PushSharp.Firebase +{ + public class FcmMessageResult + { + [JsonProperty("message_id", NullValueHandling = NullValueHandling.Ignore)] + public string MessageId { get; set; } + + [JsonProperty("registration_id", NullValueHandling = NullValueHandling.Ignore)] + public string CanonicalRegistrationId { get; set; } + + [JsonIgnore] + public FcmResponseStatus ResponseStatus { get; set; } + + [JsonProperty("error", NullValueHandling = NullValueHandling.Ignore)] + public string Error + { + get + { + switch (ResponseStatus) + { + case FcmResponseStatus.Ok: + return null; + + case FcmResponseStatus.Unavailable: + return "Unavailable"; + + case FcmResponseStatus.NotRegistered: + return "NotRegistered"; + + case FcmResponseStatus.MissingRegistrationId: + return "MissingRegistration"; + + case FcmResponseStatus.MismatchSenderId: + return "MismatchSenderId"; + + case FcmResponseStatus.MessageTooBig: + return "MessageTooBig"; + + case FcmResponseStatus.InvalidTtl: + return "InvalidTtl"; + + case FcmResponseStatus.InvalidRegistration: + return "InvalidRegistration"; + + case FcmResponseStatus.InvalidDataKey: + return "InvalidDataKey"; + + case FcmResponseStatus.InternalServerError: + return "InternalServerError"; + + case FcmResponseStatus.DeviceMessageRateExceeded: + return "DeviceMessageRateExceeded"; + + case FcmResponseStatus.TopicsMessageRateExceeded: + return "TopicsMessageRateExceeded"; + + case FcmResponseStatus.CanonicalRegistrationId: + return null; + + case FcmResponseStatus.Error: + return "Error"; + + default: + return null; + } + } + } + } +} \ No newline at end of file diff --git a/PushSharp.Firebase/FcmNotification.cs b/PushSharp.Firebase/FcmNotification.cs new file mode 100644 index 00000000..6fc797ab --- /dev/null +++ b/PushSharp.Firebase/FcmNotification.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PushSharp.Core; + +namespace PushSharp.Firebase +{ + public class FcmNotification : INotification + { + public FcmNotification() + { + RegistrationIds = new List(); + CollapseKey = null; + Data = null; + } + + public bool IsDeviceRegistrationIdValid() + { + return RegistrationIds != null && RegistrationIds.Any(); + } + + [JsonIgnore] + public object Tag { get; set; } + + [JsonIgnore] + public string MessageId { get; internal set; } + + /// + /// Registration ID or Group/Topic to send notification to. Overrides RegsitrationIds. + /// + [JsonProperty("to")] + public string To { get; set; } + + /// + /// Registration ID of the Device(s). Maximum of 1000 registration Id's per notification. + /// + [JsonProperty("registration_ids")] + public List RegistrationIds { get; set; } + + /// + /// This parameter specifies a logical expression of conditions that determine the message target. + /// more info + /// + [JsonProperty("condition")] + public string Condition { get; set; } + + /// + /// Only the latest message with the same collapse key will be delivered + /// + [JsonProperty("collapse_key")] + public string CollapseKey { get; set; } + + /// + /// Corresponds to iOS APNS priorities (Normal is 5 and high is 10). Default is Normal. + /// + /// The priority. + [JsonProperty("priority"), JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public FcmNotificationPriority? Priority { get; set; } + + /// + /// On iOS, use this field to represent content-available in the APNS payload. When a notification or message is sent and this is set to true, an inactive client app is awoken. On Android, data messages wake the app by default. On Chrome, currently not supported. + /// + /// The content available. + [JsonProperty("content_available")] + public bool? ContentAvailable { get; set; } + + /// + /// Time in seconds that a message should be kept on the server if the device is offline. Default (and maximum) is 4 weeks. + /// + [JsonProperty("time_to_live")] + public int? TimeToLive { get; set; } + + /// + /// A string containing the package name of your application. When set, messages will only be sent to registration IDs that match the package name + /// + [JsonProperty("restricted_package_name")] + public string RestrictedPackageName { get; set; } + + /// + /// If true, dry_run attribute will be sent in payload causing the notification not to be actually sent, but the result returned simulating the message + /// + [JsonProperty("dry_run")] + public bool? DryRun { get; set; } + + /// + /// JSON Payload to be sent in the message + /// + [JsonProperty("data")] + public JObject Data { get; set; } + + /// + /// Notification JSON payload, More info https://firebase.google.com/docs/cloud-messaging/http-server-ref#notification-payload-support + /// + /// The notification payload. + [JsonProperty("notification")] + public JObject Notification { get; set; } + + + internal string GetJson() + { + // If 'To' was used instead of RegistrationIds, let's make RegistrationId's null + // so we don't serialize an empty array for this property + // otherwise, google will complain that we specified both instead + if (RegistrationIds != null && RegistrationIds.Count <= 0 && !string.IsNullOrEmpty(To)) + RegistrationIds = null; + + // Ignore null values + return JsonConvert.SerializeObject(this, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + } + + public override string ToString() + { + return GetJson(); + } + + public FcmNotification Clone(string registrationID) + { + return new FcmNotification + { + Tag = Tag, + MessageId = MessageId, + Condition = Condition, + Notification = Notification, + RestrictedPackageName = RestrictedPackageName, + TimeToLive = TimeToLive, + RegistrationIds = new List { registrationID }, + CollapseKey = CollapseKey, + Data = Data, + ContentAvailable = ContentAvailable, + DryRun = DryRun, + Priority = Priority, + To = To + }; + } + } + + public enum FcmNotificationPriority + { + [EnumMember(Value = "normal")] + Normal = 5, + [EnumMember(Value = "high")] + High = 10 + } +} \ No newline at end of file diff --git a/PushSharp.Firebase/FcmResponse.cs b/PushSharp.Firebase/FcmResponse.cs new file mode 100644 index 00000000..7b64f27f --- /dev/null +++ b/PushSharp.Firebase/FcmResponse.cs @@ -0,0 +1,175 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +using Newtonsoft.Json; + +namespace PushSharp.Firebase +{ + public class FcmResponse + { + public FcmResponse() + { + MulticastId = -1; + NumberOfSuccesses = 0; + NumberOfFailures = 0; + NumberOfCanonicalIds = 0; + OriginalNotification = null; + Results = new List(); + ResponseCode = FcmResponseCode.Ok; + } + + [JsonProperty("multicast_id")] + public long MulticastId { get; set; } + + [JsonProperty("success")] + public long NumberOfSuccesses { get; set; } + + [JsonProperty("failure")] + public long NumberOfFailures { get; set; } + + [JsonProperty("canonical_ids")] + public long NumberOfCanonicalIds { get; set; } + + [JsonIgnore] + public FcmNotification OriginalNotification { get; set; } + + [JsonProperty("results")] + public List Results { get; set; } + + [JsonIgnore] + public FcmResponseCode ResponseCode { get; set; } + + public string GetSingleRegistrationID(int resultIndex) + { + if (OriginalNotification.RegistrationIds != null && OriginalNotification.RegistrationIds.Count >= (resultIndex + 1)) + return OriginalNotification.RegistrationIds[resultIndex]; + + return string.Empty; + } + } + + public enum FcmResponseCode + { + Ok, + Error, + BadRequest, + ServiceUnavailable, + InvalidAuthToken, + InternalServiceError + } + + public enum FcmResponseStatus + { + [EnumMember(Value = "Ok")] + Ok, + + [EnumMember(Value = "Error")] + Error, + + //[EnumMember(Value = "QuotaExceeded")] + //QuotaExceeded, + + //[EnumMember(Value = "DeviceQuotaExceeded")] + //DeviceQuotaExceeded, + + //[EnumMember(Value = "MissingCollapseKey")] + //MissingCollapseKey, + + [EnumMember(Value = "CanonicalRegistrationId")] + CanonicalRegistrationId, + + /// + /// Check the format of the registration token you pass to the server. + /// Make sure it matches the registration token the client app receives from registering with Firebase Notifications. + /// Do not truncate or add additional characters. + /// + [EnumMember(Value = "InvalidRegistration")] + InvalidRegistration, + + /// + /// An existing registration token may cease to be valid in a number of scenarios, including: + /// - If the client app unregisters with FCM. + /// - If the client app is automatically unregistered, which can happen if the user uninstalls the application. For example, on iOS, if the APNS Feedback Service reported the APNS token as invalid. + /// - If the registration token expires (for example, Google might decide to refresh registration tokens, or the APNS token has expired for iOS devices). + /// - If the client app is updated but the new version is not configured to receive messages. + /// + /// For all these cases, remove this registration token from the app server and stop using it to send messages. + /// + [EnumMember(Value = "NotRegistered")] + NotRegistered, + + /// + /// Check that the total size of the payload data included in a message does not exceed FCM limits: 4096 bytes for most messages, + /// or 2048 bytes in the case of messages to topics or notification messages on iOS. This includes both the keys and the values. + /// + [EnumMember(Value = "MessageTooBig")] + MessageTooBig, + + /// + /// Check that the request contains a registration token (in the registration_id in a plain text message, + /// or in the to or registration_ids field in JSON). + /// + [EnumMember(Value = "MissingRegistration")] + MissingRegistrationId, + + /// + /// The server couldn't process the request in time. Retry the same request, but you must: + /// -Honor the Retry-After header if it is included in the response from the FCM Connection Server. + /// -Implement exponential back-off in your retry mechanism. (e.g. if you waited one second before the first retry, + /// wait at least two second before the next one, then 4 seconds and so on). If you're sending multiple messages, + /// delay each one independently by an additional random amount to avoid issuing a new request for all messages at the same time. + /// + /// Senders that cause problems risk being blacklisted. + /// + [EnumMember(Value = "Unavailable")] + Unavailable, + + /// + /// A registration token is tied to a certain group of senders. When a client app registers for FCM, + /// it must specify which senders are allowed to send messages. You should use one of those sender IDs when sending messages to the client app. + /// If you switch to a different sender, the existing registration tokens won't work. + /// + [EnumMember(Value = "MismatchSenderId")] + MismatchSenderId, + + /// + /// Check that the payload data does not contain a key (such as from, or gcm, or any value prefixed by google) that is used internally by FCM. + /// Note that some words (such as collapse_key) are also used by FCM but are allowed in the payload, + /// in which case the payload value will be overridden by the FCM value. + /// + [EnumMember(Value = "InvalidDataKey")] + InvalidDataKey, + + /// + /// Check that the value used in time_to_live is an integer representing a duration in seconds between 0 and 2,419,200 (4 weeks). + /// + [EnumMember(Value = "InvalidTtl")] + InvalidTtl, + + /// + /// The server encountered an error while trying to process the request. You could retry the same request following the requirements listed in "Timeout" (see row above). + /// If the error persists, please report the problem in the android-gcm group. + /// + [EnumMember(Value = "InternalServerError")] + InternalServerError, + + /// + /// Make sure the message was addressed to a registration token whose package name matches the value passed in the request. + /// + [EnumMember(Value = "InvalidPackageName")] + InvalidPackageName, + + /// + /// The rate of messages to a particular device is too high. + /// Reduce the number of messages sent to this device and do not immediately retry sending to this device. + /// + [EnumMember(Value = "DeviceMessageRateExceeded")] + DeviceMessageRateExceeded, + + /// + /// The rate of messages to subscribers to a particular topic is too high. + /// Reduce the number of messages sent for this topic, and do not immediately retry sending. + /// + [EnumMember(Value = "TopicsMessageRateExceeded")] + TopicsMessageRateExceeded + } +} \ No newline at end of file diff --git a/PushSharp.Firebase/FcmServiceConnection.cs b/PushSharp.Firebase/FcmServiceConnection.cs new file mode 100644 index 00000000..18b4ca09 --- /dev/null +++ b/PushSharp.Firebase/FcmServiceConnection.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using PushSharp.Core; + +namespace PushSharp.Firebase +{ + public class FcmServiceConnectionFactory : IServiceConnectionFactory + { + public FcmServiceConnectionFactory(FcmConfiguration configuration) + { + Configuration = configuration; + } + + public FcmConfiguration Configuration { get; private set; } + + public IServiceConnection Create() + { + return new FcmServiceConnection(Configuration); + } + } + + public class FcmServiceBroker : ServiceBroker + { + public FcmServiceBroker(FcmConfiguration configuration) : base(new FcmServiceConnectionFactory(configuration)) + { + } + } + + public class FcmServiceConnection : IServiceConnection + { + public FcmServiceConnection(FcmConfiguration configuration) + { + Configuration = configuration; + http = new HttpClient(); + + http.DefaultRequestHeaders.UserAgent.Clear(); + http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("PushSharp", "3.0")); + http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "key=" + Configuration.SenderAuthToken); + http.DefaultRequestHeaders.TryAddWithoutValidation("Sender", "id=" + Configuration.SenderID); + } + + public FcmConfiguration Configuration { get; private set; } + + readonly HttpClient http; + + public async Task Send(FcmNotification notification) + { + var json = notification.GetJson(); + + var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); + + var response = await http.PostAsync(Configuration.FcmUrl, content); + + if (response.IsSuccessStatusCode) + { + await processResponseOk(response, notification).ConfigureAwait(false); + } + else + { + await processResponseError(response, notification).ConfigureAwait(false); + } + } + + async Task processResponseOk(HttpResponseMessage httpResponse, FcmNotification notification) + { + var multicastResult = new FcmMulticastResultException(); + + var result = new FcmResponse() + { + ResponseCode = FcmResponseCode.Ok, + OriginalNotification = notification + }; + + var str = await httpResponse.Content.ReadAsStringAsync(); + var json = JObject.Parse(str); + + result.NumberOfCanonicalIds = json.Value("canonical_ids"); + result.NumberOfFailures = json.Value("failure"); + result.NumberOfSuccesses = json.Value("success"); + result.MulticastId = json.Value("multicast_id"); + + var jsonResults = json["results"] as JArray ?? new JArray(); + + foreach (var r in jsonResults) + { + var msgResult = new FcmMessageResult(); + + msgResult.MessageId = r.Value("message_id"); + msgResult.CanonicalRegistrationId = r.Value("registration_id"); + msgResult.ResponseStatus = FcmResponseStatus.Ok; + + if (!string.IsNullOrEmpty(msgResult.CanonicalRegistrationId)) + msgResult.ResponseStatus = FcmResponseStatus.CanonicalRegistrationId; + else if (r["error"] != null) + { + var err = r.Value("error") ?? ""; + + msgResult.ResponseStatus = GetFcmResponseStatus(err); + } + + result.Results.Add(msgResult); + } + + int index = 0; + + //Loop through every result in the response + // We will raise events for each individual result so that the consumer of the library + // can deal with individual registrationid's for the notification + foreach (var r in result.Results) + { + var singleResultNotification = result.OriginalNotification.Clone(result.GetSingleRegistrationID(index)); + singleResultNotification.MessageId = r.MessageId; + + if (r.ResponseStatus == FcmResponseStatus.Ok) // Success + { + multicastResult.Succeeded.Add(singleResultNotification); + } + else if (r.ResponseStatus == FcmResponseStatus.CanonicalRegistrationId)//Need to swap reg id's, Swap Registrations Id's + { + var newRegistrationId = r.CanonicalRegistrationId; + var oldRegistrationId = string.Empty; + + if (singleResultNotification.RegistrationIds != null && singleResultNotification.RegistrationIds.Count > 0) + { + oldRegistrationId = singleResultNotification.RegistrationIds[0]; + } + else if (!string.IsNullOrEmpty(singleResultNotification.To)) + { + oldRegistrationId = singleResultNotification.To; + } + + multicastResult.Failed.Add(singleResultNotification, + new DeviceSubscriptionExpiredException(singleResultNotification) + { + OldSubscriptionId = oldRegistrationId, + NewSubscriptionId = newRegistrationId + }); + } + else if (r.ResponseStatus == FcmResponseStatus.Unavailable) // Unavailable + { + multicastResult.Failed.Add(singleResultNotification, new FcmNotificationException(singleResultNotification, "Unavailable Response Status")); + } + else if (r.ResponseStatus == FcmResponseStatus.NotRegistered) //Bad registration Id + { + var oldRegistrationId = string.Empty; + + if (singleResultNotification.RegistrationIds != null && singleResultNotification.RegistrationIds.Count > 0) + { + oldRegistrationId = singleResultNotification.RegistrationIds[0]; + } + else if (!string.IsNullOrEmpty(singleResultNotification.To)) + { + oldRegistrationId = singleResultNotification.To; + } + + multicastResult.Failed.Add(singleResultNotification, + new DeviceSubscriptionExpiredException(singleResultNotification) + { + OldSubscriptionId = oldRegistrationId + }); + } + else + { + multicastResult.Failed.Add(singleResultNotification, new FcmNotificationException(singleResultNotification, "Unknown Failure: " + r.ResponseStatus)); + } + + index++; + } + + // If we only have 1 total result, it is not *multicast*, + if (multicastResult.Succeeded.Count + multicastResult.Failed.Count == 1) + { + // If not multicast, and succeeded, don't throw any errors! + if (multicastResult.Succeeded.Count == 1) + return; + + // Otherwise, throw the one single failure we must have + throw multicastResult.Failed.First().Value; + } + + // If we get here, we must have had a multicast message + // throw if we had any failures at all (otherwise all must be successful, so throw no error + if (multicastResult.Failed.Count > 0) + throw multicastResult; + } + + async Task processResponseError(HttpResponseMessage httpResponse, FcmNotification notification) + { + string responseBody = null; + + try + { + responseBody = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false); + } + catch { } + + //401 bad auth token + if (httpResponse.StatusCode == HttpStatusCode.Unauthorized) + throw new UnauthorizedAccessException("FCM Authorization Failed"); + + if (httpResponse.StatusCode == HttpStatusCode.BadRequest) + throw new FcmNotificationException(notification, "HTTP 400 Bad Request", responseBody); + + if ((int)httpResponse.StatusCode >= 500 && (int)httpResponse.StatusCode < 600) + { + //First try grabbing the retry-after header and parsing it. + var retryAfterHeader = httpResponse.Headers.RetryAfter; + + if (retryAfterHeader != null && retryAfterHeader.Delta.HasValue) + { + var retryAfter = retryAfterHeader.Delta.Value; + throw new RetryAfterException(notification, "FCM Requested Backoff", DateTime.UtcNow + retryAfter); + } + } + + throw new FcmNotificationException(notification, "FCM HTTP Error: " + httpResponse.StatusCode, responseBody); + } + + + static FcmResponseStatus GetFcmResponseStatus(string str) + { + var enumType = typeof(FcmResponseStatus); + + foreach (var name in Enum.GetNames(enumType)) + { + var enumMemberAttribute = ((EnumMemberAttribute[])enumType.GetField(name).GetCustomAttributes(typeof(EnumMemberAttribute), true)).Single(); + + if (enumMemberAttribute.Value.Equals(str, StringComparison.InvariantCultureIgnoreCase)) + return (FcmResponseStatus)Enum.Parse(enumType, name); + } + + //Default + return FcmResponseStatus.Error; + } + } +} diff --git a/PushSharp.Firebase/Properties/AssemblyInfo.cs b/PushSharp.Firebase/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..f0416e42 --- /dev/null +++ b/PushSharp.Firebase/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PushSharp.Firebase")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("PushSharp.Firebase")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("f75a4e2f-59df-41c9-8f16-b335e42836d9")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/PushSharp.Firebase/PushSharp.Firebase.csproj b/PushSharp.Firebase/PushSharp.Firebase.csproj new file mode 100644 index 00000000..d0504fdc --- /dev/null +++ b/PushSharp.Firebase/PushSharp.Firebase.csproj @@ -0,0 +1,73 @@ + + + + + Debug + AnyCPU + {F75A4E2F-59DF-41C9-8F16-B335E42836D9} + Library + Properties + PushSharp.Firebase + PushSharp.Firebase + v4.6.1 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + + + + + + + + + + + + + + + + + + {2b44a8da-60bc-4577-a2d7-d9d53f164b2e} + PushSharp.Core + + + + + + + + \ No newline at end of file diff --git a/PushSharp.Firebase/packages.config b/PushSharp.Firebase/packages.config new file mode 100644 index 00000000..7ee8c105 --- /dev/null +++ b/PushSharp.Firebase/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/PushSharp.Tests/FcmRealTests.cs b/PushSharp.Tests/FcmRealTests.cs new file mode 100644 index 00000000..e6f717b0 --- /dev/null +++ b/PushSharp.Tests/FcmRealTests.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.ComponentModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; +using PushSharp.Firebase; + +namespace PushSharp.Tests +{ + [TestClass] + [Category("Real")] + public class FcmRealTests + { + [TestMethod] + public void Fcm_Send_Single() + { + var succeeded = 0; + var failed = 0; + var attempted = 0; + + var config = new FcmConfiguration(Settings.Instance.FcmSenderId, Settings.Instance.FcmAuthToken, null); + var broker = new FcmServiceBroker(config); + broker.OnNotificationFailed += (notification, exception) => + { + failed++; + }; + broker.OnNotificationSucceeded += (notification) => + { + succeeded++; + }; + + broker.Start(); + + foreach (var regId in Settings.Instance.FcmRegistrationIds) + { + attempted++; + + broker.QueueNotification(new FcmNotification + { + RegistrationIds = new List { regId }, + Data = JObject.Parse("{ \"somekey\" : \"somevalue\" }") + }); + } + + broker.Stop(); + + Assert.AreEqual(attempted, succeeded); + Assert.AreEqual(0, failed); + } + } +} \ No newline at end of file diff --git a/PushSharp.Tests/FcmTests.cs b/PushSharp.Tests/FcmTests.cs new file mode 100644 index 00000000..3da0a2de --- /dev/null +++ b/PushSharp.Tests/FcmTests.cs @@ -0,0 +1,34 @@ +using System.ComponentModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PushSharp.Firebase; + +namespace PushSharp.Tests +{ + [Category("FCM")] + [TestClass] + public class FcmTests + { + [TestMethod] + public void FcmNotification_Priority_Should_Serialize_As_String_High() + { + var n = new FcmNotification(); + n.Priority = FcmNotificationPriority.High; + + var str = n.ToString(); + + Assert.IsTrue(str.Contains("high")); + } + + [TestMethod] + public void FcmNotification_Priority_Should_Serialize_As_String_Normal() + { + var n = new FcmNotification(); + n.Priority = FcmNotificationPriority.Normal; + + var str = n.ToString(); + + Assert.IsTrue(str.Contains("normal")); + } + } +} + diff --git a/PushSharp.Tests/GcmRealTests.cs b/PushSharp.Tests/GcmRealTests.cs index 97ccda04..d46c8d0c 100644 --- a/PushSharp.Tests/GcmRealTests.cs +++ b/PushSharp.Tests/GcmRealTests.cs @@ -37,7 +37,7 @@ public void Gcm_Send_Single() broker.QueueNotification(new GcmNotification { RegistrationIds = new List { regId }, - Data = JObject.Parse("{ \"somekey1\" : \"somevalue 1\" }") + Data = JObject.Parse("{ \"somekey\" : \"somevalue\" }") }); } diff --git a/PushSharp.Tests/PushSharp.Tests.csproj b/PushSharp.Tests/PushSharp.Tests.csproj index 6e64fd96..07a3973c 100644 --- a/PushSharp.Tests/PushSharp.Tests.csproj +++ b/PushSharp.Tests/PushSharp.Tests.csproj @@ -40,6 +40,8 @@ + + @@ -65,6 +67,10 @@ {2468C63B-C964-4FC3-9B16-13DC17CF7D11} PushSharp.Amazon + + {f75a4e2f-59df-41c9-8f16-b335e42836d9} + PushSharp.Firebase + {94F16497-471F-433F-A99E-C455FB2D7724} PushSharp.Google diff --git a/PushSharp.Tests/Settings.cs b/PushSharp.Tests/Settings.cs index b045d6e0..4c1c9a37 100644 --- a/PushSharp.Tests/Settings.cs +++ b/PushSharp.Tests/Settings.cs @@ -61,6 +61,13 @@ public Settings() [JsonProperty("gcm_registration_ids")] public List GcmRegistrationIds { get; set; } + [JsonProperty("fcm_auth_token")] + public string FcmAuthToken { get; set; } + [JsonProperty("fcm_sender_id")] + public string FcmSenderId { get; set; } + [JsonProperty("fcm_registration_ids")] + public List FcmRegistrationIds { get; set; } + [JsonProperty("adm_client_id")] public string AdmClientId { get; set; } [JsonProperty("adm_client_secret")] diff --git a/PushSharp.sln b/PushSharp.sln index ec8c7689..3a994376 100644 --- a/PushSharp.sln +++ b/PushSharp.sln @@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PushSharp.Google", "PushSha EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PushSharp.Blackberry", "PushSharp.Blackberry\PushSharp.Blackberry.csproj", "{9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PushSharp.Firebase", "PushSharp.Firebase\PushSharp.Firebase.csproj", "{F75A4E2F-59DF-41C9-8F16-B335E42836D9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -59,6 +61,10 @@ Global {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Debug|Any CPU.Build.0 = Debug|Any CPU {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Release|Any CPU.ActiveCfg = Release|Any CPU {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12}.Release|Any CPU.Build.0 = Release|Any CPU + {F75A4E2F-59DF-41C9-8F16-B335E42836D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F75A4E2F-59DF-41C9-8F16-B335E42836D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F75A4E2F-59DF-41C9-8F16-B335E42836D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F75A4E2F-59DF-41C9-8F16-B335E42836D9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -70,5 +76,6 @@ Global {2468C63B-C964-4FC3-9B16-13DC17CF7D11} = {2B7243CB-60A5-4682-802B-B7EC3DEBCF9A} {94F16497-471F-433F-A99E-C455FB2D7724} = {2B7243CB-60A5-4682-802B-B7EC3DEBCF9A} {9F972AE9-DE47-4C26-AEE0-97E8A14F2E12} = {2B7243CB-60A5-4682-802B-B7EC3DEBCF9A} + {F75A4E2F-59DF-41C9-8F16-B335E42836D9} = {2B7243CB-60A5-4682-802B-B7EC3DEBCF9A} EndGlobalSection EndGlobal From cfa9fdcdd5e1368b39cd327084dd875bbf810511 Mon Sep 17 00:00:00 2001 From: Karimi Date: Sat, 29 Oct 2016 18:53:03 +0330 Subject: [PATCH 4/5] Add settings-sample.json to project --- PushSharp.Tests/FcmRealTests.cs | 1 + PushSharp.Tests/PushSharp.Tests.csproj | 1 + PushSharp.Tests/settings-sample.json | 25 +++++++++++++++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 PushSharp.Tests/settings-sample.json diff --git a/PushSharp.Tests/FcmRealTests.cs b/PushSharp.Tests/FcmRealTests.cs index e6f717b0..f9f7fc93 100644 --- a/PushSharp.Tests/FcmRealTests.cs +++ b/PushSharp.Tests/FcmRealTests.cs @@ -36,6 +36,7 @@ public void Fcm_Send_Single() broker.QueueNotification(new FcmNotification { + //To = "/topics/news", RegistrationIds = new List { regId }, Data = JObject.Parse("{ \"somekey\" : \"somevalue\" }") }); diff --git a/PushSharp.Tests/PushSharp.Tests.csproj b/PushSharp.Tests/PushSharp.Tests.csproj index 07a3973c..b3d7e249 100644 --- a/PushSharp.Tests/PushSharp.Tests.csproj +++ b/PushSharp.Tests/PushSharp.Tests.csproj @@ -82,6 +82,7 @@ + diff --git a/PushSharp.Tests/settings-sample.json b/PushSharp.Tests/settings-sample.json new file mode 100644 index 00000000..b33a64a4 --- /dev/null +++ b/PushSharp.Tests/settings-sample.json @@ -0,0 +1,25 @@ +{ + "apns_cert_file": "", + "apns_cert_pwd": "", + "apns_device_tokens": [ + "", + ], + + "gcm_auth_token": "", + "gcm_sender_id": "", + "gcm_registration_ids": [ + "", + ], + + "fcm_auth_token": "", + "fcm_sender_id": "", + "fcm_registration_ids": [ + "", + ], + + "adm_client_id": "", + "adm_client_secret": "", + "adm_registration_ids": [ + "", + ], +} From 75e11111499d80b8461d9b48cf8f436d3b0769e0 Mon Sep 17 00:00:00 2001 From: Karimi Date: Sat, 5 Nov 2016 14:56:40 +0330 Subject: [PATCH 5/5] Add AssemblyInfo to Firebase project --- PushSharp.Firebase/Properties/AssemblyInfo.cs | 31 +++++++------------ PushSharp.Firebase/PushSharp.Firebase.csproj | 6 ++++ 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/PushSharp.Firebase/Properties/AssemblyInfo.cs b/PushSharp.Firebase/Properties/AssemblyInfo.cs index f0416e42..5258b075 100644 --- a/PushSharp.Firebase/Properties/AssemblyInfo.cs +++ b/PushSharp.Firebase/Properties/AssemblyInfo.cs @@ -9,28 +9,19 @@ [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("PushSharp.Firebase")] -[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("redth")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("f75a4e2f-59df-41c9-8f16-b335e42836d9")] +[assembly: AssemblyVersion("1.0.*")] -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] \ No newline at end of file diff --git a/PushSharp.Firebase/PushSharp.Firebase.csproj b/PushSharp.Firebase/PushSharp.Firebase.csproj index d0504fdc..a53f01d9 100644 --- a/PushSharp.Firebase/PushSharp.Firebase.csproj +++ b/PushSharp.Firebase/PushSharp.Firebase.csproj @@ -30,6 +30,12 @@ prompt 4 + + true + + + ..\PushSharp-Signing.snk + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll