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);
+ }
+ }
+}