root/source4/torture/ldap/cldap.c

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. test_cldap_netlogon
  2. test_cldap_netlogon_flags
  3. ldap_msg_to_ldb
  4. cldap_dump_results
  5. test_cldap_netlogon_flag_ds_dns_forest
  6. test_cldap_generic
  7. torture_cldap

   1 /* 
   2    Unix SMB/CIFS mplementation.
   3 
   4    test CLDAP operations
   5    
   6    Copyright (C) Andrew Tridgell 2005
   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 3 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, see <http://www.gnu.org/licenses/>.
  20    
  21 */
  22 
  23 #include "includes.h"
  24 #include "libcli/cldap/cldap.h"
  25 #include "libcli/ldap/ldap.h"
  26 #include "librpc/gen_ndr/ndr_nbt.h"
  27 #include "librpc/gen_ndr/netlogon.h"
  28 #include "torture/torture.h"
  29 #include "lib/ldb/include/ldb.h"
  30 #include "param/param.h"
  31 
  32 #define CHECK_STATUS(status, correct) torture_assert_ntstatus_equal(tctx, status, correct, "incorrect status")
  33 
  34 #define CHECK_VAL(v, correct) torture_assert_int_equal(tctx, (v), (correct), "incorrect value");
  35 
  36 #define CHECK_STRING(v, correct) torture_assert_str_equal(tctx, v, correct, "incorrect value");
  37 /*
  38   test netlogon operations
  39 */
  40 static bool test_cldap_netlogon(struct torture_context *tctx, const char *dest)
     /* [<][>][^][v][top][bottom][index][help] */
  41 {
  42         struct cldap_socket *cldap;
  43         NTSTATUS status;
  44         struct cldap_netlogon search, empty_search;
  45         struct netlogon_samlogon_response n1;
  46         struct GUID guid;
  47         int i;
  48 
  49         cldap = cldap_socket_init(tctx, tctx->ev, lp_iconv_convenience(tctx->lp_ctx));
  50 
  51         ZERO_STRUCT(search);
  52         search.in.dest_address = dest;
  53         search.in.dest_port = lp_cldap_port(tctx->lp_ctx);
  54         search.in.acct_control = -1;
  55         search.in.version = NETLOGON_NT_VERSION_5 | NETLOGON_NT_VERSION_5EX;
  56         search.in.map_response = true;
  57 
  58         empty_search = search;
  59 
  60         printf("Trying without any attributes\n");
  61         search = empty_search;
  62         status = cldap_netlogon(cldap, tctx, &search);
  63         CHECK_STATUS(status, NT_STATUS_OK);
  64 
  65         n1 = search.out.netlogon;
  66 
  67         search.in.user         = "Administrator";
  68         search.in.realm        = n1.data.nt5_ex.dns_domain;
  69         search.in.host         = "__cldap_torture__";
  70 
  71         printf("Scanning for netlogon levels\n");
  72         for (i=0;i<256;i++) {
  73                 search.in.version = i;
  74                 printf("Trying netlogon level %d\n", i);
  75                 status = cldap_netlogon(cldap, tctx, &search);
  76                 CHECK_STATUS(status, NT_STATUS_OK);
  77         }
  78 
  79         printf("Scanning for netlogon level bits\n");
  80         for (i=0;i<31;i++) {
  81                 search.in.version = (1<<i);
  82                 printf("Trying netlogon level 0x%x\n", i);
  83                 status = cldap_netlogon(cldap, tctx, &search);
  84                 CHECK_STATUS(status, NT_STATUS_OK);
  85         }
  86 
  87         search.in.version = NETLOGON_NT_VERSION_5|NETLOGON_NT_VERSION_5EX|NETLOGON_NT_VERSION_IP;
  88 
  89         status = cldap_netlogon(cldap, tctx, &search);
  90         CHECK_STATUS(status, NT_STATUS_OK);
  91 
  92         printf("Trying with User=NULL\n");
  93 
  94         search.in.user = NULL;
  95         status = cldap_netlogon(cldap, tctx, &search);
  96         CHECK_STATUS(status, NT_STATUS_OK);
  97         CHECK_STRING(search.out.netlogon.data.nt5_ex.user_name, "");
  98         CHECK_VAL(search.out.netlogon.data.nt5_ex.command, LOGON_SAM_LOGON_RESPONSE_EX);
  99 
 100         printf("Trying with User=Administrator\n");
 101 
 102         search.in.user = "Administrator";
 103         status = cldap_netlogon(cldap, tctx, &search);
 104         CHECK_STATUS(status, NT_STATUS_OK);
 105 
 106         CHECK_STRING(search.out.netlogon.data.nt5_ex.user_name, search.in.user);
 107         CHECK_VAL(search.out.netlogon.data.nt5_ex.command, LOGON_SAM_LOGON_USER_UNKNOWN_EX);
 108 
 109         search.in.version = NETLOGON_NT_VERSION_5;
 110         status = cldap_netlogon(cldap, tctx, &search);
 111         CHECK_STATUS(status, NT_STATUS_OK);
 112 
 113         printf("Trying with User=NULL\n");
 114 
 115         search.in.user = NULL;
 116         status = cldap_netlogon(cldap, tctx, &search);
 117         CHECK_STATUS(status, NT_STATUS_OK);
 118         CHECK_STRING(search.out.netlogon.data.nt5_ex.user_name, "");
 119         CHECK_VAL(search.out.netlogon.data.nt5_ex.command, LOGON_SAM_LOGON_RESPONSE);
 120 
 121         printf("Trying with User=Administrator\n");
 122 
 123         search.in.user = "Administrator";
 124         status = cldap_netlogon(cldap, tctx, &search);
 125         CHECK_STATUS(status, NT_STATUS_OK);
 126 
 127         CHECK_STRING(search.out.netlogon.data.nt5_ex.user_name, search.in.user);
 128         CHECK_VAL(search.out.netlogon.data.nt5_ex.command, LOGON_SAM_LOGON_USER_UNKNOWN);
 129 
 130         search.in.version = NETLOGON_NT_VERSION_5 | NETLOGON_NT_VERSION_5EX;
 131 
 132         printf("Trying with a GUID\n");
 133         search.in.realm       = NULL;
 134         search.in.domain_guid = GUID_string(tctx, &n1.data.nt5_ex.domain_uuid);
 135         status = cldap_netlogon(cldap, tctx, &search);
 136         CHECK_STATUS(status, NT_STATUS_OK);
 137         CHECK_VAL(search.out.netlogon.data.nt5_ex.command, LOGON_SAM_LOGON_USER_UNKNOWN_EX);
 138         CHECK_STRING(GUID_string(tctx, &search.out.netlogon.data.nt5_ex.domain_uuid), search.in.domain_guid);
 139 
 140         printf("Trying with a incorrect GUID\n");
 141         guid = GUID_random();
 142         search.in.user        = NULL;
 143         search.in.domain_guid = GUID_string(tctx, &guid);
 144         status = cldap_netlogon(cldap, tctx, &search);
 145         CHECK_STATUS(status, NT_STATUS_NOT_FOUND);
 146 
 147         printf("Trying with a AAC\n");
 148         search.in.acct_control = ACB_WSTRUST|ACB_SVRTRUST;
 149         search.in.realm = n1.data.nt5_ex.dns_domain;
 150         status = cldap_netlogon(cldap, tctx, &search);
 151         CHECK_STATUS(status, NT_STATUS_OK);
 152         CHECK_VAL(search.out.netlogon.data.nt5_ex.command, LOGON_SAM_LOGON_RESPONSE_EX);
 153         CHECK_STRING(search.out.netlogon.data.nt5_ex.user_name, "");
 154 
 155         printf("Trying with a zero AAC\n");
 156         search.in.acct_control = 0x0;
 157         search.in.realm = n1.data.nt5_ex.dns_domain;
 158         status = cldap_netlogon(cldap, tctx, &search);
 159         CHECK_STATUS(status, NT_STATUS_OK);
 160         CHECK_VAL(search.out.netlogon.data.nt5_ex.command, LOGON_SAM_LOGON_RESPONSE_EX);
 161         CHECK_STRING(search.out.netlogon.data.nt5_ex.user_name, "");
 162 
 163         printf("Trying with a zero AAC and user=Administrator\n");
 164         search.in.acct_control = 0x0;
 165         search.in.user = "Administrator";
 166         search.in.realm = n1.data.nt5_ex.dns_domain;
 167         status = cldap_netlogon(cldap, tctx, &search);
 168         CHECK_STATUS(status, NT_STATUS_OK);
 169         CHECK_VAL(search.out.netlogon.data.nt5_ex.command, LOGON_SAM_LOGON_USER_UNKNOWN_EX);
 170         CHECK_STRING(search.out.netlogon.data.nt5_ex.user_name, "Administrator");
 171 
 172         printf("Trying with a bad AAC\n");
 173         search.in.user = NULL;
 174         search.in.acct_control = 0xFF00FF00;
 175         search.in.realm = n1.data.nt5_ex.dns_domain;
 176         status = cldap_netlogon(cldap, tctx, &search);
 177         CHECK_STATUS(status, NT_STATUS_OK);
 178         CHECK_VAL(search.out.netlogon.data.nt5_ex.command, LOGON_SAM_LOGON_RESPONSE_EX);
 179         CHECK_STRING(search.out.netlogon.data.nt5_ex.user_name, "");
 180 
 181         printf("Trying with a user only\n");
 182         search = empty_search;
 183         search.in.user = "Administrator";
 184         status = cldap_netlogon(cldap, tctx, &search);
 185         CHECK_STATUS(status, NT_STATUS_OK);
 186         CHECK_STRING(search.out.netlogon.data.nt5_ex.dns_domain, n1.data.nt5_ex.dns_domain);
 187         CHECK_STRING(search.out.netlogon.data.nt5_ex.user_name, search.in.user);
 188 
 189         printf("Trying with just a bad username\n");
 190         search.in.user = "___no_such_user___";
 191         status = cldap_netlogon(cldap, tctx, &search);
 192         CHECK_STATUS(status, NT_STATUS_OK);
 193         CHECK_STRING(search.out.netlogon.data.nt5_ex.user_name, search.in.user);
 194         CHECK_STRING(search.out.netlogon.data.nt5_ex.dns_domain, n1.data.nt5_ex.dns_domain);
 195         CHECK_VAL(search.out.netlogon.data.nt5_ex.command, LOGON_SAM_LOGON_USER_UNKNOWN_EX);
 196 
 197         printf("Trying with just a bad domain\n");
 198         search = empty_search;
 199         search.in.realm = "___no_such_domain___";
 200         status = cldap_netlogon(cldap, tctx, &search);
 201         CHECK_STATUS(status, NT_STATUS_NOT_FOUND);
 202 
 203         printf("Trying with a incorrect domain and correct guid\n");
 204         search.in.domain_guid = GUID_string(tctx, &n1.data.nt5_ex.domain_uuid);
 205         status = cldap_netlogon(cldap, tctx, &search);
 206         CHECK_STATUS(status, NT_STATUS_OK);
 207         CHECK_STRING(search.out.netlogon.data.nt5_ex.dns_domain, n1.data.nt5_ex.dns_domain);
 208         CHECK_STRING(search.out.netlogon.data.nt5_ex.user_name, "");
 209         CHECK_VAL(search.out.netlogon.data.nt5_ex.command, LOGON_SAM_LOGON_RESPONSE_EX);
 210 
 211         printf("Trying with a incorrect domain and incorrect guid\n");
 212         search.in.domain_guid = GUID_string(tctx, &guid);
 213         status = cldap_netlogon(cldap, tctx, &search);
 214         CHECK_STATUS(status, NT_STATUS_NOT_FOUND);
 215         CHECK_STRING(search.out.netlogon.data.nt5_ex.dns_domain, n1.data.nt5_ex.dns_domain);
 216         CHECK_STRING(search.out.netlogon.data.nt5_ex.user_name, "");
 217         CHECK_VAL(search.out.netlogon.data.nt5_ex.command, LOGON_SAM_LOGON_RESPONSE_EX);
 218 
 219         printf("Trying with a incorrect GUID and correct domain\n");
 220         search.in.domain_guid = GUID_string(tctx, &guid);
 221         search.in.realm = n1.data.nt5_ex.dns_domain;
 222         status = cldap_netlogon(cldap, tctx, &search);
 223         CHECK_STATUS(status, NT_STATUS_OK);
 224         CHECK_STRING(search.out.netlogon.data.nt5_ex.dns_domain, n1.data.nt5_ex.dns_domain);
 225         CHECK_STRING(search.out.netlogon.data.nt5_ex.user_name, "");
 226         CHECK_VAL(search.out.netlogon.data.nt5_ex.command, LOGON_SAM_LOGON_RESPONSE_EX);
 227 
 228         return true;
 229 }
 230 
 231 /*
 232   test cldap netlogon server type flags
 233 */
 234 static bool test_cldap_netlogon_flags(struct torture_context *tctx,
     /* [<][>][^][v][top][bottom][index][help] */
 235         const char *dest)
 236 {
 237         struct cldap_socket *cldap;
 238         NTSTATUS status;
 239         struct cldap_netlogon search;
 240         struct netlogon_samlogon_response n1;
 241         uint32_t server_type;
 242 
 243         cldap = cldap_socket_init(tctx, tctx->ev, lp_iconv_convenience(tctx->lp_ctx));
 244 
 245         printf("Printing out netlogon server type flags:\n");
 246 
 247         ZERO_STRUCT(search);
 248         search.in.dest_address = dest;
 249         search.in.dest_port = lp_cldap_port(tctx->lp_ctx);
 250         search.in.acct_control = -1;
 251         search.in.version = NETLOGON_NT_VERSION_5 | NETLOGON_NT_VERSION_5EX;
 252         search.in.map_response = true;
 253 
 254         status = cldap_netlogon(cldap, tctx, &search);
 255         CHECK_STATUS(status, NT_STATUS_OK);
 256 
 257         n1 = search.out.netlogon;
 258         if (n1.ntver == NETLOGON_NT_VERSION_5)
 259                 server_type = n1.data.nt5.server_type;
 260         else if (n1.ntver == NETLOGON_NT_VERSION_5EX)
 261                 server_type = n1.data.nt5_ex.server_type;       
 262 
 263         printf("The word is: %i\n", server_type);
 264         if (server_type & NBT_SERVER_PDC)
 265                 printf("NBT_SERVER_PDC ");
 266         if (server_type & NBT_SERVER_GC)
 267                 printf("NBT_SERVER_GC ");
 268         if (server_type & NBT_SERVER_LDAP)
 269                 printf("NBT_SERVER_LDAP ");
 270         if (server_type & NBT_SERVER_DS)
 271                 printf("NBT_SERVER_DS ");
 272         if (server_type & NBT_SERVER_KDC)
 273                 printf("NBT_SERVER_KDC ");
 274         if (server_type & NBT_SERVER_TIMESERV)
 275                 printf("NBT_SERVER_TIMESERV ");
 276         if (server_type & NBT_SERVER_CLOSEST)
 277                 printf("NBT_SERVER_CLOSEST ");
 278         if (server_type & NBT_SERVER_WRITABLE)
 279                 printf("NBT_SERVER_WRITABLE ");
 280         if (server_type & NBT_SERVER_GOOD_TIMESERV)
 281                 printf("NBT_SERVER_GOOD_TIMESERV ");
 282         if (server_type & NBT_SERVER_NDNC)
 283                 printf("NBT_SERVER_NDNC ");
 284         if (server_type & NBT_SERVER_SELECT_SECRET_DOMAIN_6)
 285                 printf("NBT_SERVER_SELECT_SECRET_DOMAIN_6");
 286         if (server_type & NBT_SERVER_FULL_SECRET_DOMAIN_6)
 287                 printf("NBT_SERVER_FULL_SECRET_DOMAIN_6");
 288         if (server_type & DS_DNS_CONTROLLER)
 289                 printf("DS_DNS_CONTROLLER ");
 290         if (server_type & DS_DNS_DOMAIN)
 291                 printf("DS_DNS_DOMAIN ");
 292         if (server_type & DS_DNS_FOREST)
 293                 printf("DS_DNS_FOREST ");
 294 
 295         printf("\n");
 296 
 297         return true;
 298 }
 299 
 300 /*
 301   convert a ldap result message to a ldb message. This allows us to
 302   use the convenient ldif dump routines in ldb to print out cldap
 303   search results
 304 */
 305 static struct ldb_message *ldap_msg_to_ldb(TALLOC_CTX *mem_ctx, struct ldb_context *ldb, struct ldap_SearchResEntry *res)
     /* [<][>][^][v][top][bottom][index][help] */
 306 {
 307         struct ldb_message *msg;
 308 
 309         msg = ldb_msg_new(mem_ctx);
 310         msg->dn = ldb_dn_new(msg, ldb, res->dn);
 311         msg->num_elements = res->num_attributes;
 312         msg->elements = talloc_steal(msg, res->attributes);
 313         return msg;
 314 }
 315 
 316 /*
 317   dump a set of cldap results
 318 */
 319 static void cldap_dump_results(struct cldap_search *search)
     /* [<][>][^][v][top][bottom][index][help] */
 320 {
 321         struct ldb_ldif ldif;
 322         struct ldb_context *ldb;
 323 
 324         if (!search || !(search->out.response)) {
 325                 return;
 326         }
 327 
 328         /* we need a ldb context to use ldb_ldif_write_file() */
 329         ldb = ldb_init(NULL, NULL);
 330 
 331         ZERO_STRUCT(ldif);
 332         ldif.msg = ldap_msg_to_ldb(ldb, ldb, search->out.response);
 333 
 334         ldb_ldif_write_file(ldb, stdout, &ldif);
 335 
 336         talloc_free(ldb);
 337 }
 338 
 339 
 340 /*
 341   test cldap netlogon server type flag "NBT_SERVER_DS_DNS_FOREST"
 342 */
 343 static bool test_cldap_netlogon_flag_ds_dns_forest(struct torture_context *tctx,
     /* [<][>][^][v][top][bottom][index][help] */
 344         const char *dest)
 345 {
 346         struct cldap_socket *cldap;
 347         NTSTATUS status;
 348         struct cldap_netlogon search;
 349         uint32_t server_type;
 350         struct netlogon_samlogon_response n1;
 351 
 352         bool result = true;
 353 
 354         cldap = cldap_socket_init(tctx, tctx->ev, lp_iconv_convenience(tctx->lp_ctx));
 355 
 356         printf("Testing netlogon server type flag NBT_SERVER_DS_DNS_FOREST: ");
 357 
 358         ZERO_STRUCT(search);
 359         search.in.dest_address = dest;
 360         search.in.dest_port = lp_cldap_port(tctx->lp_ctx);
 361         search.in.acct_control = -1;
 362         search.in.version = NETLOGON_NT_VERSION_5 | NETLOGON_NT_VERSION_5EX;
 363         search.in.map_response = true;
 364 
 365         status = cldap_netlogon(cldap, tctx, &search);
 366         CHECK_STATUS(status, NT_STATUS_OK);
 367 
 368         n1 = search.out.netlogon;
 369         if (n1.ntver == NETLOGON_NT_VERSION_5)
 370                 server_type = n1.data.nt5.server_type;
 371         else if (n1.ntver == NETLOGON_NT_VERSION_5EX)
 372                 server_type = n1.data.nt5_ex.server_type;
 373 
 374         if (server_type & DS_DNS_FOREST) {
 375                 struct cldap_search search2;
 376                 const char *attrs[] = { "defaultNamingContext", "rootDomainNamingContext", 
 377                         NULL };
 378                 struct ldb_context *ldb;
 379                 struct ldb_message *msg;
 380 
 381                 /* Trying to fetch the attributes "defaultNamingContext" and
 382                    "rootDomainNamingContext" */
 383                 ZERO_STRUCT(search2);
 384                 search2.in.dest_address = dest;
 385                 search2.in.dest_port = lp_cldap_port(tctx->lp_ctx);
 386                 search2.in.timeout = 10;
 387                 search2.in.retries = 3;
 388                 search2.in.filter = "(objectclass=*)";
 389                 search2.in.attributes = attrs;
 390 
 391                 status = cldap_search(cldap, tctx, &search2);
 392                 CHECK_STATUS(status, NT_STATUS_OK);
 393 
 394                 ldb = ldb_init(NULL, NULL);
 395 
 396                 msg = ldap_msg_to_ldb(ldb, ldb, search2.out.response);
 397 
 398                 /* Try to compare the two attributes */
 399                 if (ldb_msg_element_compare(ldb_msg_find_element(msg, attrs[0]),
 400                         ldb_msg_find_element(msg, attrs[1])))
 401                         result = false;
 402 
 403                 talloc_free(ldb);
 404         }
 405 
 406         if (result)
 407                 printf("passed\n");
 408         else
 409                 printf("failed\n");
 410 
 411         return result;
 412 }
 413 
 414 /*
 415   test generic cldap operations
 416 */
 417 static bool test_cldap_generic(struct torture_context *tctx, const char *dest)
     /* [<][>][^][v][top][bottom][index][help] */
 418 {
 419         struct cldap_socket *cldap;
 420         NTSTATUS status;
 421         struct cldap_search search;
 422         const char *attrs1[] = { "currentTime", "highestCommittedUSN", NULL };
 423         const char *attrs2[] = { "currentTime", "highestCommittedUSN", "netlogon", NULL };
 424         const char *attrs3[] = { "netlogon", NULL };
 425 
 426         cldap = cldap_socket_init(tctx, tctx->ev, lp_iconv_convenience(tctx->lp_ctx));
 427 
 428         ZERO_STRUCT(search);
 429         search.in.dest_address = dest;
 430         search.in.dest_port = lp_cldap_port(tctx->lp_ctx);
 431         search.in.timeout = 10;
 432         search.in.retries = 3;
 433 
 434         status = cldap_search(cldap, tctx, &search);
 435         CHECK_STATUS(status, NT_STATUS_OK);
 436 
 437         printf("fetching whole rootDSE\n");
 438         search.in.filter = "(objectclass=*)";
 439         search.in.attributes = NULL;
 440 
 441         status = cldap_search(cldap, tctx, &search);
 442         CHECK_STATUS(status, NT_STATUS_OK);
 443 
 444         if (DEBUGLVL(3)) cldap_dump_results(&search);
 445 
 446         printf("fetching currentTime and USN\n");
 447         search.in.filter = "(objectclass=*)";
 448         search.in.attributes = attrs1;
 449 
 450         status = cldap_search(cldap, tctx, &search);
 451         CHECK_STATUS(status, NT_STATUS_OK);
 452         
 453         if (DEBUGLVL(3)) cldap_dump_results(&search);
 454 
 455         printf("Testing currentTime, USN and netlogon\n");
 456         search.in.filter = "(objectclass=*)";
 457         search.in.attributes = attrs2;
 458 
 459         status = cldap_search(cldap, tctx, &search);
 460         CHECK_STATUS(status, NT_STATUS_OK);
 461 
 462         if (DEBUGLVL(3)) cldap_dump_results(&search);
 463 
 464         printf("Testing objectClass=* and netlogon\n");
 465         search.in.filter = "(objectclass2=*)";
 466         search.in.attributes = attrs3;
 467 
 468         status = cldap_search(cldap, tctx, &search);
 469         CHECK_STATUS(status, NT_STATUS_OK);
 470 
 471         if (DEBUGLVL(3)) cldap_dump_results(&search);
 472 
 473         printf("Testing a false expression\n");
 474         search.in.filter = "(&(objectclass=*)(highestCommittedUSN=2))";
 475         search.in.attributes = attrs1;
 476 
 477         status = cldap_search(cldap, tctx, &search);
 478         CHECK_STATUS(status, NT_STATUS_OK);
 479 
 480         if (DEBUGLVL(3)) cldap_dump_results(&search);   
 481 
 482         return true;    
 483 }
 484 
 485 bool torture_cldap(struct torture_context *torture)
     /* [<][>][^][v][top][bottom][index][help] */
 486 {
 487         bool ret = true;
 488         const char *host = torture_setting_string(torture, "host", NULL);
 489 
 490         ret &= test_cldap_netlogon(torture, host);
 491         ret &= test_cldap_netlogon_flags(torture, host);
 492         ret &= test_cldap_netlogon_flag_ds_dns_forest(torture, host);
 493         ret &= test_cldap_generic(torture, host);
 494 
 495         return ret;
 496 }
 497 

/* [<][>][^][v][top][bottom][index][help] */