Line data Source code
1 : /**
2 : Copyright (c) 2022 Roman Katuntsev <sbkarr@stappler.org>
3 : Copyright (c) 2023 Stappler LLC <admin@stappler.dev>
4 :
5 : Permission is hereby granted, free of charge, to any person obtaining a copy
6 : of this software and associated documentation files (the "Software"), to deal
7 : in the Software without restriction, including without limitation the rights
8 : to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 : copies of the Software, and to permit persons to whom the Software is
10 : furnished to do so, subject to the following conditions:
11 :
12 : The above copyright notice and this permission notice shall be included in
13 : all copies or substantial portions of the Software.
14 :
15 : THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 : IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 : FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 : AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 : LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 : OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 : THE SOFTWARE.
22 : **/
23 :
24 : #include "SPNetworkData.h"
25 :
26 : #include "SPTime.h"
27 : #include "SPValid.h"
28 : #include "SPCrypto.h"
29 : #include "SPFilesystem.h"
30 :
31 : #if MODULE_STAPPLER_BITMAP
32 : #include "SPBitmap.h"
33 : #endif
34 :
35 : #include "curl/curl.h"
36 :
37 : namespace STAPPLER_VERSIONIZED stappler::network {
38 :
39 : template <typename Interface>
40 3025 : static void Handle_destroy(HandleData<Interface> &data) {
41 3025 : if (data.process.sharedHandle) {
42 0 : curl_share_cleanup(data.process.sharedHandle);
43 : }
44 3025 : }
45 :
46 : template <typename Interface>
47 0 : static bool Handle_reset(HandleData<Interface> &data, Method method, StringView url) {
48 0 : data.send.url = url.str<Interface>();
49 0 : data.send.method = method;
50 0 : return true;
51 : }
52 :
53 : template <typename Interface>
54 700 : static void Handle_addHeader(HandleData<Interface> &data, StringView name, StringView value) {
55 700 : name.trimChars<StringView::WhiteSpace>();
56 700 : value.trimChars<StringView::WhiteSpace>();
57 :
58 700 : auto nameStr = string::tolower<Interface>(name);
59 :
60 700 : auto it = data.send.headers.find(nameStr);
61 700 : if (it != data.send.headers.end()) {
62 0 : it->second = value.str<Interface>();
63 : } else {
64 700 : data.send.headers.emplace(move(nameStr), value.str<Interface>());
65 : }
66 700 : }
67 :
68 : template <typename Interface>
69 0 : static void Handle_addMailTo(HandleData<Interface> &data, StringView name) {
70 0 : auto nameStr = name.str<Interface>();
71 0 : if (!valid::validateEmail(nameStr)) {
72 0 : log::error("NetworkHandle", "Fail to add MailTo: ", name, ": invalid email address");
73 0 : return;
74 : }
75 :
76 0 : auto lb = std::lower_bound(data.send.recipients.begin(), data.send.recipients.end(), nameStr);
77 0 : if (lb != data.send.recipients.end() && *lb != name) {
78 0 : data.send.recipients.emplace(lb, move(nameStr));
79 0 : } else if (lb != data.send.recipients.end()) {
80 0 : data.send.recipients.emplace_back(move(nameStr));
81 : }
82 0 : }
83 :
84 : template <typename Interface>
85 400 : static void Handle_setAuthority(HandleData<Interface> &data, StringView user, StringView passwd, AuthMethod method) {
86 400 : if (method == AuthMethod::PKey) {
87 0 : return;
88 : }
89 :
90 400 : data.auth.data = pair(user.str<Interface>(), passwd.str<Interface>());
91 400 : data.auth.authMethod = method;
92 : }
93 :
94 : template <typename Interface>
95 0 : static bool Handle_setPrivateKeyAuth(HandleData<Interface> &iface, const crypto::PrivateKey &pk) {
96 0 : auto pub = pk.exportPublic();
97 0 : if (!pub) {
98 0 : return false;
99 : }
100 :
101 0 : bool ret = false;
102 0 : pub.exportDer([&] (BytesView pub) {
103 0 : pk.sign([&] (BytesView sign) {
104 0 : iface.auth.data = base64::encode<Interface>(data::write(data::ValueTemplate<Interface>({
105 : data::ValueTemplate<Interface>(pub),
106 : data::ValueTemplate<Interface>(sign)
107 : })));
108 0 : iface.auth.authMethod = AuthMethod::PKey;
109 0 : ret = true;
110 : }, pub, crypto::SignAlgorithm::RSA_SHA512);
111 : });
112 0 : return ret;
113 0 : }
114 :
115 : template <typename Interface>
116 0 : static bool Handle_setPrivateKeyAuth(HandleData<Interface> &iface, BytesView data) {
117 0 : crypto::PrivateKey pk(crypto::Backend::Default, data);
118 0 : if (pk) {
119 0 : return Handle_setPrivateKeyAuth(iface, pk);
120 : }
121 0 : return false;
122 0 : }
123 :
124 : template <typename Interface>
125 0 : static void Handle_setSendFile(HandleData<Interface> &iface, StringView str, StringView type) {
126 0 : iface.send.data = str.str<Interface>();
127 0 : iface.send.size = 0;
128 0 : if (!type.empty()) {
129 0 : iface.addHeader("Content-Type", type);
130 : } else {
131 : #if LINUX
132 0 : auto t = network_getUserMime<Interface>(str);
133 0 : if (!t.empty()) {
134 0 : iface.addHeader("Content-Type", t);
135 0 : return;
136 : }
137 : #endif
138 : #if MODULE_STAPPLER_BITMAP
139 : // try image format
140 0 : auto fmt = bitmap::detectFormat(StringView(str));
141 0 : if (fmt.first != bitmap::FileFormat::Custom) {
142 0 : iface.addHeader("Content-Type", bitmap::getMimeType(fmt.first));
143 0 : return;
144 : } else {
145 0 : auto str = bitmap::getMimeType(fmt.second);
146 0 : if (!str.empty()) {
147 0 : iface.addHeader("Content-Type", str);
148 0 : return;
149 : }
150 : }
151 : #endif
152 0 : }
153 : }
154 :
155 : template <typename Interface>
156 0 : static void Handle_setSendCallback(HandleData<Interface> &iface, typename HandleData<Interface>::IOCallback &&cb, size_t size, StringView type) {
157 0 : iface.send.data = move(cb);
158 0 : iface.send.size = size;
159 0 : if (!type.empty()) {
160 0 : iface.addHeader("Content-Type", type);
161 : }
162 0 : }
163 :
164 : template <typename Interface>
165 0 : static void Handle_setSendData(HandleData<Interface> &iface, StringView data, StringView type) {
166 0 : iface.send.data = BytesView((const uint8_t *)data.data(), data.size()).bytes<Interface>();
167 0 : iface.send.size = data.size();
168 0 : if (!type.empty()) {
169 0 : iface.addHeader("Content-Type", type);
170 : }
171 0 : }
172 :
173 : template <typename Interface>
174 225 : static void Handle_setSendData(HandleData<Interface> &iface, BytesView data, StringView type) {
175 225 : iface.send.data = data.bytes<Interface>();
176 225 : iface.send.size = data.size();
177 225 : if (!type.empty()) {
178 225 : iface.addHeader("Content-Type", type);
179 : }
180 225 : }
181 :
182 : template <typename Interface>
183 0 : static void Handle_setSendData(HandleData<Interface> &iface, typename HandleData<Interface>::Bytes &&data, StringView type) {
184 0 : iface.send.size = data.size();
185 0 : iface.send.data = move(data);
186 0 : if (!type.empty()) {
187 0 : iface.addHeader("Content-Type", type);
188 : }
189 0 : }
190 :
191 : template <typename Interface>
192 0 : static void Handle_setSendData(HandleData<Interface> &iface, const uint8_t *data, size_t size, StringView type) {
193 0 : iface.send.size = size;
194 0 : iface.send.data = BytesView(data, size).bytes<Interface>();
195 0 : if (!type.empty()) {
196 0 : iface.addHeader("Content-Type", type);
197 : }
198 0 : }
199 :
200 : template <typename Interface>
201 425 : static void Handle_setSendData(HandleData<Interface> &iface, const data::ValueTemplate<Interface> &data, data::EncodeFormat fmt) {
202 425 : auto d = data::write<Interface>(data, fmt);
203 425 : iface.send.size = d.size();
204 425 : iface.send.data = move(d);
205 425 : if (fmt.format == data::EncodeFormat::Cbor || fmt.format == data::EncodeFormat::DefaultFormat) {
206 425 : iface.addHeader("Content-Type", "application/cbor");
207 0 : } else if (fmt.format == data::EncodeFormat::Json
208 0 : || fmt.format == data::EncodeFormat::Pretty
209 0 : || fmt.format == data::EncodeFormat::PrettyTime) {
210 0 : iface.addHeader("Content-Type", "application/json");
211 : }
212 425 : }
213 :
214 : template <typename Interface>
215 50 : static StringView Handle_getReceivedHeaderString(const HandleData<Interface> &iface, StringView name) {
216 50 : auto h = string::tolower<Interface>(name);
217 50 : auto i = iface.receive.parsed.find(h);
218 50 : if (i != iface.receive.parsed.end()) {
219 50 : return i->second;
220 : }
221 0 : return StringView();
222 50 : }
223 :
224 : template <typename Interface>
225 0 : static int64_t Handle_getReceivedHeaderInt(const HandleData<Interface> &iface, StringView name) {
226 0 : auto h = string::tolower<Interface>(name);
227 0 : auto i = iface.receive.parsed.find(h);
228 0 : if (i != iface.receive.parsed.end()) {
229 0 : if (!i->second.empty()) {
230 0 : return StringToNumber<int64_t>(i->second.c_str());
231 : }
232 : }
233 0 : return 0;
234 0 : }
235 :
236 : #define HANDLE_NAME(ret, name, ...) template <> ret HandleData<HANDLE_INTERFACE>::name(__VA_ARGS__)
237 : #define HANDLE_NAME_CONST(ret, name, ...) template <> auto HandleData<HANDLE_INTERFACE>::name(__VA_ARGS__) const -> ret
238 :
239 : #define HANDLE_INTERFACE memory::PoolInterface
240 :
241 0 : HANDLE_NAME(,~HandleData) { Handle_destroy(*this); }
242 0 : HANDLE_NAME(bool, reset, Method method, StringView url) { return Handle_reset(*this, method, url); }
243 0 : HANDLE_NAME_CONST(long, getResponseCode) { return process.responseCode; }
244 0 : HANDLE_NAME_CONST(long, getErrorCode) { return process.errorCode; }
245 0 : HANDLE_NAME_CONST(StringView, getError) { return process.error; }
246 0 : HANDLE_NAME(void, setCookieFile, StringView str) { process.cookieFile = filesystem::native::posixToNative<HANDLE_INTERFACE>(str); }
247 0 : HANDLE_NAME(void, setUserAgent, StringView str) { send.userAgent = str.str<HANDLE_INTERFACE>(); }
248 0 : HANDLE_NAME(void, setUrl, StringView str) { send.url = str.str<HANDLE_INTERFACE>(); }
249 0 : HANDLE_NAME(void, clearHeaders) { send.headers.clear(); }
250 0 : HANDLE_NAME(void, addHeader, StringView header, StringView value) { Handle_addHeader(*this, header, value); }
251 0 : HANDLE_NAME_CONST(const HeaderMap &, getRequestHeaders) { return send.headers; }
252 :
253 0 : HANDLE_NAME(void, setMailFrom, StringView from) { send.from = from.str<HANDLE_INTERFACE>(); }
254 0 : HANDLE_NAME(void, clearMailTo) { send.recipients.clear(); }
255 0 : HANDLE_NAME(void, addMailTo, StringView to) { Handle_addMailTo(*this, to); }
256 0 : HANDLE_NAME(void, setAuthority, StringView user, StringView passwd, AuthMethod method) { Handle_setAuthority(*this, user, passwd, method); }
257 0 : HANDLE_NAME(bool, setPrivateKeyAuth, BytesView priv) { return Handle_setPrivateKeyAuth(*this, priv); }
258 0 : HANDLE_NAME(bool, setPrivateKeyAuth, const crypto::PrivateKey &priv) { return Handle_setPrivateKeyAuth(*this, priv); }
259 0 : HANDLE_NAME(void, setProxy, StringView proxy, StringView authData) {
260 0 : auth.proxyAddress = proxy.str<HANDLE_INTERFACE>();
261 0 : auth.proxyAuth = authData.str<HANDLE_INTERFACE>();
262 0 : }
263 0 : HANDLE_NAME(void, setReceiveFile, StringView filename, bool resumeDownload) {
264 0 : receive.data = filename.str<HANDLE_INTERFACE>();
265 0 : receive.resumeDownload = resumeDownload;
266 0 : }
267 0 : HANDLE_NAME(void, setReceiveCallback, IOCallback &&cb) { receive.data = move(cb); }
268 0 : HANDLE_NAME(void, setResumeDownload, bool resumeDownload) { receive.resumeDownload = resumeDownload; }
269 0 : HANDLE_NAME(void, setResumeOffset, uint64_t offset) { receive.offset = offset; }
270 0 : HANDLE_NAME(void, setSendSize, size_t size) { send.size = size; }
271 0 : HANDLE_NAME(void, setSendFile, StringView filename, StringView type) { Handle_setSendFile(*this, filename, type); }
272 0 : HANDLE_NAME(void, setSendCallback, IOCallback &&cb, size_t outSize, StringView type) { Handle_setSendCallback(*this, move(cb), outSize, type); }
273 0 : HANDLE_NAME(void, setSendData, StringView data, StringView type) { Handle_setSendData(*this, data, type); }
274 0 : HANDLE_NAME(void, setSendData, BytesView data, StringView type) { Handle_setSendData(*this, data, type); }
275 0 : HANDLE_NAME(void, setSendData, Bytes &&data, StringView type) { Handle_setSendData(*this, move(data), type); }
276 0 : HANDLE_NAME(void, setSendData, const uint8_t *data, size_t size, StringView type) { Handle_setSendData(*this, data, size, type); }
277 0 : HANDLE_NAME(void, setSendData, const Value &value, data::EncodeFormat fmt) { Handle_setSendData(*this, value, fmt); }
278 0 : HANDLE_NAME_CONST(StringView, getReceivedHeaderString, StringView name) { return Handle_getReceivedHeaderString(*this, name); }
279 0 : HANDLE_NAME_CONST(int64_t, getReceivedHeaderInt, StringView name) { return Handle_getReceivedHeaderInt(*this, name); }
280 :
281 0 : HANDLE_NAME_CONST(Method, getMethod) { return send.method; }
282 0 : HANDLE_NAME_CONST(StringView, getUrl) { return send.url; }
283 0 : HANDLE_NAME_CONST(StringView, getCookieFile) { return process.cookieFile; }
284 0 : HANDLE_NAME_CONST(StringView, getUserAgent) { return send.userAgent; }
285 0 : HANDLE_NAME_CONST(StringView, getResponseContentType) { return receive.contentType; }
286 0 : HANDLE_NAME_CONST(const Vector<String> &, getRecievedHeaders) { return receive.headers; }
287 :
288 0 : HANDLE_NAME(void, setDebug, bool value) { process.debug = value; }
289 0 : HANDLE_NAME(void, setReuse, bool value) { process.reuse = value; }
290 0 : HANDLE_NAME(void, setShared, bool value) { process.shared = value; }
291 0 : HANDLE_NAME(void, setSilent, bool value) { process.silent = value; }
292 0 : HANDLE_NAME_CONST(const StringStream &, getDebugData) { return process.debugData; }
293 :
294 0 : HANDLE_NAME(void, setDownloadProgress, ProgressCallback &&cb) { process.downloadProgress = move(cb); }
295 0 : HANDLE_NAME(void, setUploadProgress, ProgressCallback &&cb) { process.uploadProgress = move(cb); }
296 :
297 0 : HANDLE_NAME(void, setConnectTimeout, int time) { process.connectTimeout = time; }
298 0 : HANDLE_NAME(void, setLowSpeedLimit, int time, size_t limit) { process.lowSpeedTime = time; process.lowSpeedLimit = int(limit); }
299 :
300 0 : HANDLE_NAME(void, setVerifyTls, bool value) { process.verifyTsl = value; }
301 :
302 : #undef HANDLE_INTERFACE
303 :
304 :
305 : #define HANDLE_INTERFACE memory::StandartInterface
306 :
307 3025 : HANDLE_NAME(,~HandleData) { Handle_destroy(*this); }
308 0 : HANDLE_NAME(bool, reset, Method method, StringView url) { return Handle_reset(*this, method, url); }
309 25 : HANDLE_NAME_CONST(long, getResponseCode) { return process.responseCode; }
310 0 : HANDLE_NAME_CONST(long, getErrorCode) { return process.errorCode; }
311 0 : HANDLE_NAME_CONST(StringView, getError) { return process.error; }
312 700 : HANDLE_NAME(void, setCookieFile, StringView str) { process.cookieFile = filesystem::native::posixToNative<HANDLE_INTERFACE>(str); }
313 25 : HANDLE_NAME(void, setUserAgent, StringView str) { send.userAgent = str.str<HANDLE_INTERFACE>(); }
314 0 : HANDLE_NAME(void, setUrl, StringView str) { send.url = str.str<HANDLE_INTERFACE>(); }
315 0 : HANDLE_NAME(void, clearHeaders) { send.headers.clear(); }
316 700 : HANDLE_NAME(void, addHeader, StringView header, StringView value) { Handle_addHeader(*this, header, value); }
317 0 : HANDLE_NAME_CONST(const HeaderMap &, getRequestHeaders) { return send.headers; }
318 :
319 0 : HANDLE_NAME(void, setMailFrom, StringView from) { send.from = from.str<HANDLE_INTERFACE>(); }
320 0 : HANDLE_NAME(void, clearMailTo) { send.recipients.clear(); }
321 0 : HANDLE_NAME(void, addMailTo, StringView to) { Handle_addMailTo(*this, to); }
322 400 : HANDLE_NAME(void, setAuthority, StringView user, StringView passwd, AuthMethod method) { Handle_setAuthority(*this, user, passwd, method); }
323 0 : HANDLE_NAME(bool, setPrivateKeyAuth, BytesView priv) { return Handle_setPrivateKeyAuth(*this, priv); }
324 0 : HANDLE_NAME(bool, setPrivateKeyAuth, const crypto::PrivateKey &priv) { return Handle_setPrivateKeyAuth(*this, priv); }
325 0 : HANDLE_NAME(void, setProxy, StringView proxy, StringView authData) {
326 0 : auth.proxyAddress = proxy.str<HANDLE_INTERFACE>();
327 0 : auth.proxyAuth = authData.str<HANDLE_INTERFACE>();
328 0 : }
329 0 : HANDLE_NAME(void, setReceiveFile, StringView filename, bool resumeDownload) {
330 0 : receive.data = filename.str<HANDLE_INTERFACE>();
331 0 : receive.resumeDownload = resumeDownload;
332 0 : }
333 2850 : HANDLE_NAME(void, setReceiveCallback, IOCallback &&cb) { receive.data = move(cb); }
334 0 : HANDLE_NAME(void, setResumeDownload, bool resumeDownload) { receive.resumeDownload = resumeDownload; }
335 0 : HANDLE_NAME(void, setResumeOffset, uint64_t offset) { receive.offset = offset; }
336 0 : HANDLE_NAME(void, setSendSize, size_t size) { send.size = size; }
337 0 : HANDLE_NAME(void, setSendFile, StringView filename, StringView type) { Handle_setSendFile(*this, filename, type); }
338 0 : HANDLE_NAME(void, setSendCallback, IOCallback &&cb, size_t outSize, StringView type) { Handle_setSendCallback(*this, move(cb), outSize, type); }
339 0 : HANDLE_NAME(void, setSendData, StringView data, StringView type) { Handle_setSendData(*this, data, type); }
340 225 : HANDLE_NAME(void, setSendData, BytesView data, StringView type) { Handle_setSendData(*this, data, type); }
341 0 : HANDLE_NAME(void, setSendData, Bytes &&data, StringView type) { Handle_setSendData(*this, move(data), type); }
342 0 : HANDLE_NAME(void, setSendData, const uint8_t *data, size_t size, StringView type) { Handle_setSendData(*this, data, size, type); }
343 425 : HANDLE_NAME(void, setSendData, const Value &value, data::EncodeFormat fmt) { Handle_setSendData(*this, value, fmt); }
344 50 : HANDLE_NAME_CONST(StringView, getReceivedHeaderString, StringView name) { return Handle_getReceivedHeaderString(*this, name); }
345 0 : HANDLE_NAME_CONST(int64_t, getReceivedHeaderInt, StringView name) { return Handle_getReceivedHeaderInt(*this, name); }
346 :
347 0 : HANDLE_NAME_CONST(Method, getMethod) { return send.method; }
348 0 : HANDLE_NAME_CONST(StringView, getUrl) { return send.url; }
349 0 : HANDLE_NAME_CONST(StringView, getCookieFile) { return process.cookieFile; }
350 0 : HANDLE_NAME_CONST(StringView, getUserAgent) { return send.userAgent; }
351 0 : HANDLE_NAME_CONST(StringView, getResponseContentType) { return receive.contentType; }
352 0 : HANDLE_NAME_CONST(const Vector<String> &, getRecievedHeaders) { return receive.headers; }
353 :
354 0 : HANDLE_NAME(void, setDebug, bool value) { process.debug = value; }
355 0 : HANDLE_NAME(void, setReuse, bool value) { process.reuse = value; }
356 0 : HANDLE_NAME(void, setShared, bool value) { process.shared = value; }
357 0 : HANDLE_NAME(void, setSilent, bool value) { process.silent = value; }
358 0 : HANDLE_NAME_CONST(const StringStream &, getDebugData) { return process.debugData; }
359 :
360 25 : HANDLE_NAME(void, setDownloadProgress, ProgressCallback &&cb) { process.downloadProgress = move(cb); }
361 25 : HANDLE_NAME(void, setUploadProgress, ProgressCallback &&cb) { process.uploadProgress = move(cb); }
362 :
363 0 : HANDLE_NAME(void, setConnectTimeout, int time) { process.connectTimeout = time; }
364 0 : HANDLE_NAME(void, setLowSpeedLimit, int time, size_t limit) { process.lowSpeedTime = time; process.lowSpeedLimit = int(limit); }
365 :
366 25 : HANDLE_NAME(void, setVerifyTls, bool value) { process.verifyTsl = value; }
367 :
368 : #undef HANDLE_INTERFACE
369 :
370 : template <> const ReceiveData<memory::StandartInterface>::DataSource &
371 25 : HandleData<memory::StandartInterface>::getReceiveDataSource() const {
372 25 : return receive.data;
373 : }
374 :
375 : template <> const ReceiveData<memory::PoolInterface>::DataSource &
376 0 : HandleData<memory::PoolInterface>::getReceiveDataSource() const {
377 0 : return receive.data;
378 : }
379 :
380 : template <> const SendData<memory::StandartInterface>::DataSource &
381 0 : HandleData<memory::StandartInterface>::getSendDataSource() const {
382 0 : return send.data;
383 : }
384 :
385 : template <> const SendData<memory::PoolInterface>::DataSource &
386 0 : HandleData<memory::PoolInterface>::getSendDataSource() const {
387 0 : return send.data;
388 : }
389 :
390 : template <> void
391 25 : HandleData<memory::StandartInterface>::setHeaderCallback(HeaderCallback &&cb) {
392 25 : receive.headerCallback = move(cb);
393 25 : }
394 :
395 : template <> void
396 0 : HandleData<memory::PoolInterface>::setHeaderCallback(HeaderCallback &&cb) {
397 0 : receive.headerCallback = move(cb);
398 0 : }
399 :
400 : template <> const HandleData<memory::StandartInterface>::HeaderCallback &
401 0 : HandleData<memory::StandartInterface>::getHeaderCallback() const {
402 0 : return receive.headerCallback;
403 : }
404 :
405 : template <> const HandleData<memory::PoolInterface>::HeaderCallback &
406 0 : HandleData<memory::PoolInterface>::getHeaderCallback() const {
407 0 : return receive.headerCallback;
408 : }
409 :
410 : }
411 :
412 : #undef SP_TERMINATED_DATA
|