diff --git a/PushSharp.Apple/ApnsConfiguration.cs b/PushSharp.Apple/ApnsConfiguration.cs index 464a5e62..6a16a870 100644 --- a/PushSharp.Apple/ApnsConfiguration.cs +++ b/PushSharp.Apple/ApnsConfiguration.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Collections.Generic; +using System.Net; using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; @@ -82,7 +83,7 @@ void Initialize (ApnsServerEnvironment serverEnvironment, X509Certificate2 certi FeedbackIntervalMinutes = 10; FeedbackTimeIsUTC = false; - + AdditionalCertificates = new List (); AddLocalAndMachineCertificateStores = false; @@ -100,7 +101,6 @@ void Initialize (ApnsServerEnvironment serverEnvironment, X509Certificate2 certi InternalBatchFailureRetryCount = 1; } - void CheckIsApnsCertificate () { if (Certificate != null) { @@ -136,6 +136,22 @@ public void OverrideFeedbackServer (string host, int port) FeedbackPort = port; } + public void SetProxy(string proxyHost, int proxyPort) + { + UseProxy = true; + ProxyHost = proxyHost; + ProxyPort = proxyPort; + ProxyCredentials = CredentialCache.DefaultNetworkCredentials; + } + + public void SetProxy(string proxyHost, int proxyPort, string userName, string password, string domain) + { + UseProxy = true; + ProxyHost = proxyHost; + ProxyPort = proxyPort; + ProxyCredentials = new NetworkCredential(userName, password, domain); + } + public string Host { get; private set; } public int Port { get; private set; } @@ -144,6 +160,14 @@ public void OverrideFeedbackServer (string host, int port) public int FeedbackPort { get; private set; } + public bool UseProxy { get; private set; } + + public string ProxyHost { get; private set; } + + public int ProxyPort { get; private set; } + + public NetworkCredential ProxyCredentials { get; private set; } + public X509Certificate2 Certificate { get; private set; } public List AdditionalCertificates { get; private set; } @@ -205,4 +229,4 @@ public enum ApnsServerEnvironment { Production } } -} \ No newline at end of file +} diff --git a/PushSharp.Apple/ApnsConnection.cs b/PushSharp.Apple/ApnsConnection.cs index 0908b166..9414c4f2 100644 --- a/PushSharp.Apple/ApnsConnection.cs +++ b/PushSharp.Apple/ApnsConnection.cs @@ -7,7 +7,11 @@ using System.Collections.Generic; using System.Threading; using System.Net; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; using PushSharp.Core; +using System.Diagnostics; namespace PushSharp.Apple { @@ -68,7 +72,6 @@ public ApnsConnection (ApnsConfiguration configuration) byte[] buffer = new byte[6]; int id; - SemaphoreSlim connectingSemaphore = new SemaphoreSlim (1); SemaphoreSlim batchSendSemaphore = new SemaphoreSlim (1); object notificationBatchQueueLock = new object (); @@ -127,28 +130,37 @@ async Task SendBatch () Log.Info ("APNS-Client[{0}]: Sending Batch ID={1}, Count={2}", id, batchId, toSend.Count); - try { + try + { var data = createBatch (toSend); - if (data != null && data.Length > 0) { + if (data != null && data.Length > 0) + { - for (var i = 0; i <= Configuration.InternalBatchFailureRetryCount; i++) { + for (var i = 0; i <= Configuration.InternalBatchFailureRetryCount; i++) + { await connectingSemaphore.WaitAsync (); - try { + try + { // See if we need to connect if (!socketCanWrite () || i > 0) await connect (); - } finally { + } + finally + { connectingSemaphore.Release (); } - try { + try + { await networkStream.WriteAsync(data, 0, data.Length).ConfigureAwait(false); break; - } catch (Exception ex) when (i != Configuration.InternalBatchFailureRetryCount) { + } + catch (Exception ex) when (i != Configuration.InternalBatchFailureRetryCount) + { Log.Info("APNS-CLIENT[{0}]: Retrying Batch: Batch ID={1}, Error={2}", id, batchId, ex); } } @@ -156,11 +168,18 @@ async Task SendBatch () foreach (var n in toSend) sent.Add (new SentNotification (n)); } - - } catch (Exception ex) { + } + catch (ApnsConnectionException 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; + } + 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); @@ -239,7 +258,7 @@ async Task Reader () 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); @@ -321,25 +340,48 @@ async Task connect () Log.Info ("APNS-Client[{0}]: Connecting (Batch ID={1})", id, batchId); - client = new TcpClient (); + client = new TcpClient(); - try { - await client.ConnectAsync (Configuration.Host, Configuration.Port).ConfigureAwait (false); + try + { + if (!Configuration.UseProxy) + { + await client.ConnectAsync (Configuration.Host, Configuration.Port).ConfigureAwait (false); + } + else + { + var proxyHelper = new ProxyHelper { ProxyConnectionExceptionCreator = (message) => new ApnsConnectionException(message) }; + proxyHelper.BeforeConnect += () => Log.Info("APNS-Client[{0}]: Connecting Proxy (Batch ID={1})", id, batchId); + proxyHelper.AfterConnect += (status) => Log.Info("APNS-Client[{0}]: Proxy Connected (Batch ID={1}) : {2}", id, batchId, status); + await proxyHelper.Connect(client, Configuration.Host, Configuration.Port, Configuration.ProxyHost, Configuration.ProxyPort, Configuration.ProxyCredentials).ConfigureAwait(false); + } //Set keep alive on the socket may help maintain our APNS connection - try { - client.Client.SetSocketOption (SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); - } catch { + 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 { + try + { + SetSocketKeepAliveValues(client.Client, (int)Configuration.KeepAlivePeriod.TotalMilliseconds, (int)Configuration.KeepAliveRetryPeriod.TotalMilliseconds); } - } catch (Exception ex) { - throw new ApnsConnectionException ("Failed to Connect, check your firewall settings!", ex); + catch + { + } + } + catch (ApnsConnectionException) + { + throw; + } + 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 diff --git a/PushSharp.Apple/ApnsFeedbackService.cs b/PushSharp.Apple/ApnsFeedbackService.cs index 57fb887a..fdf17087 100644 --- a/PushSharp.Apple/ApnsFeedbackService.cs +++ b/PushSharp.Apple/ApnsFeedbackService.cs @@ -8,6 +8,7 @@ using System.Net.Security; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using PushSharp.Core; namespace PushSharp.Apple { @@ -24,6 +25,11 @@ public FeedbackService (ApnsConfiguration configuration) public event FeedbackReceivedDelegate FeedbackReceived; public void Check () + { + this.GetTokenExpirations(); + + } + public IEnumerable GetTokenExpirations() { var encoding = Encoding.ASCII; @@ -32,9 +38,22 @@ public void Check () var certificates = new X509CertificateCollection(); certificates.Add(certificate); - var client = new TcpClient (Configuration.FeedbackHost, Configuration.FeedbackPort); + var client = new TcpClient(); + Log.Info("APNS-FeedbackService: Connecting"); + if (Configuration.UseProxy) + { + var proxyHelper = new ProxyHelper { ProxyConnectionExceptionCreator = (message) => new ApnsConnectionException(message) }; + proxyHelper.BeforeConnect += () => Log.Info("APNS-FeedbackService: Connecting Proxy"); + proxyHelper.AfterConnect += (status) => Log.Info("APNS-FeedbackService: Proxy Connected : {0}", status); + proxyHelper.Connect(client, Configuration.FeedbackHost, Configuration.FeedbackPort, Configuration.ProxyHost, Configuration.ProxyPort, Configuration.ProxyCredentials).Wait(); + } + else + { + client.Connect(Configuration.FeedbackHost, Configuration.FeedbackPort); + } + Log.Info("APNS-FeedbackService: Connected"); - var stream = new SslStream (client.GetStream(), true, + var stream = new SslStream(client.GetStream(), true, (sender, cert, chain, sslErrs) => { return true; }, (sender, targetHost, localCerts, remoteCert, acceptableIssuers) => { return certificate; }); @@ -44,32 +63,35 @@ public void Check () //Set up byte[] buffer = new byte[4096]; int recd = 0; - var data = new List (); + var data = new List(); + + Log.Info("APNS-FeedbackService: Getting expirations"); //Get the first feedback recd = stream.Read(buffer, 0, buffer.Length); + var tokenBatch = new List(); //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]); - + 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 (); + 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); + 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); + 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) @@ -77,50 +99,68 @@ public void Check () if (BitConverter.IsLittleEndian) - Array.Reverse (tokenLengthBuffer); - var tokenLength = BitConverter.ToInt16 (tokenLengthBuffer, 0); + Array.Reverse(tokenLengthBuffer); + var tokenLength = BitConverter.ToInt16(tokenLengthBuffer, 0); - if (data.Count >= 4 + 2 + tokenLength) { + if (data.Count >= 4 + 2 + tokenLength) + { - var tokenBuffer = data.GetRange (6, tokenLength).ToArray (); + 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 (); + var token = BitConverter.ToString(tokenBuffer).Replace("-", "").ToLower().Trim(); // Remove what we parsed from the buffer - data.RemoveRange (0, 4 + 2 + tokenLength); + data.RemoveRange(0, 4 + 2 + tokenLength); + tokenBatch.Add(new ApnsTokeExpirationInfo(token, timestamp)); // Raise the event to the consumer var evt = FeedbackReceived; if (evt != null) - evt (token, timestamp); - } else { + evt(token, timestamp); + + } + else + { continue; } - } //Read the next feedback - recd = stream.Read (buffer, 0, buffer.Length); + recd = stream.Read(buffer, 0, buffer.Length); } try { - stream.Close (); + stream.Close(); stream.Dispose(); } catch { } - try + try { - client.Client.Shutdown (SocketShutdown.Both); - client.Client.Dispose (); + client.Client.Shutdown(SocketShutdown.Both); + client.Client.Dispose(); } catch { } - try { client.Close (); } catch { } + try { client.Close(); } catch { } + Log.Info("APNS-FeedbackService: {0} expiration(s) received.", tokenBatch.Count); + return tokenBatch; } } + + public class ApnsTokeExpirationInfo + { + public ApnsTokeExpirationInfo(string token, DateTime timestamp) + { + this.Token = token; + this.Timestamp = timestamp; + } + + public string Token { get; private set; } + public DateTime Timestamp { get; private set; } + } } diff --git a/PushSharp.Apple/PushSharp.Apple.csproj b/PushSharp.Apple/PushSharp.Apple.csproj index 3c9344a0..c43093ef 100644 --- a/PushSharp.Apple/PushSharp.Apple.csproj +++ b/PushSharp.Apple/PushSharp.Apple.csproj @@ -1,4 +1,4 @@ - + Debug diff --git a/PushSharp.Core/ProxyHelper.cs b/PushSharp.Core/ProxyHelper.cs new file mode 100644 index 00000000..9a6506c6 --- /dev/null +++ b/PushSharp.Core/ProxyHelper.cs @@ -0,0 +1,86 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace PushSharp.Core +{ + public delegate void BeforeConnectDelegate(); + + public delegate void AfterConnectDelegate(string status); + + public class ProxyHelper + { + public Func ProxyConnectionExceptionCreator { get; set; } + public event BeforeConnectDelegate BeforeConnect; + public event AfterConnectDelegate AfterConnect; + + public async Task Connect(TcpClient client, string targetHost, int targetPort, string proxyHost, int proxyPort, NetworkCredential proxyCredential = null) + { + this.OnBeforeConnect(); + await client.ConnectAsync(proxyHost, proxyPort).ConfigureAwait(false); + var stream = client.GetStream(); + var authorization = string.Empty; + if (proxyCredential != null) + { + var credential = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{proxyCredential.UserName}:{proxyCredential.Password}")); + authorization = $"\r\nAuthorization: Basic {credential}"; + } + var buffer = + Encoding.UTF8.GetBytes( + string.Format("CONNECT {0}:{1} HTTP/1.1\r\nHost: {0}:{1}{2}\r\nProxy-Connection: keep-alive\r\n\r\n", targetHost, targetPort, authorization)); + await stream.WriteAsync(buffer, 0, buffer.Length); + await stream.FlushAsync(); + buffer = new byte[client.Client.ReceiveBufferSize]; + using (var resp = new MemoryStream()) + { + do + { + var len = await stream.ReadAsync(buffer, 0, buffer.Length); + resp.Write(buffer, 0, len); + } + while (client.Client.Available > 0); + buffer = resp.ToArray(); + } + var content = Encoding.UTF8.GetString(buffer); + var i = content.IndexOf('\r'); + string statusCode; + if (i > 9) + { + statusCode = content.Substring(9, i - 9); + } + else + { + var max = content.Length; + if (max > 50) + { + max = 50; + } + var append = max == 50 ? "..." : string.Empty; + statusCode = content.Substring(0, max) + append; + } + this.OnAfterConnect(statusCode); + if (!statusCode.StartsWith("200")) + { + var fac = this.ProxyConnectionExceptionCreator; + if (fac != null) + { + throw fac($"Proxy returned {statusCode}. Check proxy settings and if it allows ssl through ports different of 443."); + } + throw new Exception($"Proxy returned {statusCode}. Check proxy settings and if it allows ssl through ports different of 443."); + } + } + + protected virtual void OnBeforeConnect() + { + this.BeforeConnect?.Invoke(); + } + + protected virtual void OnAfterConnect(string status) + { + this.AfterConnect?.Invoke(status); + } + } +} \ No newline at end of file diff --git a/PushSharp.Core/PushSharp.Core.csproj b/PushSharp.Core/PushSharp.Core.csproj index e46f9756..f78d4861 100644 --- a/PushSharp.Core/PushSharp.Core.csproj +++ b/PushSharp.Core/PushSharp.Core.csproj @@ -1,4 +1,4 @@ - + Debug @@ -38,6 +38,7 @@ + diff --git a/PushSharp.Google/GcmConfiguration.cs b/PushSharp.Google/GcmConfiguration.cs index 91f320ff..f0939105 100644 --- a/PushSharp.Google/GcmConfiguration.cs +++ b/PushSharp.Google/GcmConfiguration.cs @@ -1,4 +1,5 @@ using System; +using System.Net; namespace PushSharp.Google { @@ -24,6 +25,30 @@ public GcmConfiguration (string optionalSenderID, string senderAuthToken, string this.ValidateServerCertificate = false; } + public void SetProxy(string host, int port) + { + UseProxy = true; + ProxyHost = host; + ProxyPort = port; + ProxyCredentials = CredentialCache.DefaultNetworkCredentials; + } + + public void SetProxy(string host, int port, string username, string pass, string domain) + { + UseProxy = true; + ProxyHost = host; + ProxyPort = port; + ProxyCredentials = new NetworkCredential(username, pass, domain); + } + + public bool UseProxy { get; private set; } + + public string ProxyHost { get; private set; } + + public int ProxyPort { get; private set; } + + public NetworkCredential ProxyCredentials { get; private set; } + public string SenderID { get; private set; } public string SenderAuthToken { get; private set; } diff --git a/PushSharp.Google/GcmServiceConnection.cs b/PushSharp.Google/GcmServiceConnection.cs index b3b539c5..1e1f4e4f 100644 --- a/PushSharp.Google/GcmServiceConnection.cs +++ b/PushSharp.Google/GcmServiceConnection.cs @@ -38,8 +38,14 @@ public class GcmServiceConnection : IServiceConnection public GcmServiceConnection (GcmConfiguration configuration) { Configuration = configuration; - http = new HttpClient (); - + if (Configuration.UseProxy) + { + http = GetClientHTTPProxy(Configuration.ProxyHost, Configuration.ProxyPort, Configuration.ProxyCredentials); + } + else + { + http = new HttpClient(); + } http.DefaultRequestHeaders.UserAgent.Clear (); http.DefaultRequestHeaders.UserAgent.Add (new ProductInfoHeaderValue ("PushSharp", "3.0")); http.DefaultRequestHeaders.TryAddWithoutValidation ("Authorization", "key=" + Configuration.SenderAuthToken); @@ -212,5 +218,31 @@ static GcmResponseStatus GetGcmResponseStatus (string str) //Default return GcmResponseStatus.Error; } + + private HttpClient GetClientHTTPProxy(string host, int port, NetworkCredential credentials) + { + HttpClient client = null; + try + { + string proxyUri = string.Format("{0}:{1}", host, port); + WebProxy proxy = new WebProxy(proxyUri, false) + { + Credentials = credentials + }; + HttpClientHandler httpClientHandler = new HttpClientHandler() + { + Proxy = proxy, + PreAuthenticate = false + }; + client = new HttpClient(httpClientHandler); + } + catch (Exception ex) + { + client = null; + Log.Error(ex.Message); + throw; + } + return client; + } } }