diff --git a/Inotify/Common/XmlHelper.cs b/Inotify/Common/XmlHelper.cs new file mode 100644 index 0000000..04bed4b --- /dev/null +++ b/Inotify/Common/XmlHelper.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Serialization; + +namespace Inotify.Common +{ + public static class XmlHelper + { + private static void XmlSerializeInternal(Stream stream, object o, Encoding encoding) + { + if (o == null) + throw new ArgumentNullException("o"); + if (encoding == null) + throw new ArgumentNullException("encoding"); + + XmlSerializer serializer = new XmlSerializer(o.GetType()); + + XmlWriterSettings settings = new XmlWriterSettings(); + settings.Indent = true; + settings.NewLineChars = "\r\n"; + settings.Encoding = encoding; + settings.IndentChars = " "; + + using (XmlWriter writer = XmlWriter.Create(stream, settings)) + { + serializer.Serialize(writer, o); + writer.Close(); + } + } + + /// + /// 将一个对象序列化为XML字符串 + /// + /// 要序列化的对象 + /// 编码方式 + /// 序列化产生的XML字符串 + public static string XmlSerialize(object o, Encoding encoding) + { + using (MemoryStream stream = new MemoryStream()) + { + XmlSerializeInternal(stream, o, encoding); + + stream.Position = 0; + using (StreamReader reader = new StreamReader(stream, encoding)) + { + return reader.ReadToEnd(); + } + } + } + + /// + /// 将一个对象按XML序列化的方式写入到一个文件 + /// + /// 要序列化的对象 + /// 保存文件路径 + /// 编码方式 + public static void XmlSerializeToFile(object o, string path, Encoding encoding) + { + if (string.IsNullOrEmpty(path)) + throw new ArgumentNullException("path"); + + using (FileStream file = new FileStream(path, FileMode.Create, FileAccess.Write)) + { + XmlSerializeInternal(file, o, encoding); + } + } + + /// + /// 从XML字符串中反序列化对象 + /// + /// 结果对象类型 + /// 包含对象的XML字符串 + /// 编码方式 + /// 反序列化得到的对象 + public static T XmlDeserialize(string s, Encoding encoding) + { + if (string.IsNullOrEmpty(s)) + throw new ArgumentNullException("s"); + if (encoding == null) + throw new ArgumentNullException("encoding"); + + XmlSerializer mySerializer = new XmlSerializer(typeof(T)); + using (MemoryStream ms = new MemoryStream(encoding.GetBytes(s))) + { + using (StreamReader sr = new StreamReader(ms, encoding)) + { + return (T)mySerializer.Deserialize(sr); + } + } + } + + /// + /// 读入一个文件,并按XML的方式反序列化对象。 + /// + /// 结果对象类型 + /// 文件路径 + /// 编码方式 + /// 反序列化得到的对象 + public static T XmlDeserializeFromFile(string path, Encoding encoding) + { + if (string.IsNullOrEmpty(path)) + throw new ArgumentNullException("path"); + if (encoding == null) + throw new ArgumentNullException("encoding"); + + string xml = File.ReadAllText(path, encoding); + return XmlDeserialize(xml, encoding); + } + } +} diff --git a/Inotify/Controllers/BarkControlor.cs b/Inotify/Controllers/BarkControllor.cs similarity index 93% rename from Inotify/Controllers/BarkControlor.cs rename to Inotify/Controllers/BarkControllor.cs index 1124545..4675201 100644 --- a/Inotify/Controllers/BarkControlor.cs +++ b/Inotify/Controllers/BarkControllor.cs @@ -14,7 +14,7 @@ namespace Inotify.Controllers [ApiController] [Route("/")] - public class BarkControlor : BaseControlor + public class BarkControllor : BaseController { [HttpGet, Route("Ping")] public JsonResult Ping() @@ -47,8 +47,7 @@ namespace Inotify.Controllers [HttpGet, Route("Register")] public JsonResult Register(string? act, string? key, string? devicetoken, string? device_key) { - return !string.IsNullOrEmpty(device_key) ? -Register(device_key) : Register(act, key, devicetoken); + return !string.IsNullOrEmpty(device_key) ? Register(device_key) : Register(act, key, devicetoken); } [HttpPost, Route("Register")] @@ -90,7 +89,8 @@ Register(device_key) : Register(act, key, devicetoken); if (barkSendAuthInfo == null) { - device_key = Guid.NewGuid().ToString("N").ToUpper(); + if(string.IsNullOrEmpty(device_key)) + device_key = Guid.NewGuid().ToString("N").ToUpper(); barkAuth = new BarkAuth() { DeviceKey = device_key, DeviceToken = device_token, IsArchive = "1", AutoMaticallyCopy = "1", Sound = "1107" }; barkSendAuthInfo = new SendAuthInfo() { diff --git a/Inotify/Controllers/BaseControlor.cs b/Inotify/Controllers/BaseController.cs similarity index 73% rename from Inotify/Controllers/BaseControlor.cs rename to Inotify/Controllers/BaseController.cs index 6de3884..23fbd05 100644 --- a/Inotify/Controllers/BaseControlor.cs +++ b/Inotify/Controllers/BaseController.cs @@ -1,8 +1,11 @@ using Inotify.Common; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System; +using System.IO; using System.Linq; using System.Security.Claims; +using System.Xml; namespace Inotify.Controllers { @@ -35,7 +38,7 @@ namespace Inotify.Controllers } - public class BaseControlor : ControllerBase + public class BaseController : ControllerBase { public string UserName { @@ -128,5 +131,38 @@ namespace Inotify.Controllers { return new JsonResult(obj); } + + protected string GetPostParams(HttpContext context) + { + string param = string.Empty; + if (context.Request.Method.ToLower().Equals("post")) + { + param += "[post]"; + foreach (var key in context.Request.Form.Keys.ToList()) + { + param += key + ":" + context.Request.Form[key].ToString(); + } + } + else if (context.Request.Method.ToLower().Equals("get")) + { + param += "[get]" + context.Request.QueryString.Value; + } + else + { + param += "[" + context.Request.Method + "]"; + } + + return param; + } + + protected string GetPostXML() + { + Stream reqStream = Request.Body; + using (StreamReader reader = new StreamReader(reqStream)) + { + return reader.ReadToEnd(); + } + + } } } diff --git a/Inotify/Controllers/OAuthControlor.cs b/Inotify/Controllers/OAuthController.cs similarity index 99% rename from Inotify/Controllers/OAuthControlor.cs rename to Inotify/Controllers/OAuthController.cs index b0128e6..ff33034 100644 --- a/Inotify/Controllers/OAuthControlor.cs +++ b/Inotify/Controllers/OAuthController.cs @@ -20,7 +20,7 @@ namespace Inotify.Controllers { [ApiController] [Route("api/oauth")] - public class OAuthController : BaseControlor + public class OAuthController : BaseController { private readonly IGitHubLogin m_gitHubLogin; diff --git a/Inotify/Controllers/SendControlor.cs b/Inotify/Controllers/SendController.cs similarity index 83% rename from Inotify/Controllers/SendControlor.cs rename to Inotify/Controllers/SendController.cs index 4549874..af54a70 100644 --- a/Inotify/Controllers/SendControlor.cs +++ b/Inotify/Controllers/SendController.cs @@ -7,10 +7,10 @@ namespace Inotify.Controllers { [ApiController] [Route("api")] - public class SendController : BaseControlor + public class SendController : BaseController { [HttpGet, Route("send")] - public JsonResult Send(string token, string title, string? data, string? key) + public JsonResult Send(string? token, string? title, string? data) { if (DBManager.Instance.IsToken(token, out bool hasActive)) { @@ -23,8 +23,7 @@ namespace Inotify.Controllers { Token = token, Title = title, - Data = data, - Key = key, + Data = data }; if (SendTaskManager.Instance.SendMessage(message)) @@ -34,8 +33,8 @@ namespace Inotify.Controllers } else { - key = token; - if (DBManager.Instance.IsSendKey(token, out bool isActive, out token)) + var key = token; + if (DBManager.Instance.IsSendKey(key, out bool isActive, out token)) { if (!isActive) { diff --git a/Inotify/Controllers/SettingControlor.cs b/Inotify/Controllers/SettingController.cs similarity index 99% rename from Inotify/Controllers/SettingControlor.cs rename to Inotify/Controllers/SettingController.cs index ddf97fc..5494075 100644 --- a/Inotify/Controllers/SettingControlor.cs +++ b/Inotify/Controllers/SettingController.cs @@ -12,7 +12,7 @@ namespace Inotify.Controllers { [ApiController] [Route("api/setting")] - public class SettingControlor : BaseControlor + public class SettingController : BaseController { [HttpGet, Authorize(Policys.SystemOrUsers)] public JsonResult Index() diff --git a/Inotify/Controllers/SetttingSysControlor.cs b/Inotify/Controllers/SetttingSysController.cs similarity index 99% rename from Inotify/Controllers/SetttingSysControlor.cs rename to Inotify/Controllers/SetttingSysController.cs index 9d9338e..b1817ba 100644 --- a/Inotify/Controllers/SetttingSysControlor.cs +++ b/Inotify/Controllers/SetttingSysController.cs @@ -10,7 +10,7 @@ namespace Inotify.Controllers { [ApiController] [Route("api/settingsys")] - public class SetttingSysControlor : BaseControlor + public class SetttingSysController : BaseController { [HttpGet, Route("GetGlobal"), Authorize(Policys.Systems)] public IActionResult GetGlobal() diff --git a/Inotify/Controllers/WeiXinCallBackController.cs b/Inotify/Controllers/WeiXinCallBackController.cs new file mode 100644 index 0000000..aad111d --- /dev/null +++ b/Inotify/Controllers/WeiXinCallBackController.cs @@ -0,0 +1,544 @@ +using Inotify.Common; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Serialization; + +namespace Inotify.Controllers +{ + [ApiController] + [Route("api/weixin")] + public class WeiXinCallBackController : BaseController + { + [HttpGet] + public string Get(string msg_signature, string timestamp, string nonce, string echostr) + { + + var replyEchoStr = string.Empty; + var WXBizMsgCrypt = new WXBizMsgCrypt("", "", ""); + var result = WXBizMsgCrypt.VerifyURL(msg_signature, timestamp, nonce, echostr, ref replyEchoStr); + if (result == 0) + { + return replyEchoStr; + } + return result.ToString(); + } + + [HttpPost] + public JsonResult Post(string? msg_signature, string? timestamp, string? nonce) + { + + try + { + var reqStream = Request.Body; + string postData = ""; + using (StreamReader reader = new StreamReader(reqStream)) + { + postData = reader.ReadToEnd(); + } + + var msg = string.Empty; + var WXBizMsgCrypt = new WXBizMsgCrypt("", "", ""); + var result = WXBizMsgCrypt.DecryptMsg(msg_signature, timestamp, nonce, postData, ref msg); + if (result == 0) + { + var serializer = new XmlSerializer(typeof(xml)); + } + } + catch + { + + } + return OK(); + } + } + + public class xml + { + public string ToUserName { get; set; } + + public string FromUserName { get; set; } + + public string CreateTime { get; set; } + + public string MsgType { get; set; } + + public string Content { get; set; } + + public string MsgId { get; set; } + + public string AgentID { get; set; } + + } + + class Cryptography + { + public static UInt32 HostToNetworkOrder(UInt32 inval) + { + UInt32 outval = 0; + for (int i = 0; i < 4; i++) + outval = (outval << 8) + ((inval >> (i * 8)) & 255); + return outval; + } + + public static Int32 HostToNetworkOrder(Int32 inval) + { + Int32 outval = 0; + for (int i = 0; i < 4; i++) + outval = (outval << 8) + ((inval >> (i * 8)) & 255); + return outval; + } + /// + /// 解密方法 + /// + /// 密文 + /// + /// + /// + public static string AES_decrypt(String Input, string EncodingAESKey, ref string corpid) + { + byte[] Key; + Key = Convert.FromBase64String(EncodingAESKey + "="); + byte[] Iv = new byte[16]; + Array.Copy(Key, Iv, 16); + byte[] btmpMsg = AES_decrypt(Input, Iv, Key); + + int len = BitConverter.ToInt32(btmpMsg, 16); + len = IPAddress.NetworkToHostOrder(len); + + + byte[] bMsg = new byte[len]; + byte[] bCorpid = new byte[btmpMsg.Length - 20 - len]; + Array.Copy(btmpMsg, 20, bMsg, 0, len); + Array.Copy(btmpMsg, 20 + len, bCorpid, 0, btmpMsg.Length - 20 - len); + string oriMsg = Encoding.UTF8.GetString(bMsg); + corpid = Encoding.UTF8.GetString(bCorpid); + + + return oriMsg; + } + + public static String AES_encrypt(String Input, string EncodingAESKey, string corpid) + { + byte[] Key; + Key = Convert.FromBase64String(EncodingAESKey + "="); + byte[] Iv = new byte[16]; + Array.Copy(Key, Iv, 16); + string Randcode = CreateRandCode(16); + byte[] bRand = Encoding.UTF8.GetBytes(Randcode); + byte[] bCorpid = Encoding.UTF8.GetBytes(corpid); + byte[] btmpMsg = Encoding.UTF8.GetBytes(Input); + byte[] bMsgLen = BitConverter.GetBytes(HostToNetworkOrder(btmpMsg.Length)); + byte[] bMsg = new byte[bRand.Length + bMsgLen.Length + bCorpid.Length + btmpMsg.Length]; + + Array.Copy(bRand, bMsg, bRand.Length); + Array.Copy(bMsgLen, 0, bMsg, bRand.Length, bMsgLen.Length); + Array.Copy(btmpMsg, 0, bMsg, bRand.Length + bMsgLen.Length, btmpMsg.Length); + Array.Copy(bCorpid, 0, bMsg, bRand.Length + bMsgLen.Length + btmpMsg.Length, bCorpid.Length); + + return AES_encrypt(bMsg, Iv, Key); + + } + private static string CreateRandCode(int codeLen) + { + string codeSerial = "2,3,4,5,6,7,a,c,d,e,f,h,i,j,k,m,n,p,r,s,t,A,C,D,E,F,G,H,J,K,M,N,P,Q,R,S,U,V,W,X,Y,Z"; + if (codeLen == 0) + { + codeLen = 16; + } + string[] arr = codeSerial.Split(','); + string code = ""; + int randValue = -1; + Random rand = new Random(unchecked((int)DateTime.Now.Ticks)); + for (int i = 0; i < codeLen; i++) + { + randValue = rand.Next(0, arr.Length - 1); + code += arr[randValue]; + } + return code; + } + + private static String AES_encrypt(String Input, byte[] Iv, byte[] Key) + { + var aes = new RijndaelManaged(); + //秘钥的大小,以位为单位 + aes.KeySize = 256; + //支持的块大小 + aes.BlockSize = 128; + //填充模式 + aes.Padding = PaddingMode.PKCS7; + aes.Mode = CipherMode.CBC; + aes.Key = Key; + aes.IV = Iv; + var encrypt = aes.CreateEncryptor(aes.Key, aes.IV); + byte[] xBuff = null; + + using (var ms = new MemoryStream()) + { + using (var cs = new CryptoStream(ms, encrypt, CryptoStreamMode.Write)) + { + byte[] xXml = Encoding.UTF8.GetBytes(Input); + cs.Write(xXml, 0, xXml.Length); + } + xBuff = ms.ToArray(); + } + String Output = Convert.ToBase64String(xBuff); + return Output; + } + + private static String AES_encrypt(byte[] Input, byte[] Iv, byte[] Key) + { + var aes = new RijndaelManaged(); + //秘钥的大小,以位为单位 + aes.KeySize = 256; + //支持的块大小 + aes.BlockSize = 128; + //填充模式 + //aes.Padding = PaddingMode.PKCS7; + aes.Padding = PaddingMode.None; + aes.Mode = CipherMode.CBC; + aes.Key = Key; + aes.IV = Iv; + var encrypt = aes.CreateEncryptor(aes.Key, aes.IV); + byte[] xBuff = null; + + #region 自己进行PKCS7补位,用系统自己带的不行 + byte[] msg = new byte[Input.Length + 32 - Input.Length % 32]; + Array.Copy(Input, msg, Input.Length); + byte[] pad = KCS7Encoder(Input.Length); + Array.Copy(pad, 0, msg, Input.Length, pad.Length); + #endregion + + #region 注释的也是一种方法,效果一样 + //ICryptoTransform transform = aes.CreateEncryptor(); + //byte[] xBuff = transform.TransformFinalBlock(msg, 0, msg.Length); + #endregion + + using (var ms = new MemoryStream()) + { + using (var cs = new CryptoStream(ms, encrypt, CryptoStreamMode.Write)) + { + cs.Write(msg, 0, msg.Length); + } + xBuff = ms.ToArray(); + } + + String Output = Convert.ToBase64String(xBuff); + return Output; + } + + private static byte[] KCS7Encoder(int text_length) + { + int block_size = 32; + // 计算需要填充的位数 + int amount_to_pad = block_size - (text_length % block_size); + if (amount_to_pad == 0) + { + amount_to_pad = block_size; + } + // 获得补位所用的字符 + char pad_chr = chr(amount_to_pad); + string tmp = ""; + for (int index = 0; index < amount_to_pad; index++) + { + tmp += pad_chr; + } + return Encoding.UTF8.GetBytes(tmp); + } + /** + * 将数字转化成ASCII码对应的字符,用于对明文进行补码 + * + * @param a 需要转化的数字 + * @return 转化得到的字符 + */ + static char chr(int a) + { + + byte target = (byte)(a & 0xFF); + return (char)target; + } + private static byte[] AES_decrypt(String Input, byte[] Iv, byte[] Key) + { + RijndaelManaged aes = new RijndaelManaged(); + aes.KeySize = 256; + aes.BlockSize = 128; + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.None; + aes.Key = Key; + aes.IV = Iv; + var decrypt = aes.CreateDecryptor(aes.Key, aes.IV); + byte[] xBuff = null; + using (var ms = new MemoryStream()) + { + using (var cs = new CryptoStream(ms, decrypt, CryptoStreamMode.Write)) + { + byte[] xXml = Convert.FromBase64String(Input); + byte[] msg = new byte[xXml.Length + 32 - xXml.Length % 32]; + Array.Copy(xXml, msg, xXml.Length); + cs.Write(xXml, 0, xXml.Length); + } + xBuff = decode2(ms.ToArray()); + } + return xBuff; + } + private static byte[] decode2(byte[] decrypted) + { + int pad = (int)decrypted[decrypted.Length - 1]; + if (pad < 1 || pad > 32) + { + pad = 0; + } + byte[] res = new byte[decrypted.Length - pad]; + Array.Copy(decrypted, 0, res, 0, decrypted.Length - pad); + return res; + } + } + + class WXBizMsgCrypt + { + string m_sToken; + string m_sEncodingAESKey; + string m_sReceiveId; + enum WXBizMsgCryptErrorCode + { + WXBizMsgCrypt_OK = 0, + WXBizMsgCrypt_ValidateSignature_Error = -40001, + WXBizMsgCrypt_ParseXml_Error = -40002, + WXBizMsgCrypt_ComputeSignature_Error = -40003, + WXBizMsgCrypt_IllegalAesKey = -40004, + WXBizMsgCrypt_ValidateCorpid_Error = -40005, + WXBizMsgCrypt_EncryptAES_Error = -40006, + WXBizMsgCrypt_DecryptAES_Error = -40007, + WXBizMsgCrypt_IllegalBuffer = -40008, + WXBizMsgCrypt_EncodeBase64_Error = -40009, + WXBizMsgCrypt_DecodeBase64_Error = -40010 + }; + + //构造函数 + // @param sToken: 企业微信后台,开发者设置的Token + // @param sEncodingAESKey: 企业微信后台,开发者设置的EncodingAESKey + // @param sReceiveId: 不同场景含义不同,详见文档说明 + public WXBizMsgCrypt(string sToken, string sEncodingAESKey, string sReceiveId) + { + m_sToken = sToken; + m_sReceiveId = sReceiveId; + m_sEncodingAESKey = sEncodingAESKey; + } + + //验证URL + // @param sMsgSignature: 签名串,对应URL参数的msg_signature + // @param sTimeStamp: 时间戳,对应URL参数的timestamp + // @param sNonce: 随机串,对应URL参数的nonce + // @param sEchoStr: 随机串,对应URL参数的echostr + // @param sReplyEchoStr: 解密之后的echostr,当return返回0时有效 + // @return:成功0,失败返回对应的错误码 + public int VerifyURL(string sMsgSignature, string sTimeStamp, string sNonce, string sEchoStr, ref string sReplyEchoStr) + { + int ret = 0; + if (m_sEncodingAESKey.Length != 43) + { + return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_IllegalAesKey; + } + ret = VerifySignature(m_sToken, sTimeStamp, sNonce, sEchoStr, sMsgSignature); + if (0 != ret) + { + return ret; + } + sReplyEchoStr = ""; + string cpid = ""; + try + { + sReplyEchoStr = Cryptography.AES_decrypt(sEchoStr, m_sEncodingAESKey, ref cpid); //m_sReceiveId); + } + catch (Exception) + { + sReplyEchoStr = ""; + return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_DecryptAES_Error; + } + if (cpid != m_sReceiveId) + { + sReplyEchoStr = ""; + return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ValidateCorpid_Error; + } + return 0; + } + + // 检验消息的真实性,并且获取解密后的明文 + // @param sMsgSignature: 签名串,对应URL参数的msg_signature + // @param sTimeStamp: 时间戳,对应URL参数的timestamp + // @param sNonce: 随机串,对应URL参数的nonce + // @param sPostData: 密文,对应POST请求的数据 + // @param sMsg: 解密后的原文,当return返回0时有效 + // @return: 成功0,失败返回对应的错误码 + public int DecryptMsg(string sMsgSignature, string sTimeStamp, string sNonce, string sPostData, ref string sMsg) + { + if (m_sEncodingAESKey.Length != 43) + { + return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_IllegalAesKey; + } + XmlDocument doc = new XmlDocument(); + XmlNode root; + string sEncryptMsg; + try + { + doc.LoadXml(sPostData); + root = doc.FirstChild; + sEncryptMsg = root["Encrypt"].InnerText; + } + catch (Exception) + { + return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ParseXml_Error; + } + //verify signature + int ret = 0; + ret = VerifySignature(m_sToken, sTimeStamp, sNonce, sEncryptMsg, sMsgSignature); + if (ret != 0) + return ret; + //decrypt + string cpid = ""; + try + { + sMsg = Cryptography.AES_decrypt(sEncryptMsg, m_sEncodingAESKey, ref cpid); + } + catch (FormatException) + { + sMsg = ""; + return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_DecodeBase64_Error; + } + catch (Exception) + { + sMsg = ""; + return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_DecryptAES_Error; + } + if (cpid != m_sReceiveId) + return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ValidateCorpid_Error; + return 0; + } + + //将企业号回复用户的消息加密打包 + // @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串 + // @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp + // @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce + // @param sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串, + // 当return返回0时有效 + // return:成功0,失败返回对应的错误码 + public int EncryptMsg(string sReplyMsg, string sTimeStamp, string sNonce, ref string sEncryptMsg) + { + if (m_sEncodingAESKey.Length != 43) + { + return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_IllegalAesKey; + } + string raw = ""; + try + { + raw = Cryptography.AES_encrypt(sReplyMsg, m_sEncodingAESKey, m_sReceiveId); + } + catch (Exception) + { + return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_EncryptAES_Error; + } + string MsgSigature = ""; + int ret = 0; + ret = GenarateSinature(m_sToken, sTimeStamp, sNonce, raw, ref MsgSigature); + if (0 != ret) + return ret; + sEncryptMsg = ""; + + string EncryptLabelHead = ""; + string MsgSigLabelHead = ""; + string TimeStampLabelHead = ""; + string NonceLabelHead = ""; + sEncryptMsg = sEncryptMsg + "" + EncryptLabelHead + raw + EncryptLabelTail; + sEncryptMsg = sEncryptMsg + MsgSigLabelHead + MsgSigature + MsgSigLabelTail; + sEncryptMsg = sEncryptMsg + TimeStampLabelHead + sTimeStamp + TimeStampLabelTail; + sEncryptMsg = sEncryptMsg + NonceLabelHead + sNonce + NonceLabelTail; + sEncryptMsg += ""; + return 0; + } + + public class DictionarySort : System.Collections.IComparer + { + public int Compare(object oLeft, object oRight) + { + string sLeft = oLeft as string; + string sRight = oRight as string; + int iLeftLength = sLeft.Length; + int iRightLength = sRight.Length; + int index = 0; + while (index < iLeftLength && index < iRightLength) + { + if (sLeft[index] < sRight[index]) + return -1; + else if (sLeft[index] > sRight[index]) + return 1; + else + index++; + } + return iLeftLength - iRightLength; + + } + } + //Verify Signature + private static int VerifySignature(string sToken, string sTimeStamp, string sNonce, string sMsgEncrypt, string sSigture) + { + string hash = ""; + int ret = 0; + ret = GenarateSinature(sToken, sTimeStamp, sNonce, sMsgEncrypt, ref hash); + if (ret != 0) + return ret; + if (hash == sSigture) + return 0; + else + { + return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ValidateSignature_Error; + } + } + + public static int GenarateSinature(string sToken, string sTimeStamp, string sNonce, string sMsgEncrypt, ref string sMsgSignature) + { + ArrayList AL = new ArrayList(); + AL.Add(sToken); + AL.Add(sTimeStamp); + AL.Add(sNonce); + AL.Add(sMsgEncrypt); + AL.Sort(new DictionarySort()); + string raw = ""; + for (int i = 0; i < AL.Count; ++i) + { + raw += AL[i]; + } + + SHA1 sha; + ASCIIEncoding enc; + string hash = ""; + try + { + sha = new SHA1CryptoServiceProvider(); + enc = new ASCIIEncoding(); + byte[] dataToHash = enc.GetBytes(raw); + byte[] dataHashed = sha.ComputeHash(dataToHash); + hash = BitConverter.ToString(dataHashed).Replace("-", ""); + hash = hash.ToLower(); + } + catch (Exception) + { + return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ComputeSignature_Error; + } + sMsgSignature = hash; + return 0; + } + } +} diff --git a/Inotify/Inotify.csproj b/Inotify/Inotify.csproj index d1333c9..bab38da 100644 --- a/Inotify/Inotify.csproj +++ b/Inotify/Inotify.csproj @@ -18,6 +18,7 @@ + @@ -25,6 +26,7 @@ + diff --git a/Inotify/Sends/Products/BarkSendTemplate.cs b/Inotify/Sends/Products/BarkSendTemplate.cs index c0facee..f54529b 100644 --- a/Inotify/Sends/Products/BarkSendTemplate.cs +++ b/Inotify/Sends/Products/BarkSendTemplate.cs @@ -1,4 +1,7 @@ -using Newtonsoft.Json.Linq; +using CorePush.Apple; +using Jose; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; @@ -39,147 +42,82 @@ namespace Inotify.Sends.Products private static string TeamID; - private static CngKey SecretKey; + private static ApnSender apnSender; public override bool SendMessage(SendMessage message) { - if (SecretKey == null) + if (apnSender == null) { KeyID = SendCacheStore.GetSystemValue("barkKeyId"); TeamID = SendCacheStore.GetSystemValue("barkTeamId"); var privateKey = SendCacheStore.GetSystemValue("barkPrivateKey"); var privateKeyContent = privateKey.Split('\n')[1]; var decodeKey = Convert.FromBase64String(privateKeyContent); - SecretKey = CngKey.Import(decodeKey, CngKeyBlobFormat.Pkcs8PrivateBlob); - } - - if (Auth.DeviceToken == null) - { - return false; - } - - var payload = CreatePayload(message); - var accessToken = CreateAccessToken(payload); - var result = CreatePush(payload, accessToken, Auth.DeviceToken); - - return result; - - } - - private string CreatePayload(SendMessage message) - { - var expiration = DateTime.Now.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - var expirationSeconds = (long)expiration.TotalSeconds; - - var alert = new Dictionary(); - if (!string.IsNullOrEmpty(message.Data)) - { - alert.Add("body", message.Data); - } - - if (!string.IsNullOrEmpty(message.Title)) - { - alert.Add("title", message.Title); - } - - var aps = new Dictionary - { - { "category", "Bark" }, - { "sound", Auth.Sound }, - { "badge", "0" }, - { "mutable-content", "1" }, - { "alert", alert } - }; - - var payload = new Dictionary - { - { "aps", aps }, - { "isarchive", Auth.IsArchive }, - { "automaticallycopy", Auth.AutoMaticallyCopy }, - { "iss", TeamID}, - { "iat", expirationSeconds} - }; - - if (!string.IsNullOrEmpty(message.Title)) - { - payload.Add("copy", message.Title); - } - - var payloadString = JObject.FromObject(payload).ToString(); - - - return payloadString; - } - - private string CreateAccessToken(string payload) - { - using ECDsaCng dsa = new ECDsaCng(SecretKey) - { - HashAlgorithm = CngAlgorithm.Sha256 - }; - var headers = JObject.FromObject(new - { - alg = "ES256", - kid = KeyID - }).ToString(); - - var unsignedJwtData = Convert.ToBase64String(Encoding.UTF8.GetBytes(headers)) + "." + Convert.ToBase64String(Encoding.UTF8.GetBytes(payload)); - var signature = dsa.SignData(Encoding.UTF8.GetBytes(unsignedJwtData)); - return unsignedJwtData + "." + Convert.ToBase64String(signature); - } - - private bool CreatePush(string payload, string accessToken, string device_Tokne) - { - try - { - var data = Encoding.UTF8.GetBytes(payload); - var request = new HttpRequestMessage + var apnSettings = new ApnSettings() { - Version = new Version(2, 0), - RequestUri = new Uri($"https://api.development.push.apple.com:443/3/device/{device_Tokne}") + + TeamId = TeamID, + AppBundleIdentifier = "me.fin.bark", + P8PrivateKey = privateKeyContent, + ServerType = ApnServerType.Production, + P8PrivateKeyId = KeyID, }; - request.Headers.Add("authorization", string.Format("bearer {0}", accessToken)); - request.Headers.Add("apns-id", Guid.NewGuid().ToString()); - request.Headers.Add("apns-expiration", "0"); - request.Headers.Add("apns-priority", "10"); - request.Headers.Add("apns-topic", "me.fin.bark"); - request.Method = HttpMethod.Post; - request.Content = new ByteArrayContent(data); - var httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.Connection.Add("Keep-Alive"); - - var task = httpClient.SendAsync(request); - task.Wait(); - var responseMessage = task.Result; - if (responseMessage.StatusCode == System.Net.HttpStatusCode.OK) - { - var _response_uuid = ""; - if (responseMessage.Headers.TryGetValues("apns-id", out IEnumerable values)) - { - _response_uuid = values.First(); - Console.WriteLine($"success: '{_response_uuid}'"); - return true; - } - else - { - return false; - } - } - else - { - var respoinseBody = responseMessage.Content.ReadAsStringAsync().Result; - var responseJson = JObject.Parse(respoinseBody); - var reason = responseJson.Value("reason"); - Console.WriteLine($"failure: '{reason}'"); - } - } - catch (Exception ex) - { - Console.WriteLine($"exception: '{ex.Message}'"); + apnSender = new ApnSender(apnSettings, new HttpClient()); } + + var payload = new AppleNotification( + Guid.NewGuid(), + message.Data, + message.Title); + var response = apnSender.Send(payload, Auth.DeviceToken); + + if (response.IsSuccess) + return true; return false; } + + } + + public class AppleNotification + { + public class ApsPayload + { + public class Alert + { + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("body")] + public string Body { get; set; } + } + + [JsonProperty("alert")] + public Alert AlertBody { get; set; } + + [JsonProperty("apns-push-type")] + public string PushType { get; set; } = "alert"; + } + + public AppleNotification(Guid id, string message, string title = "") + { + Id = id; + + Aps = new ApsPayload + { + AlertBody = new ApsPayload.Alert + { + Title = title, + Body = message + } + }; + } + + [JsonProperty("aps")] + public ApsPayload Aps { get; set; } + + [JsonProperty("id")] + public Guid Id { get; set; } } } diff --git a/Inotify/Startup.cs b/Inotify/Startup.cs index 4fa769e..d76d133 100644 --- a/Inotify/Startup.cs +++ b/Inotify/Startup.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Rewrite; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -88,6 +89,10 @@ namespace Inotify { options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All); }); + + services.Configure(x => x.AllowSynchronousIO = true) + .Configure(x => x.AllowSynchronousIO = true); + } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) @@ -97,6 +102,12 @@ namespace Inotify app.UseDeveloperExceptionPage(); } + app.Use(next => context => + { + context.Request.EnableBuffering(); + return next(context); + }); + var options = new RewriteOptions(); options.Add(rewriteContext => { @@ -105,30 +116,34 @@ namespace Inotify { var queryValue = rewriteContext.HttpContext.Request.QueryString.Value; match = Regex.Match(queryValue, @"^\?act=(.*)/(.*)/(.*)/(.*)$"); - var groups = match.Groups; + if (match.Success) { + var groups = match.Groups; rewriteContext.HttpContext.Request.Path = @"/api/send"; - rewriteContext.HttpContext.Request.QueryString = new QueryString($"?key={groups[2]}&title={groups[3]}&date={groups[4]}"); + rewriteContext.HttpContext.Request.QueryString = new QueryString($"?token={groups[2]}&title={groups[3]}&date={groups[4]}"); } else { match = Regex.Match(queryValue, @"^\?act=(.*)/(.*)/(.*)$"); if (match.Success) { + var groups = match.Groups; rewriteContext.HttpContext.Request.Path = @"/api/send"; - rewriteContext.HttpContext.Request.QueryString = new QueryString($"?key={groups[2]}&title={groups[3]}"); + rewriteContext.HttpContext.Request.QueryString = new QueryString($"?token={groups[2]}&title={groups[3]}"); } else { match = Regex.Match(queryValue, @"^\?act=(.*)/(.*)$"); if (match.Success) { + var groups = match.Groups; rewriteContext.HttpContext.Request.Path = @"/api/send"; - rewriteContext.HttpContext.Request.QueryString = new QueryString($"?key={groups[2]}"); + rewriteContext.HttpContext.Request.QueryString = new QueryString($"?token={groups[2]}"); } else if (rewriteContext.HttpContext.Request.QueryString.Value.StartsWith("?")) { + var groups = match.Groups; rewriteContext.HttpContext.Request.Path = @"/info"; rewriteContext.HttpContext.Request.QueryString = new QueryString(); } diff --git a/Inotify/ThridPart/CorePush/Apple/ApnSender.cs b/Inotify/ThridPart/CorePush/Apple/ApnSender.cs new file mode 100644 index 0000000..be8c95d --- /dev/null +++ b/Inotify/ThridPart/CorePush/Apple/ApnSender.cs @@ -0,0 +1,172 @@ +using CorePush.Interfaces; +using CorePush.Utils; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace CorePush.Apple +{ + /// + /// HTTP2 Apple Push Notification sender + /// + public class ApnSender : IApnSender + { + private static readonly ConcurrentDictionary> tokens = new ConcurrentDictionary>(); + private static readonly Dictionary servers = new Dictionary + { + {ApnServerType.Development, "https://api.development.push.apple.com:443" }, + {ApnServerType.Production, "https://api.push.apple.com:443" } + }; + + private const string apnidHeader = "apns-id"; + private const int tokenExpiresMinutes = 50; + + private readonly ApnSettings settings; + private readonly HttpClient http; + + /// + /// Apple push notification sender constructor + /// + /// Apple Push Notification settings + public ApnSender(ApnSettings settings, HttpClient http) + { + this.settings = settings ?? throw new ArgumentNullException(nameof(settings)); + this.http = http ?? throw new ArgumentNullException(nameof(http)); + } + + /// + /// Serialize and send notification to APN. Please see how your message should be formatted here: + /// https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW1 + /// Payload will be serialized using Newtonsoft.Json package. + /// !IMPORTANT: If you send many messages at once, make sure to retry those calls. Apple typically doesn't like + /// to receive too many requests and may ocasionally respond with HTTP 429. Just try/catch this call and retry as needed. + /// + /// Throws exception when not successful + /// + public ApnsResponse Send(object notification, + string deviceToken, + string apnsId = null, + int apnsExpiration = 0, + int apnsPriority = 10, + bool isBackground = false, + CancellationToken cancellationToken = default) + { + + var task= SendAsync(notification, deviceToken, apnsId, apnsExpiration, apnsPriority, isBackground, cancellationToken); + task.Wait(); + return task.Result; + + } + + + public async Task SendAsync( + object notification, + string deviceToken, + string apnsId = null, + int apnsExpiration = 0, + int apnsPriority = 10, + bool isBackground = false, + CancellationToken cancellationToken = default) + { + var path = $"/3/device/{deviceToken}"; + var json = JsonHelper.Serialize(notification); + + var request = new HttpRequestMessage(HttpMethod.Post, new Uri(servers[settings.ServerType] + path)) + { + Version = new Version(2, 0), + Content = new StringContent(json) + }; + + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", GetJwtToken()); + request.Headers.TryAddWithoutValidation(":method", "POST"); + request.Headers.TryAddWithoutValidation(":path", path); + request.Headers.Add("apns-topic", settings.AppBundleIdentifier); + request.Headers.Add("apns-expiration", apnsExpiration.ToString()); + request.Headers.Add("apns-priority", apnsPriority.ToString()); + request.Headers.Add("apns-push-type", isBackground ? "background" : "alert"); // for iOS 13 required + + if (!string.IsNullOrWhiteSpace(apnsId)) + { + request.Headers.Add(apnidHeader, apnsId); + } + + using (var response = await http.SendAsync(request, cancellationToken)) + { + var succeed = response.IsSuccessStatusCode; + var content = await response.Content.ReadAsStringAsync(); + var error = JsonHelper.Deserialize(content); + + return new ApnsResponse + { + IsSuccess = succeed, + Error = error + }; + } + } + + private string GetJwtToken() + { + var (token, date) = tokens.GetOrAdd(settings.AppBundleIdentifier, _ => new Tuple(CreateJwtToken(), DateTime.UtcNow)); + if (date < DateTime.UtcNow.AddMinutes(-tokenExpiresMinutes)) + { + tokens.TryRemove(settings.AppBundleIdentifier, out _); + return GetJwtToken(); + } + + return token; + } + + private string CreateJwtToken() + { + var header = JsonHelper.Serialize(new { alg = "ES256", kid = CleanP8Key(settings.P8PrivateKeyId) }); + var payload = JsonHelper.Serialize(new { iss = settings.TeamId, iat = ToEpoch(DateTime.UtcNow) }); + var headerBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(header)); + var payloadBasae64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(payload)); + var unsignedJwtData = $"{headerBase64}.{payloadBasae64}"; + var unsignedJwtBytes = Encoding.UTF8.GetBytes(unsignedJwtData); + + using (var dsa = AppleCryptoHelper.GetEllipticCurveAlgorithm(CleanP8Key(settings.P8PrivateKey))) + { + var signature = dsa.SignData(unsignedJwtBytes, 0, unsignedJwtBytes.Length, HashAlgorithmName.SHA256); + return $"{unsignedJwtData}.{Convert.ToBase64String(signature)}"; + } + } + + private static int ToEpoch(DateTime time) + { + var span = DateTime.UtcNow - new DateTime(1970, 1, 1); + return Convert.ToInt32(span.TotalSeconds); + } + + private static string CleanP8Key(string p8Key) + { + // If we have an empty p8Key, then don't bother doing any tasks. + if (string.IsNullOrEmpty(p8Key)) + { + return p8Key; + } + + var lines = p8Key.Split(new [] { '\n' }).ToList(); + + if (0 != lines.Count && lines[0].StartsWith("-----BEGIN PRIVATE KEY-----")) + { + lines.RemoveAt(0); + } + + if (0 != lines.Count && lines[lines.Count - 1].StartsWith("-----END PRIVATE KEY-----")) + { + lines.RemoveAt(lines.Count - 1); + } + + var result = string.Join(string.Empty, lines); + + return result; + } + } +} diff --git a/Inotify/ThridPart/CorePush/Apple/ApnServerType.cs b/Inotify/ThridPart/CorePush/Apple/ApnServerType.cs new file mode 100644 index 0000000..f530f0e --- /dev/null +++ b/Inotify/ThridPart/CorePush/Apple/ApnServerType.cs @@ -0,0 +1,8 @@ +namespace CorePush.Apple +{ + public enum ApnServerType + { + Development, + Production + } +} diff --git a/Inotify/ThridPart/CorePush/Apple/ApnSettings.cs b/Inotify/ThridPart/CorePush/Apple/ApnSettings.cs new file mode 100644 index 0000000..621e948 --- /dev/null +++ b/Inotify/ThridPart/CorePush/Apple/ApnSettings.cs @@ -0,0 +1,30 @@ +namespace CorePush.Apple +{ + public class ApnSettings + { + /// + /// p8 certificate string + /// + public string P8PrivateKey { get; set; } + + /// + /// 10 digit p8 certificate id. Usually a part of a downloadable certificate filename + /// + public string P8PrivateKeyId { get; set; } + + /// + /// Apple 10 digit team id + /// + public string TeamId { get; set; } + + /// + /// App slug / bundle name + /// + public string AppBundleIdentifier { get; set; } + + /// + /// Development or Production server + /// + public ApnServerType ServerType { get; set; } + } +} \ No newline at end of file diff --git a/Inotify/ThridPart/CorePush/Apple/ApnsResponse.cs b/Inotify/ThridPart/CorePush/Apple/ApnsResponse.cs new file mode 100644 index 0000000..f3a1aa9 --- /dev/null +++ b/Inotify/ThridPart/CorePush/Apple/ApnsResponse.cs @@ -0,0 +1,50 @@ +namespace CorePush.Apple +{ + public class ApnsResponse + { + public bool IsSuccess { get; set; } + + public ApnsError Error { get; set; } + } + + public class ApnsError + { + public ReasonEnum Reason {get; set;} + public long? Timestamp {get; set; } + } + + /// + /// https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW15 + /// + public enum ReasonEnum + { + BadCollapseId, + BadDeviceToken, + BadExpirationDate, + BadMessageId, + BadPriority, + BadTopic, + DeviceTokenNotForTopic, + DuplicateHeaders, + IdleTimeout, + MissingDeviceToken, + MissingTopic, + PayloadEmpty, + TopicDisallowed, + BadCertificate, + BadCertificateEnvironment, + ExpiredProviderToken, + Forbidden, + InvalidProviderToken, + MissingProviderToken, + BadPath, + MethodNotAllowed, + Unregistered, + PayloadTooLarge, + TooManyProviderTokenUpdates, + TooManyRequests, + InternalServerError, + ServiceUnavailable, + Shutdown, + } +} diff --git a/Inotify/ThridPart/CorePush/Google/FcmResponse.cs b/Inotify/ThridPart/CorePush/Google/FcmResponse.cs new file mode 100644 index 0000000..8d9803a --- /dev/null +++ b/Inotify/ThridPart/CorePush/Google/FcmResponse.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace CorePush.Google +{ + public class FcmResponse + { + [JsonProperty("multicast_id")] + public string MulticastId { get; set; } + + [JsonProperty("canonical_ids")] + public int CanonicalIds { get; set; } + + /// + /// Success count + /// + public int Success { get; set; } + + /// + /// Failure count + /// + public int Failure { get; set; } + + /// + /// Results + /// + public List Results { get; set; } + + /// + /// Returns value indicating notification sent success or failure + /// + public bool IsSuccess() + { + return Success > 0 && Failure == 0; + } + } +} diff --git a/Inotify/ThridPart/CorePush/Google/FcmResult.cs b/Inotify/ThridPart/CorePush/Google/FcmResult.cs new file mode 100644 index 0000000..30ae523 --- /dev/null +++ b/Inotify/ThridPart/CorePush/Google/FcmResult.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace CorePush.Google +{ + public class FcmResult + { + [JsonProperty("message_id")] + public string MessageId { get; set; } + + [JsonProperty("registration_id")] + public string RegistrationId { get; set; } + + public string Error { get; set; } + } +} diff --git a/Inotify/ThridPart/CorePush/Google/FcmSender.cs b/Inotify/ThridPart/CorePush/Google/FcmSender.cs new file mode 100644 index 0000000..2817eba --- /dev/null +++ b/Inotify/ThridPart/CorePush/Google/FcmSender.cs @@ -0,0 +1,81 @@ +using CorePush.Interfaces; +using CorePush.Utils; +using Newtonsoft.Json.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using System.Threading; + +namespace CorePush.Google +{ + /// + /// Firebase message sender + /// + public class FcmSender : IFcmSender + { + private readonly string fcmUrl = "https://fcm.googleapis.com/fcm/send"; + private readonly FcmSettings settings; + private readonly HttpClient http; + + public FcmSender(FcmSettings settings, HttpClient http) + { + this.settings = settings; + this.http = http; + } + + /// + /// Send firebase notification. + /// Please check out payload formats: + /// https://firebase.google.com/docs/cloud-messaging/concept-options#notifications + /// The SendAsync method will add/replace "to" value with deviceId + /// + /// Device token (will add `to` to the payload) + /// Notification payload that will be serialized using Newtonsoft.Json package + /// Throws exception when not successful + public Task SendAsync(string deviceId, object payload, CancellationToken cancellationToken = default) + { + var jsonObject = JObject.FromObject(payload); + jsonObject.Remove("to"); + jsonObject.Add("to", JToken.FromObject(deviceId)); + + return SendAsync(jsonObject, cancellationToken); + } + + /// + /// Send firebase notification. + /// Please check out payload formats: + /// https://firebase.google.com/docs/cloud-messaging/concept-options#notifications + /// The SendAsync method will add/replace "to" value with deviceId + /// + /// Notification payload that will be serialized using Newtonsoft.Json package + /// Throws exception when not successful + public async Task SendAsync(object payload, CancellationToken cancellationToken = default) + { + var serialized = JsonHelper.Serialize(payload); + + using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, fcmUrl)) + { + httpRequest.Headers.Add("Authorization", $"key = {settings.ServerKey}"); + + if (!string.IsNullOrEmpty(settings.SenderId)) + { + httpRequest.Headers.Add("Sender", $"id = {settings.SenderId}"); + } + + httpRequest.Content = new StringContent(serialized, Encoding.UTF8, "application/json"); + + using (var response = await http.SendAsync(httpRequest, cancellationToken)) + { + var responseString = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException("Firebase notification error: " + responseString); + } + + return JsonHelper.Deserialize(responseString); + } + } + } + } +} diff --git a/Inotify/ThridPart/CorePush/Google/FcmSettings.cs b/Inotify/ThridPart/CorePush/Google/FcmSettings.cs new file mode 100644 index 0000000..a121c84 --- /dev/null +++ b/Inotify/ThridPart/CorePush/Google/FcmSettings.cs @@ -0,0 +1,15 @@ +namespace CorePush.Google +{ + public class FcmSettings + { + /// + /// FCM Sender ID + /// + public string SenderId { get; set; } + + /// + /// FCM Server Key + /// + public string ServerKey { get; set; } + } +} \ No newline at end of file diff --git a/Inotify/ThridPart/CorePush/Interfaces/IApnSender.cs b/Inotify/ThridPart/CorePush/Interfaces/IApnSender.cs new file mode 100644 index 0000000..9955dbd --- /dev/null +++ b/Inotify/ThridPart/CorePush/Interfaces/IApnSender.cs @@ -0,0 +1,18 @@ +using CorePush.Apple; +using System.Threading; +using System.Threading.Tasks; + +namespace CorePush.Interfaces +{ + public interface IApnSender + { + Task SendAsync( + object notification, + string deviceToken, + string apnsId = null, + int apnsExpiration = 0, + int apnsPriority = 10, + bool isBackground = false, + CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/Inotify/ThridPart/CorePush/Interfaces/IFcmSender.cs b/Inotify/ThridPart/CorePush/Interfaces/IFcmSender.cs new file mode 100644 index 0000000..8b4649f --- /dev/null +++ b/Inotify/ThridPart/CorePush/Interfaces/IFcmSender.cs @@ -0,0 +1,12 @@ +using CorePush.Google; +using System.Threading; +using System.Threading.Tasks; + +namespace CorePush.Interfaces +{ + public interface IFcmSender + { + Task SendAsync(string deviceId, object payload, CancellationToken cancellationToken = default); + Task SendAsync(object payload, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/Inotify/ThridPart/CorePush/Utils/AppleCryptoHelper.cs b/Inotify/ThridPart/CorePush/Utils/AppleCryptoHelper.cs new file mode 100644 index 0000000..088b006 --- /dev/null +++ b/Inotify/ThridPart/CorePush/Utils/AppleCryptoHelper.cs @@ -0,0 +1,28 @@ + +using System; +using System.Security.Cryptography; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Security; + +namespace CorePush.Utils +{ + public static class AppleCryptoHelper + { + public static ECDsa GetEllipticCurveAlgorithm(string privateKey) + { + var keyParams = (ECPrivateKeyParameters) PrivateKeyFactory.CreateKey(Convert.FromBase64String(privateKey)); + var q = keyParams.Parameters.G.Multiply(keyParams.D).Normalize(); + + return ECDsa.Create(new ECParameters + { + Curve = ECCurve.CreateFromValue(keyParams.PublicKeyParamSet.Id), + D = keyParams.D.ToByteArrayUnsigned(), + Q = + { + X = q.XCoord.GetEncoded(), + Y = q.YCoord.GetEncoded() + } + }); + } + } +} diff --git a/Inotify/ThridPart/CorePush/Utils/JsonHelper.cs b/Inotify/ThridPart/CorePush/Utils/JsonHelper.cs new file mode 100644 index 0000000..f564212 --- /dev/null +++ b/Inotify/ThridPart/CorePush/Utils/JsonHelper.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace CorePush.Utils +{ + public static class JsonHelper + { + private static readonly JsonSerializerSettings settings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + NullValueHandling = NullValueHandling.Ignore, + }; + + public static string Serialize(object obj) + { + return JsonConvert.SerializeObject(obj, settings); + } + + public static TObject Deserialize(string json) + { + return JsonConvert.DeserializeObject(json, settings); + } + } +}