private PushEasyResult CreateClientAndStream(PushEasyConfiguration configuration, int port, out TcpClient client, out SslStream stream)
        {
            client = null;
            stream = null;

            // create certificate from path with password
            var certificate = new X509Certificate2(File.ReadAllBytes(configuration.APNSCertificatePath), configuration.APNSCertificatePassword, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
            // need a collection for some calls
            var certificates = new X509Certificate2Collection();

            certificates.Add(certificate);

            var host = !configuration.UseSandbox ? _hostLive : _hostSandbox;

            // connect to apple
            client = new TcpClient();
            client.Connect(host, port);
            client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);

            // open stream to write/read
            stream = new SslStream(client.GetStream(), false, (object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors policyErrors) => { return(true); }, (sender, targetHost, localCerts, remoteCert, acceptableIssuers) => certificate);
            try
            {
                stream.AuthenticateAsClient(host, certificates, System.Security.Authentication.SslProtocols.Tls, false);
            }
            catch (Exception ex)
            {
                return(new PushEasyResult(PushEasyResult.Results.Error, PushEasyResult.Errors.Connection, "Could not create SslStream. Error: " + ex.ToString()));
            }

            if (!stream.IsMutuallyAuthenticated)
            {
                return(new PushEasyResult(PushEasyResult.Results.Error, PushEasyResult.Errors.Connection, "Stream is not mutally authenticated."));
            }

            if (!stream.CanWrite)
            {
                return(new PushEasyResult(PushEasyResult.Results.Error, PushEasyResult.Errors.Connection, "Cannot write to stream."));
            }

            return(null);
        }
        internal override void Send(PushEasyConfiguration configuration, List <PushEasyNotification> notifications)
        {
            if (configuration.APNSCertificatePath == null || !File.Exists(configuration.APNSCertificatePath))
            {
                this.BaseProviderAssignResults(notifications, new PushEasyResult(PushEasyResult.Results.Error, PushEasyResult.Errors.Data, "APNSCertificatePath of PushEasyConfiguration not initialized or file does not exist."));
                return;
            }

            var tokens   = new List <byte[]>();
            var payloads = new List <byte[]>();

            // check for invalid tokens or payload
            var regexValidDeviceToken = new Regex(@"^[0-9A-F]+$", RegexOptions.IgnoreCase);

            foreach (var notification in notifications)
            {
                // check token validity by format
                if (!regexValidDeviceToken.Match(notification.Token).Success)
                {
                    notification.Result = new PushEasyResult(PushEasyResult.Results.Error, PushEasyResult.Errors.Device, "Invalid token format.");
                    continue;
                }

                var token = new byte[notification.Token.Length / 2];

                // try to convert data to apns format
                try
                {
                    for (int i = 0; i < token.Length; ++i)
                    {
                        token[i] = byte.Parse(notification.Token.Substring(i * 2, 2), System.Globalization.NumberStyles.HexNumber);
                    }
                }
                catch (Exception ex)
                {
                    notification.Result = new PushEasyResult(PushEasyResult.Results.Error, PushEasyResult.Errors.Device, "Could not convert token. Error: " + ex);
                    continue;
                }

                // check token length
                if (token.Length < 32)
                {
                    notification.Result = new PushEasyResult(PushEasyResult.Results.Error, PushEasyResult.Errors.Device, "Invalid token length.");
                    continue;
                }

                // create payload
                var jSONAps = new Dictionary <string, object>();
                // badge
                if (notification.Badge != null && notification.Badge.Value > 0)
                {
                    jSONAps.Add("badge", notification.Badge ?? 0);
                }
                // alert
                var jsonAlert = new Dictionary <string, object>();
                jsonAlert.Add("body", notification.Text);
                jSONAps.Add("alert", jsonAlert);
                if (notification.Sound != null)
                {
                    jSONAps.Add("sound", notification.Sound);
                }

                var jsonPayload = new Dictionary <string, object>();
                jsonPayload.Add("aps", jSONAps);

                // message payload
                if (notification.Payload != null && notification.Payload.Any())
                {
                    foreach (var entry in notification.Payload)
                    {
                        if (string.IsNullOrEmpty(entry.Key))
                        {
                            continue;
                        }

                        if (entry.Key.ToLower() == "aps")
                        {
                            continue;
                        }

                        jsonPayload.Add(entry.Key, this.BaseProviderComplexToSimple(entry.Value));
                    }
                }

                string error         = null;
                var    payloadString = this.BaseProviderToJsonString(jsonPayload, out error);

                if (error != null)
                {
                    notification.Result = new PushEasyResult(PushEasyResult.Results.Error, PushEasyResult.Errors.Data, "Could not convert APNS payload to json. Error: " + error);
                    continue;
                }

                var payload = Encoding.UTF8.GetBytes(payloadString);

                if (payload.Length > 2048)
                {
                    notification.Result = new PushEasyResult(PushEasyResult.Results.Error, PushEasyResult.Errors.Data, string.Format("APNS payload size ({0} bytes) exceeded max size of 2048 bytes.", payload.Length));
                    continue;
                }

                // at this point the notification is valid
                tokens.Add(token);
                payloads.Add(payload);
            }

            // filter invalid notifications
            for (var i = 0; i < notifications.Count; ++i)
            {
                if (notifications[i].Result.Result == PushEasyResult.Results.Error)
                {
                    notifications.RemoveAt(i);
                    i -= 1;
                }
            }

            if (notifications.Count == 0)
            {
                return;
            }

            TcpClient client = null;
            SslStream stream = null;

            var result = this.CreateClientAndStream(configuration, _portSend, out client, out stream);

            if (result != null)
            {
                this.BaseProviderAssignResults(notifications, result);
                return;
            }

            // get data to write to stream
            var data = new List <byte>();

            for (var index = 0; index < notifications.Count; ++index)
            {
                // create notification data
                var dataNotification = new List <byte>();

                // 1. Device Token
                dataNotification.Add(0x01);
                dataNotification.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(Convert.ToInt16(tokens[index].Length))));
                dataNotification.AddRange(tokens[index]);

                // 2. Payload
                dataNotification.Add(0x02);
                dataNotification.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(Convert.ToInt16(payloads[index].Length))));
                dataNotification.AddRange(payloads[index]);

                // 3. Identifier
                dataNotification.Add(0x03);
                dataNotification.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((Int16)4)));
                dataNotification.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(index)));

                // 4. Expiration
                dataNotification.Add(0x04);
                dataNotification.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((Int16)4)));
                dataNotification.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((int)DateTime.UtcNow.AddMonths(1).Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds)));

                // 5. Priority
                dataNotification.Add(0x05);
                dataNotification.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((Int16)1)));
                dataNotification.Add(5);        //LowPriority ? (byte)5 : (byte)10;

                data.Add(0x02);                 // COMMAND 2 for new format
                data.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((Int32)dataNotification.Count)));
                data.AddRange(dataNotification);
            }

            // check for response
            var errorIndex  = -1;
            var errorStatus = APNSErrorStatusCodes.Unknown;

            var startedOn = DateTime.UtcNow;

            if (data.Count > 0)
            {
                // write the data
                stream.Write(data.ToArray(), 0, data.Count);

                for (var i = 0; i < 10; ++i)
                {
                    // give apple some time to write to the socket
                    Thread.Sleep(100);

                    // check if something available
                    if (client.Client.Available > 0)
                    {
                        var buffer = new byte[6];
                        var length = stream.Read(buffer, 0, buffer.Length);

                        if (length > 0)
                        {
                            var status = (int)buffer[1];

                            // If we made it here, we did receive some data, so let's parse the error
                            errorStatus = Enum.IsDefined(typeof(APNSErrorStatusCodes), status) ? (APNSErrorStatusCodes)status : APNSErrorStatusCodes.Unknown;
                            // get the identifier of the device failing
                            errorIndex = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(buffer, 2));
                        }

                        break;
                    }
                }
            }

            var completedOn = DateTime.UtcNow;

            this.DisposeClientAndStream(client, stream);

            for (var index = 0; index < notifications.Count; ++index)
            {
                var notification = notifications[index];

                // error received?
                if (index == errorIndex)
                {
                    var error = PushEasyResult.Errors.Unknown;
                    switch (errorStatus)
                    {
                    case APNSErrorStatusCodes.MissingDeviceToken:
                    case APNSErrorStatusCodes.InvalidToken:
                        error = PushEasyResult.Errors.Device;
                        break;

                    case APNSErrorStatusCodes.MissingTopic:
                    case APNSErrorStatusCodes.MissingPayload:
                    case APNSErrorStatusCodes.InvalidTokenSize:
                    case APNSErrorStatusCodes.InvalidTopicSize:
                    case APNSErrorStatusCodes.InvalidPayloadSize:
                    case APNSErrorStatusCodes.NoErrors:
                    case APNSErrorStatusCodes.ProcessingError:
                    case APNSErrorStatusCodes.Shutdown:
                    case APNSErrorStatusCodes.ConnectionError:
                    case APNSErrorStatusCodes.Unknown:
                        error = PushEasyResult.Errors.Data;
                        break;
                    }

                    notification.Result = new PushEasyResult(PushEasyResult.Results.Error, error, "APNS returned an error: " + errorStatus.ToString(), startedOn);

                    // since apple will cancel after the first error, we need to trigger the next sending operation
                    if (index < notifications.Count - 1)
                    {
                        this.Send(configuration, notifications.Skip(index + 1).ToList());
                    }

                    break;
                }

                // no error
                notification.Result = new PushEasyResult(PushEasyResult.Results.Success, startedOn, completedOn);
            }
        }
        internal override IEnumerable <PushEasyNotification> Check(PushEasyConfiguration configuration)
        {
            TcpClient client = null;
            SslStream stream = null;

            var result = this.CreateClientAndStream(configuration, _portCheck, out client, out stream);

            if (result != null)
            {
                return(new List <PushEasyNotification> {
                    new PushEasyNotification {
                        Result = result
                    }
                });
            }

            var buffer    = new byte[4096];
            var bytesRead = 0;
            var data      = new List <byte>();

            // get all data from apple
            // it could be many, but since we create a complete list of notifications anyway, we can do it that way, to keep the stream up shortly
            while (true)
            {
                try
                {
                    bytesRead = stream.Read(buffer, 0, buffer.Length);
                }
                catch
                {
                    break;
                }

                // completed?
                if (bytesRead == 0)
                {
                    break;
                }

                // append to possible previous data
                for (int i = 0; i < bytesRead; i++)
                {
                    data.Add(buffer[i]);
                }
            }

            this.DisposeClientAndStream(client, stream);

            var notifications = new List <PushEasyNotification>();

            var lengthSeconds     = 4;
            var lengthTokenLength = 2;
            var lengthTokenMin    = 32;
            // calculate minimum size for a valid packet
            var lengthMin = lengthSeconds + lengthTokenLength + lengthTokenMin;

            // proccess data
            // (we dont care for the timestamp in simple push, so its commented out)
            while (data.Count >= lengthMin)
            {
                // get seconds buffer
                // var secondsBuffer = data.GetRange(0, lengthSeconds).ToArray();
                // get token length buffer (not the token itself, only the length)
                var tokenLengthBuffer = data.GetRange(lengthSeconds, lengthTokenLength).ToArray();

                // Check endianness and reverse if needed
                if (BitConverter.IsLittleEndian)
                {
                    // Array.Reverse(secondsBuffer);
                    Array.Reverse(tokenLengthBuffer);
                }

                // get the actual length of the token
                var lengthToken = BitConverter.ToInt16(tokenLengthBuffer, 0);
                // at this point we finaly know the whole length of the package
                var lengthTotal = lengthSeconds + lengthTokenLength + lengthToken;

                // sanity check if we received enough data
                if (data.Count < lengthTotal)
                {
                    break;
                }

                // get timestamp
                // var seconds = BitConverter.ToInt32(secondsBuffer, 0);
                // var timestamp = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(seconds);

                // get token
                var tokenBuffer = data.GetRange(lengthSeconds + lengthTokenLength, lengthToken).ToArray();
                var token       = BitConverter.ToString(tokenBuffer).Replace("-", "").ToLower().Trim();

                // Remove what we parsed from the buffer
                data.RemoveRange(0, lengthTotal);

                notifications.Add(new PushEasyNotification {
                    Device = PushEasyNotification.Devices.iOS, Token = token, Result = new PushEasyResult(PushEasyResult.Results.Error, PushEasyResult.Errors.Device)
                });
            }

            return(notifications);
        }
Beispiel #4
0
 /// <summary>
 /// Checks for invalidated devices.
 /// </summary>
 /// <param name="configuration"></param>
 /// <returns></returns>
 internal abstract IEnumerable <PushEasyNotification> Check(PushEasyConfiguration configuration);
Beispiel #5
0
 /// <summary>
 /// Sends notifications with the given configuration.
 /// </summary>
 /// <param name="configuration"></param>
 /// <param name="notifications"></param>
 internal abstract void Send(PushEasyConfiguration configuration, List <PushEasyNotification> notifications);
        internal override void Send(PushEasyConfiguration configuration, List <PushEasyNotification> notifications)
        {
            // create payload
            // all registrationIds of the devices to send to
            var registrationIds = new List <string>();

            foreach (var notification in notifications)
            {
                registrationIds.Add(notification.Token);
            }

            // data is equal among devices, since they are grouped
            var notificationFirst = notifications.FirstOrDefault();
            var data = new Dictionary <string, object>();

            // text
            data.Add("text", notificationFirst.Text);
            // sound
            if (notificationFirst.Sound != null)
            {
                data.Add("sound", notificationFirst.Sound);
            }
            // message payload
            if (notificationFirst.Payload != null && notificationFirst.Payload != null)
            {
                foreach (var entry in notificationFirst.Payload)
                {
                    if (entry.Key == null)
                    {
                        continue;
                    }

                    if (entry.Key.ToLower() == "text")
                    {
                        continue;
                    }

                    if (entry.Key.ToLower() == "sound")
                    {
                        continue;
                    }

                    data.Add(entry.Key, this.BaseProviderComplexToSimple(entry.Value));
                }
            }

            // the payload for FireBase api
            var json = new Dictionary <string, object>();

            json.Add("data", data);
            json.Add("registration_ids", registrationIds);

            // create request body
            string error             = null;
            var    jsonRequestString = this.BaseProviderToJsonString(json, out error);

            if (error != null)
            {
                this.BaseProviderAssignResults(notifications, new PushEasyResult(PushEasyResult.Results.Error, PushEasyResult.Errors.Data, "Could not convert FireBase payload to json. Error: " + error));
                return;
            }

            var startedOn = DateTime.UtcNow;

            // send to FireBase and receive response string
            byte[] jsonResponseData = null;
            using (var client = new WebClient())
            {
                client.Headers[HttpRequestHeader.ContentType]   = "application/json";
                client.Headers[HttpRequestHeader.Authorization] = "key=" + configuration.FirebaseProjectAPIKey;

                try
                {
                    jsonResponseData = client.UploadData(_firebaseUrl, Encoding.UTF8.GetBytes(jsonRequestString));
                }
                catch (Exception ex)
                {
                    this.BaseProviderAssignResults(notifications, new PushEasyResult(PushEasyResult.Results.Error, PushEasyResult.Errors.Connection, "Did not get a response from FireBase. Error: " + ex.ToString(), startedOn));
                    return;
                }
            }

            var completedOn = DateTime.UtcNow;

            var jsonResponseString = Encoding.UTF8.GetString(jsonResponseData);

            // parse to objects
            error = null;
            Dictionary <string, object> jsonResponse = this.BaseProviderFromJsonString <Dictionary <string, object> >(jsonResponseString, out error);

            if (error != null)
            {
                this.BaseProviderAssignResults(notifications, new PushEasyResult(PushEasyResult.Results.Error, PushEasyResult.Errors.Data, "Could not convert FireBase json response. Error: " + error, startedOn));
                return;
            }

            // check for results
            if (jsonResponse == null || !jsonResponse.ContainsKey("results") || !(jsonResponse["results"] is object[]))
            {
                this.BaseProviderAssignResults(notifications, new PushEasyResult(PushEasyResult.Results.Error, PushEasyResult.Errors.Data, "Invalid response from FireBase. results array missing.", startedOn));
                return;
            }

            var jsonResults = jsonResponse["results"] as object[];

            if (jsonResults.Length != notifications.Count())
            {
                this.BaseProviderAssignResults(notifications, new PushEasyResult(PushEasyResult.Results.Error, PushEasyResult.Errors.Data, string.Format("Invalid results count ({0}) from FireBase. Did match to notifications ({1}).", jsonResults.Length, notifications.Count), startedOn));
                return;
            }

            // did receive valid results from FireBase
            // check devices
            for (var i = 0; i < jsonResults.Length; ++i)
            {
                var jsonResult   = jsonResults[i] as Dictionary <string, object>;
                var notification = notifications[i];

                if (jsonResult == null)
                {
                    notification.Result = new PushEasyResult(PushEasyResult.Results.Error, PushEasyResult.Errors.Data, "Missing result in response for that device", startedOn);
                }
                else if (jsonResult.ContainsKey("message_id"))
                {
                    notification.Result = new PushEasyResult(PushEasyResult.Results.Success, startedOn, completedOn);
                }
                else if (jsonResult.ContainsKey("error"))
                {
                    notification.Result = new PushEasyResult(PushEasyResult.Results.Error, PushEasyResult.Errors.Device, jsonResult["error"].ToString(), startedOn);
                }
                else
                {
                    notification.Result = new PushEasyResult(PushEasyResult.Results.Error, PushEasyResult.Errors.Data, "Unknown response from FireBase for device. Response: " + string.Join(", ", jsonResult), startedOn);
                }
            }
        }
 internal override IEnumerable <PushEasyNotification> Check(PushEasyConfiguration configuration)
 {
     // not supported by firebase
     return(null);
 }