From 9d8eb28ebd941694d8dcbc66c6e5c80cf90a206d Mon Sep 17 00:00:00 2001 From: Rainer Gerhards Date: Sun, 2 Mar 2008 10:19:25 +0000 Subject: - added class tcps - added class tcps_sess - changed imtcp to use new classes; seems to work; imgssapi currently broken --- tcpsrv.c | 762 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 762 insertions(+) create mode 100644 tcpsrv.c (limited to 'tcpsrv.c') diff --git a/tcpsrv.c b/tcpsrv.c new file mode 100644 index 00000000..0453b2d4 --- /dev/null +++ b/tcpsrv.c @@ -0,0 +1,762 @@ +/* tcpsrv.c + * + * Common code for plain TCP based servers. This is currently being + * utilized by imtcp and imgssapi. I suspect that when we implement + * SSL/TLS, that module could also use tcpsrv. + * + * There are actually two classes within the tcpserver code: one is + * the tcpsrv itself, the other one is its sessions. This is a helper + * class to tcpsrv. + * + * The common code here calls upon specific functionality by using + * callbacks. The specialised input modules need to set the proper + * callbacks before the code is run. The tcpsrv then calls back + * into the specific input modules at the appropriate time. + * + * NOTE: read comments in module-template.h to understand how this file + * works! + * + * File begun on 2007-12-21 by RGerhards (extracted from syslogd.c) + * + * Copyright 2007, 2008 Rainer Gerhards and Adiscon GmbH. + * + * This file is part of rsyslog. + * + * Rsyslog is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Rsyslog is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Rsyslog. If not, see . + * + * A copy of the GPL can be found in the file "COPYING" in this distribution. + */ + +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if HAVE_FCNTL_H +#include +#endif +#include "rsyslog.h" +#include "syslogd.h" +#include "cfsysline.h" +#include "module-template.h" +#include "net.h" +#include "srUtils.h" +#include "conf.h" +#include "tcpsrv.h" +#include "obj.h" + + +/* defines */ +#define TCPSESS_MAX_DEFAULT 200 /* default for nbr of tcp sessions if no number is given */ + +/* static data */ +DEFobjStaticHelpers +DEFobjCurrIf(conf) +DEFobjCurrIf(tcps_sess) + +static char *TCPLstnPort = "0"; /* read-only after startup */ +static tcps_sess_t **pTCPSessions; +/* The thread-safeness of the sesion table is doubtful */ + + + +/* code to free all sockets within a socket table. + * A socket table is a descriptor table where the zero + * element has the count of elements. This is used for + * listening sockets. The socket table itself is also + * freed. + * A POINTER to this structure must be provided, thus + * double indirection! + * rgerhards, 2007-06-28 + */ +static void freeAllSockets(int **socks) +{ + assert(socks != NULL); + assert(*socks != NULL); + while(**socks) { + dbgprintf("Closing socket %d.\n", (*socks)[**socks]); + close((*socks)[**socks]); + (**socks)--; + } + free(*socks); + socks = NULL; +} + + +/* configure TCP listener settings. This is called during command + * line parsing. The argument following -t is supplied as an argument. + * The format of this argument is + * ", " + * Typically, there is no whitespace between port and session number. + * (but it may be...). + * NOTE: you can not use dbgprintf() in here - the dbgprintf() system is + * not yet initilized when this function is called. + * rgerhards, 2007-06-21 + * We can also not use logerror(), as that system is also not yet + * initialized... rgerhards, 2007-06-28 + */ +static void +configureTCPListen(tcpsrv_t *pThis, char *cOptarg) +{ + register int i; + register char *pArg = cOptarg; + + assert(cOptarg != NULL); + ISOBJ_TYPE_assert(pThis, tcpsrv); + + /* extract port */ + i = 0; + while(isdigit((int) *pArg)) { + i = i * 10 + *pArg++ - '0'; + } + + if( i >= 0 && i <= 65535) { + TCPLstnPort = cOptarg; + } else { + logerrorSz("Invalid TCP listen port %s - changed to 514.\n", cOptarg); + TCPLstnPort = "514"; + } +} + + +#if 0 // I think this is no longer needed +static void +configureTCPListenSessMax(char *cOptarg) +{ + register int i; + register char *pArg = cOptarg; + + assert(cOptarg != NULL); + + /* number of sessions */ + i = 0; + while(isdigit((int) *pArg)) { + i = i * 10 + *pArg++ - '0'; + } + + if(i > 0) + pThis->iSessMax = i; + else { + /* too small, need to adjust */ + logerrorSz("TCP session max configured to %s - changing to 1.\n", cOptarg); + pThis->iSessMax = 1; + } +} +#endif + + +/* Initialize the session table + * returns 0 if OK, somewhat else otherwise + */ +static rsRetVal +TCPSessTblInit(tcpsrv_t *pThis) +{ + DEFiRet; + + ISOBJ_TYPE_assert(pThis, tcpsrv); + assert(pTCPSessions == NULL); + + dbgprintf("Allocating buffer for %d TCP sessions.\n", pThis->iSessMax); + if((pTCPSessions = (tcps_sess_t **) calloc(pThis->iSessMax, sizeof(tcps_sess_t *))) == NULL) { + dbgprintf("Error: TCPSessInit() could not alloc memory for TCP session table.\n"); + ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY); + } + +finalize_it: + RETiRet; +} + + +/* find a free spot in the session table. If the table + * is full, -1 is returned, else the index of the free + * entry (0 or higher). + */ +static int +TCPSessTblFindFreeSpot(tcpsrv_t *pThis) +{ + register int i; + + ISOBJ_TYPE_assert(pThis, tcpsrv); + + for(i = 0 ; i < pThis->iSessMax ; ++i) { + if(pTCPSessions[i] == NULL) + break; + } + + return((i < pThis->iSessMax) ? i : -1); +} + + +/* Get the next session index. Free session tables entries are + * skipped. This function is provided the index of the last + * session entry, or -1 if no previous entry was obtained. It + * returns the index of the next session or -1, if there is no + * further entry in the table. Please note that the initial call + * might as well return -1, if there is no session at all in the + * session table. + */ +static int +TCPSessGetNxtSess(tcpsrv_t *pThis, int iCurr) +{ + register int i; + + ISOBJ_TYPE_assert(pThis, tcpsrv); + for(i = iCurr + 1 ; i < pThis->iSessMax ; ++i) + if(pTCPSessions[i] != NULL) + break; + + return((i < pThis->iSessMax) ? i : -1); +} + + +/* De-Initialize TCP listner sockets. + * This function deinitializes everything, including freeing the + * session table. No TCP listen receive operations are permitted + * unless the subsystem is reinitialized. + * rgerhards, 2007-06-21 + */ +static void deinit_tcp_listener(tcpsrv_t *pThis) +{ + int iTCPSess; + + ISOBJ_TYPE_assert(pThis, tcpsrv); + assert(pTCPSessions != NULL); + + /* close all TCP connections! */ + iTCPSess = TCPSessGetNxtSess(pThis, -1); + while(iTCPSess != -1) { + tcps_sess.Destruct(&pTCPSessions[iTCPSess]); + /* now get next... */ + iTCPSess = TCPSessGetNxtSess(pThis, iTCPSess); + } + + /* we are done with the session table - so get rid of it... + */ + free(pTCPSessions); + pTCPSessions = NULL; /* just to make sure... */ + + /* finally close the listen sockets themselfs */ + freeAllSockets(&pThis->pSocksLstn); +} + + +/* Initialize TCP sockets (for listener) + * This function returns either NULL (which means it failed) or + * a pointer to an array of file descriptiors. If the pointer is + * returned, the zeroest element [0] contains the count of valid + * descriptors. The descriptors themself follow in range + * [1] ... [num-descriptors]. It is guaranteed that each of these + * descriptors is valid, at least when this function returns. + * Please note that technically the array may be larger than the number + * of valid pointers stored in it. The memory overhead is minimal, so + * we do not bother to re-allocate an array of the exact size. Logically, + * the array still contains the exactly correct number of descriptors. + */ +static int *create_tcp_socket(tcpsrv_t *pThis) +{ + struct addrinfo hints, *res, *r; + int error, maxs, *s, *socks, on = 1; +DEFiRet; + + ISOBJ_TYPE_assert(pThis, tcpsrv); + + if(!strcmp(TCPLstnPort, "0")) + TCPLstnPort = "514"; + /* use default - we can not do service db update, because there is + * no IANA-assignment for syslog/tcp. In the long term, we might + * re-use RFC 3195 port of 601, but that would probably break to + * many existing configurations. + * rgerhards, 2007-06-28 + */ + dbgprintf("creating tcp socket on port %s\n", TCPLstnPort); + memset(&hints, 0, sizeof(hints)); + hints.ai_flags = AI_PASSIVE | AI_NUMERICSERV; + hints.ai_family = family; + hints.ai_socktype = SOCK_STREAM; + + error = getaddrinfo(NULL, TCPLstnPort, &hints, &res); + if(error) { + logerror((char*) gai_strerror(error)); + return NULL; + } +RUNLOG; + + /* Count max number of sockets we may open */ + for (maxs = 0, r = res; r != NULL ; r = r->ai_next, maxs++) + /* EMPTY */; + socks = malloc((maxs+1) * sizeof(int)); + if (socks == NULL) { + logerror("couldn't allocate memory for TCP listen sockets, suspending TCP message reception."); + freeaddrinfo(res); + return NULL; + } +RUNLOG; + + *socks = 0; /* num of sockets counter at start of array */ + s = socks + 1; + for (r = res; r != NULL ; r = r->ai_next) { + *s = socket(r->ai_family, r->ai_socktype, r->ai_protocol); + if (*s < 0) { + if(!(r->ai_family == PF_INET6 && errno == EAFNOSUPPORT)) + logerror("create_tcp_socket(), socket"); + /* it is debatable if PF_INET with EAFNOSUPPORT should + * also be ignored... + */ + continue; + } + +#ifdef IPV6_V6ONLY + if (r->ai_family == AF_INET6) { + int iOn = 1; + if (setsockopt(*s, IPPROTO_IPV6, IPV6_V6ONLY, + (char *)&iOn, sizeof (iOn)) < 0) { + logerror("TCP setsockopt"); + close(*s); + *s = -1; + continue; + } + } +#endif + if (setsockopt(*s, SOL_SOCKET, SO_REUSEADDR, + (char *) &on, sizeof(on)) < 0 ) { + logerror("TCP setsockopt(REUSEADDR)"); + close(*s); + *s = -1; + continue; + } + + /* We need to enable BSD compatibility. Otherwise an attacker + * could flood our log files by sending us tons of ICMP errors. + */ +RUNLOG; +#ifndef BSD + if (should_use_so_bsdcompat()) { + if (setsockopt(*s, SOL_SOCKET, SO_BSDCOMPAT, + (char *) &on, sizeof(on)) < 0) { + logerror("TCP setsockopt(BSDCOMPAT)"); + close(*s); + *s = -1; + continue; + } + } +#endif + + if( (bind(*s, r->ai_addr, r->ai_addrlen) < 0) +#ifndef IPV6_V6ONLY + && (errno != EADDRINUSE) +#endif + ) { + logerror("TCP bind"); + close(*s); + *s = -1; + continue; + } +RUNLOG; + + if( listen(*s,pThis->iSessMax / 10 + 5) < 0) { + /* If the listen fails, it most probably fails because we ask + * for a too-large backlog. So in this case we first set back + * to a fixed, reasonable, limit that should work. Only if + * that fails, too, we give up. + */ + logerrorInt("listen with a backlog of %d failed - retrying with default of 32.", + pThis->iSessMax / 10 + 5); + if(listen(*s, 32) < 0) { + logerror("TCP listen, suspending tcp inet"); + close(*s); + *s = -1; + continue; + } + } + + (*socks)++; + s++; + } +RUNLOG; + + if(res != NULL) + freeaddrinfo(res); + + if(Debug && *socks != maxs) + dbgprintf("We could initialize %d TCP listen sockets out of %d we received " + "- this may or may not be an error indication.\n", *socks, maxs); + + if(*socks == 0) { + logerror("No TCP listen socket could successfully be initialized, " + "message reception via TCP disabled.\n"); + free(socks); + return(NULL); + } + +RUNLOG; + /* OK, we had success. Now it is also time to + * initialize our connections + */ + if(TCPSessTblInit(pThis) != 0) { + /* OK, we are in some trouble - we could not initialize the + * session table, so we can not continue. We need to free all + * we have assigned so far, because we can not really use it... + */ + logerror("Could not initialize TCP session table, suspending TCP message reception."); + freeAllSockets(&socks); /* prevent a socket leak */ + return(NULL); + } + +dbgprintf("TCP socket(s) successfully created.\n"); + return(socks); +} + + +/* Accept new TCP connection; make entry in session table. If there + * is no more space left in the connection table, the new TCP + * connection is immediately dropped. + */ +static int +SessAccept(tcpsrv_t *pThis, int fd) +{ + DEFiRet; + + int newConn; + int iSess; + struct sockaddr_storage addr; + socklen_t addrlen = sizeof(struct sockaddr_storage); + uchar fromHost[NI_MAXHOST]; + uchar fromHostFQDN[NI_MAXHOST]; + + ISOBJ_TYPE_assert(pThis, tcpsrv); + + newConn = accept(fd, (struct sockaddr*) &addr, &addrlen); + if (newConn < 0) { + logerror("tcp accept, ignoring error and connection request"); + return -1; + } + + /* Add to session list */ + iSess = TCPSessTblFindFreeSpot(pThis); + if(iSess == -1) { + errno = 0; + logerror("too many tcp sessions - dropping incoming request"); + close(newConn); + return -1; + } + + /* OK, we have a "good" index... */ + /* get the host name */ + if(cvthname(&addr, fromHost, fromHostFQDN) != RS_RET_OK) { + /* we seem to have something malicous - at least we + * are now told to discard the connection request. + * Error message has been generated by cvthname. + */ + close (newConn); + return -1; + } + + /* Here we check if a host is permitted to send us + * syslog messages. If it isn't, we do not further + * process the message but log a warning (if we are + * configured to do this). + * rgerhards, 2005-09-26 + */ + if(!pThis->pIsPermittedHost((struct sockaddr*) &addr, (char*) fromHostFQDN)) + { + dbgprintf("%s is not an allowed sender\n", (char *) fromHostFQDN); + if(option_DisallowWarning) { + errno = 0; + logerrorSz("TCP message from disallowed sender %s discarded", + (char*)fromHost); + } + close(newConn); + return -1; + } + + /* OK, we have an allowed sender, so let's continue */ + /* we first need to construct a new session object */ + CHKiRet(tcps_sess.Construct(&pTCPSessions[iSess])); + CHKiRet(tcps_sess.SetHost(pTCPSessions[iSess], fromHost)); + CHKiRet(tcps_sess.SetSock(pTCPSessions[iSess], newConn)); + CHKiRet(tcps_sess.SetMsgIdx(pTCPSessions[iSess], 0)); + CHKiRet(tcps_sess.ConstructFinalize(pTCPSessions[iSess])); + +finalize_it: +RUNLOG_VAR("%d", iRet); + if(iRet != RS_RET_OK) + iSess = -1; // TODO: change this to be fully iRet compliant ;) + + ENDfunc + return iSess; +} + + +/* This function is called to gather input. + */ +static rsRetVal +Run(tcpsrv_t *pThis) +{ + DEFiRet; + int maxfds; + int nfds; + int i; + int iTCPSess; + fd_set readfds; + + ISOBJ_TYPE_assert(pThis, tcpsrv); + + /* this is an endless loop - it is terminated when the thread is + * signalled to do so. This, however, is handled by the framework, + * right into the sleep below. + */ + while(1) { + maxfds = 0; + FD_ZERO (&readfds); + + /* Add the TCP listen sockets to the list of read descriptors. + */ + if(pThis->pSocksLstn != NULL && *pThis->pSocksLstn) { + for (i = 0; i < *pThis->pSocksLstn; i++) { + /* The if() below is theoretically not needed, but I leave it in + * so that a socket may become unsuable during execution. That + * feature is not yet supported by the current code base. + */ + if (pThis->pSocksLstn[i+1] != -1) { + if(Debug) + debugListenInfo(pThis->pSocksLstn[i+1], "TCP"); + FD_SET(pThis->pSocksLstn[i+1], &readfds); + if(pThis->pSocksLstn[i+1]>maxfds) maxfds=pThis->pSocksLstn[i+1]; + } + } + /* do the sessions */ + iTCPSess = TCPSessGetNxtSess(pThis, -1); + while(iTCPSess != -1) { + int fdSess; + fdSess = pTCPSessions[iTCPSess]->sock; // TODO: NOT CLEAN!, use method + dbgprintf("Adding TCP Session %d\n", fdSess); + FD_SET(fdSess, &readfds); + if (fdSess>maxfds) maxfds=fdSess; + /* now get next... */ + iTCPSess = TCPSessGetNxtSess(pThis, iTCPSess); + } + } + + if(Debug) { + // TODO: name in dbgprintf! + dbgprintf("-------- calling select, active file descriptors (max %d): ", maxfds); + for (nfds = 0; nfds <= maxfds; ++nfds) + if ( FD_ISSET(nfds, &readfds) ) + dbgprintf("%d ", nfds); + dbgprintf("\n"); + } + + /* wait for io to become ready */ + nfds = select(maxfds+1, (fd_set *) &readfds, NULL, NULL, NULL); + + for (i = 0; i < *pThis->pSocksLstn; i++) { + if (FD_ISSET(pThis->pSocksLstn[i+1], &readfds)) { + dbgprintf("New connect on TCP inetd socket: #%d\n", pThis->pSocksLstn[i+1]); + pThis->pOnSessAccept(pThis, pThis->pSocksLstn[i+1]); + --nfds; /* indicate we have processed one */ + } + } + + /* now check the sessions */ + iTCPSess = TCPSessGetNxtSess(pThis, -1); + while(nfds && iTCPSess != -1) { + int fdSess; + int state; + fdSess = pTCPSessions[iTCPSess]->sock; // TODO: not clean, use method + if(FD_ISSET(fdSess, &readfds)) { + char buf[MAXLINE]; + dbgprintf("tcp session socket with new data: #%d\n", fdSess); + + /* Receive message */ + state = pThis->pRcvData(pTCPSessions[iTCPSess], buf, sizeof(buf)); + if(state == 0) { + pThis->pOnRegularClose(pTCPSessions[iTCPSess]); + tcps_sess.Destruct(&pTCPSessions[iTCPSess]); + } else if(state == -1) { + logerrorInt("TCP session %d will be closed, error ignored\n", fdSess); + pThis->pOnErrClose(pTCPSessions[iTCPSess]); + tcps_sess.Destruct(&pTCPSessions[iTCPSess]); + } else { + /* valid data received, process it! */ + if(tcps_sess.DataRcvd(pTCPSessions[iTCPSess], buf, state) == 0) { + /* in this case, something went awfully wrong. + * We are instructed to terminate the session. + */ + logerrorInt("Tearing down TCP Session %d - see " + "previous messages for reason(s)\n", + iTCPSess); + pThis->pOnErrClose(pTCPSessions[iTCPSess]); + tcps_sess.Destruct(&pTCPSessions[iTCPSess]); + } + } + --nfds; /* indicate we have processed one */ + } + iTCPSess = TCPSessGetNxtSess(pThis, iTCPSess); + } + } + + RETiRet; +} + + + + +/* Standard-Constructor + */ +BEGINobjConstruct(tcpsrv) /* be sure to specify the object type also in END macro! */ + pThis->pSocksLstn = NULL; + pThis->iSessMax = 200; // TODO: useful default ;) +ENDobjConstruct(tcpsrv) + + +/* ConstructionFinalizer + */ +static rsRetVal +tcpsrvConstructFinalize(tcpsrv_t __attribute__((unused)) *pThis) +{ + DEFiRet; + ISOBJ_TYPE_assert(pThis, tcpsrv); + pThis->pSocksLstn = create_tcp_socket(pThis); + + RETiRet; +} + + +/* destructor for the tcpsrv object */ +BEGINobjDestruct(tcpsrv) /* be sure to specify the object type also in END and CODESTART macros! */ +CODESTARTobjDestruct(tcpsrv) + deinit_tcp_listener(pThis); +ENDobjDestruct(tcpsrv) + + +/* debugprint for the tcpsrv object */ +BEGINobjDebugPrint(tcpsrv) /* be sure to specify the object type also in END and CODESTART macros! */ +CODESTARTobjDebugPrint(tcpsrv) +ENDobjDebugPrint(tcpsrv) + +/* set functions */ +static rsRetVal +SetCBIsPermittedHost(tcpsrv_t *pThis, int (*pCB)(struct sockaddr *addr, char *fromHostFQDN)) +{ + DEFiRet; + pThis->pIsPermittedHost = pCB; + RETiRet; +} + +static rsRetVal +SetCBRcvData(tcpsrv_t *pThis, int (*pRcvData)(tcps_sess_t*, char*, size_t)) +{ + DEFiRet; + pThis->pRcvData = pRcvData; + RETiRet; +} + +static rsRetVal +SetCBOnListenDeinit(tcpsrv_t *pThis, int (*pCB)(void*)) +{ + DEFiRet; + pThis->pOnListenDeinit = pCB; + RETiRet; +} + +static rsRetVal +SetCBOnSessAccept(tcpsrv_t *pThis, rsRetVal (*pCB)(tcpsrv_t*,int)) +{ + DEFiRet; + pThis->pOnSessAccept = pCB; + RETiRet; +} + +static rsRetVal +SetCBOnRegularClose(tcpsrv_t *pThis, rsRetVal (*pCB)(tcps_sess_t*)) +{ + DEFiRet; + pThis->pOnRegularClose = pCB; + RETiRet; +} + +static rsRetVal +SetCBOnErrClose(tcpsrv_t *pThis, rsRetVal (*pCB)(tcps_sess_t*)) +{ + DEFiRet; + pThis->pOnErrClose = pCB; + RETiRet; +} + + + +/* queryInterface function + * rgerhards, 2008-02-29 + */ +BEGINobjQueryInterface(tcpsrv) +CODESTARTobjQueryInterface(tcpsrv) + if(pIf->ifVersion != tcpsrvCURR_IF_VERSION) { /* check for current version, increment on each change */ + ABORT_FINALIZE(RS_RET_INTERFACE_NOT_SUPPORTED); + } + + /* ok, we have the right interface, so let's fill it + * Please note that we may also do some backwards-compatibility + * work here (if we can support an older interface version - that, + * of course, also affects the "if" above). + */ + //pIf->freeAllSockets = freeAllSockets; + //pIf->configureTCPListen = configureTCPListen; + //pIf->configureTCPListenSessMax = configureTCPListenSessMax; + //pIf->TCPSessAccept = TCPSessAccept; + pIf->DebugPrint = tcpsrvDebugPrint; + pIf->Construct = tcpsrvConstruct; + pIf->ConstructFinalize = tcpsrvConstructFinalize; + pIf->Destruct = tcpsrvDestruct; + + pIf->SessAccept = SessAccept; + pIf->configureTCPListen = configureTCPListen; + pIf->Run = Run; + + pIf->SetCBIsPermittedHost = SetCBIsPermittedHost; + pIf->SetCBRcvData = SetCBRcvData; + pIf->SetCBOnListenDeinit = SetCBOnListenDeinit; + pIf->SetCBOnSessAccept = SetCBOnSessAccept; + pIf->SetCBOnRegularClose = SetCBOnRegularClose; + pIf->SetCBOnErrClose = SetCBOnErrClose; + +finalize_it: +ENDobjQueryInterface(tcpsrv) + + +/* Initialize our class. Must be called as the very first method + * before anything else is called inside this class. + * rgerhards, 2008-02-29 + */ +//BEGINObjClassInit(tcpsrv, 1, OBJ_IS_LOADABLE_MODULE) /* class, version - CHANGE class also in END MACRO! */ +BEGINObjClassInit(tcpsrv, 1, OBJ_IS_CORE_MODULE) /* class, version - CHANGE class also in END MACRO! */ + /* request objects we use */ + CHKiRet(objUse(tcps_sess, "tcps_sess")); + CHKiRet(objUse(conf, CORE_COMPONENT)); + + /* set our own handlers */ + OBJSetMethodHandler(objMethod_DEBUGPRINT, tcpsrvDebugPrint); + OBJSetMethodHandler(objMethod_CONSTRUCTION_FINALIZER, tcpsrvConstructFinalize); +ENDObjClassInit(tcpsrv) + + + +/* vim:set ai: + */ -- cgit