1 module appbase.gateway.sms;
2 
3 import std.string;
4 import std.conv;
5 import std.base64;
6 import std.net.curl;
7 import std.uri;
8 import std.json;
9 import std.typecons : Tuple;
10 
11 import crypto.aes;
12 import appbase.utils;
13 import appbase.utils.xml;
14 
15 struct Sms1
16 {
17     /++
18     The document of soap response:
19     <?xml version="1.0" encoding="utf-8"?>
20     <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
21     	<soap:Body>
22     		<SendSMS_2Response xmlns="http://tempuri.org/">
23     			<SendSMS_2Result>
24     				<xs:schema id="NewDataSet" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
25     					<xs:element name="NewDataSet" msdata:IsDataSet="true" msdata:UseCurrentLocale="true">
26     						<xs:complexType>
27     							<xs:choice minOccurs="0" maxOccurs="unbounded">
28     								<xs:element name="Table1">
29     									<xs:complexType>
30     										<xs:sequence>
31     											<xs:element name="Result" type="xs:long" minOccurs="0" />
32     											<xs:element name="Description" type="xs:string" minOccurs="0" />
33     										</xs:sequence>
34     									</xs:complexType>
35     								</xs:element>
36     							</xs:choice>
37     						</xs:complexType>
38     					</xs:element>
39     				</xs:schema>
40     				<diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">
41     					<NewDataSet xmlns="">
42     						<Table1 diffgr:id="Table11" msdata:rowOrder="0" diffgr:hasChanges="inserted">
43     							<Result>-6</Result>
44     							<Description>手机号码格式错误</Description>
45     						</Table1>
46     					</NewDataSet>
47     				</diffgr:diffgram>
48     			</SendSMS_2Result>
49     		</SendSMS_2Response>
50     	</soap:Body>
51     </soap:Envelope>
52     +/
53     static Tuple!(int, string) send(const string gatewayUrl, const string account, const string key, const string mobile, const string content)
54     {
55         Tuple!(int, string) result;
56 
57         const string timeStamp = dateTimeToString(now);
58         const string sign = MD5(account ~ timeStamp ~ content ~ mobile ~ timeStamp.replace("-", "").replace(":", "") ~ key);
59         const string request = format(`<?xml version="1.0" encoding="utf-8"?>
60             <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
61               <soap12:Body>
62                 <SendSMS_2 xmlns="http://tempuri.org/">
63                   <RegCode>%s</RegCode>
64                   <TimeStamp>%s</TimeStamp>
65                   <Sign>%s</Sign>
66                   <Content>%s</Content>
67                   <To>%s</To>
68                   <SendTime>%s</SendTime>
69                 </SendSMS_2>
70               </soap12:Body>
71             </soap12:Envelope>`, account, timeStamp, sign, content, mobile, timeStamp.replace(" ", "T"));
72 
73         auto http = HTTP(gatewayUrl);
74         http.setPostData(request, "application/soap+xml; charset=utf-8");
75         string response;
76         http.onReceive = (ubyte[] data) { response ~= cast(string)data; return data.length; };
77 
78         try
79         {
80             http.perform();
81         }
82         catch (Exception e)
83         {
84             result[0] = -1001;
85             result[1] = e.msg;
86 
87             return result;
88         }
89 
90         if (response.empty)
91         {
92             result[0] = -1002;
93             result[1] = "Response empty.";
94 
95             return result;
96         }
97 
98         DocumentParser xml;
99         try
100         {
101             xml = new DocumentParser(response);
102         }
103         catch (Exception e)
104         {
105             result[0] = -1003;
106             result[1] = e.msg;
107 
108             return result;
109         }
110 
111         string ret_code, ret_description;
112         xml.onStartTag["Table1"] = (ElementParser xml)
113         {
114             xml.onEndTag["Result"]      = (in Element e) { ret_code        = e.text(); };
115             xml.onEndTag["Description"] = (in Element e) { ret_description = e.text(); };
116             xml.parse();
117         };
118         xml.parse();
119 
120         result[0] = as!int(ret_code, -1004);
121         result[1] = ret_description;
122 
123         return result;
124     }
125 }
126 
127 // for Sms1
128 unittest
129 {
130     import std.stdio : writeln;
131     writeln(Sms1.send("https://....", "account", "key", "135xxxxxxxx", "content"));
132 }
133 
134 struct Sms2
135 {
136     static Tuple!(int, string) send(const string gatewayUrl, const string account, const string key, const string id, const string mobile, const string templateCode, const string templateParams, const string smsSign)
137     {
138         Tuple!(int, string) result;
139 
140         string url = buildRequestUrl(gatewayUrl, account, key, id, mobile, templateCode, templateParams, smsSign);
141 
142         string response;
143         try
144         {
145             response = cast(string)get(url);
146         }
147         catch (Exception e)
148         {
149             result[0] = -2;
150             result[1] = e.msg;
151 
152             return result;
153         }
154 
155         JSONValue json;
156         try
157         {
158             json = parseJSON(response);
159         }
160         catch (Exception e)
161         {
162             result[0] = -3;
163             result[1] = e.msg;
164 
165             return result;
166         }
167 
168         string res_code, res_message, res_timestamp, res_body, res_sign;
169         try
170         {
171             res_code = json["code"].str;
172             res_message = decodeComponent!dchar(json["message"].str.to!dstring).to!string;
173             res_timestamp = decodeComponent(json["timestamp"].str);
174             res_body = decodeComponent(json["body"].str);
175             res_sign = json["sign"].str;
176         }
177         catch (Exception e)
178         {
179             result[0] = -4;
180             result[1] = e.msg;
181 
182             return result;
183         }
184 
185         if (!checkResponseSign(key, res_code, res_message, res_body, res_timestamp, res_sign))
186         {
187             result[0] = -5;
188             result[1] = "Received data signature error";
189 
190             return result;
191         }
192 
193         if (res_code != "0")
194         {
195             result[0] = -6;
196             result[1] = res_message ~ ", code: " ~ res_code;
197 
198             return result;
199         }
200 
201         // res_body = decryptAES(res_body);
202         // JSONValue json_body;
203 
204         // try
205         // {
206         //     json_body = parseJSON(res_body);
207         // }
208         // catch (Exception e)
209         // {
210         //     result[0] = -7;
211         //     result[1] = e.msg;
212 
213         //     return result;
214         // }
215 
216         result[0] = 0;
217         return result;
218     }
219 
220 private:
221 
222     static ubyte[] AES_IV = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
223 
224     static string encryptAES(const string key, const string data)
225     {
226         char[] bkey = cast(char[])strToByte_hex(MD5(key));
227         return Base64.encode(AESUtils.encrypt!AES128(cast(ubyte[])data, bkey, AES_IV, PaddingMode.PKCS5));
228     }
229 
230     static string decryptAES(const string key, const string data)
231     {
232         char[] bkey = cast(char[])strToByte_hex(MD5(key));
233         return cast(string)AESUtils.decrypt!AES128(Base64.decode(data), bkey, AES_IV, PaddingMode.PKCS5);
234     }
235 
236     static string buildRequestUrl(const string gatewayUrl, const string account, const string key, const string id, const string mobile, const string templateCode, const string templateParams, const string smsSign)
237     {
238         string content = encryptAES(key,
239             format(`{ "action":"SendSms", "smsid":"%s", "phone":"%s", "signName":"%s", "templateCode":"%s", "templateParams":"{%s}", "sendTime":"", "version":"1.0.0" }`,
240                 id, mobile, smsSign, templateCode, templateParams));
241         string timestamp = dateTimeToString(now);
242         string sign = MD5(format("apiAccount=%s&body=%s&timestamp=%s&apikey=%s", account, content, timestamp, key));
243 
244         return format("%s?apiAccount=%s&body=%s&timestamp=%s&sign=%s", gatewayUrl, account, encodeComponent(content), encodeComponent(timestamp), sign);
245     }
246 
247     static bool checkResponseSign(const string key, const string code, const string message, const string content, const string timestamp, const string sign)
248     {
249         string local_sign = MD5(format("body=%s&code=%s&message=%s&timestamp=%s&apikey=%s", content, code, message, timestamp, key));
250         return (local_sign == sign.toUpper());
251     }
252 }
253 
254 // for Sms2
255 unittest
256 {
257     import std.stdio : writeln;
258     writeln(Sms2.send("ipaddress", "account", "key", "1", "135xxxxxxxx", "TP001", "a,b", "【签名】"));
259 }