This commit was manufactured by cvs2svn to create branch 'SAMBA_3_0'.
[sfrench/samba-autobuild/.git] / source / nsswitch / winbindd_ads.c
1 /* 
2    Unix SMB/CIFS implementation.
3
4    Winbind ADS backend functions
5
6    Copyright (C) Andrew Tridgell 2001
7    
8    This program is free software; you can redistribute it and/or modify
9    it under the terms of the GNU General Public License as published by
10    the Free Software Foundation; either version 2 of the License, or
11    (at your option) any later version.
12    
13    This program is distributed in the hope that it will be useful,
14    but WITHOUT ANY WARRANTY; without even the implied warranty of
15    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16    GNU General Public License for more details.
17    
18    You should have received a copy of the GNU General Public License
19    along with this program; if not, write to the Free Software
20    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21 */
22
23 #include "winbindd.h"
24
25 #ifdef HAVE_ADS
26
27 #undef DBGC_CLASS
28 #define DBGC_CLASS DBGC_WINBIND
29
30 /* the realm of our primary LDAP server */
31 static char *primary_realm;
32
33
34 /*
35   return our ads connections structure for a domain. We keep the connection
36   open to make things faster
37 */
38 static ADS_STRUCT *ads_cached_connection(struct winbindd_domain *domain)
39 {
40         ADS_STRUCT *ads;
41         ADS_STATUS status;
42
43         if (domain->private) {
44                 return (ADS_STRUCT *)domain->private;
45         }
46
47         /* we don't want this to affect the users ccache */
48         setenv("KRB5CCNAME", "MEMORY:winbind_ccache", 1);
49
50         ads = ads_init(domain->alt_name, domain->name, NULL);
51         if (!ads) {
52                 DEBUG(1,("ads_init for domain %s failed\n", domain->name));
53                 return NULL;
54         }
55
56         /* the machine acct password might have change - fetch it every time */
57         SAFE_FREE(ads->auth.password);
58         ads->auth.password = secrets_fetch_machine_password();
59
60         if (primary_realm) {
61                 SAFE_FREE(ads->auth.realm);
62                 ads->auth.realm = strdup(primary_realm);
63         }
64
65         status = ads_connect(ads);
66         if (!ADS_ERR_OK(status) || !ads->config.realm) {
67                 extern struct winbindd_methods msrpc_methods;
68                 DEBUG(1,("ads_connect for domain %s failed: %s\n", 
69                          domain->name, ads_errstr(status)));
70                 ads_destroy(&ads);
71
72                 /* if we get ECONNREFUSED then it might be a NT4
73                    server, fall back to MSRPC */
74                 if (status.error_type == ADS_ERROR_SYSTEM &&
75                     status.err.rc == ECONNREFUSED) {
76                         DEBUG(1,("Trying MSRPC methods\n"));
77                         domain->methods = &msrpc_methods;
78                 }
79                 return NULL;
80         }
81
82         /* remember our primary realm for trusted domain support */
83         if (!primary_realm) {
84                 primary_realm = strdup(ads->config.realm);
85         }
86
87         domain->private = (void *)ads;
88         return ads;
89 }
90
91 /* useful utility */
92 static void sid_from_rid(struct winbindd_domain *domain, uint32 rid, DOM_SID *sid)
93 {
94         sid_copy(sid, &domain->sid);
95         sid_append_rid(sid, rid);
96 }
97
98
99 /* Query display info for a realm. This is the basic user list fn */
100 static NTSTATUS query_user_list(struct winbindd_domain *domain,
101                                TALLOC_CTX *mem_ctx,
102                                uint32 *num_entries, 
103                                WINBIND_USERINFO **info)
104 {
105         ADS_STRUCT *ads = NULL;
106         const char *attrs[] = {"userPrincipalName",
107                                "sAMAccountName",
108                                "name", "objectSid", "primaryGroupID", 
109                                "sAMAccountType", NULL};
110         int i, count;
111         ADS_STATUS rc;
112         void *res = NULL;
113         void *msg = NULL;
114         NTSTATUS status = NT_STATUS_UNSUCCESSFUL;
115
116         *num_entries = 0;
117
118         DEBUG(3,("ads: query_user_list\n"));
119
120         ads = ads_cached_connection(domain);
121         if (!ads) goto done;
122
123         rc = ads_search_retry(ads, &res, "(objectCategory=user)", attrs);
124         if (!ADS_ERR_OK(rc)) {
125                 DEBUG(1,("query_user_list ads_search: %s\n", ads_errstr(rc)));
126                 goto done;
127         }
128
129         count = ads_count_replies(ads, res);
130         if (count == 0) {
131                 DEBUG(1,("query_user_list: No users found\n"));
132                 goto done;
133         }
134
135         (*info) = talloc_zero(mem_ctx, count * sizeof(**info));
136         if (!*info) {
137                 status = NT_STATUS_NO_MEMORY;
138                 goto done;
139         }
140
141         i = 0;
142
143         for (msg = ads_first_entry(ads, res); msg; msg = ads_next_entry(ads, msg)) {
144                 char *name, *gecos;
145                 DOM_SID sid;
146                 uint32 rid, group;
147                 uint32 atype;
148
149                 if (!ads_pull_uint32(ads, msg, "sAMAccountType", &atype) ||
150                     ads_atype_map(atype) != SID_NAME_USER) {
151                         DEBUG(1,("Not a user account? atype=0x%x\n", atype));
152                         continue;
153                 }
154
155                 name = ads_pull_username(ads, mem_ctx, msg);
156                 gecos = ads_pull_string(ads, mem_ctx, msg, "name");
157                 if (!ads_pull_sid(ads, msg, "objectSid", &sid)) {
158                         DEBUG(1,("No sid for %s !?\n", name));
159                         continue;
160                 }
161                 if (!ads_pull_uint32(ads, msg, "primaryGroupID", &group)) {
162                         DEBUG(1,("No primary group for %s !?\n", name));
163                         continue;
164                 }
165
166                 if (!sid_peek_check_rid(&domain->sid, &sid, &rid)) {
167                         DEBUG(1,("No rid for %s !?\n", name));
168                         continue;
169                 }
170
171                 (*info)[i].acct_name = name;
172                 (*info)[i].full_name = gecos;
173                 (*info)[i].user_rid = rid;
174                 (*info)[i].group_rid = group;
175                 i++;
176         }
177
178         (*num_entries) = i;
179         status = NT_STATUS_OK;
180
181         DEBUG(3,("ads query_user_list gave %d entries\n", (*num_entries)));
182
183 done:
184         if (res) ads_msgfree(ads, res);
185
186         return status;
187 }
188
189 /* list all domain groups */
190 static NTSTATUS enum_dom_groups(struct winbindd_domain *domain,
191                                 TALLOC_CTX *mem_ctx,
192                                 uint32 *num_entries, 
193                                 struct acct_info **info)
194 {
195         ADS_STRUCT *ads = NULL;
196         const char *attrs[] = {"userPrincipalName", "sAMAccountName",
197                                "name", "objectSid", 
198                                "sAMAccountType", NULL};
199         int i, count;
200         ADS_STATUS rc;
201         void *res = NULL;
202         void *msg = NULL;
203         NTSTATUS status = NT_STATUS_UNSUCCESSFUL;
204         uint32 group_flags;
205
206         *num_entries = 0;
207
208         DEBUG(3,("ads: enum_dom_groups\n"));
209
210         ads = ads_cached_connection(domain);
211         if (!ads) goto done;
212
213         rc = ads_search_retry(ads, &res, "(objectCategory=group)", attrs);
214         if (!ADS_ERR_OK(rc)) {
215                 DEBUG(1,("enum_dom_groups ads_search: %s\n", ads_errstr(rc)));
216                 goto done;
217         }
218
219         count = ads_count_replies(ads, res);
220         if (count == 0) {
221                 DEBUG(1,("enum_dom_groups: No groups found\n"));
222                 goto done;
223         }
224
225         (*info) = talloc_zero(mem_ctx, count * sizeof(**info));
226         if (!*info) {
227                 status = NT_STATUS_NO_MEMORY;
228                 goto done;
229         }
230
231         i = 0;
232         
233         group_flags = ATYPE_GLOBAL_GROUP;
234         if ( domain->native_mode )
235                 group_flags |= ATYPE_LOCAL_GROUP;
236
237         for (msg = ads_first_entry(ads, res); msg; msg = ads_next_entry(ads, msg)) {
238                 char *name, *gecos;
239                 DOM_SID sid;
240                 uint32 rid;
241                 uint32 account_type;
242
243                 if (!ads_pull_uint32(ads, msg, "sAMAccountType", &account_type) || !(account_type & group_flags) ) 
244                         continue; 
245                         
246                 name = ads_pull_username(ads, mem_ctx, msg);
247                 gecos = ads_pull_string(ads, mem_ctx, msg, "name");
248                 if (!ads_pull_sid(ads, msg, "objectSid", &sid)) {
249                         DEBUG(1,("No sid for %s !?\n", name));
250                         continue;
251                 }
252
253                 if (!sid_peek_check_rid(&domain->sid, &sid, &rid)) {
254                         DEBUG(1,("No rid for %s !?\n", name));
255                         continue;
256                 }
257
258                 fstrcpy((*info)[i].acct_name, name);
259                 fstrcpy((*info)[i].acct_desc, gecos);
260                 (*info)[i].rid = rid;
261                 i++;
262         }
263
264         (*num_entries) = i;
265
266         status = NT_STATUS_OK;
267
268         DEBUG(3,("ads enum_dom_groups gave %d entries\n", (*num_entries)));
269
270 done:
271         if (res) ads_msgfree(ads, res);
272
273         return status;
274 }
275
276 /* list all domain local groups */
277 static NTSTATUS enum_local_groups(struct winbindd_domain *domain,
278                                 TALLOC_CTX *mem_ctx,
279                                 uint32 *num_entries, 
280                                 struct acct_info **info)
281 {
282         /*
283          * This is a stub function only as we returned the domain 
284          * ocal groups in enum_dom_groups() if the domain->native field
285          * was true.  This is a simple performance optimization when
286          * using LDAP.
287          *
288          * if we ever need to enumerate domain local groups separately, 
289          * then this the optimization in enum_dom_groups() will need 
290          * to be split out
291          */
292         *num_entries = 0;
293         
294         return NT_STATUS_OK;
295 }
296
297 /* convert a single name to a sid in a domain */
298 static NTSTATUS name_to_sid(struct winbindd_domain *domain,
299                             const char *name,
300                             DOM_SID *sid,
301                             enum SID_NAME_USE *type)
302 {
303         ADS_STRUCT *ads;
304
305         DEBUG(3,("ads: name_to_sid\n"));
306
307         ads = ads_cached_connection(domain);
308         if (!ads) 
309                 return NT_STATUS_UNSUCCESSFUL;
310
311         return ads_name_to_sid(ads, name, sid, type);
312 }
313
314 /* convert a sid to a user or group name */
315 static NTSTATUS sid_to_name(struct winbindd_domain *domain,
316                             TALLOC_CTX *mem_ctx,
317                             DOM_SID *sid,
318                             char **name,
319                             enum SID_NAME_USE *type)
320 {
321         ADS_STRUCT *ads = NULL;
322         DEBUG(3,("ads: sid_to_name\n"));
323         ads = ads_cached_connection(domain);
324         if (!ads) 
325                 return NT_STATUS_UNSUCCESSFUL;
326
327         return ads_sid_to_name(ads, mem_ctx, sid, name, type);
328 }
329
330
331 /* convert a DN to a name, rid and name type 
332    this might become a major speed bottleneck if groups have
333    lots of users, in which case we could cache the results
334 */
335 static BOOL dn_lookup(ADS_STRUCT *ads, TALLOC_CTX *mem_ctx,
336                       const char *dn,
337                       char **name, uint32 *name_type, uint32 *rid)
338 {
339         char *exp;
340         void *res = NULL;
341         const char *attrs[] = {"userPrincipalName", "sAMAccountName",
342                                "objectSid", "sAMAccountType", NULL};
343         ADS_STATUS rc;
344         uint32 atype;
345         DOM_SID sid;
346         char *escaped_dn = escape_ldap_string_alloc(dn);
347
348         if (!escaped_dn) {
349                 return False;
350         }
351
352         asprintf(&exp, "(distinguishedName=%s)", dn);
353         rc = ads_search_retry(ads, &res, exp, attrs);
354         SAFE_FREE(exp);
355         SAFE_FREE(escaped_dn);
356
357         if (!ADS_ERR_OK(rc)) {
358                 goto failed;
359         }
360
361         (*name) = ads_pull_username(ads, mem_ctx, res);
362
363         if (!ads_pull_uint32(ads, res, "sAMAccountType", &atype)) {
364                 goto failed;
365         }
366         (*name_type) = ads_atype_map(atype);
367
368         if (!ads_pull_sid(ads, res, "objectSid", &sid) || 
369             !sid_peek_rid(&sid, rid)) {
370                 goto failed;
371         }
372
373         if (res) ads_msgfree(ads, res);
374         return True;
375
376 failed:
377         if (res) ads_msgfree(ads, res);
378         return False;
379 }
380
381 /* Lookup user information from a rid */
382 static NTSTATUS query_user(struct winbindd_domain *domain, 
383                            TALLOC_CTX *mem_ctx, 
384                            uint32 user_rid, 
385                            WINBIND_USERINFO *info)
386 {
387         ADS_STRUCT *ads = NULL;
388         const char *attrs[] = {"userPrincipalName", 
389                                "sAMAccountName",
390                                "name", "objectSid", 
391                                "primaryGroupID", NULL};
392         ADS_STATUS rc;
393         int count;
394         void *msg = NULL;
395         char *exp;
396         DOM_SID sid;
397         char *sidstr;
398         NTSTATUS status = NT_STATUS_UNSUCCESSFUL;
399
400         DEBUG(3,("ads: query_user\n"));
401
402         sid_from_rid(domain, user_rid, &sid);
403
404         ads = ads_cached_connection(domain);
405         if (!ads) goto done;
406
407         sidstr = sid_binstring(&sid);
408         asprintf(&exp, "(objectSid=%s)", sidstr);
409         rc = ads_search_retry(ads, &msg, exp, attrs);
410         free(exp);
411         free(sidstr);
412         if (!ADS_ERR_OK(rc)) {
413                 DEBUG(1,("query_user(rid=%d) ads_search: %s\n", user_rid, ads_errstr(rc)));
414                 goto done;
415         }
416
417         count = ads_count_replies(ads, msg);
418         if (count != 1) {
419                 DEBUG(1,("query_user(rid=%d): Not found\n", user_rid));
420                 goto done;
421         }
422
423         info->acct_name = ads_pull_username(ads, mem_ctx, msg);
424         info->full_name = ads_pull_string(ads, mem_ctx, msg, "name");
425         if (!ads_pull_sid(ads, msg, "objectSid", &sid)) {
426                 DEBUG(1,("No sid for %d !?\n", user_rid));
427                 goto done;
428         }
429         if (!ads_pull_uint32(ads, msg, "primaryGroupID", &info->group_rid)) {
430                 DEBUG(1,("No primary group for %d !?\n", user_rid));
431                 goto done;
432         }
433         
434         if (!sid_peek_check_rid(&domain->sid,&sid, &info->user_rid)) {
435                 DEBUG(1,("No rid for %d !?\n", user_rid));
436                 goto done;
437         }
438
439         status = NT_STATUS_OK;
440
441         DEBUG(3,("ads query_user gave %s\n", info->acct_name));
442 done:
443         if (msg) ads_msgfree(ads, msg);
444
445         return status;
446 }
447
448
449 /* Lookup groups a user is a member of. */
450 static NTSTATUS lookup_usergroups(struct winbindd_domain *domain,
451                                   TALLOC_CTX *mem_ctx,
452                                   uint32 user_rid, 
453                                   uint32 *num_groups, uint32 **user_gids)
454 {
455         ADS_STRUCT *ads = NULL;
456         const char *attrs[] = {"distinguishedName", NULL};
457         const char *attrs2[] = {"tokenGroups", "primaryGroupID", NULL};
458         ADS_STATUS rc;
459         int count;
460         void *msg = NULL;
461         char *exp;
462         char *user_dn;
463         DOM_SID *sids;
464         int i;
465         uint32 primary_group;
466         DOM_SID sid;
467         char *sidstr;
468         NTSTATUS status = NT_STATUS_UNSUCCESSFUL;
469
470         *num_groups = 0;
471
472         DEBUG(3,("ads: lookup_usergroups\n"));
473
474         (*num_groups) = 0;
475
476         sid_from_rid(domain, user_rid, &sid);
477
478         ads = ads_cached_connection(domain);
479         if (!ads) goto done;
480
481         sidstr = sid_binstring(&sid);
482         asprintf(&exp, "(objectSid=%s)", sidstr);
483         rc = ads_search_retry(ads, &msg, exp, attrs);
484         free(exp);
485         free(sidstr);
486         if (!ADS_ERR_OK(rc)) {
487                 DEBUG(1,("lookup_usergroups(rid=%d) ads_search: %s\n", user_rid, ads_errstr(rc)));
488                 goto done;
489         }
490
491         user_dn = ads_pull_string(ads, mem_ctx, msg, "distinguishedName");
492
493         if (msg) ads_msgfree(ads, msg);
494
495         rc = ads_search_retry_dn(ads, &msg, user_dn, attrs2);
496         if (!ADS_ERR_OK(rc)) {
497                 DEBUG(1,("lookup_usergroups(rid=%d) ads_search tokenGroups: %s\n", user_rid, ads_errstr(rc)));
498                 goto done;
499         }
500
501         if (!ads_pull_uint32(ads, msg, "primaryGroupID", &primary_group)) {
502                 DEBUG(1,("%s: No primary group for rid=%d !?\n", domain->name, user_rid));
503                 goto done;
504         }
505
506         count = ads_pull_sids(ads, mem_ctx, msg, "tokenGroups", &sids) + 1;
507         (*user_gids) = (uint32 *)talloc_zero(mem_ctx, sizeof(uint32) * count);
508         (*user_gids)[(*num_groups)++] = primary_group;
509
510         for (i=1;i<count;i++) {
511                 uint32 rid;
512                 if (!sid_peek_check_rid(&domain->sid, &sids[i-1], &rid)) continue;
513                 (*user_gids)[*num_groups] = rid;
514                 (*num_groups)++;
515         }
516
517         status = NT_STATUS_OK;
518         DEBUG(3,("ads lookup_usergroups for rid=%d\n", user_rid));
519 done:
520         if (msg) ads_msgfree(ads, msg);
521
522         return status;
523 }
524
525 /*
526   find the members of a group, given a group rid and domain
527  */
528 static NTSTATUS lookup_groupmem(struct winbindd_domain *domain,
529                                 TALLOC_CTX *mem_ctx,
530                                 uint32 group_rid, uint32 *num_names, 
531                                 uint32 **rid_mem, char ***names, 
532                                 uint32 **name_types)
533 {
534         DOM_SID group_sid;
535         ADS_STATUS rc;
536         int count;
537         void *res=NULL;
538         ADS_STRUCT *ads = NULL;
539         char *exp;
540         NTSTATUS status = NT_STATUS_UNSUCCESSFUL;
541         char *sidstr;
542         const char *attrs[] = {"member", NULL};
543         char **members;
544         int i, num_members;
545
546         *num_names = 0;
547
548         ads = ads_cached_connection(domain);
549         if (!ads) goto done;
550
551         sid_from_rid(domain, group_rid, &group_sid);
552         sidstr = sid_binstring(&group_sid);
553
554         /* search for all members of the group */
555         asprintf(&exp, "(objectSid=%s)",sidstr);
556         rc = ads_search_retry(ads, &res, exp, attrs);
557         free(exp);
558         free(sidstr);
559
560         if (!ADS_ERR_OK(rc)) {
561                 DEBUG(1,("query_user_list ads_search: %s\n", ads_errstr(rc)));
562                 goto done;
563         }
564
565         count = ads_count_replies(ads, res);
566         if (count == 0) {
567                 status = NT_STATUS_OK;
568                 goto done;
569         }
570
571         members = ads_pull_strings(ads, mem_ctx, res, "member");
572         if (!members) {
573                 /* no members? ok ... */
574                 status = NT_STATUS_OK;
575                 goto done;
576         }
577
578         /* now we need to turn a list of members into rids, names and name types 
579            the problem is that the members are in the form of distinguised names
580         */
581         for (i=0;members[i];i++) /* noop */ ;
582         num_members = i;
583
584         (*rid_mem) = talloc_zero(mem_ctx, sizeof(uint32) * num_members);
585         (*name_types) = talloc_zero(mem_ctx, sizeof(uint32) * num_members);
586         (*names) = talloc_zero(mem_ctx, sizeof(char *) * num_members);
587
588         for (i=0;i<num_members;i++) {
589                 uint32 name_type, rid;
590                 char *name;
591
592                 if (dn_lookup(ads, mem_ctx, members[i], &name, &name_type, &rid)) {
593                     (*names)[*num_names] = name;
594                     (*name_types)[*num_names] = name_type;
595                     (*rid_mem)[*num_names] = rid;
596                     (*num_names)++;
597                 }
598         }       
599
600         status = NT_STATUS_OK;
601         DEBUG(3,("ads lookup_groupmem for rid=%d\n", group_rid));
602 done:
603         if (res) ads_msgfree(ads, res);
604
605         return status;
606 }
607
608
609 /* find the sequence number for a domain */
610 static NTSTATUS sequence_number(struct winbindd_domain *domain, uint32 *seq)
611 {
612         ADS_STRUCT *ads = NULL;
613         ADS_STATUS rc;
614
615         *seq = DOM_SEQUENCE_NONE;
616
617         ads = ads_cached_connection(domain);
618         if (!ads) return NT_STATUS_UNSUCCESSFUL;
619
620         rc = ads_USN(ads, seq);
621         if (!ADS_ERR_OK(rc)) {
622                 /* its a dead connection */
623                 ads_destroy(&ads);
624                 domain->private = NULL;
625         }
626         return ads_ntstatus(rc);
627 }
628
629 /* get a list of trusted domains */
630 static NTSTATUS trusted_domains(struct winbindd_domain *domain,
631                                 TALLOC_CTX *mem_ctx,
632                                 uint32 *num_domains,
633                                 char ***names,
634                                 char ***alt_names,
635                                 DOM_SID **dom_sids)
636 {
637         ADS_STRUCT *ads;
638         ADS_STATUS rc;
639
640         *num_domains = 0;
641         *names = NULL;
642
643         ads = ads_cached_connection(domain);
644         if (!ads) return NT_STATUS_UNSUCCESSFUL;
645
646         rc = ads_trusted_domains(ads, mem_ctx, num_domains, names, alt_names, dom_sids);
647
648         return ads_ntstatus(rc);
649 }
650
651 /* find the domain sid for a domain */
652 static NTSTATUS domain_sid(struct winbindd_domain *domain, DOM_SID *sid)
653 {
654         ADS_STRUCT *ads;
655         ADS_STATUS rc;
656
657         ads = ads_cached_connection(domain);
658         if (!ads) return NT_STATUS_UNSUCCESSFUL;
659
660         rc = ads_domain_sid(ads, sid);
661
662         if (!ADS_ERR_OK(rc)) {
663                 /* its a dead connection */
664                 ads_destroy(&ads);
665                 domain->private = NULL;
666         }
667
668         return ads_ntstatus(rc);
669 }
670
671
672 /* find alternate names list for the domain - for ADS this is the
673    netbios name */
674 static NTSTATUS alternate_name(struct winbindd_domain *domain)
675 {
676         ADS_STRUCT *ads;
677         ADS_STATUS rc;
678         TALLOC_CTX *ctx;
679         char *workgroup;
680
681         ads = ads_cached_connection(domain);
682         if (!ads) return NT_STATUS_UNSUCCESSFUL;
683
684         if (!(ctx = talloc_init("alternate_name"))) {
685                 return NT_STATUS_NO_MEMORY;
686         }
687
688         rc = ads_workgroup_name(ads, ctx, &workgroup);
689
690         if (ADS_ERR_OK(rc)) {
691                 fstrcpy(domain->name, workgroup);
692                 fstrcpy(domain->alt_name, ads->config.realm);
693                 strupper(domain->alt_name);
694                 strupper(domain->name);
695         }
696
697         talloc_destroy(ctx);
698
699         return ads_ntstatus(rc);        
700 }
701
702 /* the ADS backend methods are exposed via this structure */
703 struct winbindd_methods ads_methods = {
704         True,
705         query_user_list,
706         enum_dom_groups,
707         enum_local_groups,
708         name_to_sid,
709         sid_to_name,
710         query_user,
711         lookup_usergroups,
712         lookup_groupmem,
713         sequence_number,
714         trusted_domains,
715         domain_sid,
716         alternate_name
717 };
718
719 #endif