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); }
/// <summary> /// Checks for invalidated devices. /// </summary> /// <param name="configuration"></param> /// <returns></returns> internal abstract IEnumerable <PushEasyNotification> Check(PushEasyConfiguration configuration);
/// <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); }