1 | #include "globals.h"
|
---|
2 |
|
---|
3 | #ifdef CS_CACHEEX
|
---|
4 |
|
---|
5 | #include "cscrypt/md5.h"
|
---|
6 | #include "module-cacheex.h"
|
---|
7 | #include "module-cw-cycle-check.h"
|
---|
8 | #include "oscam-chk.h"
|
---|
9 | #include "oscam-client.h"
|
---|
10 | #include "oscam-conf.h"
|
---|
11 | #include "oscam-ecm.h"
|
---|
12 | #include "oscam-lock.h"
|
---|
13 | #include "oscam-net.h"
|
---|
14 | #include "oscam-string.h"
|
---|
15 | #include "oscam-time.h"
|
---|
16 | #include "oscam-work.h"
|
---|
17 |
|
---|
18 | #define cs_cacheex_matcher "oscam.cacheex"
|
---|
19 |
|
---|
20 | extern uint8_t cc_node_id[8];
|
---|
21 | extern uint8_t camd35_node_id[8];
|
---|
22 | extern CS_MUTEX_LOCK ecmcache_lock;
|
---|
23 | extern struct ecm_request_t *ecmcwcache;
|
---|
24 | extern CS_MUTEX_LOCK hitcache_lock;
|
---|
25 |
|
---|
26 | uint8_t cacheex_peer_id[8];
|
---|
27 |
|
---|
28 | static LLIST *invalid_cws;
|
---|
29 | static struct csp_ce_hit_t *cspec_hitcache;
|
---|
30 | static uint32_t cspec_hitcache_size;
|
---|
31 |
|
---|
32 | void cacheex_init(void) {
|
---|
33 | // Init random node id
|
---|
34 | get_random_bytes(cacheex_peer_id, 8);
|
---|
35 | #ifdef MODULE_CCCAM
|
---|
36 | memcpy(cacheex_peer_id, cc_node_id, 8);
|
---|
37 | #endif
|
---|
38 | #ifdef MODULE_CAMD35_TCP
|
---|
39 | memcpy(camd35_node_id, cacheex_peer_id, 8);
|
---|
40 | #endif
|
---|
41 | }
|
---|
42 |
|
---|
43 | void cacheex_clear_account_stats(struct s_auth *account) {
|
---|
44 | account->cwcacheexgot = 0;
|
---|
45 | account->cwcacheexpush = 0;
|
---|
46 | account->cwcacheexhit = 0;
|
---|
47 | }
|
---|
48 |
|
---|
49 | void cacheex_clear_client_stats(struct s_client *client) {
|
---|
50 | client->cwcacheexgot = 0;
|
---|
51 | client->cwcacheexpush = 0;
|
---|
52 | client->cwcacheexhit = 0;
|
---|
53 | }
|
---|
54 |
|
---|
55 | int32_t cacheex_add_stats(struct s_client *cl, uint16_t caid, uint16_t srvid, uint32_t prid, uint8_t direction)
|
---|
56 | {
|
---|
57 | if (!cfg.cacheex_enable_stats)
|
---|
58 | return -1;
|
---|
59 |
|
---|
60 | // create list if doesn't exist
|
---|
61 | if (!cl->ll_cacheex_stats)
|
---|
62 | cl->ll_cacheex_stats = ll_create("ll_cacheex_stats");
|
---|
63 |
|
---|
64 | time_t now = time((time_t*)0);
|
---|
65 | LL_ITER itr = ll_iter_create(cl->ll_cacheex_stats);
|
---|
66 | S_CACHEEX_STAT_ENTRY *cacheex_stats_entry;
|
---|
67 |
|
---|
68 | // check for existing entry
|
---|
69 | while ((cacheex_stats_entry = ll_iter_next(&itr))) {
|
---|
70 | if (cacheex_stats_entry->cache_srvid == srvid &&
|
---|
71 | cacheex_stats_entry->cache_caid == caid &&
|
---|
72 | cacheex_stats_entry->cache_prid == prid &&
|
---|
73 | cacheex_stats_entry->cache_direction == direction) {
|
---|
74 | // we already have this entry - just add count and time
|
---|
75 | cacheex_stats_entry->cache_count++;
|
---|
76 | cacheex_stats_entry->cache_last = now;
|
---|
77 | return cacheex_stats_entry->cache_count;
|
---|
78 | }
|
---|
79 | }
|
---|
80 |
|
---|
81 | // if we land here we have to add a new entry
|
---|
82 | if (cs_malloc(&cacheex_stats_entry, sizeof(S_CACHEEX_STAT_ENTRY))) {
|
---|
83 | cacheex_stats_entry->cache_caid = caid;
|
---|
84 | cacheex_stats_entry->cache_srvid = srvid;
|
---|
85 | cacheex_stats_entry->cache_prid = prid;
|
---|
86 | cacheex_stats_entry->cache_count = 1;
|
---|
87 | cacheex_stats_entry->cache_last = now;
|
---|
88 | cacheex_stats_entry->cache_direction = direction;
|
---|
89 | ll_iter_insert(&itr, cacheex_stats_entry);
|
---|
90 | return 1;
|
---|
91 | }
|
---|
92 | return 0;
|
---|
93 | }
|
---|
94 |
|
---|
95 |
|
---|
96 | int8_t cacheex_maxhop(struct s_client *cl)
|
---|
97 | {
|
---|
98 | int maxhop = 10;
|
---|
99 | if (cl->reader && cl->reader->cacheex.maxhop)
|
---|
100 | maxhop = cl->reader->cacheex.maxhop;
|
---|
101 | else if (cl->account && cl->account->cacheex.maxhop)
|
---|
102 | maxhop = cl->account->cacheex.maxhop;
|
---|
103 | return maxhop;
|
---|
104 | }
|
---|
105 |
|
---|
106 | static void cacheex_cache_push_to_client(struct s_client *cl, ECM_REQUEST *er)
|
---|
107 | {
|
---|
108 | add_job(cl, ACTION_CACHE_PUSH_OUT, er, 0);
|
---|
109 | }
|
---|
110 |
|
---|
111 | /**
|
---|
112 | * cacheex modes:
|
---|
113 | *
|
---|
114 | * cacheex=1 CACHE PULL:
|
---|
115 | * Situation: oscam A reader1 has cacheex=1, oscam B account1 has cacheex=1
|
---|
116 | * oscam A gets a ECM request, reader1 send this request to oscam B, oscam B checks his cache
|
---|
117 | * a. not found in cache: return NOK
|
---|
118 | * a. found in cache: return OK+CW
|
---|
119 | * b. not found in cache, but found pending request: wait max cacheexwaittime and check again
|
---|
120 | * oscam B never requests new ECMs
|
---|
121 | *
|
---|
122 | * CW-flow: B->A
|
---|
123 | *
|
---|
124 | * cacheex=2 CACHE PUSH:
|
---|
125 | * Situation: oscam A reader1 has cacheex=2, oscam B account1 has cacheex=2
|
---|
126 | * if oscam B gets a CW, its pushed to oscam A
|
---|
127 | * reader has normal functionality and can request ECMs
|
---|
128 | *
|
---|
129 | * Problem: oscam B can only push if oscam A is connected
|
---|
130 | * Problem or feature?: oscam A reader can request ecms from oscam B
|
---|
131 | *
|
---|
132 | * CW-flow: B->A
|
---|
133 | *
|
---|
134 | * cacheex=3 REVERSE CACHE PUSH:
|
---|
135 | * Situation: oscam A reader1 has cacheex=3, oscam B account1 has cacheex=3
|
---|
136 | * if oscam A gets a CW, its pushed to oscam B
|
---|
137 | *
|
---|
138 | * oscam A never requests new ECMs
|
---|
139 | *
|
---|
140 | * CW-flow: A->B
|
---|
141 | */
|
---|
142 | void cacheex_cache_push(ECM_REQUEST *er)
|
---|
143 | {
|
---|
144 | if (er->rc >= E_NOTFOUND && er->rc != E_UNHANDLED) //Maybe later we could support other rcs
|
---|
145 | return; //NOT FOUND/Invalid
|
---|
146 |
|
---|
147 | if (er->cacheex_pushed || (er->ecmcacheptr && er->ecmcacheptr->cacheex_pushed))
|
---|
148 | return;
|
---|
149 |
|
---|
150 | int64_t grp;
|
---|
151 | if (er->selected_reader)
|
---|
152 | grp = er->selected_reader->grp;
|
---|
153 | else
|
---|
154 | grp = er->grp;
|
---|
155 |
|
---|
156 | //cacheex=2 mode: push (server->remote)
|
---|
157 | struct s_client *cl;
|
---|
158 | cs_readlock(&clientlist_lock);
|
---|
159 | for (cl=first_client->next; cl; cl=cl->next) {
|
---|
160 | if (er->cacheex_src != cl) {
|
---|
161 | if (get_module(cl)->num == R_CSP) { // always send to csp cl
|
---|
162 | if (!er->cacheex_src) cacheex_cache_push_to_client(cl, er); // but not if the origin was cacheex (might loop)
|
---|
163 | } else if (cl->typ == 'c' && !cl->dup && cl->account && cl->account->cacheex.mode == 2) { //send cache over user
|
---|
164 | if (get_module(cl)->c_cache_push // cache-push able
|
---|
165 | && (!grp || (cl->grp & grp)) //Group-check
|
---|
166 | && chk_srvid(cl, er) //Service-check
|
---|
167 | && (chk_caid(er->caid, &cl->ctab) > 0)) //Caid-check
|
---|
168 | {
|
---|
169 | cacheex_cache_push_to_client(cl, er);
|
---|
170 | }
|
---|
171 | }
|
---|
172 | }
|
---|
173 | }
|
---|
174 | cs_readunlock(&clientlist_lock);
|
---|
175 |
|
---|
176 | //cacheex=3 mode: reverse push (reader->server)
|
---|
177 |
|
---|
178 | cs_readlock(&readerlist_lock);
|
---|
179 | cs_readlock(&clientlist_lock);
|
---|
180 |
|
---|
181 | struct s_reader *rdr;
|
---|
182 | for (rdr = first_active_reader; rdr; rdr = rdr->next) {
|
---|
183 | cl = rdr->client;
|
---|
184 | if (cl && er->cacheex_src != cl && rdr->cacheex.mode == 3) { //send cache over reader
|
---|
185 | if (rdr->ph.c_cache_push
|
---|
186 | && (!grp || (rdr->grp & grp)) //Group-check
|
---|
187 | && chk_srvid(cl, er) //Service-check
|
---|
188 | && chk_ctab(er->caid, &rdr->ctab)) //Caid-check
|
---|
189 | {
|
---|
190 | cacheex_cache_push_to_client(cl, er);
|
---|
191 | }
|
---|
192 | }
|
---|
193 | }
|
---|
194 |
|
---|
195 | cs_readunlock(&clientlist_lock);
|
---|
196 | cs_readunlock(&readerlist_lock);
|
---|
197 |
|
---|
198 | er->cacheex_pushed = 1;
|
---|
199 | if (er->ecmcacheptr) er->ecmcacheptr->cacheex_pushed = 1;
|
---|
200 | }
|
---|
201 |
|
---|
202 | static inline struct s_cacheex_matcher *is_cacheex_matcher_matching(ECM_REQUEST *from_er, ECM_REQUEST *to_er)
|
---|
203 | {
|
---|
204 | struct s_cacheex_matcher *entry = cfg.cacheex_matcher;
|
---|
205 | int8_t v_ok = (from_er && to_er)?2:1;
|
---|
206 | while (entry) {
|
---|
207 | int8_t ok = 0;
|
---|
208 | if (from_er
|
---|
209 | && (!entry->caid || entry->caid == from_er->caid)
|
---|
210 | && (!entry->provid || entry->provid == from_er->prid)
|
---|
211 | && (!entry->srvid || entry->srvid == from_er->srvid)
|
---|
212 | && (!entry->chid || entry->chid == from_er->chid)
|
---|
213 | && (!entry->pid || entry->pid == from_er->pid)
|
---|
214 | && (!entry->ecmlen || entry->ecmlen == from_er->ecmlen))
|
---|
215 | ok++;
|
---|
216 |
|
---|
217 | if (to_er
|
---|
218 | && (!entry->to_caid || entry->to_caid == to_er->caid)
|
---|
219 | && (!entry->to_provid || entry->to_provid == to_er->prid)
|
---|
220 | && (!entry->to_srvid || entry->to_srvid == to_er->srvid)
|
---|
221 | && (!entry->to_chid || entry->to_chid == to_er->chid)
|
---|
222 | && (!entry->to_pid || entry->to_pid == to_er->pid)
|
---|
223 | && (!entry->to_ecmlen || entry->to_ecmlen == to_er->ecmlen))
|
---|
224 | ok++;
|
---|
225 |
|
---|
226 | if (ok == v_ok) {
|
---|
227 | if (!from_er || !to_er || from_er->srvid == to_er->srvid)
|
---|
228 | return entry;
|
---|
229 | }
|
---|
230 | entry = entry->next;
|
---|
231 | }
|
---|
232 | return NULL;
|
---|
233 | }
|
---|
234 |
|
---|
235 | bool cacheex_is_match_alias(struct s_client *cl, ECM_REQUEST *er) {
|
---|
236 | return cl && cl->account && cl->account->cacheex.mode == 1 && is_cacheex_matcher_matching(NULL, er);
|
---|
237 | }
|
---|
238 |
|
---|
239 | inline int8_t cacheex_match_alias(struct s_client *cl, ECM_REQUEST *er, ECM_REQUEST *ecm)
|
---|
240 | {
|
---|
241 | if (cl && cl->account && cl->account->cacheex.mode == 1) {
|
---|
242 | struct s_cacheex_matcher *entry = is_cacheex_matcher_matching(ecm, er);
|
---|
243 | if (entry) {
|
---|
244 | int32_t diff = comp_timeb(&er->tps, &ecm->tps);
|
---|
245 | if (diff > entry->valid_from && diff < entry->valid_to) {
|
---|
246 | #ifdef WITH_DEBUG
|
---|
247 | if (D_CACHEEX & cs_dblevel){
|
---|
248 | char result[CXM_FMT_LEN] = { 0 };
|
---|
249 | int32_t s, size = CXM_FMT_LEN;
|
---|
250 | s = ecmfmt(entry->caid, 0, entry->provid, entry->chid, entry->pid, entry->srvid, entry->ecmlen, 0, 0, 0, result, size);
|
---|
251 | s += snprintf(result+s, size-s, " = ");
|
---|
252 | s += ecmfmt(entry->to_caid, 0, entry->to_provid, entry->to_chid, entry->to_pid, entry->to_srvid, entry->to_ecmlen, 0, 0, 0, result+s, size-s);
|
---|
253 | s += snprintf(result+s, size-s, " valid %d/%d", entry->valid_from, entry->valid_to);
|
---|
254 | cs_debug_mask(D_CACHEEX, "cacheex-matching for: %s", result);
|
---|
255 | }
|
---|
256 | #endif
|
---|
257 | return 1;
|
---|
258 | }
|
---|
259 | }
|
---|
260 | }
|
---|
261 | return 0;
|
---|
262 | }
|
---|
263 |
|
---|
264 | static pthread_mutex_t invalid_cws_mutex;
|
---|
265 |
|
---|
266 | static void add_invalid_cw(uint8_t *cw) {
|
---|
267 | pthread_mutex_lock(&invalid_cws_mutex);
|
---|
268 | if (!invalid_cws)
|
---|
269 | invalid_cws = ll_create("invalid cws");
|
---|
270 | uint8_t *cw2;
|
---|
271 | if (cs_malloc(&cw2, 16)) {
|
---|
272 | memcpy(cw2, cw, 16);
|
---|
273 | ll_append(invalid_cws, cw2);
|
---|
274 | while (ll_count(invalid_cws) > 32) {
|
---|
275 | ll_remove_first_data(invalid_cws);
|
---|
276 | }
|
---|
277 | }
|
---|
278 | pthread_mutex_unlock(&invalid_cws_mutex);
|
---|
279 | }
|
---|
280 |
|
---|
281 | static int32_t is_invalid_cw(uint8_t *cw) {
|
---|
282 | if (!invalid_cws) return 0;
|
---|
283 |
|
---|
284 | pthread_mutex_lock(&invalid_cws_mutex);
|
---|
285 | LL_LOCKITER *li = ll_li_create(invalid_cws, 0);
|
---|
286 | uint8_t *cw2;
|
---|
287 | int32_t invalid = 0;
|
---|
288 | while ((cw2 = ll_li_next(li)) && !invalid) {
|
---|
289 | invalid = (memcmp(cw, cw2, 16) == 0);
|
---|
290 | }
|
---|
291 | ll_li_destroy(li);
|
---|
292 | pthread_mutex_unlock(&invalid_cws_mutex);
|
---|
293 | return invalid;
|
---|
294 | }
|
---|
295 |
|
---|
296 | static int32_t cacheex_add_to_cache_int(struct s_client *cl, ECM_REQUEST *er, int8_t csp)
|
---|
297 | {
|
---|
298 | if (!cl)
|
---|
299 | return 0;
|
---|
300 | if (!csp && cl->reader && cl->reader->cacheex.mode!=2) { //from reader
|
---|
301 | cs_debug_mask(D_CACHEEX, "CACHEX received, but disabled for %s", username(cl));
|
---|
302 | return 0;
|
---|
303 | }
|
---|
304 | if (!csp && !cl->reader && cl->account && cl->account->cacheex.mode!=3) { //from user
|
---|
305 | cs_debug_mask(D_CACHEEX, "CACHEX received, but disabled for %s", username(cl));
|
---|
306 | return 0;
|
---|
307 | }
|
---|
308 | if (!csp && !cl->reader && !cl->account) { //not active!
|
---|
309 | cs_debug_mask(D_CACHEEX, "CACHEX received, but invalid client state %s", username(cl));
|
---|
310 | return 0;
|
---|
311 | }
|
---|
312 |
|
---|
313 | if (er->rc < E_NOTFOUND) { //=FOUND Check CW:
|
---|
314 | uint8_t i, c;
|
---|
315 | uint8_t null=0;
|
---|
316 | for (i = 0; i < 16; i += 4) {
|
---|
317 | c = ((er->cw[i] + er->cw[i + 1] + er->cw[i + 2]) & 0xff);
|
---|
318 | null |= (er->cw[i] | er->cw[i + 1] | er->cw[i + 2]);
|
---|
319 | if (er->cw[i + 3] != c) {
|
---|
320 | cs_ddump_mask(D_CACHEEX, er->cw, 16, "push received cw with chksum error from %s", csp ? "csp" : username(cl));
|
---|
321 | cl->cwcacheexerr++;
|
---|
322 | if (cl->account)
|
---|
323 | cl->account->cwcacheexerr++;
|
---|
324 | return 0;
|
---|
325 | }
|
---|
326 | }
|
---|
327 |
|
---|
328 | if (null==0) {
|
---|
329 | cs_ddump_mask(D_CACHEEX, er->cw, 16, "push received null cw from %s", csp ? "csp" : username(cl));
|
---|
330 | cl->cwcacheexerr++;
|
---|
331 | if (cl->account)
|
---|
332 | cl->account->cwcacheexerr++;
|
---|
333 | return 0;
|
---|
334 | }
|
---|
335 |
|
---|
336 | if (is_invalid_cw(er->cw)) {
|
---|
337 | cs_ddump_mask(D_TRACE, er->cw, 16, "push received invalid cw from %s", csp ? "csp" : username(cl));
|
---|
338 | cl->cwcacheexerrcw++;
|
---|
339 | if (cl->account)
|
---|
340 | cl->account->cwcacheexerrcw++;
|
---|
341 | return 0;
|
---|
342 | }
|
---|
343 | }
|
---|
344 |
|
---|
345 | er->grp |= cl->grp; //extend group instead overwriting, this fixes some funny not founds and timeouts when using more cacheex readers with different groups
|
---|
346 | // er->ocaid = er->caid;
|
---|
347 | if (er->rc < E_NOTFOUND) //map FOUND to CACHEEX
|
---|
348 | er->rc = E_CACHEEX;
|
---|
349 | er->cacheex_src = cl;
|
---|
350 | er->client = NULL; //No Owner! So no fallback!
|
---|
351 |
|
---|
352 | if (er->ecmlen) {
|
---|
353 | int32_t offset = 3;
|
---|
354 | if ((er->caid >> 8) == 0x17)
|
---|
355 | offset = 13;
|
---|
356 | unsigned char md5tmp[MD5_DIGEST_LENGTH];
|
---|
357 | memcpy(er->ecmd5, MD5(er->ecm+offset, er->ecmlen-offset, md5tmp), CS_ECMSTORESIZE);
|
---|
358 | cacheex_update_hash(er);
|
---|
359 | //csp has already initialized these hashcode
|
---|
360 |
|
---|
361 | update_chid(er);
|
---|
362 | }
|
---|
363 |
|
---|
364 | struct ecm_request_t *ecm = check_cwcache(er, cl);
|
---|
365 |
|
---|
366 | add_hitcache(cl, er, ecm);
|
---|
367 |
|
---|
368 | // {
|
---|
369 | // char h1[20];
|
---|
370 | // char h2[10];
|
---|
371 | // cs_hexdump(0, er->ecmd5, sizeof(er->ecmd5), h1, sizeof(h1));
|
---|
372 | // cs_hexdump(0, (const uchar*)&er->csp_hash, sizeof(er->csp_hash), h2, sizeof(h2));
|
---|
373 | // debug_ecm(D_TRACE, "cache push check %s: %s %s %s rc=%d found cache: %s", username(cl), buf, h1, h2, er->rc, ecm==NULL?"no":"yes");
|
---|
374 | // }
|
---|
375 |
|
---|
376 | if (!ecm) {
|
---|
377 | uint8_t cwcycle_act = cwcycle_check_act(er->caid);
|
---|
378 | if (er->rc < E_NOTFOUND) { // Do NOT add cacheex - not founds!
|
---|
379 | if (!cwcycle_act) {
|
---|
380 | cs_writelock(&ecmcache_lock);
|
---|
381 | er->next = ecmcwcache;
|
---|
382 | ecmcwcache = er;
|
---|
383 | cs_writeunlock(&ecmcache_lock);
|
---|
384 | }
|
---|
385 | er->selected_reader = cl->reader;
|
---|
386 |
|
---|
387 | cacheex_cache_push(er); //cascade push!
|
---|
388 |
|
---|
389 | if (er->rc < E_NOTFOUND)
|
---|
390 | cacheex_add_stats(cl, er->caid, er->srvid, er->prid, 1);
|
---|
391 |
|
---|
392 | cl->cwcacheexgot++;
|
---|
393 | if (cl->account)
|
---|
394 | cl->account->cwcacheexgot++;
|
---|
395 | first_client->cwcacheexgot++;
|
---|
396 | if (cwcycle_act)
|
---|
397 | er->rc = E_NOTFOUND; //need to free
|
---|
398 | }
|
---|
399 | debug_ecm(D_CACHEEX, "got pushed %sECM %s from %s (%s)", (er->rc == E_UNHANDLED)?"request ":"", buf, csp ? "csp" : username(cl),(cwcycle_act)?"on":"off");
|
---|
400 |
|
---|
401 | return er->rc < E_NOTFOUND ? 1 : 0;
|
---|
402 | } else {
|
---|
403 | if (er->rc < ecm->rc) {
|
---|
404 | if (ecm->csp_lastnodes == NULL) {
|
---|
405 | ecm->csp_lastnodes = er->csp_lastnodes;
|
---|
406 | er->csp_lastnodes = NULL;
|
---|
407 | }
|
---|
408 | ecm->cacheex_src = cl;
|
---|
409 | ecm->cacheex_pushed = 0;
|
---|
410 |
|
---|
411 | write_ecm_answer(cl->reader, ecm, er->rc, er->rcEx, er->cw, ecm->msglog);
|
---|
412 |
|
---|
413 | if (er->rc < E_NOTFOUND)
|
---|
414 | ecm->selected_reader = cl->reader;
|
---|
415 |
|
---|
416 | cacheex_cache_push(ecm); //cascade push!
|
---|
417 |
|
---|
418 | if (er->rc < E_NOTFOUND)
|
---|
419 | cacheex_add_stats(cl, er->caid, er->srvid, er->prid, 1);
|
---|
420 |
|
---|
421 | cl->cwcacheexgot++;
|
---|
422 | if (cl->account)
|
---|
423 | cl->account->cwcacheexgot++;
|
---|
424 | first_client->cwcacheexgot++;
|
---|
425 |
|
---|
426 | debug_ecm(D_CACHEEX| D_CSPCWC, "replaced pushed ECM %s from %s", buf, csp ? "csp" : username(cl));
|
---|
427 | } else {
|
---|
428 | if (er->rc < E_NOTFOUND && memcmp(er->cw, ecm->cw, sizeof(er->cw)) != 0) {
|
---|
429 | add_invalid_cw(ecm->cw);
|
---|
430 | add_invalid_cw(er->cw);
|
---|
431 |
|
---|
432 | cl->cwcacheexerrcw++;
|
---|
433 | if (cl->account)
|
---|
434 | cl->account->cwcacheexerrcw++;
|
---|
435 |
|
---|
436 | char cw1[16*3+2], cw2[16*3+2];
|
---|
437 | cs_hexdump(0, er->cw, 16, cw1, sizeof(cw1));
|
---|
438 | cs_hexdump(0, ecm->cw, 16, cw2, sizeof(cw2));
|
---|
439 |
|
---|
440 | char ip1[20]="", ip2[20]="";
|
---|
441 | if (cl)
|
---|
442 | cs_strncpy(ip1, cs_inet_ntoa(cl->ip), sizeof(ip1));
|
---|
443 | if (ecm->cacheex_src)
|
---|
444 | cs_strncpy(ip2, cs_inet_ntoa(ecm->cacheex_src->ip), sizeof(ip2));
|
---|
445 | else if (ecm->selected_reader)
|
---|
446 | cs_strncpy(ip2, cs_inet_ntoa(ecm->selected_reader->client->ip), sizeof(ip2));
|
---|
447 |
|
---|
448 | void *el = ll_has_elements(er->csp_lastnodes);
|
---|
449 | uint64_t node1 = el?(*(uint64_t*)el):0;
|
---|
450 |
|
---|
451 | el = ll_has_elements(ecm->csp_lastnodes);
|
---|
452 | uint64_t node2 = el?(*(uint64_t*)el):0;
|
---|
453 |
|
---|
454 | el = ll_last_element(er->csp_lastnodes);
|
---|
455 | uint64_t node3 = el?(*(uint64_t*)el):0;
|
---|
456 |
|
---|
457 | el = ll_last_element(ecm->csp_lastnodes);
|
---|
458 | uint64_t node4 = el?(*(uint64_t*)el):0;
|
---|
459 |
|
---|
460 | debug_ecm(D_TRACE| D_CSPCWC, "WARNING: Different CWs %s from %s(%s)<>%s(%s): %s<>%s nodes %llX %llX %llX %llX", buf,
|
---|
461 | csp ? "csp" : username(cl), ip1,
|
---|
462 | ecm->cacheex_src?username(ecm->cacheex_src):(ecm->selected_reader?ecm->selected_reader->label:"unknown/csp"), ip2,
|
---|
463 | cw1, cw2,
|
---|
464 | (long long unsigned int)node1,
|
---|
465 | (long long unsigned int)node2,
|
---|
466 | (long long unsigned int)node3,
|
---|
467 | (long long unsigned int)node4);
|
---|
468 |
|
---|
469 | //char ecmd51[17*3];
|
---|
470 | //cs_hexdump(0, er->ecmd5, 16, ecmd51, sizeof(ecmd51));
|
---|
471 | //char csphash1[5*3];
|
---|
472 | //cs_hexdump(0, (void*)&er->csp_hash, 4, csphash1, sizeof(csphash1));
|
---|
473 | //char ecmd52[17*3];
|
---|
474 | //cs_hexdump(0, ecm->ecmd5, 16, ecmd52, sizeof(ecmd52));
|
---|
475 | //char csphash2[5*3];
|
---|
476 | //cs_hexdump(0, (void*)&ecm->csp_hash, 4, csphash2, sizeof(csphash2));
|
---|
477 | //debug_ecm(D_TRACE, "WARNING: Different CWs %s from %s<>%s: %s<>%s %s<>%s %s<>%s", buf,
|
---|
478 | // csp ? "csp" : username(cl),
|
---|
479 | // ecm->cacheex_src?username(ecm->cacheex_src):"unknown/csp",
|
---|
480 | // cw1, cw2,
|
---|
481 | // ecmd51, ecmd52,
|
---|
482 | // csphash1, csphash2
|
---|
483 | // );
|
---|
484 | } else {
|
---|
485 | debug_ecm(D_CACHEEX| D_CSPCWC, "ignored duplicate pushed ECM %s from %s", buf, csp ? "csp" : username(cl));
|
---|
486 | }
|
---|
487 | }
|
---|
488 | return 0;
|
---|
489 | }
|
---|
490 | }
|
---|
491 |
|
---|
492 | void cacheex_add_to_cache(struct s_client *cl, ECM_REQUEST *er)
|
---|
493 | {
|
---|
494 | if (!cacheex_add_to_cache_int(cl, er, 0))
|
---|
495 | free_ecm(er);
|
---|
496 | }
|
---|
497 |
|
---|
498 | void cacheex_add_to_cache_from_csp(struct s_client *cl, ECM_REQUEST *er)
|
---|
499 | {
|
---|
500 | if (!cacheex_add_to_cache_int(cl, er, 1))
|
---|
501 | free_ecm(er);
|
---|
502 | }
|
---|
503 |
|
---|
504 | //Format:
|
---|
505 | //caid:prov:srvid:pid:chid:ecmlen=caid:prov:srvid:pid:chid:ecmlen[,validfrom,validto]
|
---|
506 | //validfrom: default=-2000
|
---|
507 | //validto: default=4000
|
---|
508 | //valid time if found in cache
|
---|
509 | static struct s_cacheex_matcher *cacheex_matcher_read_int(void) {
|
---|
510 | FILE *fp = open_config_file(cs_cacheex_matcher);
|
---|
511 | if (!fp)
|
---|
512 | return NULL;
|
---|
513 |
|
---|
514 | char token[1024];
|
---|
515 | unsigned char type;
|
---|
516 | int32_t i, ret, count=0;
|
---|
517 | struct s_cacheex_matcher *new_cacheex_matcher = NULL, *entry, *last=NULL;
|
---|
518 | uint32_t line = 0;
|
---|
519 |
|
---|
520 | while (fgets(token, sizeof(token), fp)) {
|
---|
521 | line++;
|
---|
522 | if (strlen(token) <= 1) continue;
|
---|
523 | if (token[0]=='#' || token[0]=='/') continue;
|
---|
524 | if (strlen(token)>100) continue;
|
---|
525 |
|
---|
526 | for (i=0;i<(int)strlen(token);i++) {
|
---|
527 | if ((token[i]==':' || token[i]==' ') && token[i+1]==':') {
|
---|
528 | memmove(token+i+2, token+i+1, strlen(token)-i+1);
|
---|
529 | token[i+1]='0';
|
---|
530 | }
|
---|
531 | if (token[i]=='#' || token[i]=='/') {
|
---|
532 | token[i]='\0';
|
---|
533 | break;
|
---|
534 | }
|
---|
535 | }
|
---|
536 |
|
---|
537 | type = 'm';
|
---|
538 | uint32_t caid=0, provid=0, srvid=0, pid=0, chid=0, ecmlen=0;
|
---|
539 | uint32_t to_caid=0, to_provid=0, to_srvid=0, to_pid=0, to_chid=0, to_ecmlen=0;
|
---|
540 | int32_t valid_from=-2000, valid_to=4000;
|
---|
541 |
|
---|
542 | ret = sscanf(token, "%c:%4x:%6x:%4x:%4x:%4x:%4X=%4x:%6x:%4x:%4x:%4x:%4X,%4d,%4d",
|
---|
543 | &type,
|
---|
544 | &caid, &provid, &srvid, &pid, &chid, &ecmlen,
|
---|
545 | &to_caid, &to_provid, &to_srvid, &to_pid, &to_chid, &to_ecmlen,
|
---|
546 | &valid_from, &valid_to);
|
---|
547 |
|
---|
548 | type = tolower(type);
|
---|
549 |
|
---|
550 | if (ret<7 || type != 'm')
|
---|
551 | continue;
|
---|
552 |
|
---|
553 | if (!cs_malloc(&entry, sizeof(struct s_cacheex_matcher))) {
|
---|
554 | fclose(fp);
|
---|
555 | return new_cacheex_matcher;
|
---|
556 | }
|
---|
557 | count++;
|
---|
558 | entry->line=line;
|
---|
559 | entry->type=type;
|
---|
560 | entry->caid=caid;
|
---|
561 | entry->provid=provid;
|
---|
562 | entry->srvid=srvid;
|
---|
563 | entry->pid=pid;
|
---|
564 | entry->chid=chid;
|
---|
565 | entry->ecmlen=ecmlen;
|
---|
566 | entry->to_caid=to_caid;
|
---|
567 | entry->to_provid=to_provid;
|
---|
568 | entry->to_srvid=to_srvid;
|
---|
569 | entry->to_pid=to_pid;
|
---|
570 | entry->to_chid=to_chid;
|
---|
571 | entry->to_ecmlen=to_ecmlen;
|
---|
572 | entry->valid_from=valid_from;
|
---|
573 | entry->valid_to=valid_to;
|
---|
574 |
|
---|
575 | cs_debug_mask(D_TRACE, "cacheex-matcher: %c: %04X:%06X:%04X:%04X:%04X:%02X = %04X:%06X:%04X:%04X:%04X:%02X valid %d/%d",
|
---|
576 | entry->type, entry->caid, entry->provid, entry->srvid, entry->pid, entry->chid, entry->ecmlen,
|
---|
577 | entry->to_caid, entry->to_provid, entry->to_srvid, entry->to_pid, entry->to_chid, entry->to_ecmlen,
|
---|
578 | entry->valid_from, entry->valid_to);
|
---|
579 |
|
---|
580 | if (!new_cacheex_matcher) {
|
---|
581 | new_cacheex_matcher=entry;
|
---|
582 | last = new_cacheex_matcher;
|
---|
583 | } else {
|
---|
584 | last->next = entry;
|
---|
585 | last = entry;
|
---|
586 | }
|
---|
587 | }
|
---|
588 |
|
---|
589 | if (count)
|
---|
590 | cs_log("%d entries read from %s", count, cs_cacheex_matcher);
|
---|
591 |
|
---|
592 | fclose(fp);
|
---|
593 |
|
---|
594 | return new_cacheex_matcher;
|
---|
595 | }
|
---|
596 |
|
---|
597 | void cacheex_load_config_file(void) {
|
---|
598 | struct s_cacheex_matcher *entry, *old_list;
|
---|
599 |
|
---|
600 | old_list = cfg.cacheex_matcher;
|
---|
601 | cfg.cacheex_matcher = cacheex_matcher_read_int();
|
---|
602 |
|
---|
603 | while (old_list) {
|
---|
604 | entry = old_list->next;
|
---|
605 | free(old_list);
|
---|
606 | old_list = entry;
|
---|
607 | }
|
---|
608 | }
|
---|
609 |
|
---|
610 | static int32_t cacheex_ecm_hash_calc(uchar *buf, int32_t n) {
|
---|
611 | int32_t i, h = 0;
|
---|
612 | for (i = 0; i < n; i++) {
|
---|
613 | h = 31 * h + buf[i];
|
---|
614 | }
|
---|
615 | return h;
|
---|
616 | }
|
---|
617 |
|
---|
618 | void cacheex_update_hash(ECM_REQUEST *er) {
|
---|
619 | er->csp_hash = cacheex_ecm_hash_calc(er->ecm+3, er->ecmlen-3);
|
---|
620 | }
|
---|
621 |
|
---|
622 | /**
|
---|
623 | * csp cacheex hit cache
|
---|
624 | **/
|
---|
625 |
|
---|
626 | void add_hitcache(struct s_client *cl, ECM_REQUEST *er, ECM_REQUEST *ecm) {
|
---|
627 | bool upd_hit = true;
|
---|
628 | if (!cfg.cacheex_wait_timetab.n)
|
---|
629 | return;
|
---|
630 | uint32_t cacheex_wait_time = get_cacheex_wait_time(er,NULL);
|
---|
631 | if (!cacheex_wait_time)
|
---|
632 | return;
|
---|
633 | if (er->rc < E_NOTFOUND) {
|
---|
634 |
|
---|
635 | if (ecm){
|
---|
636 | struct s_reader *cl_rdr = cl->reader;
|
---|
637 | if (cl_rdr) {
|
---|
638 | struct s_reader *rdr;
|
---|
639 | struct s_ecm_answer *ea;
|
---|
640 | for(ea = ecm->matching_rdr; ea; ea = ea->next) {
|
---|
641 | rdr = ea->reader;
|
---|
642 | if (cl_rdr == rdr && cl_rdr->cacheex.mode == 2 && ((ea->status & REQUEST_ANSWERED) == REQUEST_ANSWERED)){
|
---|
643 | cs_debug_mask(D_CACHEEX|D_CSPCWC,"[ADD_HITCACHE] skip add self request");
|
---|
644 | return; //don't add hit cache, reader requested self
|
---|
645 | }
|
---|
646 | }
|
---|
647 | }
|
---|
648 |
|
---|
649 | if (er->rc >= ecm->rc && er->rc < E_NOTFOUND && (ecm->tps.millitm - er->tps.millitm) > 0 && cacheex_wait_time) {
|
---|
650 | cs_debug_mask(D_CACHEEX|D_CSPCWC,"[ADD_HITCACHE] skip add too old");
|
---|
651 | return; //check ignored duplicate time, is over wait time don't add hit cache
|
---|
652 | }
|
---|
653 | }
|
---|
654 |
|
---|
655 | cs_writelock(&hitcache_lock);
|
---|
656 | CSPCEHIT *ch = check_hitcache(er,cl,0);//, *ch_t = NULL;
|
---|
657 | if (!ch && cs_malloc(&ch, sizeof(CSPCEHIT))) {
|
---|
658 | upd_hit = false;
|
---|
659 | ch->ecmlen = er->ecmlen;
|
---|
660 | ch->caid = er->caid;
|
---|
661 | ch->prid = er->prid;
|
---|
662 | ch->srvid = er->srvid;
|
---|
663 | ch->grp = 0;
|
---|
664 | ch->prev = ch->next = NULL;
|
---|
665 | if (cspec_hitcache) {
|
---|
666 | cspec_hitcache->prev = ch;
|
---|
667 | ch->next = cspec_hitcache;
|
---|
668 | }
|
---|
669 | cspec_hitcache = ch;
|
---|
670 | cspec_hitcache_size++;
|
---|
671 | }
|
---|
672 | if (ch){
|
---|
673 | cs_debug_mask(D_CACHEEX|D_CSPCWC,"[CSPCEHIT] add_hitcache %s entry ecmlen %d caid %04X provid %06X srvid %04X grp %"PRIu64" next %s size %d", upd_hit?"upd":"add", ch->ecmlen, ch->caid, ch->prid, ch->srvid, ch->grp, (ch->next)?"Yes":"No", cspec_hitcache_size);
|
---|
674 | ch->grp |= er->grp;
|
---|
675 | ch->time = time(NULL); //always update time;
|
---|
676 |
|
---|
677 | if (upd_hit && ch->prev){ //is ch->prev NULL we are top in list, no move
|
---|
678 | if (ch->next) {
|
---|
679 | ch->prev->next = ch->next;
|
---|
680 | ch->next->prev = ch->prev;
|
---|
681 | } else {
|
---|
682 | ch->prev->next = NULL;
|
---|
683 | }
|
---|
684 | ch->prev = NULL;
|
---|
685 | cspec_hitcache->prev = ch;
|
---|
686 | ch->next = cspec_hitcache;
|
---|
687 | cspec_hitcache = ch;
|
---|
688 | }
|
---|
689 |
|
---|
690 | }
|
---|
691 | cs_writeunlock(&hitcache_lock);
|
---|
692 | }
|
---|
693 | }
|
---|
694 |
|
---|
695 | struct csp_ce_hit_t *check_hitcache(ECM_REQUEST *er, struct s_client *cl, uint8_t lock) {
|
---|
696 | time_t now = time(NULL);
|
---|
697 | time_t timeout = now-cfg.max_cache_time;
|
---|
698 | CSPCEHIT *ch;
|
---|
699 | uint64_t grp = cl?cl->grp:0;
|
---|
700 | uint8_t fs=0;
|
---|
701 |
|
---|
702 | if (lock) cs_readlock(&hitcache_lock);
|
---|
703 | for (ch = cspec_hitcache; ch; ch = ch->next) {
|
---|
704 | if (ch->time < timeout) {
|
---|
705 | ch = NULL;
|
---|
706 | break;
|
---|
707 | }
|
---|
708 | fs |= 1;
|
---|
709 | if (!((er->caid == ch->caid) && (er->prid == ch->prid) && (er->srvid == ch->srvid)))
|
---|
710 | continue;
|
---|
711 | fs |= 2;
|
---|
712 | if ((ch->ecmlen && er->ecmlen && ch->ecmlen != er->ecmlen))
|
---|
713 | continue;
|
---|
714 | if (lock) {
|
---|
715 | fs |= 4;
|
---|
716 | if ((grp && ch->grp && !(grp & ch->grp))){
|
---|
717 | continue;
|
---|
718 | }
|
---|
719 | } else {
|
---|
720 | fs |= 4;
|
---|
721 | }
|
---|
722 | fs |= 8;
|
---|
723 | break;
|
---|
724 | }
|
---|
725 | if (lock) cs_readunlock(&hitcache_lock);
|
---|
726 | if ((fs != 15) && ch) {
|
---|
727 | cs_log("[CSPCEHIT] check_hitcache error on check hitcache");
|
---|
728 | ch = NULL;
|
---|
729 | }
|
---|
730 | cs_debug_mask(D_CACHEEX| D_CSPCWC,"[CSPCEHIT] check_hitcache %s hit found max stage %d caid %04X prov %06X serv %04X grp %"PRIu64" lock %s", (fs == 15)?"yes":"no", fs, er->caid, er->prid, er->srvid, grp, lock?"yes":"no");
|
---|
731 | return ch;
|
---|
732 | }
|
---|
733 |
|
---|
734 | void cleanup_hitcache(void) {
|
---|
735 | CSPCEHIT *current = NULL, *prv, *temp;
|
---|
736 | int32_t count = 0, mcc = cfg.max_cache_count;
|
---|
737 | int32_t mct = cfg.max_cache_time + (cfg.max_cache_time / 2); // 1,5
|
---|
738 | time_t now = time(NULL);
|
---|
739 |
|
---|
740 | cs_writelock(&hitcache_lock);
|
---|
741 | /*for(current = cspec_hitcache, prv = NULL; current; prv=current, current = current->next, count++) {
|
---|
742 | cs_debug_mask(D_CACHEEX,"[CSPCEHIT] cleanup time %d ecmlen %d caid %04X provid %06X srvid %04X grp %llu count %d", (int32_t)now-current->time, current->ecmlen, current->caid, current->prid, current->srvid, current->grp, count);
|
---|
743 | if (count > 25)
|
---|
744 | break;
|
---|
745 | }*/
|
---|
746 |
|
---|
747 | for(current = cspec_hitcache, prv = NULL; current; prv=current, current = current->next, count++) {
|
---|
748 | if ((now - current->time) < mct && count < mcc) { // delete old Entry to hold list small
|
---|
749 | continue;
|
---|
750 | }
|
---|
751 | if (prv) {
|
---|
752 | prv->next = NULL;
|
---|
753 | } else {
|
---|
754 | cspec_hitcache = NULL;
|
---|
755 | }
|
---|
756 | break; //we need only once, all follow to old or cache max size
|
---|
757 | }
|
---|
758 | cs_writeunlock(&hitcache_lock);
|
---|
759 | cspec_hitcache_size = count;
|
---|
760 |
|
---|
761 | if (current)
|
---|
762 | cs_debug_mask(D_CACHEEX|D_CSPCWC,"[CSPCEHIT] cleanup list new size %d ct %d", cspec_hitcache_size, mct);
|
---|
763 |
|
---|
764 | if (current) {
|
---|
765 | while (current) {
|
---|
766 | temp = current->next;
|
---|
767 | free(current);
|
---|
768 | current = NULL;
|
---|
769 | current = temp;
|
---|
770 | }
|
---|
771 | }
|
---|
772 | }
|
---|
773 |
|
---|
774 | uint32_t get_cacheex_wait_time(ECM_REQUEST *er, struct s_client *cl) {
|
---|
775 | int32_t i,dwtime= -1,awtime=-1;
|
---|
776 | CSPCEHIT *ch;
|
---|
777 |
|
---|
778 | for (i = 0; i < cfg.cacheex_wait_timetab.n; i++) {
|
---|
779 | if (i == 0 && cfg.cacheex_wait_timetab.caid[i] <= 0) {
|
---|
780 | dwtime = cfg.cacheex_wait_timetab.dwtime[i];
|
---|
781 | awtime = cfg.cacheex_wait_timetab.awtime[i];
|
---|
782 | continue; //check other, only valid for unset
|
---|
783 | }
|
---|
784 |
|
---|
785 | if (cfg.cacheex_wait_timetab.caid[i] == er->caid || cfg.cacheex_wait_timetab.caid[i] == er->caid>>8 || ((cfg.cacheex_wait_timetab.cmask[i]>=0 && (er->caid & cfg.cacheex_wait_timetab.cmask[i]) == cfg.cacheex_wait_timetab.caid[i]) || cfg.cacheex_wait_timetab.caid[i] == -1)) {
|
---|
786 | if ((cfg.cacheex_wait_timetab.prid[i]>=0 && cfg.cacheex_wait_timetab.prid[i] == (int32_t)er->prid) || cfg.cacheex_wait_timetab.prid[i] == -1) {
|
---|
787 | if ((cfg.cacheex_wait_timetab.srvid[i]>=0 && cfg.cacheex_wait_timetab.srvid[i] == er->srvid) || cfg.cacheex_wait_timetab.srvid[i] == -1) {
|
---|
788 | dwtime = cfg.cacheex_wait_timetab.dwtime[i];
|
---|
789 | awtime = cfg.cacheex_wait_timetab.awtime[i];
|
---|
790 | break;
|
---|
791 | }
|
---|
792 | }
|
---|
793 |
|
---|
794 | };
|
---|
795 |
|
---|
796 | }
|
---|
797 | if (awtime > 0 && dwtime <= 0) {
|
---|
798 | return awtime;
|
---|
799 | }
|
---|
800 | if (cl == NULL) {
|
---|
801 | if (dwtime < 0)
|
---|
802 | dwtime = 0;
|
---|
803 | return dwtime;
|
---|
804 | }
|
---|
805 | if (awtime > 0 || dwtime > 0) {
|
---|
806 | //if found last in cache return dynwaittime else alwayswaittime
|
---|
807 | ch = check_hitcache(er,cl,1);
|
---|
808 | if (ch)
|
---|
809 | return dwtime>=awtime?dwtime:awtime;
|
---|
810 | else
|
---|
811 | return awtime>0?awtime:0;
|
---|
812 | }
|
---|
813 | return 0;
|
---|
814 | }
|
---|
815 |
|
---|
816 | int32_t chk_csp_ctab(ECM_REQUEST *er, CECSPVALUETAB *tab) {
|
---|
817 | if (!er->caid || !tab->n)
|
---|
818 | return 1; // nothing setup we add all
|
---|
819 | int32_t i;
|
---|
820 | for (i = 0; i < tab->n; i++) {
|
---|
821 |
|
---|
822 | if (tab->caid[i] > 0) {
|
---|
823 | if (tab->caid[i] == er->caid || tab->caid[i] == er->caid>>8 || ((tab->cmask[i]>=0 && (er->caid & tab->cmask[i]) == tab->caid[i]) || tab->caid[i] == -1)) {
|
---|
824 | if ((tab->prid[i]>=0 && tab->prid[i] == (int32_t)er->prid) || tab->prid[i] == -1) {
|
---|
825 | if ((tab->srvid[i]>=0 && tab->srvid[i] == er->srvid) || tab->srvid[i] == -1) {
|
---|
826 | return 1;
|
---|
827 | }
|
---|
828 | }
|
---|
829 | }
|
---|
830 | }
|
---|
831 | }
|
---|
832 | return 0;
|
---|
833 | }
|
---|
834 |
|
---|
835 | uint8_t check_cacheex_filter(struct s_client *cl, ECM_REQUEST *er) {
|
---|
836 | CECSP *ce_csp = NULL;
|
---|
837 | uint8_t ret = 1;
|
---|
838 | if (cl->typ == 'c') {
|
---|
839 | if (cl->account && cl->account->cacheex.mode==3) {
|
---|
840 | ce_csp = &cl->account->cacheex;
|
---|
841 | }
|
---|
842 | } else if (cl->typ == 'p'){
|
---|
843 | if (cl->reader && cl->reader->cacheex.mode==2) {
|
---|
844 | ce_csp = &cl->reader->cacheex;
|
---|
845 | }
|
---|
846 | }
|
---|
847 |
|
---|
848 | if (ce_csp) {
|
---|
849 | if (!chk_csp_ctab(er, &ce_csp->filter_caidtab))
|
---|
850 | ret = 0;
|
---|
851 | if (er->rc != E_FOUND && !ce_csp->allow_request)
|
---|
852 | ret = 0;
|
---|
853 | if (ce_csp->drop_csp && !checkECMD5(er))
|
---|
854 | ret = 0;
|
---|
855 | }
|
---|
856 | if (!ret)
|
---|
857 | free(er);
|
---|
858 | return ret;
|
---|
859 | }
|
---|
860 |
|
---|
861 | #endif
|
---|