bool netcat_resolvehost(nc_host_t *dst, const char *name) { int i; struct hostent *hostent; struct in_addr res_addr; #ifdef USE_IPV6 struct in6_addr res6_addr; #endif assert(name && name[0]); debug_v(("netcat_resolvehost(dst=%p, name=\"%s\")", (void *)dst, name)); /* reset all fields of the dst struct */ memset(dst, 0, sizeof(*dst)); /* try to see if `name' is a numeric address, in case try reverse lookup */ if (netcat_inet_pton(AF_INET, name, &res_addr)) { memcpy(&dst->host.iaddrs[0], &res_addr, sizeof(dst->host.iaddrs[0])); strncpy(dst->host.addrs[0], netcat_inet_ntop(AF_INET, &res_addr), sizeof(dst->host.addrs[0])); /* if opt_numeric is set or we don't require verbosity, we are done */ if (opt_numeric) return TRUE; /* failures to look up a PTR record are *not* considered fatal */ hostent = gethostbyaddr((char *)&res_addr, sizeof(res_addr), AF_INET); if (!hostent) ncprint(NCPRINT_VERB2 | NCPRINT_WARNING, _("Inverse name lookup failed for `%s'"), name); else { strncpy(dst->host.name, hostent->h_name, MAXHOSTNAMELEN - 2); /* now do the direct lookup to see if the PTR was authoritative */ hostent = gethostbyname(dst->host.name); /* Any kind of failure in this section results in a host not auth warning, and the dst->host.name field cleaned (I don't care if there is a PTR, if it's unauthoritative). */ if (!hostent || !hostent->h_addr_list[0]) { ncprint(NCPRINT_VERB1 | NCPRINT_WARNING, _("Host %s isn't authoritative! (direct lookup failed)"), dst->host.addrs[0]); goto check_failed; } for (i = 0; hostent->h_addr_list[i] && (i < MAXINETADDRS); i++) if (!memcmp(&dst->host.iaddrs[0], hostent->h_addr_list[i], sizeof(dst->host.iaddrs[0]))) return TRUE; /* resolving verified, it's AUTH */ ncprint(NCPRINT_VERB1 | NCPRINT_WARNING, _("Host %s isn't authoritative! (direct lookup mismatch)"), dst->host.addrs[0]); ncprint(NCPRINT_VERB1, _(" %s -> %s BUT %s -> %s"), dst->host.addrs[0], dst->host.name, dst->host.name, netcat_inet_ntop(AF_INET, hostent->h_addr_list[0])); check_failed: memset(dst->host.name, 0, sizeof(dst->host.name)); } /* if hostent */ } #ifdef USE_IPV6 /* same as above, but check for an IPv6 address notation */ else if (netcat_inet_pton(AF_INET6, name, &res6_addr)) { memcpy(&dst->host6.iaddrs[0], &res6_addr, sizeof(dst->host6.iaddrs[0])); strncpy(dst->host6.addrs[0], netcat_inet_ntop(AF_INET6, &res_addr), sizeof(dst->host6.addrs[0])); /* if opt_numeric is set or we don't require verbosity, we are done */ if (opt_numeric) return TRUE; } #endif else { /* couldn't translate: it must be a name! */ bool host_auth_taken = FALSE; /* if the opt_numeric option is set, we must not use DNS in any way */ if (opt_numeric) return FALSE; /* failures to look up a name are reported to the calling function */ if (!(hostent = gethostbyname(name))) return FALSE; /* now I need to handle the host aliases (CNAME). If we lookup host www.bighost.foo, which is an alias for www.bighost.mux.foo, the hostent struct will contain the real name in h_name, which is not what we want for the output purpose (the user doesn't want to see something he didn't type. So assume the lookup name as the "official" name and fetch the ips for the reverse lookup. */ debug(("(lookup) lookup=\"%s\" official=\"%s\" (should match)\n", name, hostent->h_name)); strncpy(dst->host.name, name, MAXHOSTNAMELEN - 1); /* now save all the available ip addresses (no more than MAXINETADDRS) */ for (i = 0; hostent->h_addr_list[i] && (i < MAXINETADDRS); i++) { memcpy(&dst->host.iaddrs[i], hostent->h_addr_list[i], sizeof(dst->host.iaddrs[0])); strncpy(dst->host.addrs[i], netcat_inet_ntop(AF_INET, &dst->host.iaddrs[i]), sizeof(dst->host.addrs[0])); } /* end of foreach addr, part A */ /* for speed purposes, skip the authoritative checking if we haven't got any verbosity level set. note that this will cause invalid results in the dst struct, but we don't care at this point. (FIXME: ?) */ if (!is_logging_enabled()) return TRUE; /* do inverse lookups in a separated loop for each collected addresses */ for (i = 0; dst->host.iaddrs[i].s_addr && (i < MAXINETADDRS); i++) { hostent = gethostbyaddr((char *)&dst->host.iaddrs[i], sizeof(dst->host.iaddrs[0]), AF_INET); if (!hostent || !hostent->h_name) { ncprint(NCPRINT_VERB1 | NCPRINT_WARNING, _("Inverse name lookup failed for `%s'"), dst->host.addrs[i]); continue; } /* now the case. hostnames aren't case sensitive because of this we may find a different case for the authoritative hostname. For the same previous reason we may want to keep the user typed case, but this time we are going to override it because this tool is a "network exploration tool", thus it's good to see the case they chose for this host. */ if (strcasecmp(dst->host.name, hostent->h_name)) { int xcmp; char savedhost[MAXHOSTNAMELEN]; /* refering to the flowchart (see the drafts directory contained in this package), try to guess the real hostname */ strncpy(savedhost, hostent->h_name, sizeof(savedhost)); savedhost[sizeof(savedhost) - 1] = 0; /* ok actually the given host and the reverse-resolved address doesn't match, so try to see if we can find the real machine name. In order to this to happen the originally found address must match with the newly found hostname directly resolved. If this doesn't, or if this resolve fails, then fall back to the original warning message: they have a DNS misconfigured! */ hostent = gethostbyname(savedhost); if (!hostent) continue; /* FIXME: missing information analysis */ for (xcmp = 0; hostent->h_addr_list[xcmp] && (xcmp < MAXINETADDRS); xcmp++) { if (!memcmp(&dst->host.iaddrs[i], hostent->h_addr_list[xcmp], sizeof(dst->host.iaddrs[0]))) goto found_real_host; } ncprint(NCPRINT_WARNING | NCPRINT_VERB1, _("This host's reverse DNS doesn't match! %s -- %s"), hostent->h_name, dst->host.name); continue; found_real_host: ncprint(NCPRINT_NOTICE | NCPRINT_VERB2, _("Real hostname for %s [%s] is %s"), dst->host.name, dst->host.addrs[i], savedhost); continue; } else if (!host_auth_taken) { /* case: take only the first one as auth */ strncpy(dst->host.name, hostent->h_name, sizeof(dst->host.name)); host_auth_taken = TRUE; } } /* end of foreach addr, part B */ } return TRUE; }
int main(int argc, char *argv[]) { int c, glob_ret = EXIT_FAILURE; int total_ports, left_ports, accept_ret = -1, connect_ret = -1; struct sigaction sv; nc_port_t local_port; /* local port specified with -p option */ nc_host_t local_host; /* local host for bind()ing operations */ nc_host_t remote_host; nc_sock_t listen_sock; nc_sock_t connect_sock; nc_sock_t stdio_sock; nc_ports_t old_flag = NULL; memset(&local_port, 0, sizeof(local_port)); memset(&local_host, 0, sizeof(local_host)); memset(&remote_host, 0, sizeof(remote_host)); memset(&listen_sock, 0, sizeof(listen_sock)); memset(&connect_sock, 0, sizeof(connect_sock)); memset(&stdio_sock, 0, sizeof(stdio_sock)); listen_sock.domain = NETCAT_DOMAIN_IPV4; connect_sock.domain = NETCAT_DOMAIN_IPV4; #ifdef ENABLE_NLS setlocale(LC_MESSAGES, ""); bindtextdomain(PACKAGE, LOCALEDIR); textdomain(PACKAGE); #endif /* set up the signal handling system */ sigemptyset(&sv.sa_mask); sv.sa_flags = 0; sv.sa_handler = got_int; sigaction(SIGINT, &sv, NULL); sv.sa_handler = got_term; sigaction(SIGTERM, &sv, NULL); sv.sa_handler = got_usr1; sigaction(SIGUSR1, &sv, NULL); /* ignore some boring signals */ sv.sa_handler = SIG_IGN; sigaction(SIGPIPE, &sv, NULL); sigaction(SIGURG, &sv, NULL); /* if no args given at all, take them from stdin and generate argv */ if (argc == 1) netcat_commandline_read(&argc, &argv); /* check for command line switches */ while (TRUE) { int option_index = 0; static const struct option long_options[] = { { "close", no_argument, NULL, 'c' }, { "debug", no_argument, NULL, 'd' }, { "exec", required_argument, NULL, 'e' }, { "gateway", required_argument, NULL, 'g' }, { "pointer", required_argument, NULL, 'G' }, { "help", no_argument, NULL, 'h' }, { "interval", required_argument, NULL, 'i' }, { "ipv4", no_argument, NULL, '4' }, { "ipv6", no_argument, NULL, '6' }, { "listen", no_argument, NULL, 'l' }, { "tunnel", required_argument, NULL, 'L' }, { "dont-resolve", no_argument, NULL, 'n' }, { "convert", required_argument, NULL, 'N' }, /* FIXME: proposal: A Ascii? */ { "output", required_argument, NULL, 'o' }, { "local-port", required_argument, NULL, 'p' }, { "tunnel-port", required_argument, NULL, 'P' }, { "randomize", no_argument, NULL, 'r' }, { "source", required_argument, NULL, 's' }, { "tunnel-source", required_argument, NULL, 'S' }, #ifndef USE_OLD_COMPAT { "tcp", no_argument, NULL, 't' }, { "telnet", no_argument, NULL, 'T' }, #else { "tcp", no_argument, NULL, 1 }, { "telnet", no_argument, NULL, 't' }, #endif { "udp", no_argument, NULL, 'u' }, { "verbose", no_argument, NULL, 'v' }, { "version", no_argument, NULL, 'V' }, { "hexdump", no_argument, NULL, 'x' }, { "wait", required_argument, NULL, 'w' }, { "zero", no_argument, NULL, 'z' }, { 0, 0, 0, 0 } }; c = getopt_long(argc, argv, "46cde:g:G:hi:lL:no:p:P:rs:S:tTuvVxw:z", long_options, &option_index); if (c == -1) break; switch (c) { case '4': /* don't use IPv6 protocol */ opt_domain = NETCAT_DOMAIN_IPV4; break; case '6': /* use IPv6 protocol */ opt_domain = NETCAT_DOMAIN_IPV6; break; case 'c': /* close connection on EOF from stdin */ opt_eofclose = TRUE; break; case 'd': /* enable debugging */ opt_debug = TRUE; break; case 'e': /* prog to exec */ if (opt_exec) ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("Cannot specify `-e' option double")); opt_exec = strdup(optarg); break; case 'G': /* srcrt gateways pointer val */ break; case 'g': /* srcroute hops */ break; case 'h': /* display help and exit */ netcat_printhelp(argv[0]); exit(EXIT_SUCCESS); case 'i': /* line/ports interval time (seconds) */ opt_interval = atoi(optarg); if (opt_interval < 0) ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("Invalid interval time \"%s\""), optarg); break; case 'l': /* mode flag: listen mode */ if (netcat_mode != NETCAT_UNSPEC) ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("You can specify mode flags (`-l' and `-L') only once")); netcat_mode = NETCAT_LISTEN; break; case 'L': /* mode flag: tunnel mode */ if (netcat_mode != NETCAT_UNSPEC) ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("You can specify mode flags (`-l' and `-L') only once")); if (opt_zero) ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("`-L' and `-z' options are incompatible")); do { char *div, *p, *pbuf = strdup(optarg); div = p = pbuf; /* if the address has '[' unbalanced, threat is as plain address */ if (div[0] == '[') { div = strchr(div, ']'); if (div != NULL) { *div++ = '\0'; p++; /* skip the '[' */ } } div = strchr(div, ':'); if (div && *(div + 1)) *div++ = '\0'; else { ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("Invalid target string for `-L' option")); } /* lookup the remote address and the remote port for tunneling */ if (!netcat_resolvehost(&connect_sock.remote, optarg)) ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("Couldn't resolve tunnel target host: %s"), optarg); if (!netcat_getport(&connect_sock.port, div, 0)) ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("Invalid tunnel target port: %s"), div); /* takes the options configured so far as they may change */ connect_sock.domain = opt_domain; connect_sock.proto = opt_proto; connect_sock.timeout = opt_wait; netcat_mode = NETCAT_TUNNEL; } while (FALSE); break; case 'N': /* ascii line ends conversion, use with care */ if (!strcasecmp(optarg, "none")) opt_ascii_conversion = NETCAT_CONVERT_NONE; else if (!strcasecmp(optarg, "crlf")) opt_ascii_conversion = NETCAT_CONVERT_CRLF; else if (!strcasecmp(optarg, "cr")) opt_ascii_conversion = NETCAT_CONVERT_CR; else if (!strcasecmp(optarg, "lf")) opt_ascii_conversion = NETCAT_CONVERT_LF; else { ncprint(NCPRINT_NORMAL, _("Conversion must be one of: none crlf cr lf")); ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("Invalid conversion specified: %s"), optarg); } break; case 'n': /* numeric-only, no DNS lookups */ opt_numeric = TRUE; break; case 'o': /* output hexdump log to file */ opt_outputfile = strdup(optarg); opt_hexdump = TRUE; /* implied */ break; case 'p': /* local source port */ if (!netcat_getport(&local_port, optarg, 0)) ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("Invalid local port: %s"), optarg); break; case 'P': /* used only in tunnel mode (source port) */ if (!netcat_getport(&connect_sock.local_port, optarg, 0)) ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("Invalid tunnel connect port: %s"), optarg); break; case 'r': /* randomize various things */ opt_random = TRUE; break; case 's': /* local source address */ /* lookup the source address and assign it to the connection address */ if (!netcat_resolvehost(&local_host, optarg)) ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("Couldn't resolve local host: %s"), optarg); break; case 'S': /* used only in tunnel mode (source ip) */ if (!netcat_resolvehost(&connect_sock.local, optarg)) ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("Couldn't resolve tunnel local host: %s"), optarg); break; case 1: /* use TCP protocol (default) */ #ifndef USE_OLD_COMPAT case 't': #endif opt_proto = NETCAT_PROTO_TCP; break; #ifdef USE_OLD_COMPAT case 't': #endif case 'T': /* answer telnet codes */ opt_telnet = TRUE; break; case 'u': /* use UDP protocol */ opt_proto = NETCAT_PROTO_UDP; break; case 'v': /* be verbose (twice=more verbose) */ opt_verbose++; break; case 'V': /* display version and exit */ netcat_printversion(); exit(EXIT_SUCCESS); case 'w': /* wait time (in seconds), 0 means no timeout */ opt_wait = atoi(optarg); if (opt_wait < 0) ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("Invalid wait-time: %s"), optarg); break; case 'x': /* hexdump traffic */ opt_hexdump = TRUE; break; case 'z': /* little or no data xfer */ if (netcat_mode == NETCAT_TUNNEL) ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("`-L' and `-z' options are incompatible")); opt_zero = TRUE; break; default: ncprint(NCPRINT_EXIT, _("Try `%s --help' for more information."), argv[0]); } } if (opt_zero && opt_exec) ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("`-e' and `-z' options are incompatible")); /* initialize the flag buffer to keep track of the specified ports */ //netcat_flag_init(65535); old_flag = netcat_ports_init(); #ifndef DEBUG /* check for debugging support */ if (opt_debug) ncprint(NCPRINT_WARNING, _("Debugging support not compiled, option `-d' discarded. Using maximum verbosity.")); #endif /* randomize only if needed */ if (opt_random) #ifdef USE_RANDOM SRAND(time(0)); #else ncprint(NCPRINT_WARNING, _("Randomization support not compiled, option `-r' discarded.")); #endif /* handle the -o option. exit on failure */ if (opt_outputfile) { output_fp = fopen(opt_outputfile, "w"); if (!output_fp) ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("Failed to open output file: %s (%s)"), opt_outputfile, strerror(errno)); } else output_fp = stderr; /* FIXME: now we can resolve special hostnames */ debug_v(("Trying to parse non-args parameters (argc=%d, optind=%d)", argc, optind)); /* try to get an hostname parameter */ if (optind < argc) { const char *get_host = argv[optind++]; if (!netcat_resolvehost(&remote_host, get_host)) ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("Couldn't resolve host \"%s\""), get_host); } /* now loop all the other (maybe optional) parameters for port-ranges */ while (optind < argc) { const char *get_argv = argv[optind++]; char *q, *parse = strdup(get_argv); int port_lo = 0, port_hi = 65535; nc_port_t port_tmp; if (!(q = strchr(parse, '-'))) /* simple number? */ q = strchr(parse, ':'); /* try with the other separator */ if (!q) { if (netcat_getport(&port_tmp, parse, 0)) netcat_ports_insert(old_flag, port_tmp.num, port_tmp.num); else goto got_err; } else { /* could be in the forms: N1-N2, -N2, N1- */ *q++ = 0; if (*parse) { if (netcat_getport(&port_tmp, parse, 0)) port_lo = port_tmp.num; else goto got_err; } if (*q) { if (netcat_getport(&port_tmp, q, 0)) port_hi = port_tmp.num; else goto got_err; } if (!*parse && !*q) /* don't accept the form '-' */ goto got_err; netcat_ports_insert(old_flag, port_lo, port_hi); } free(parse); continue; got_err: free(parse); ncprint(NCPRINT_ERROR, _("Invalid port specification: %s"), get_argv); exit(EXIT_FAILURE); } debug_dv(("Arguments parsing complete! Total ports=%d", netcat_ports_count(old_flag))); #if 0 /* pure debugging code */ c = 0; while ((c = netcat_ports_next(old_flag, c))) { printf("Got port=%d\n", c); } exit(0); #endif /* Handle listen mode and tunnel mode (whose index number is higher) */ if (netcat_mode > NETCAT_CONNECT) { /* in tunnel mode the opt_zero flag is illegal, while on listen mode it means that no connections should be accepted. For UDP it means that no remote addresses should be used as default endpoint, which means that we can't send anything. In both situations, stdin is no longer useful, so close it. */ if (opt_zero) { close(STDIN_FILENO); use_stdin = FALSE; } /* prepare the socket var and start listening */ listen_sock.proto = opt_proto; listen_sock.timeout = opt_wait; memcpy(&listen_sock.local, &local_host, sizeof(listen_sock.local)); memcpy(&listen_sock.local_port, &local_port, sizeof(listen_sock.local_port)); memcpy(&listen_sock.remote, &remote_host, sizeof(listen_sock.remote)); accept_ret = core_listen(&listen_sock); /* in zero I/O mode the core_tcp_listen() call will always return -1 (ETIMEDOUT) since no connections are accepted, because of this our job is completed now. */ if (accept_ret < 0) { /* since i'm planning to make `-z' compatible with `-L' I need to check the exact error that caused this failure. */ if (opt_zero && (errno == ETIMEDOUT)) exit(0); ncprint(NCPRINT_VERB1 | NCPRINT_EXIT, _("Listen mode failed: %s"), strerror(errno)); } /* if we are in listen mode, run the core loop and exit when it returns. otherwise now it's the time to connect to the target host and tunnel them together (which means passing to the next section. */ if (netcat_mode == NETCAT_LISTEN) { if (opt_exec) { ncprint(NCPRINT_VERB2, _("Passing control to the specified program")); ncexec(&listen_sock); /* this won't return */ } core_readwrite(&listen_sock, &stdio_sock); debug_dv(("Listen: EXIT")); } else { /* otherwise we are in tunnel mode. The connect_sock var was already initialized by the command line arguments. */ assert(netcat_mode == NETCAT_TUNNEL); connect_ret = core_connect(&connect_sock); /* connection failure? (we cannot get this in UDP mode) */ if (connect_ret < 0) { assert(connect_sock.proto != NETCAT_PROTO_UDP); ncprint(NCPRINT_VERB1, "%s: %s", netcat_strid(connect_sock.domain, &connect_sock.remote, &connect_sock.port), strerror(errno)); } else { glob_ret = EXIT_SUCCESS; core_readwrite(&listen_sock, &connect_sock); debug_dv(("Tunnel: EXIT (ret=%d)", glob_ret)); } } /* all jobs should be ok, go to the cleanup */ goto main_exit; } /* end of listen and tunnel mode handling */ /* we need to connect outside, this is the connect mode */ netcat_mode = NETCAT_CONNECT; /* first check that a host parameter was given */ if (!remote_host.host.iaddrs[0].s_addr) { /* FIXME: The Networking specifications state that host address "0" is a valid host to connect to but this broken check will assume as not specified. */ ncprint(NCPRINT_NORMAL, _("%s: missing hostname argument"), argv[0]); ncprint(NCPRINT_EXIT, _("Try `%s --help' for more information."), argv[0]); } /* since ports are the second argument, checking ports might be enough */ total_ports = netcat_ports_count(old_flag); if (total_ports == 0) ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("No ports specified for connection")); c = 0; /* must be set to 0 for netcat_flag_next() */ left_ports = total_ports; while (left_ports > 0) { /* `c' is the port number independently of the sorting method (linear or random). While in linear mode it is also used to fetch the next port number */ if (opt_random) c = netcat_ports_rand(old_flag); else c = netcat_ports_next(old_flag, c); left_ports--; /* decrease the total ports number to try */ /* since we are nonblocking now, we can start as many connections as we want but it's not a great idea connecting more than one host at time */ connect_sock.proto = opt_proto; connect_sock.timeout = opt_wait; memcpy(&connect_sock.local, &local_host, sizeof(connect_sock.local)); memcpy(&connect_sock.local_port, &local_port, sizeof(connect_sock.local_port)); memcpy(&connect_sock.remote, &remote_host, sizeof(connect_sock.remote)); netcat_getport(&connect_sock.port, NULL, c); /* FIXME: in udp mode and NETCAT_CONNECT, opt_zero is senseless */ connect_ret = core_connect(&connect_sock); /* connection failure? (we cannot get this in UDP mode) */ if (connect_ret < 0) { int ncprint_flags = NCPRINT_VERB1; assert(connect_sock.proto != NETCAT_PROTO_UDP); /* if we are portscanning or multiple connecting show only open ports with verbosity level 1. */ if (total_ports > 1) ncprint_flags = NCPRINT_VERB2; ncprint(ncprint_flags, "%s: %s", netcat_strid(connect_sock.domain, &connect_sock.remote, &connect_sock.port), strerror(errno)); continue; /* go with next port */ } /* when portscanning (or checking a single port) we are happy if AT LEAST ONE port is available. */ glob_ret = EXIT_SUCCESS; if (opt_zero) { shutdown(connect_ret, 2); close(connect_ret); } else { if (opt_exec) { ncprint(NCPRINT_VERB2, _("Passing control to the specified program")); ncexec(&connect_sock); /* this won't return */ } core_readwrite(&connect_sock, &stdio_sock); /* FIXME: add a small delay */ debug_v(("Connect: EXIT")); /* both signals are handled inside core_readwrite(), but while the SIGINT signal is fully handled, the SIGTERM requires some action from outside that function, because of this that flag is not cleared. */ if (got_sigterm) break; } } /* end of while (left_ports > 0) */ /* all basic modes should return here for the final cleanup */ main_exit: debug_v(("Main: EXIT (cleaning up)")); netcat_printstats(FALSE); return glob_ret; } /* end of main() */