using System; using System.Text; using System.Net; using System.Net.Sockets; using System.Runtime.InteropServices; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using FrontendWebApi.Models; using Microsoft.CodeAnalysis.CSharp.Syntax; using Repository.BackendRepository.Interface; using System.Linq; /// /// HiNet 企業簡訊 API for .NetCore 2.0 /// Web : https://sms.hinet.net /// Email : hiair@hinet.net /// /// Changelog : /// 2018/04/19 - 初版 /// namespace FrontendWebApi.ApiControllers { /// /// Request類別 /// enum RequestType : byte { LOGIN = 0, SEND = 1, QUERY = 2, SENDLONG = 11, QUERYLONG = 12, SENDFOREIGN = 15, CANCEL = 16, CANCELLONG = 17, SENDFOREIGNLONG = 18 } /// /// 訊息編碼 /// enum MsgCoding : byte { BIG5 = 1, BINARY = 2, UCS2 = 3, UTF8 = 4 } /// /// 其他定數常數 /// enum MsgConst : byte { UNUSED = 0 } /// /// 資料長度定義 /// enum LENGTH : int { RESPSET = 80, REQSET = 100, CONTENT = 160, REQUEST = 266, RESPONSE = 244, SMS_LEN = 70, ASCII_SMS_LEN = 159, LONG_ASCII_MSG_LEN = 153, LONG_MSG_LEN = 67, MAX_LONG_SPLIT = 10 } [Serializable] [StructLayout(LayoutKind.Sequential, Pack = 1)] /// /// 傳送資料到hiAirV2的資料結構 /// struct MsgRequest { public byte type; //訊息型態 public byte coding; //訊息編碼種類 public byte priority; //訊息優先權 public byte countryCode; //手機國碼 public byte setLen; //set[] 訊息內容的長度 public byte contentLen; //content[]訊息內容的長度 // 訊息相關資料設定 [MarshalAs(UnmanagedType.ByValArray, SizeConst = (int)LENGTH.REQSET)] public byte[] set; // 簡訊內容 [MarshalAs(UnmanagedType.ByValArray, SizeConst = (int)LENGTH.CONTENT)] public byte[] content; } [Serializable] [StructLayout(LayoutKind.Sequential, Pack = 1)] /// /// 從hiAirV2接收資料的資料結構 /// struct MsgResponse { public byte code; //回傳訊息代碼 public byte coding; //訊息編碼種類 public byte setLen; //set[] 訊息內容的長度 public byte contentLen; //content[]訊息內容的長度 // 訊息相關資料 [MarshalAs(UnmanagedType.ByValArray, SizeConst = (int)LENGTH.RESPSET)] public byte[] set; // MessageID或其他文字描述 [MarshalAs(UnmanagedType.ByValArray, SizeConst = (int)LENGTH.CONTENT)] public byte[] content; } /// /// HiNet企業簡訊 Hiair .Net Core API /// public class HiNetController : MyBaseApiController { private readonly IBackendRepository backendRepository; public Socket socket; // 傳送型態 private const String SEND_NOW = "01"; //即時傳送 private const String SEND_EXPIRE = "02"; //截止重送時間 private const String SEND_RESERVE = "03"; //預約傳送 private const String SEND_RESERVE_EXPIRE = "04"; //預約傳送+截止重送時間 // 回覆結果的訊息說明 private StringBuilder retMessage = new StringBuilder(); public HiNetController(IBackendRepository backendRepository) { this.backendRepository = backendRepository; } /// /// 將MsgRequest結構轉為byte array /// /// MsgRequest結構 /// byte array byte[] encode(MsgRequest req) { int size = Marshal.SizeOf(req); byte[] buf = new byte[size]; IntPtr ptr = Marshal.AllocHGlobal(size); Marshal.StructureToPtr(req, ptr, true); Marshal.Copy(ptr, buf, 0, size); Marshal.FreeHGlobal(ptr); return buf; } /// /// 將byte array轉為MsgResponse結構 /// /// byte array /// MsgResponse結構 MsgResponse decode(byte[] buf) { MsgResponse rep = new MsgResponse(); int size = buf.Length; IntPtr ptr = Marshal.AllocHGlobal(size); Marshal.Copy(buf, 0, ptr, size); rep = (MsgResponse)Marshal.PtrToStructure(ptr, rep.GetType()); Marshal.FreeHGlobal(ptr); return rep; } /// /// 建立與hiAirV2的連線 /// /// Server IP /// Server Port /// 登入帳號 /// 登入密碼 /// 0表示連線正常,-3,-4表示異常或斷線。其餘回傳值請參考規格書 public int StartCon(String serverName, int port, String userID, String passwd) { // 回傳值變數 int ret = 0; int timeout = 1000 * 60; //60sec socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.ReceiveTimeout = timeout; socket.SendTimeout = timeout; // 將資料清空 retMessage.Clear(); try { socket.Connect(serverName, port); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex.Message); retMessage = new StringBuilder("Socket Create Error!"); return -3; } // 設定登入帳號與密碼 String msgSet = String.Format("{0}\0{1}\0", userID, passwd); String msgContent = String.Empty; // 設定Request資料 MsgRequest req = new MsgRequest(); req.type = (byte)RequestType.LOGIN; req.coding = (byte)MsgCoding.BIG5; req.priority = (byte)MsgConst.UNUSED; req.countryCode = (byte)MsgConst.UNUSED; req.setLen = (byte)msgSet.Length; req.contentLen = (byte)msgContent.Length; req.set = Encoding.ASCII.GetBytes(msgSet.PadRight((int)LENGTH.REQSET, '\0')); req.content = Encoding.ASCII.GetBytes(msgContent.PadRight((int)LENGTH.CONTENT, '\0')); // 傳送與接收資料 ret = sendToserver(req); return ret; } /// /// 立即傳送文字簡訊 /// /// 門號 /// 簡訊內容 /// 0表示訊息成功傳送至主機,其餘回傳值請參考規格書 public int SendMsg(String mobileNum, String SMSMessage) { return this.Send(mobileNum, SMSMessage, String.Empty, String.Empty); } /// /// 預約傳送文字簡訊 /// /// 門號 /// 簡訊內容 /// 預約傳送時間,格式yymmddhhmmss /// 0表示訊息成功傳送至主機,其餘回傳值請參考規格書 public int SendMsg_Reserve(String mobileNum, String SMSMessage, String reserveTime) { return this.Send(mobileNum, SMSMessage, reserveTime, String.Empty); } /// /// 立即傳送文字簡訊加重送截止時間 /// /// 門號 /// 簡訊內容 /// 重送截止時間,單位分鐘0001 ~ 1440 /// 0表示訊息成功傳送至主機,其餘回傳值請參考規格書 public int SendMsg_Expire(String mobileNum, String SMSMessage, String expireTime) { return this.Send(mobileNum, SMSMessage, String.Empty, expireTime); } /// /// 預約傳送文字簡訊加重送截止時間 /// /// 門號 /// 簡訊內容 /// 預約傳送時間,格式yymmddhhmmss /// 重送截止時間,單位分鐘0001 ~ 1440 /// 0表示訊息成功傳送至主機,其餘回傳值請參考規格書 public int SendMsg_Reserve_Expire(String mobileNum, String SMSMessage, String reserveTime, String expireTime) { return this.Send(mobileNum, SMSMessage, reserveTime, expireTime); } /// /// 傳送文字簡訊(封裝程式僅供內部呼叫用) /// /// 門號 /// 簡訊內容 /// 預約傳送時間,格式yymmddhhmmss /// 重送截止時間,單位分鐘0001 ~ 1440 /// 0表示訊息成功傳送至主機,其餘回傳值請參考規格書 private int Send(String mobileNum, String SMSMessage, String reserveTime, String expireTime) { int ret = 0; retMessage.Clear(); byte[] utf8 = Encoding.UTF8.GetBytes(SMSMessage); byte[] ucs2 = Encoding.GetEncoding("UTF-16BE").GetBytes(SMSMessage); int contentLength = 0; MsgRequest req = new MsgRequest(); // 若UTF8 Bytes長度與原資料長度相等,表示為純英數字 if (utf8.Length == SMSMessage.Length) { //純英數 req.coding = (byte)MsgCoding.BIG5; contentLength = SMSMessage.Length; if (SMSMessage.Length > (int)LENGTH.ASCII_SMS_LEN) { retMessage = new StringBuilder("message content length exceeded"); return -5; } } else { req.coding = (byte)MsgCoding.UCS2; //非純英數 contentLength = ucs2.Length; if (SMSMessage.Length > (int)LENGTH.SMS_LEN) { retMessage = new StringBuilder("message content length exceeded"); return -5; } } // 帶+號為國外文字簡訊,否則為國內簡訊 byte msgType = mobileNum.StartsWith("+") ? (byte)RequestType.SENDFOREIGN : (byte)RequestType.SEND; String msgSet; if (String.IsNullOrEmpty(reserveTime) && String.IsNullOrEmpty(expireTime) == false) { // 立即傳送 msgSet = String.Format("{0}\0{1}\0{2}\0", mobileNum, SEND_EXPIRE, expireTime); } else if (String.IsNullOrEmpty(reserveTime) == false && String.IsNullOrEmpty(expireTime)) { // 立即傳送加重送截止時間 msgSet = String.Format("{0}\0{1}\0{2}\0", mobileNum, SEND_RESERVE, reserveTime); } else if (String.IsNullOrEmpty(reserveTime) == false && String.IsNullOrEmpty(expireTime) == false) { // 預約傳送 msgSet = String.Format("{0}\0{1}\0{2}\0{3}\0", mobileNum, SEND_RESERVE_EXPIRE, reserveTime, expireTime); } else { // 預約傳送加重送截止時間 msgSet = String.Format("{0}\0{1}\0", mobileNum, SEND_NOW); } // 設定Request資料 req.type = msgType; req.priority = (byte)MsgConst.UNUSED; req.countryCode = (byte)MsgConst.UNUSED; req.setLen = (byte)msgSet.Length; req.contentLen = (byte)contentLength; req.set = Encoding.ASCII.GetBytes(msgSet.PadRight((int)LENGTH.REQSET, '\0')); if (req.coding == (byte)MsgCoding.UCS2) { //非純英數字以UCS2 byte傳送 req.content = Encoding.GetEncoding("UTF-16BE").GetBytes(SMSMessage.PadRight((int)LENGTH.CONTENT, '\0')); } else { //純英數字以BIG5 byte傳送 req.content = Encoding.ASCII.GetBytes(SMSMessage.PadRight((int)LENGTH.CONTENT, '\0')); } // 傳送與接收資料 ret = this.sendToserver(req); return ret; } /// /// 立即傳送長簡訊 /// 長簡訊中文每個分則長度為67個字,純英數字每個為則為153個字。 /// 若中文簡訊沒大於70個字,或是純英數字沒大於159個字,請使用一般簡訊SendMsg相關Method。 /// 否則將被拆為多則傳送或依實際使用則數計費 /// /// 門號 /// 簡訊內容 /// public int SendMsg_Long(String mobileNum, String SMSMessage) { return this.SendLong(mobileNum, SMSMessage, String.Empty, String.Empty); } /// /// 傳送長簡訊加重送期限 /// 長簡訊中文每個分則長度為67個字,純英數字每個為則為153個字。 /// 若中文簡訊沒大於70個字,或是純英數字沒大於159個字,請使用一般簡訊SendMsg相關Method。 /// 否則將被拆為多則傳送或依實際使用則數計費 /// /// 門號 /// 簡訊內容 /// 重送截止時間,單位分鐘0001 ~ 1440 /// public int SendMsg_Long_Expire(String mobileNum, String SMSMessage, String expireTime) { return this.SendLong(mobileNum, SMSMessage, String.Empty, expireTime); } /// /// 傳送長簡訊加預約時間 /// 長簡訊中文每個分則長度為67個字,純英數字每個為則為153個字。 /// 若中文簡訊沒大於70個字,或是純英數字沒大於159個字,請使用一般簡訊SendMsg相關Method。 /// 否則將被拆為多則傳送或依實際使用則數計費 /// /// 門號 /// 簡訊內容 /// 預約傳送時間,格式yymmddhhmmss /// public int SendMsg_Long_Reserve(String mobileNum, String SMSMessage, String reserveTime) { return this.SendLong(mobileNum, SMSMessage, reserveTime, String.Empty); } /// /// 傳送長簡訊加預約時間與重送期限 /// 長簡訊中文每個分則長度為67個字,純英數字每個為則為153個字。 /// 若中文簡訊沒大於70個字,或是純英數字沒大於159個字,請使用一般簡訊SendMsg相關Method。 /// 否則將被拆為多則傳送或依實際使用則數計費 /// /// 門號 /// 簡訊內容 /// 預約傳送時間,格式yymmddhhmmss /// 重送截止時間,單位分鐘0001 ~ 1440 /// public int SendMsg_Long_Reserve_Expire(String mobileNum, String SMSMessage, String reserveTime, String expireTime) { return this.SendLong(mobileNum, SMSMessage, reserveTime, expireTime); } /// /// 傳送長簡訊(封裝程式僅供內部呼叫用) /// 長簡訊中文每個分則長度為67個字,純英數字每個為則為153個字。 /// 若中文簡訊沒大於70個字,或是純英數字沒大於159個字,請使用一般簡訊SendMsg*。 /// 否則將被拆為多則傳送或依實際使用則數計費 /// /// 門號 /// 簡訊內容 /// 預約傳送時間,格式yymmddhhmmss /// 重送截止時間,單位分鐘0001 ~ 1440 /// private int SendLong(String mobileNum, String SMSMessage, String reserveTime, String expireTime) { int ret = 0; retMessage.Clear(); byte[] utf8 = Encoding.UTF8.GetBytes(SMSMessage); byte[] ucs2 = Encoding.GetEncoding("UTF-16BE").GetBytes(SMSMessage); int msg_len = 0; MsgCoding coding; if (utf8.Length == SMSMessage.Length) { coding = MsgCoding.BIG5; msg_len = (int)LENGTH.LONG_ASCII_MSG_LEN; // 純英數字每則分則最長153個字 } else { coding = MsgCoding.UCS2; msg_len = (int)LENGTH.LONG_MSG_LEN; // 非純英數字每則分則最長67個字 } // 計算需要拆成多少則分則 double n = Math.Ceiling((double)SMSMessage.Length / (double)msg_len); double numOfMsg = Math.Round(n); // 需要發送numOfMsg+1則 for (int cnt = 0; cnt <= numOfMsg; cnt++) { if (cnt == 0) { // 第一則發送metadata MsgRequest req = new MsgRequest(); // 判斷是否為國外門號 req.type = mobileNum.StartsWith("+") ? (byte)RequestType.SENDFOREIGNLONG : (byte)RequestType.SENDLONG; String msgSet; if (String.IsNullOrEmpty(reserveTime) && String.IsNullOrEmpty(expireTime) == false) { // 立即傳送 msgSet = String.Format("{0}\0{1}\0{2}\0", mobileNum, SEND_EXPIRE, expireTime); } else if (String.IsNullOrEmpty(reserveTime) == false && String.IsNullOrEmpty(expireTime)) { // 立即傳送加重送截止時間 msgSet = String.Format("{0}\0{1}\0{2}\0", mobileNum, SEND_RESERVE, reserveTime); } else if (String.IsNullOrEmpty(reserveTime) == false && String.IsNullOrEmpty(expireTime) == false) { // 預約傳送 msgSet = String.Format("{0}\0{1}\0{2}\0{3}\0", mobileNum, SEND_RESERVE_EXPIRE, reserveTime, expireTime); } else { // 預約傳送加重送截止時間 msgSet = String.Format("{0}\0{1}\0", mobileNum, SEND_NOW); } // 第一則用不到 String msgContent = String.Format("\0"); req.coding = (byte)coding; req.priority = (byte)MsgConst.UNUSED; req.countryCode = (byte)MsgConst.UNUSED; req.setLen = (byte)msgSet.Length; req.contentLen = (byte)numOfMsg; // 填通數 req.set = Encoding.ASCII.GetBytes(msgSet.PadRight((int)LENGTH.REQSET, '\0')); req.content = Encoding.ASCII.GetBytes(msgContent.PadRight((int)LENGTH.CONTENT, '\0')); ret = sendToserver(req); } else { // 第二則開始送簡訊內容 MsgRequest req = new MsgRequest(); req.type = (byte)cnt; // 填第幾通 req.coding = (byte)coding; req.priority = (byte)MsgConst.UNUSED; req.countryCode = (byte)MsgConst.UNUSED; // 用不到 String msgSet = String.Format("\0"); req.setLen = (byte)msgSet.Length; req.set = Encoding.ASCII.GetBytes(msgSet.PadRight((int)LENGTH.REQSET, '\0')); // 取分則內容 String partial_msg; try { partial_msg = SMSMessage.Substring((cnt - 1) * msg_len, msg_len); } catch { // ArgumentOutOfRangeException: 超過指定長度就取到字串結尾 partial_msg = SMSMessage.Substring((cnt - 1) * msg_len); } if (req.coding == (byte)MsgCoding.UCS2) { // 非純英數字以UCS2 byte傳送 byte[] buf = Encoding.GetEncoding("UTF-16BE").GetBytes(partial_msg); req.content = Encoding.GetEncoding("UTF-16BE").GetBytes(partial_msg.PadRight((int)LENGTH.CONTENT, '\0')); req.contentLen = (byte)buf.Length; } else { // 純英數字以BIG5 byte傳送 byte[] buf = Encoding.ASCII.GetBytes(partial_msg); req.content = Encoding.ASCII.GetBytes(partial_msg.PadRight((int)LENGTH.CONTENT, '\0')); req.contentLen = (byte)buf.Length; } ret = sendToserver(req); } if (ret != 0) { // 若中途有錯誤直接中斷迴圈不送 System.Diagnostics.Debug.WriteLine(String.Format("SendLong() Exit => {0} : {1}", ret, retMessage)); return ret; } } return ret; } /// /// 傳送與接收資料 /// /// Request結構 /// 回應代碼 private int sendToserver(MsgRequest req) { int ret; retMessage.Clear(); // 將MsgRequest結構轉byte array後傳送 byte[] data = encode(req); try { int sent = socket.Send(data); if (sent != (int)LENGTH.REQUEST) { retMessage = new StringBuilder("Socket Send Data Error!"); System.Diagnostics.Debug.WriteLine("sent length:" + sent); return -4; } } catch { retMessage = new StringBuilder("Socket Send Data Error!"); return -4; } // 將接收到的byte arry轉為MsgResponse結構 byte[] buf = new byte[(int)LENGTH.RESPONSE]; try { int recv = socket.Receive(buf); if (recv != (int)LENGTH.RESPONSE) { retMessage = new StringBuilder("Socket Receive Data Error!"); System.Diagnostics.Debug.WriteLine("recv length:" + recv); return -4; } } catch { retMessage = new StringBuilder("Socket Receive Data Error!"); return -4; } MsgResponse rep = decode(buf); // 回傳代碼 ret = rep.code; // 回傳的文字描述或MessageID retMessage = new StringBuilder(Encoding.ASCII.GetString(rep.content).TrimEnd((Char)0)); System.Diagnostics.Debug.WriteLine(String.Format("sendToServer() result => {0} : {1}", ret, retMessage)); return ret; } /// /// 查詢一般簡訊狀態 /// /// 訊息ID /// 0表示訊息已成功送達對方,其餘回傳值請參考規格書 public int QueryMsg(String messageID) { retMessage.Clear(); return this.Query(RequestType.QUERY, messageID); } /// /// 查詢長簡訊狀態 /// /// 訊息ID /// 0表示訊息已成功送達對方,其餘回傳值請參考規格書 public int QueryLongMsg(String messageID) { retMessage.Clear(); return this.Query(RequestType.QUERYLONG, messageID); } /// /// 查詢簡訊狀態 /// /// 查詢類別 /// 訊息ID /// 0表示訊息已成功送達對方,其餘回傳值請參考規格書 private int Query(RequestType type, String messageID) { int ret = 0; retMessage.Clear(); String msgSet = String.Format("{0}\0", messageID); String msgContent = String.Empty; // 設定Request資料 MsgRequest req = new MsgRequest(); req.type = (byte)type; req.coding = (byte)MsgCoding.BIG5; req.priority = (byte)MsgConst.UNUSED; req.countryCode = (byte)MsgConst.UNUSED; req.setLen = (byte)msgSet.Length; req.contentLen = (byte)msgContent.Length; req.set = Encoding.ASCII.GetBytes(msgSet.PadRight((int)LENGTH.REQSET, '\0')); req.content = Encoding.ASCII.GetBytes(msgContent.PadRight((int)LENGTH.CONTENT, '\0')); ret = sendToserver(req); return ret; } /// /// 取消預約一般簡訊 /// /// 訊息ID /// 0表示訊息已取消成功,其餘回傳值請參考規格書 public int CancelMsg(String messageID) { retMessage.Clear(); return this.Cancel(RequestType.CANCEL, messageID); } /// /// 取消預約長簡訊 /// /// 訊息ID /// 0表示訊息已取消成功,其餘回傳值請參考規格書 public int CancelLongMsg(String messageID) { retMessage.Clear(); return this.Cancel(RequestType.CANCELLONG, messageID); } /// /// 取消預約簡訊(封裝程式僅供內部呼叫用) /// /// 預約類別 /// 訊息ID /// 0表示訊息已取消成功,其餘回傳值請參考規格書 private int Cancel(RequestType type, String messageID) { int ret = 0; retMessage.Clear(); String msgSet = String.Format("{0}\0", messageID); String msgContent = String.Empty; // 設定Request資料 MsgRequest req = new MsgRequest(); req.type = (byte)type; req.coding = (byte)MsgCoding.BIG5; req.priority = (byte)MsgConst.UNUSED; req.countryCode = (byte)MsgConst.UNUSED; req.setLen = (byte)msgSet.Length; req.contentLen = (byte)msgContent.Length; req.set = Encoding.ASCII.GetBytes(msgSet.PadRight((int)LENGTH.REQSET, '\0')); req.content = Encoding.ASCII.GetBytes(msgContent.PadRight((int)LENGTH.CONTENT, '\0')); ret = sendToserver(req); return ret; } /// /// 取得描述文字或MessageID /// /// 文字或MessageID public String Get_Message() { return this.retMessage.ToString(); } /// /// 與簡訊主機中斷連線 /// public void EndCon() { retMessage.Clear(); if (socket.Connected) { socket.Disconnect(false); } } /// /// hinet發送信息 /// /// /// [HttpPost] [Route("api/HiNetMsg")] public ActionResult> HiNetMsg([FromBody] HiNetInput input) { ApiResult apiResult = new ApiResult(jwt_str); //if (!jwtlife) //{ // apiResult.Code = "5000"; // return BadRequest(apiResult); //} try { var hiNetConfig = backendRepository.GetAllAsync($@"select system_key Name, system_value Value from variable where system_type = 'HiNetConfig'").Result; var config = new HiNetConfig(); config.ip = hiNetConfig.Where(x => x.Name == "IP").Select(x => x.Value).FirstOrDefault(); config.port = Int32.Parse(hiNetConfig.Where(x => x.Name == "Port").Select(x => x.Value).FirstOrDefault()); config.account = hiNetConfig.Where(x => x.Name == "Account").Select(x => x.Value).FirstOrDefault(); config.password = hiNetConfig.Where(x => x.Name == "Password").Select(x => x.Value).FirstOrDefault(); int retCode = this.StartCon(config.ip, config.port, config.account, config.password); string retContent = this.Get_Message(); if (retCode == 0) { //發送文字簡訊並回傳狀態碼 retCode = this.SendMsg(input.Number, input.Msg); //取得messageID或文字描述 retContent = this.Get_Message(); apiResult.Data = retCode + " : " + retContent; } else { apiResult.Code = "5000"; apiResult.Msg = retCode + " : " + retContent; return BadRequest(apiResult); } this.EndCon(); } catch (Exception exception) { apiResult.Code = "9999"; Logger.LogError("【" + controllerName + "/" + actionName + "】" + exception.Message); return Ok(apiResult); } return Ok(apiResult); } } }