Skip to content
Snippets Groups Projects
Select Git revision
  • dd_msg_reader_list_personal_email_in_reverse_choose_msg_fix
  • dailybuild_linux-x64
  • dailybuild_win32
  • master default protected
  • sqlite
  • rip_abstraction
  • dailybuild_macos-armv8
  • dd_file_lister_filanem_in_desc_color
  • mode7
  • dd_msg_reader_are_you_there_warning_improvement
  • c23-playing
  • syncterm-1.3
  • syncterm-1.2
  • test-build
  • hide_remote_connection_with_telgate
  • 638-can-t-control-c-during-a-file-search
  • add_body_to_pager_email
  • mingw32-build
  • cryptlib-3.4.7
  • ree/mastermind
  • sbbs320d
  • syncterm-1.6
  • syncterm-1.5
  • syncterm-1.4
  • sbbs320b
  • syncterm-1.3
  • syncterm-1.2
  • syncterm-1.2rc6
  • syncterm-1.2rc5
  • push
  • syncterm-1.2rc4
  • syncterm-1.2rc2
  • syncterm-1.2rc1
  • sbbs319b
  • sbbs318b
  • goodbuild_linux-x64_Sep-01-2020
  • goodbuild_win32_Sep-01-2020
  • goodbuild_linux-x64_Aug-31-2020
  • goodbuild_win32_Aug-31-2020
  • goodbuild_win32_Aug-30-2020
40 results

main.cpp

Blame
  • main.cpp 159.86 KiB
    /* Synchronet terminal server thread and related functions */
    
    /* $Id$ */
    // vi: tabstop=4
    
    /****************************************************************************
     * @format.tab-size 4		(Plain Text/Source Code File Header)			*
     * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
     *																			*
     * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
     *																			*
     * This program 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 2			*
     * of the License, or (at your option) any later version.					*
     * See the GNU General Public License for more details: gpl.txt or			*
     * http://www.fsf.org/copyleft/gpl.html										*
     *																			*
     * Anonymous FTP access to the most recent released source is available at	*
     * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
     *																			*
     * Anonymous CVS access to the development source and modification history	*
     * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
     * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
     *     (just hit return, no password is necessary)							*
     * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
     *																			*
     * For Synchronet coding style and modification guidelines, see				*
     * http://www.synchro.net/source.html										*
     *																			*
     * You are encouraged to submit any modifications (preferably in Unix diff	*
     * format) via e-mail to mods@synchro.net									*
     *																			*
     * Note: If this box doesn't appear square, then you need to fix your tabs.	*
     ****************************************************************************/
    
    #include "sbbs.h"
    #include "ident.h"
    #include "telnet.h" 
    #include "netwrap.h"
    #include "js_rtpool.h"
    #include "js_request.h"
    #include "ssl.h"
    #include <multisock.h>
    #include <limits.h>		// HOST_NAME_MAX
    
    #ifdef __unix__
    	#include <sys/un.h>
    #endif
    
    //#define SBBS_TELNET_ENVIRON_SUPPORT 1
    //---------------------------------------------------------------------------
    
    #define TELNET_SERVER "Synchronet Terminal Server"
    #define STATUS_WFC	"Listening"
    
    #define TIMEOUT_THREAD_WAIT		60			// Seconds (was 15)
    #define IO_THREAD_BUF_SIZE	   	20000		// Bytes
    #define TIMEOUT_MUTEX_FILE		12*60*60
    
    // Globals
    #ifdef _WIN32
    	HANDLE		exec_mutex=NULL;
    	HINSTANCE	hK32=NULL;
    
    	#if defined(_DEBUG) && defined(_MSC_VER)
    			HANDLE	debug_log=INVALID_HANDLE_VALUE;
    		   _CrtMemState mem_chkpoint;
    	#endif // _DEBUG && _MSC_VER
    
    #endif // _WIN32
    
    #ifdef USE_CRYPTLIB
    	#define SSH_END()	if(ssh) { pthread_mutex_lock(&sbbs->ssh_mutex); cryptDestroySession(sbbs->ssh_session); pthread_mutex_unlock(&sbbs->ssh_mutex); }
    #else
    	#define	SSH_END()
    #endif
    
    volatile time_t	uptime=0;
    volatile ulong	served=0;
    
    static	protected_uint32_t node_threads_running;
    		
    char 	lastuseron[LEN_ALIAS+1];  /* Name of user last online */
    RingBuf* node_inbuf[MAX_NODES];
    SOCKET	spy_socket[MAX_NODES];
    #ifdef __unix__
    SOCKET	uspy_socket[MAX_NODES];	  /* UNIX domain spy sockets */
    #endif
    SOCKET	node_socket[MAX_NODES];
    struct xpms_set				*ts_set;
    static	sbbs_t*	sbbs=NULL;
    static	scfg_t	scfg;
    static	char *	text[TOTAL_TEXT];
    static	WORD	first_node;
    static	WORD	last_node;
    static	bool	terminate_server=false;
    static	str_list_t recycle_semfiles;
    static	str_list_t shutdown_semfiles;
    #ifdef _THREAD_SUID_BROKEN
    int	thread_suid_broken=TRUE;			/* NPTL is no longer broken */
    #endif
    
    extern "C" {
    
    static bbs_startup_t* startup=NULL;
    
    static const char* status(const char* str)
    {
    	if(startup!=NULL && startup->status!=NULL)
    	    startup->status(startup->cbdata,str);
    	return str;
    }
    
    static void update_clients()
    {
    	if(startup!=NULL && startup->clients!=NULL)
    		startup->clients(startup->cbdata,protected_uint32_value(node_threads_running));
    }
    
    void client_on(SOCKET sock, client_t* client, BOOL update)
    {
    	if(startup!=NULL && startup->client_on!=NULL)
    		startup->client_on(startup->cbdata,TRUE,sock,client,update);
    }
    
    static void client_off(SOCKET sock)
    {
    	if(startup!=NULL && startup->client_on!=NULL)
    		startup->client_on(startup->cbdata,FALSE,sock,NULL,FALSE);
    }
    
    static void thread_up(BOOL setuid)
    {
    	if(startup!=NULL && startup->thread_up!=NULL)
    		startup->thread_up(startup->cbdata,TRUE,setuid);
    }
    
    static void thread_down()
    {
    	if(startup!=NULL && startup->thread_up!=NULL)
    		startup->thread_up(startup->cbdata,FALSE,FALSE);
    }
    
    int lputs(int level, const char* str)
    {
    	if(level <= LOG_ERR) {
    		errorlog(&scfg,startup==NULL ? NULL:startup->host_name, str);
    		if(startup!=NULL && startup->errormsg!=NULL)
    			startup->errormsg(startup->cbdata,level,str);
    	}
    
    	if(startup==NULL || startup->lputs==NULL || str==NULL || level > startup->log_level)
        	return(0);
    
    #if defined(_WIN32)
    	if(IsBadCodePtr((FARPROC)startup->lputs))
    		return(0);
    #endif
    
        return(startup->lputs(startup->cbdata,level,str));
    }
    
    int lprintf(int level, const char *fmt, ...)
    {
    	va_list argptr;
    	char sbuf[1024];
    
        va_start(argptr,fmt);
        vsnprintf(sbuf,sizeof(sbuf),fmt,argptr);
    	sbuf[sizeof(sbuf)-1]=0;
        va_end(argptr);
        return(lputs(level,sbuf));
    }
    
    int eprintf(int level, const char *fmt, ...)
    {
    	va_list argptr;
    	char sbuf[1024];
    
        va_start(argptr,fmt);
        vsnprintf(sbuf,sizeof(sbuf),fmt,argptr);
    	sbuf[sizeof(sbuf)-1]=0;
        va_end(argptr);
    
    	if(level <= LOG_ERR) {
    		errorlog(&scfg,startup==NULL ? NULL:startup->host_name, sbuf);
    		if(startup!=NULL && startup->errormsg!=NULL)
    			startup->errormsg(startup->cbdata,level,sbuf);
    	}
    
        if(startup==NULL || startup->event_lputs==NULL || level > startup->log_level)
            return(0);
    
    	strip_ctrl(sbuf, sbuf);
        return(startup->event_lputs(startup->event_cbdata,level,sbuf));
    }
    
    struct main_sock_cb_data {
    	bbs_startup_t	*startup;
    	const char		*protocol;
    };
    
    void sock_cb(SOCKET sock, void *cb_data)
    {
    	char	error_str[256];
    	struct main_sock_cb_data *cb=(struct main_sock_cb_data *)cb_data;
    
    	if(cb->startup && cb->startup->socket_open)
    		cb->startup->socket_open(cb->startup->cbdata, TRUE);
    	if(set_socket_options(&scfg, sock, cb->protocol, error_str, sizeof(error_str)))
    		lprintf(LOG_ERR,"%04d !ERROR %s",sock,error_str);
    }
    
    void sock_close_cb(SOCKET sock, void *cb_data)
    {
    	bbs_startup_t	*su=(bbs_startup_t *)cb_data;
    
    	if(su && su->socket_open)
    		su->socket_open(su->cbdata, FALSE);
    }
    
    SOCKET open_socket(int type, const char* protocol)
    {
    	SOCKET	sock;
    	char	error[256];
    
    	sock=socket(AF_INET, type, IPPROTO_IP);
    	if(sock!=INVALID_SOCKET && startup!=NULL && startup->socket_open!=NULL) 
    		startup->socket_open(startup->cbdata,TRUE);
    	if(sock!=INVALID_SOCKET && set_socket_options(&scfg, sock, protocol, error, sizeof(error)))
    		lprintf(LOG_ERR,"%04d !ERROR %s",sock,error);
    
    	return(sock);
    }
    
    SOCKET accept_socket(SOCKET s, union xp_sockaddr* addr, socklen_t* addrlen)
    {
    	SOCKET	sock;
    
    	sock=accept(s,&addr->addr,addrlen);
    	if(sock!=INVALID_SOCKET && startup!=NULL && startup->socket_open!=NULL)
    		startup->socket_open(startup->cbdata,TRUE);
    
    	return(sock);
    }
    
    int close_socket(SOCKET sock)
    {
    	int		result;
    
    	if(sock==INVALID_SOCKET || sock==0)
    		return(0);
    
    	shutdown(sock,SHUT_RDWR);	/* required on Unix */
    	result=closesocket(sock);
    	if(startup!=NULL && startup->socket_open!=NULL)
    		startup->socket_open(startup->cbdata,FALSE);
    	if(result!=0 && ERROR_VALUE!=ENOTSOCK)
    		lprintf(LOG_WARNING,"!ERROR %d closing socket %d",ERROR_VALUE,sock);
    	return(result);
    }
    
    /* TODO: IPv6 */
    u_long resolve_ip(char *addr)
    {
    	HOSTENT*	host;
    	char*		p;
    
    	if(*addr==0)
    		return((u_long)INADDR_NONE);
    
    	for(p=addr;*p;p++)
    		if(*p!='.' && !isdigit((uchar)*p))
    			break;
    	if(!(*p))
    		return(inet_addr(addr));
    	if((host=gethostbyname(addr))==NULL) 
    		return((u_long)INADDR_NONE);
    	return(*((ulong*)host->h_addr_list[0]));
    }
    
    } /* extern "C" */
    
    #ifdef _WINSOCKAPI_
    
    WSADATA WSAData;
    #define SOCKLIB_DESC WSAData.szDescription
    static BOOL WSAInitialized=FALSE;
    
    static BOOL winsock_startup(void)
    {
    	int		status;             /* Status Code */
    
        if((status = WSAStartup(MAKEWORD(1,1), &WSAData))==0) {
    		lprintf(LOG_DEBUG,"%s %s",WSAData.szDescription, WSAData.szSystemStatus);
    		WSAInitialized=TRUE;
    		return(TRUE);
    	}
    
        lprintf(LOG_CRIT,"!WinSock startup ERROR %d", status);
    	return(FALSE);
    }
    
    #else /* No WINSOCK */
    
    #define winsock_startup()	(TRUE)
    #define SOCKLIB_DESC NULL
    
    #endif
    
    DLLEXPORT void DLLCALL sbbs_srand()
    {
    	DWORD seed;
    
    	xp_randomize();
    #if defined(HAS_DEV_RANDOM) && defined(RANDOM_DEV)
    	int     rf,rd=0;
    
    	if((rf=open(RANDOM_DEV, O_RDONLY|O_NONBLOCK))!=-1) {
    		rd=read(rf, &seed, sizeof(seed));
    		close(rf);
    	}
    	if (rd != sizeof(seed))
    #endif
    		seed = time32(NULL) ^ (uintmax_t)GetCurrentThreadId();
    
     	srand(seed);
    	sbbs_random(10);	/* Throw away first number */
    }
    
    int DLLCALL sbbs_random(int n)
    {
    	return(xp_random(n));
    }
    
    #ifdef JAVASCRIPT
    
    static js_server_props_t js_server_props;
    
    JSBool	
    DLLCALL js_CreateArrayOfStrings(JSContext* cx, JSObject* parent, const char* name, char* str[],uintN flags)
    {
    	JSObject*	array;
    	JSString*	js_str;
    	jsval		val;
    	size_t		i;
    	jsuint		len=0;
    		
    	if(JS_GetProperty(cx,parent,name,&val) && val!=JSVAL_VOID)
    		array=JSVAL_TO_OBJECT(val);
    	else
    		if((array=JS_NewArrayObject(cx, 0, NULL))==NULL)	/* Assertion here, in _heap_alloc_dbg, June-21-2004 */
    			return(JS_FALSE);								/* Caused by nntpservice.js? */
    
    	if(!JS_DefineProperty(cx, parent, name, OBJECT_TO_JSVAL(array)
    		,NULL,NULL,flags))
    		return(JS_FALSE);
    
    	if(!JS_GetArrayLength(cx, array, &len))
    		return(JS_FALSE);
    
    	for(i=0;str[i]!=NULL;i++) {
    		if((js_str = JS_NewStringCopyZ(cx, str[i]))==NULL)
    			break;
    		val = STRING_TO_JSVAL(js_str);
    		if(!JS_SetElement(cx, array, len+i, &val))
    			break;
    	}
    
    	return(JS_TRUE);
    }
    
    /* Convert from Synchronet-specific jsSyncMethodSpec to JSAPI's JSFunctionSpec */
    
    JSBool
    DLLCALL js_DescribeSyncObject(JSContext* cx, JSObject* obj, const char* str, int ver)
    {
    	JSString* js_str = JS_NewStringCopyZ(cx, str);
    
    	if(js_str==NULL)
    		return(JS_FALSE);
    
    	if(ver < 10000)		/* auto convert 313 to 31300 */
    		ver*=100;
    
    	return(JS_DefineProperty(cx,obj,"_description"
    			,STRING_TO_JSVAL(js_str),NULL,NULL,JSPROP_READONLY)
    		&& JS_DefineProperty(cx,obj,"_ver"
    			,INT_TO_JSVAL(ver),NULL,NULL,JSPROP_READONLY));
    }
    
    JSBool
    DLLCALL js_DescribeSyncConstructor(JSContext* cx, JSObject* obj, const char* str)
    {
    	JSString* js_str = JS_NewStringCopyZ(cx, str);
    
    	if(js_str==NULL)
    		return(JS_FALSE);
    
    	return(JS_DefineProperty(cx,obj,"_constructor"
    		,STRING_TO_JSVAL(js_str),NULL,NULL,JSPROP_READONLY));
    }
    
    #ifdef BUILD_JSDOCS
    
    static const char* method_array_name = "_method_list";
    static const char* propver_array_name = "_property_ver_list";
    
    /*
     * from jsatom.c:
     * Keep this in sync with jspubtd.h -- an assertion below will insist that
     * its length match the JSType enum's JSTYPE_LIMIT limit value.
     */
    static const char *js_type_str[] = {
        "void",			// changed from "undefined"
        "object",
        "function",
        "string",
        "number",
        "boolean",
    	"null",
    	"xml",
    	"array",
    	"alias",
    	"undefined"
    };
    
    JSBool
    DLLCALL js_DefineSyncProperties(JSContext *cx, JSObject *obj, jsSyncPropertySpec* props)
    {
    	uint		i;
    	long		ver;
    	jsval		val;
    	jsuint		len=0;
    	JSObject*	array;
    
    	if((array=JS_NewArrayObject(cx, 0, NULL))==NULL)
    		return(JS_FALSE);
    
    	if(!JS_DefineProperty(cx, obj, propver_array_name, OBJECT_TO_JSVAL(array)
    		,NULL,NULL,JSPROP_READONLY))
    		return(JS_FALSE);
    
    	for(i=0;props[i].name;i++) {
    		if(!JS_DefinePropertyWithTinyId(cx, obj, /* Never reserve any "slots" for properties */
    			props[i].name,props[i].tinyid, JSVAL_VOID, NULL, NULL, props[i].flags|JSPROP_SHARED))
    			return(JS_FALSE);
    		if(props[i].flags&JSPROP_ENUMERATE) {	/* No need to version invisible props */
    			if((ver=props[i].ver) < 10000)		/* auto convert 313 to 31300 */
    				ver*=100;
    			val = INT_TO_JSVAL(ver);
    			if(!JS_SetElement(cx, array, len++, &val))
    				return(JS_FALSE);
    		}
    	}
    
    	return(JS_TRUE);
    }
    
    JSBool 
    DLLCALL js_DefineSyncMethods(JSContext* cx, JSObject* obj, jsSyncMethodSpec *funcs)
    {
    	int			i;
    	jsuint		len=0;
    	long		ver;
    	jsval		val;
    	JSObject*	method;
    	JSObject*	method_array;
    	JSString*	js_str;
    	size_t		str_len=0;
    	char		*str=NULL;
    
    	/* Return existing method_list array if it's already been created */
    	if(JS_GetProperty(cx,obj,method_array_name,&val) && val!=JSVAL_VOID) {
    		method_array=JSVAL_TO_OBJECT(val);
    		// If the first item is already in the list, don't do anything.
    		if(!JS_GetArrayLength(cx, method_array, &len))
    			return(JS_FALSE);
    		for(i=0; i<len; i++) {
    			if(JS_GetElement(cx, method_array, i, &val)!=JS_TRUE || val == JSVAL_VOID)
    				continue;
    			JS_GetProperty(cx, JSVAL_TO_OBJECT(val), "name", &val);
    			JSVALUE_TO_RASTRING(cx, val, str, &str_len, NULL);
    			if(str==NULL)
    				continue;
    			if(strcmp(str, funcs[0].name)==0)
    				return(JS_TRUE);
    		}
    	}
    	else {
    		if((method_array=JS_NewArrayObject(cx, 0, NULL))==NULL) 
    			return(JS_FALSE);
    		if(!JS_DefineProperty(cx, obj, method_array_name, OBJECT_TO_JSVAL(method_array)
    				, NULL, NULL, 0))
    			return(JS_FALSE);
    	}
    
    	for(i=0;funcs[i].name;i++) {
    
    		if(!JS_DefineFunction(cx, obj, funcs[i].name, funcs[i].call, funcs[i].nargs, 0))
    			return(JS_FALSE);
    
    		if(funcs[i].type==JSTYPE_ALIAS)
    			continue;
    
    		method = JS_NewObject(cx, NULL, NULL, method_array);	/* exception here June-7-2003 */
    
    		if(method==NULL)
    			return(JS_FALSE);
    
    		if(funcs[i].name!=NULL) {
    			if((js_str=JS_NewStringCopyZ(cx,funcs[i].name))==NULL)
    				return(JS_FALSE);
    			val = STRING_TO_JSVAL(js_str);
    			JS_SetProperty(cx, method, "name", &val);
    		}
    
    		val = INT_TO_JSVAL(funcs[i].nargs);
    		if(!JS_SetProperty(cx, method, "nargs", &val))
    			return(JS_FALSE);
    
    		if((js_str=JS_NewStringCopyZ(cx,js_type_str[funcs[i].type]))==NULL)
    			return(JS_FALSE);
    		val = STRING_TO_JSVAL(js_str);
    		JS_SetProperty(cx, method, "type", &val);
    
    		if(funcs[i].args!=NULL) {
    			if((js_str=JS_NewStringCopyZ(cx,funcs[i].args))==NULL)
    				return(JS_FALSE);
    			val = STRING_TO_JSVAL(js_str);
    			JS_SetProperty(cx, method, "args", &val);
    		}
    
    		if(funcs[i].desc!=NULL) { 
    			if((js_str=JS_NewStringCopyZ(cx,funcs[i].desc))==NULL)
    				return(JS_FALSE);
    			val = STRING_TO_JSVAL(js_str);
    			JS_SetProperty(cx, method, "desc", &val);
    		}
    
    		if(funcs[i].ver) {
    			if((ver=funcs[i].ver) < 10000)		/* auto convert 313 to 31300 */
    				ver*=100;
    			val = INT_TO_JSVAL(ver);
    			JS_SetProperty(cx,method, "ver", &val);
    		}
    
    		val=OBJECT_TO_JSVAL(method);
    		if(!JS_SetElement(cx, method_array, len+i, &val))
    			return(JS_FALSE);
    	}
    
    	return(JS_TRUE);
    }
    
    /*
     * Always resolve all here since
     * 1) We'll always be enumerating anyways
     * 2) The speed penalty won't be seen in production code anyways
     */
    JSBool
    DLLCALL js_SyncResolve(JSContext* cx, JSObject* obj, char *name, jsSyncPropertySpec* props, jsSyncMethodSpec* funcs, jsConstIntSpec* consts, int flags)
    {
    	JSBool	ret=JS_TRUE;
    
    	if(props) {
    		if(!js_DefineSyncProperties(cx, obj, props))
    			ret=JS_FALSE;
    	}
    		
    	if(funcs) {
    		if(!js_DefineSyncMethods(cx, obj, funcs))
    			ret=JS_FALSE;
    	}
    
    	if(consts) {
    		if(!js_DefineConstIntegers(cx, obj, consts, flags))
    			ret=JS_FALSE;
    	}
    
    	return(ret);
    }
    
    #else // NON-JSDOCS
    
    JSBool
    DLLCALL js_DefineSyncProperties(JSContext *cx, JSObject *obj, jsSyncPropertySpec* props)
    {
    	uint i;
    
    	for(i=0;props[i].name;i++) 
    		if(!JS_DefinePropertyWithTinyId(cx, obj, 
    			props[i].name,props[i].tinyid, JSVAL_VOID, NULL, NULL, props[i].flags|JSPROP_SHARED))
    			return(JS_FALSE);
    
    	return(JS_TRUE);
    }
    
    
    JSBool 
    DLLCALL js_DefineSyncMethods(JSContext* cx, JSObject* obj, jsSyncMethodSpec *funcs)
    {
    	uint i;
    
    	for(i=0;funcs[i].name;i++)
    		if(!JS_DefineFunction(cx, obj, funcs[i].name, funcs[i].call, funcs[i].nargs, 0))
    			return(JS_FALSE);
    	return(JS_TRUE);
    }
    
    JSBool
    DLLCALL js_SyncResolve(JSContext* cx, JSObject* obj, char *name, jsSyncPropertySpec* props, jsSyncMethodSpec* funcs, jsConstIntSpec* consts, int flags)
    {
    	uint i;
    	jsval	val;
    
    	if(props) {
    		for(i=0;props[i].name;i++) {
    			if(name==NULL || strcmp(name, props[i].name)==0) {
    				if(!JS_DefinePropertyWithTinyId(cx, obj, 
    						props[i].name,props[i].tinyid, JSVAL_VOID, NULL, NULL, props[i].flags|JSPROP_SHARED))
    					return(JS_FALSE);
    				if(name)
    					return(JS_TRUE);
    			}
    		}
    	}
    	if(funcs) {
    		for(i=0;funcs[i].name;i++) {
    			if(name==NULL || strcmp(name, funcs[i].name)==0) {
    				if(!JS_DefineFunction(cx, obj, funcs[i].name, funcs[i].call, funcs[i].nargs, 0))
    					return(JS_FALSE);
    				if(name)
    					return(JS_TRUE);
    			}
    		}
    	}
    	if(consts) {
    		for(i=0;consts[i].name;i++) {
    			if(name==NULL || strcmp(name, consts[i].name)==0) {
    				val=INT_TO_JSVAL(consts[i].val);
    
    				if(!JS_DefineProperty(cx, obj, consts[i].name, val ,NULL, NULL, flags))
    					return(JS_FALSE);
    
    				if(name)
    					return(JS_TRUE);
    			}
    		}
    	}
    
    	return(JS_TRUE);
    }
    
    #endif
    
    /* This is a stream-lined version of JS_DefineConstDoubles */
    JSBool
    DLLCALL js_DefineConstIntegers(JSContext* cx, JSObject* obj, jsConstIntSpec* ints, int flags)
    {
    	uint	i;
    	jsval	val;
    
    	for(i=0;ints[i].name;i++) {
    		val=INT_TO_JSVAL(ints[i].val);
    
    		if(!JS_DefineProperty(cx, obj, ints[i].name, val ,NULL, NULL, flags))
    			return(JS_FALSE);
    	}
    
    	return(JS_TRUE);
    }
    
    static JSBool
    js_log(JSContext *cx, uintN argc, jsval *arglist)
    {
    	jsval *argv=JS_ARGV(cx, arglist);
        uintN		i=0;
    	int32		level=LOG_INFO;
        JSString*	str=NULL;
    	sbbs_t*		sbbs;
    	jsrefcount	rc;
    	char		*line=NULL;
    	size_t		line_sz=0;
    
    	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
    
    	if((sbbs=(sbbs_t*)JS_GetContextPrivate(cx))==NULL)
    		return(JS_FALSE);
    
    	if(argc > 1 && JSVAL_IS_NUMBER(argv[i])) {
    		if(!JS_ValueToInt32(cx,argv[i++],&level))
    			return JS_FALSE;
    	}
    
        for(; i<argc; i++) {
    		if((str=JS_ValueToString(cx, argv[i]))==NULL)
    			return(JS_FALSE);
    		JSSTRING_TO_RASTRING(cx, str, line, &line_sz, NULL);
    		if(line==NULL)
    		    return(JS_FALSE);
    		rc=JS_SUSPENDREQUEST(cx);
    		if(sbbs->online==ON_LOCAL) {
    			if(startup!=NULL && startup->event_lputs!=NULL && level <= startup->log_level) {
    				startup->event_lputs(startup->event_cbdata,level,line);
    			}
    		} else
    			lprintf(level,"Node %d %s", sbbs->cfg.node_num, line);
    		JS_RESUMEREQUEST(cx, rc);
    	}
    	free(line);
    
    	if(str==NULL)
    		JS_SET_RVAL(cx, arglist, JSVAL_VOID);
    	else
    		JS_SET_RVAL(cx, arglist, STRING_TO_JSVAL(str));
        return(JS_TRUE);
    }
    
    static JSBool
    js_read(JSContext *cx, uintN argc, jsval *arglist)
    {
    	jsval *argv=JS_ARGV(cx, arglist);
    	uchar*		buf;
    	int32		len=128;
    	sbbs_t*		sbbs;
    	jsrefcount	rc;
    
    	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
    
    	if((sbbs=(sbbs_t*)JS_GetContextPrivate(cx))==NULL)
    		return(JS_FALSE);
    
    	if(argc) {
    		if(!JS_ValueToInt32(cx,argv[0],&len))
    			return JS_FALSE;
    	}
    
    	if((buf=(uchar*)malloc(len))==NULL)
    		return(JS_TRUE);
    
    	rc=JS_SUSPENDREQUEST(cx);
    	len=RingBufRead(&sbbs->inbuf,buf,len);
    	JS_RESUMEREQUEST(cx, rc);
    
    	if(len>0)
    		JS_SET_RVAL(cx, arglist, STRING_TO_JSVAL(JS_NewStringCopyN(cx,(char*)buf,len)));
    
    	free(buf);
    	return(JS_TRUE);
    }
    
    static JSBool
    js_readln(JSContext *cx, uintN argc, jsval *arglist)
    {
    	jsval *argv=JS_ARGV(cx, arglist);
    	char*		buf;
    	int32		len=128;
    	sbbs_t*		sbbs;
    	jsrefcount	rc;
    
    	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
    
    	if((sbbs=(sbbs_t*)JS_GetContextPrivate(cx))==NULL)
    		return(JS_FALSE);
    
    	if(argc) {
    		if(!JS_ValueToInt32(cx,argv[0],&len))
    			return JS_FALSE;
    	}
    
    	if((buf=(char*)malloc(len))==NULL)
    		return(JS_TRUE);
    
    	rc=JS_SUSPENDREQUEST(cx);
    	len=sbbs->getstr(buf,len,K_NONE);
    	JS_RESUMEREQUEST(cx, rc);
    
    	if(len>0)
    		JS_SET_RVAL(cx, arglist, STRING_TO_JSVAL(JS_NewStringCopyZ(cx,buf)));
    
    	free(buf);
    	return(JS_TRUE);
    }
    
    static JSBool
    js_write(JSContext *cx, uintN argc, jsval *arglist)
    {
    	jsval *argv=JS_ARGV(cx, arglist);
        uintN		i;
        JSString*	str=NULL;
    	sbbs_t*		sbbs;
    	jsrefcount	rc;
    	char		*cstr=NULL;
    	size_t		cstr_sz=0;
    
    	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
    
    	if((sbbs=(sbbs_t*)JS_GetContextPrivate(cx))==NULL)
    		return(JS_FALSE);
    
        for (i = 0; i < argc; i++) {
    		if((str=JS_ValueToString(cx, argv[i]))==NULL)
    			return(JS_FALSE);
    		JSSTRING_TO_RASTRING(cx, str, cstr, &cstr_sz, NULL);
    		if(cstr==NULL)
    		    return(JS_FALSE);
    		rc=JS_SUSPENDREQUEST(cx);
    		if(sbbs->online==ON_LOCAL)
    			eprintf(LOG_INFO,"%s",cstr);
    		else
    			sbbs->bputs(cstr);
    		JS_RESUMEREQUEST(cx, rc);
    	}
    
    	if(str==NULL)
    		JS_SET_RVAL(cx, arglist, JSVAL_VOID);
    	else
    		JS_SET_RVAL(cx, arglist, STRING_TO_JSVAL(str));
        return(JS_TRUE);
    }
    
    static JSBool
    js_write_raw(JSContext *cx, uintN argc, jsval *arglist)
    {
    	jsval *argv=JS_ARGV(cx, arglist);
        uintN		i;
        char*		str=NULL;
        size_t		str_sz=0;
    	size_t		len;
    	sbbs_t*		sbbs;
    	jsrefcount	rc;
    
    	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
    
    	if((sbbs=(sbbs_t*)JS_GetContextPrivate(cx))==NULL)
    		return(JS_FALSE);
    
        for (i = 0; i < argc; i++) {
    		JSVALUE_TO_RASTRING(cx, argv[i], str, &str_sz, &len);
    		if(str==NULL)
    		    return(JS_FALSE);
    		rc=JS_SUSPENDREQUEST(cx);
    		sbbs->putcom(str, len);
    		JS_RESUMEREQUEST(cx, rc);
    	}
    
        return(JS_TRUE);
    }
    
    static JSBool
    js_writeln(JSContext *cx, uintN argc, jsval *arglist)
    {
    	sbbs_t*		sbbs;
    	jsrefcount	rc;
    
    	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
    
    	if((sbbs=(sbbs_t*)JS_GetContextPrivate(cx))==NULL)
    		return(JS_FALSE);
    
    	js_write(cx,argc,arglist);
    	rc=JS_SUSPENDREQUEST(cx);
    	if(sbbs->online==ON_REMOTE)
    		sbbs->bputs(crlf);
    	JS_RESUMEREQUEST(cx, rc);
    
        return(JS_TRUE);
    }
    
    static JSBool
    js_printf(JSContext *cx, uintN argc, jsval *arglist)
    {
    	jsval *argv=JS_ARGV(cx, arglist);
    	char*		p;
    	sbbs_t*		sbbs;
    	jsrefcount	rc;
    
    	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
    
    	if((sbbs=(sbbs_t*)JS_GetContextPrivate(cx))==NULL)
    		return(JS_FALSE);
    
    	if((p = js_sprintf(cx, 0, argc, argv))==NULL) {
    		JS_ReportError(cx,"js_sprintf failed");
    		return(JS_FALSE);
    	}
    
    	rc=JS_SUSPENDREQUEST(cx);
    	if(sbbs->online==ON_LOCAL)
    		eprintf(LOG_INFO,"%s",p);
    	else
    		sbbs->bputs(p);
    	JS_RESUMEREQUEST(cx, rc);
    
    	JS_SET_RVAL(cx, arglist, STRING_TO_JSVAL(JS_NewStringCopyZ(cx, p)));
    
    	js_sprintf_free(p);
    
        return(JS_TRUE);
    }
    
    static JSBool
    js_alert(JSContext *cx, uintN argc, jsval *arglist)
    {
    	jsval *argv=JS_ARGV(cx, arglist);
    	sbbs_t*		sbbs;
    	jsrefcount	rc;
    	char		*cstr;
    
    	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
    
    	if((sbbs=(sbbs_t*)JS_GetContextPrivate(cx))==NULL)
    		return(JS_FALSE);
    
    	JSVALUE_TO_MSTRING(cx, argv[0], cstr, NULL);
    	if(cstr==NULL)
    	    return(JS_FALSE);
    
    	rc=JS_SUSPENDREQUEST(cx);
    	sbbs->attr(sbbs->cfg.color[clr_err]);
    	sbbs->bputs(cstr);
    	free(cstr);
    	sbbs->attr(LIGHTGRAY);
    	sbbs->bputs(crlf);
    	JS_RESUMEREQUEST(cx, rc);
    
    	JS_SET_RVAL(cx, arglist, argv[0]);
    
        return(JS_TRUE);
    }
    
    static JSBool
    js_confirm(JSContext *cx, uintN argc, jsval *arglist)
    {
    	jsval *argv=JS_ARGV(cx, arglist);
    	sbbs_t*		sbbs;
    	jsrefcount	rc;
    	char		*cstr;
    
    	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
    
    	if((sbbs=(sbbs_t*)JS_GetContextPrivate(cx))==NULL)
    		return(JS_FALSE);
    
    	JSVALUE_TO_MSTRING(cx, argv[0], cstr, NULL);
    	if(cstr==NULL)
    	    return(JS_FALSE);
    
    	rc=JS_SUSPENDREQUEST(cx);
    	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->yesno(cstr)));
    	free(cstr);
    	JS_RESUMEREQUEST(cx, rc);
    	return(JS_TRUE);
    }
    
    static JSBool
    js_deny(JSContext *cx, uintN argc, jsval *arglist)
    {
    	jsval *argv=JS_ARGV(cx, arglist);
    	sbbs_t*		sbbs;
    	jsrefcount	rc;
    	char		*cstr;
    
    	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
    
    	if((sbbs=(sbbs_t*)JS_GetContextPrivate(cx))==NULL)
    		return(JS_FALSE);
    
    	JSVALUE_TO_MSTRING(cx, argv[0], cstr, NULL);
    	if(cstr==NULL)
    	    return(JS_FALSE);
    
    	rc=JS_SUSPENDREQUEST(cx);
    	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->noyes(cstr)));
    	free(cstr);
    	JS_RESUMEREQUEST(cx, rc);
    	return(JS_TRUE);
    }
    
    
    static JSBool
    js_prompt(JSContext *cx, uintN argc, jsval *arglist)
    {
    	jsval *argv=JS_ARGV(cx, arglist);
    	char		instr[81];
        JSString *	str;
    	sbbs_t*		sbbs;
    	jsrefcount	rc;
        char*		prompt=NULL;
    
    	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
    
    	if((sbbs=(sbbs_t*)JS_GetContextPrivate(cx))==NULL)
    		return(JS_FALSE);
    
    	if(argc) {
    		JSVALUE_TO_MSTRING(cx, argv[0], prompt, NULL);
    		if(prompt==NULL)
    			return(JS_FALSE);
    	}
    
    	if(argc>1) {
    		JSVALUE_TO_STRBUF(cx, argv[1], instr, sizeof(instr), NULL);
    	} else
    		instr[0]=0;
    
    	rc=JS_SUSPENDREQUEST(cx);
    	if(prompt != NULL) {
    		sbbs->bprintf("\1n\1y\1h%s\1w: ",prompt);
    		free(prompt);
    	}
    
    	if(!sbbs->getstr(instr,sizeof(instr)-1,K_EDIT)) {
    		JS_SET_RVAL(cx, arglist, JSVAL_NULL);
    		JS_RESUMEREQUEST(cx, rc);
    		return(JS_TRUE);
    	}
    	JS_RESUMEREQUEST(cx, rc);
    
    	if((str=JS_NewStringCopyZ(cx, instr))==NULL)
    	    return(JS_FALSE);
    
    	JS_SET_RVAL(cx, arglist, STRING_TO_JSVAL(str));
        return(JS_TRUE);
    }
    
    static jsSyncMethodSpec js_global_functions[] = {
    	{"log",				js_log,				1,	JSTYPE_STRING,	JSDOCSTR("[level,] value [,value]")
    	,JSDOCSTR("add a line of text to the server and/or system log, "
    		"<i>values</i> are typically string constants or variables, "
    		"<i>level</i> is the debug level/priority (default: <tt>LOG_INFO</tt>)")
    	,311
    	},
    	{"read",			js_read,			0,	JSTYPE_STRING,	JSDOCSTR("[count]")
    	,JSDOCSTR("read up to count characters from input stream")
    	,311
    	},
    	{"readln",			js_readln,			0,	JSTYPE_STRING,	JSDOCSTR("[count]")
    	,JSDOCSTR("read a single line, up to count characters, from input stream")
    	,311
    	},
    	{"write",			js_write,			0,	JSTYPE_VOID,	JSDOCSTR("value [,value]")
    	,JSDOCSTR("send one or more values (typically strings) to the server output")
    	,311
    	},
    	{"write_raw",			js_write_raw,			0,	JSTYPE_VOID,	JSDOCSTR("value [,value]")
    	,JSDOCSTR("send a stream of bytes (possibly containing NULLs or special control code sequences) to the server output")
    	,314
    	},
    	{"print",			js_writeln,			0,	JSTYPE_ALIAS },
        {"writeln",         js_writeln,         0,	JSTYPE_VOID,	JSDOCSTR("value [,value]")
    	,JSDOCSTR("send a line of text to the console or event log with automatic line termination (CRLF), "
    		"<i>values</i> are typically string constants or variables (AKA print)")
    	,311
    	},
        {"printf",          js_printf,          1,	JSTYPE_STRING,	JSDOCSTR("string format [,value][,value]")
    	,JSDOCSTR("print a formatted string - <small>CAUTION: for experienced C programmers ONLY</small>")
    	,310
    	},	
    	{"alert",			js_alert,			1,	JSTYPE_VOID,	JSDOCSTR("value")
    	,JSDOCSTR("print an alert message (ala client-side JS)")
    	,310
    	},
    	{"prompt",			js_prompt,			1,	JSTYPE_STRING,	JSDOCSTR("[value]")
    	,JSDOCSTR("displays a prompt (<i>value</i>) and returns a string of user input (ala clent-side JS)")
    	,310
    	},
    	{"confirm",			js_confirm,			1,	JSTYPE_BOOLEAN,	JSDOCSTR("value")
    	,JSDOCSTR("displays a Yes/No prompt and returns <i>true</i> or <i>false</i> "
    		"based on user's confirmation (ala client-side JS, <i>true</i> = yes)")
    	,310
    	},
    	{"deny",			js_deny,			1,	JSTYPE_BOOLEAN,	JSDOCSTR("value")
    	,JSDOCSTR("displays a No/Yes prompt and returns <i>true</i> or <i>false</i> "
    		"based on user's denial (<i>true</i> = no)")
    	,31501
    	},
        {0}
    };
    
    static void
    js_ErrorReporter(JSContext *cx, const char *message, JSErrorReport *report)
    {
    	char	line[64];
    	char	file[MAX_PATH+1];
    	sbbs_t*	sbbs;
    	const char*	warning;
    	jsrefcount	rc;
    	int		log_level;
    	char	nodestr[128];
    
    	if((sbbs=(sbbs_t*)JS_GetContextPrivate(cx))==NULL)
    		return;
    
        if(sbbs->cfg.node_num)
        	SAFEPRINTF(nodestr,"Node %d",sbbs->cfg.node_num);
        else
        	SAFECOPY(nodestr,sbbs->client_name);
    	
    	if(report==NULL) {
    		lprintf(LOG_ERR,"%s !JavaScript: %s", nodestr, message);
    		return;
        }
    
    	if(report->filename)
    		SAFEPRINTF(file," %s",report->filename);
    	else
    		file[0]=0;
    
    	if(report->lineno)
    		SAFEPRINTF(line," line %d",report->lineno);
    	else
    		line[0]=0;
    
    	if(JSREPORT_IS_WARNING(report->flags)) {
    		if(JSREPORT_IS_STRICT(report->flags))
    			warning="strict warning";
    		else
    			warning="warning";
    		log_level = LOG_WARNING;
    	} else {
    		warning=nulstr;
    		log_level = LOG_ERR;
    	}
    
    	rc=JS_SUSPENDREQUEST(cx);
    	if(sbbs->online==ON_LOCAL) 
    		eprintf(log_level,"!JavaScript %s%s%s: %s",warning,file,line,message);
    	else {
    		lprintf(log_level,"%s !JavaScript %s%s%s: %s",nodestr,warning,file,line,message);
    		sbbs->bprintf("!JavaScript %s%s%s: %s\r\n",warning,file,line,message);
    	}
    	JS_RESUMEREQUEST(cx, rc);
    }
    
    bool sbbs_t::js_init(ulong* stack_frame)
    {
    	char		node[128];
    
        if(cfg.node_num)
        	SAFEPRINTF(node,"Node %d",cfg.node_num);
        else
        	SAFECOPY(node,client_name);
    
    	if(startup->js.max_bytes==0)			startup->js.max_bytes=JAVASCRIPT_MAX_BYTES;
    	if(startup->js.cx_stack==0)				startup->js.cx_stack=JAVASCRIPT_CONTEXT_STACK;
    
    	lprintf(LOG_DEBUG,"%s JavaScript: Creating runtime: %lu bytes"
    		,node,startup->js.max_bytes);
    
    	if((js_runtime = jsrt_GetNew(startup->js.max_bytes, 1000, __FILE__, __LINE__))==NULL)
    		return(false);
    
    	lprintf(LOG_DEBUG,"%s JavaScript: Initializing context (stack: %lu bytes)"
    		,node,startup->js.cx_stack);
    
        if((js_cx = JS_NewContext(js_runtime, startup->js.cx_stack))==NULL)
    		return(false);
    	JS_BEGINREQUEST(js_cx);
    	
    	memset(&js_callback,0,sizeof(js_callback));
    	js_callback.limit = startup->js.time_limit;
    	js_callback.gc_interval = startup->js.gc_interval;
    	js_callback.yield_interval = startup->js.yield_interval;
    	js_callback.terminated = &terminated;
    	js_callback.auto_terminate = TRUE;
    
    	bool success=false;
    	bool rooted=false;
    
    	do {
    
    		JS_SetErrorReporter(js_cx, js_ErrorReporter);
    
    		JS_SetContextPrivate(js_cx, this);	/* Store a pointer to sbbs_t instance */
    
    		/* Global Objects (including system, js, client, Socket, MsgBase, File, User, etc. */
    		if(!js_CreateCommonObjects(js_cx, &scfg, &cfg, js_global_functions
    					,uptime, startup->host_name, SOCKLIB_DESC	/* system */
    					,&js_callback								/* js */
    					,&startup->js
    					,&client, client_socket, -1					/* client */
    					,&js_server_props							/* server */
    					,&js_glob
    			))
    			break;
    		rooted=true;
    
    #ifdef BUILD_JSDOCS
    		js_CreateUifcObject(js_cx, js_glob);
    		js_CreateConioObject(js_cx, js_glob);
    #endif
    
    		/* BBS Object */
    		if(js_CreateBbsObject(js_cx, js_glob)==NULL)
    			break;
    
    		/* Console Object */
    		if(js_CreateConsoleObject(js_cx, js_glob)==NULL)
    			break;
    
    		success=true;
    
    	} while(0);
    
    	if(!success) {
    		if(rooted)
    			JS_RemoveObjectRoot(js_cx, &js_glob);
    		JS_ENDREQUEST(js_cx);
    		JS_DestroyContext(js_cx);
    		js_cx=NULL;
    		return(false);
    	}
    	else
    		JS_ENDREQUEST(js_cx);
    
    	return(true);
    }
    
    void sbbs_t::js_cleanup(const char* node)
    {
    	/* Free Context */
    	if(js_cx!=NULL) {	
    		lprintf(LOG_DEBUG,"%s JavaScript: Destroying context",node);
    		JS_BEGINREQUEST(js_cx);
    		JS_RemoveObjectRoot(js_cx, &js_glob);
    		JS_ENDREQUEST(js_cx);
    		JS_DestroyContext(js_cx);
    		js_cx=NULL;
    	}
    
    	if(js_runtime!=NULL) {
    		lprintf(LOG_DEBUG,"%s JavaScript: Destroying runtime",node);
    		jsrt_Release(js_runtime);
    		js_runtime=NULL;
    	}
    }
    
    void sbbs_t::js_create_user_objects(void)
    {
    	if(js_cx==NULL)
    		return;
    	
    	JS_BEGINREQUEST(js_cx);
    	if(!js_CreateUserObjects(js_cx, js_glob, &cfg, &useron, &client, NULL, subscan)) 
    		lprintf(LOG_ERR,"!JavaScript ERROR creating user objects");
    	JS_ENDREQUEST(js_cx);
    }
    
    extern "C" BOOL DLLCALL js_CreateCommonObjects(JSContext* js_cx
    										,scfg_t* cfg				/* common */
    										,scfg_t* node_cfg			/* node-specific */
    										,jsSyncMethodSpec* methods	/* global */
    										,time_t uptime				/* system */
    										,char* host_name			/* system */
    										,char* socklib_desc			/* system */
    										,js_callback_t* cb			/* js */
    										,js_startup_t* js_startup	/* js */
    										,client_t* client			/* client */
    										,SOCKET client_socket		/* client */
    										,CRYPT_CONTEXT session		/* client */
    										,js_server_props_t* props	/* server */
    										,JSObject** glob
    										)
    {
    	BOOL	success=FALSE;
    
    	if(node_cfg==NULL)
    		node_cfg=cfg;
    
    	/* Global Object */
    	if(!js_CreateGlobalObject(js_cx, cfg, methods, js_startup, glob))
    		return(FALSE);
    
    	do {
    		/* System Object */
    		if(js_CreateSystemObject(js_cx, *glob, node_cfg, uptime, host_name, socklib_desc)==NULL)
    			break;
    
    		/* Internal JS Object */
    		if(cb!=NULL 
    			&& js_CreateInternalJsObject(js_cx, *glob, cb, js_startup)==NULL)
    			break;
    
    		/* Client Object */
    		if(client!=NULL 
    			&& js_CreateClientObject(js_cx, *glob, "client", client, client_socket, session)==NULL)
    			break;
    
    		/* Server */
    		if(props!=NULL
    			&& js_CreateServerObject(js_cx, *glob, props)==NULL)
    			break;
    
    		/* Socket Class */
    		if(js_CreateSocketClass(js_cx, *glob)==NULL)
    			break;
    
    		/* Queue Class */
    		if(js_CreateQueueClass(js_cx, *glob)==NULL)
    			break;
    
    		/* MsgBase Class */
    		if(js_CreateMsgBaseClass(js_cx, *glob, cfg)==NULL)
    			break;
    
    		/* File Class */
    		if(js_CreateFileClass(js_cx, *glob)==NULL)
    			break;
    
    		/* User class */
    		if(js_CreateUserClass(js_cx, *glob, cfg)==NULL) 
    			break;
    
    		/* COM Class */
    		if(js_CreateCOMClass(js_cx, *glob)==NULL)
    			break;
    
    		/* CryptContext Class */
    		if(js_CreateCryptContextClass(js_cx, *glob)==NULL)
    			break;
    
    		/* Area Objects */
    		if(!js_CreateUserObjects(js_cx, *glob, cfg, /* user: */NULL, client, /* html_index_fname: */NULL, /* subscan: */NULL)) 
    			break;
    
    		success=TRUE;
    	} while(0);
    
    	if(!success)
    		JS_RemoveObjectRoot(js_cx, glob);
    
    	return(success);
    }
    
    #endif	/* JAVASCRIPT */
    
    static BYTE* telnet_interpret(sbbs_t* sbbs, BYTE* inbuf, int inlen,
      									BYTE* outbuf, int& outlen)
    {
    	BYTE*   first_iac=NULL;
    	BYTE*	first_cr=NULL;
    	int 	i;
    
    	if(inlen<1) {
    		outlen=0;
    		return(inbuf);	// no length? No interpretation
    	}
    
        first_iac=(BYTE*)memchr(inbuf, TELNET_IAC, inlen);
    
    	if(!(sbbs->telnet_mode&TELNET_MODE_GATE) 
    		&& sbbs->telnet_remote_option[TELNET_BINARY_TX]!=TELNET_WILL
    		&& !(sbbs->console&CON_RAW_IN)) {
    		if(sbbs->telnet_last_rxch==CR)
    			first_cr=inbuf;
    		else
    			first_cr=(BYTE*)memchr(inbuf, CR, inlen);
    	}
    
        if(!sbbs->telnet_cmdlen	&& first_iac==NULL && first_cr==NULL) {
            outlen=inlen;
            return(inbuf);	// no interpretation needed
        }
    
        if(first_iac!=NULL || first_cr!=NULL) {
    		if(first_iac!=NULL && (first_cr==NULL || first_iac<first_cr))
       			outlen=first_iac-inbuf;
    		else
    			outlen=first_cr-inbuf;
    	    memcpy(outbuf, inbuf, outlen);
        } else
        	outlen=0;
    
        for(i=outlen;i<inlen;i++) {
    		if(!(sbbs->telnet_mode&TELNET_MODE_GATE) 
    			&& sbbs->telnet_remote_option[TELNET_BINARY_TX]!=TELNET_WILL
    			&& !(sbbs->console&CON_RAW_IN)) {
    			if(sbbs->telnet_last_rxch==CR
    				&& (inbuf[i]==LF || inbuf[i]==0)) { // CR/LF or CR/NUL, ignore 2nd char
    #if 0 /* Debug CR/LF problems */
    				lprintf(LOG_INFO,"Node %d CR/%02Xh detected and ignored"
    					,sbbs->cfg.node_num, inbuf[i]);
    #endif
    				sbbs->telnet_last_rxch=inbuf[i];
    				continue;
    			}
    			sbbs->telnet_last_rxch=inbuf[i];
    		}
    
            if(inbuf[i]==TELNET_IAC && sbbs->telnet_cmdlen==1) { /* escaped 255 */
                sbbs->telnet_cmdlen=0;
                outbuf[outlen++]=TELNET_IAC;
                continue;
            }
            if(inbuf[i]==TELNET_IAC || sbbs->telnet_cmdlen) {
    
    			if(sbbs->telnet_cmdlen<sizeof(sbbs->telnet_cmd))
    				sbbs->telnet_cmd[sbbs->telnet_cmdlen++]=inbuf[i];
    
    			uchar command	= sbbs->telnet_cmd[1];
    			uchar option	= sbbs->telnet_cmd[2];
    
    			if(sbbs->telnet_cmdlen>=2 && command==TELNET_SB) {
    				if(inbuf[i]==TELNET_SE 
    					&& sbbs->telnet_cmd[sbbs->telnet_cmdlen-2]==TELNET_IAC) {
    
    					if(startup->options&BBS_OPT_DEBUG_TELNET)
    						lprintf(LOG_DEBUG,"Node %d %s telnet sub-negotiation command: %s"
    	                		,sbbs->cfg.node_num
    							,sbbs->telnet_mode&TELNET_MODE_GATE ? "passed-through" : "received"
    							,telnet_opt_desc(option));
    
    					/* sub-option terminated */
    					if(option==TELNET_TERM_TYPE
    						&& sbbs->telnet_cmd[3]==TELNET_TERM_IS) {
    						safe_snprintf(sbbs->terminal,sizeof(sbbs->terminal),"%.*s",(int)sbbs->telnet_cmdlen-6,sbbs->telnet_cmd+4);
    						lprintf(LOG_DEBUG,"Node %d %s telnet terminal type: %s"
    	                		,sbbs->cfg.node_num
    							,sbbs->telnet_mode&TELNET_MODE_GATE ? "passed-through" : "received"
    							,sbbs->terminal);
    
    					} else if(option==TELNET_TERM_SPEED
    						&& sbbs->telnet_cmd[3]==TELNET_TERM_IS) {
    						char speed[128];
    						safe_snprintf(speed,sizeof(speed),"%.*s",(int)sbbs->telnet_cmdlen-6,sbbs->telnet_cmd+4);
    						lprintf(LOG_DEBUG,"Node %d %s telnet terminal speed: %s"
    	                		,sbbs->cfg.node_num
    							,sbbs->telnet_mode&TELNET_MODE_GATE ? "passed-through" : "received"
    							,speed);
    						sbbs->cur_rate=atoi(speed);
    						sbbs->cur_cps=sbbs->cur_rate/10;
    #ifdef SBBS_TELNET_ENVIRON_SUPPORT
    					} else if(option==TELNET_NEW_ENVIRON
    						&& sbbs->telnet_cmd[3]==TELNET_ENVIRON_IS) {
    						BYTE*	p;
    						BYTE*   end=sbbs->telnet_cmd+(sbbs->telnet_cmdlen-2);
    						for(p=sbbs->telnet_cmd+4; p < end; ) {
    							if(*p==TELNET_ENVIRON_VAR || *p==TELNET_ENVIRON_USERVAR) {
    								BYTE type=*p++;
    								char* name=(char*)p;
    								/* RFC 1572: The characters following a "type" up to the next "type" or VALUE specify the variable name. */
    								while(p < end) {
    									if(*p==TELNET_ENVIRON_VAR || *p==TELNET_ENVIRON_USERVAR || *p == TELNET_ENVIRON_VALUE)
    										break;
    									p++;
    								}
    								if(p < end) {
    									char* value=(char*)p+1;
    									*(p++)=0;
    									while(p < end) {
    										if(*p==TELNET_ENVIRON_VAR || *p==TELNET_ENVIRON_USERVAR || *p == TELNET_ENVIRON_VALUE)
    											break;
    										p++;
    									}
    									*p=0;
    									lprintf(LOG_DEBUG,"Node %d telnet %s %s environment variable '%s' = '%s'"
    	                					,sbbs->cfg.node_num
    										,sbbs->telnet_mode&TELNET_MODE_GATE ? "passed-through" : "received"
    										,type==TELNET_ENVIRON_VAR ? "well-known" : "user-defined"
    										,name
    										,value);
    									if(strcmp(name,"USER") == 0) {
    										SAFECOPY(sbbs->rlogin_name, value);
    									}
    								}
    							} else
    								p++;
    						}
    #endif
    					} else if(option==TELNET_SEND_LOCATION) {
    						safe_snprintf(sbbs->telnet_location
    							,sizeof(sbbs->telnet_location)
    							,"%.*s",(int)sbbs->telnet_cmdlen-5,sbbs->telnet_cmd+3);
    						lprintf(LOG_DEBUG,"Node %d %s telnet location: %s"
    	                		,sbbs->cfg.node_num
    							,sbbs->telnet_mode&TELNET_MODE_GATE ? "passed-through" : "received"
    							,sbbs->telnet_location);
    
    					} else if(option==TELNET_NEGOTIATE_WINDOW_SIZE) {
    						long cols = (sbbs->telnet_cmd[3]<<8) | sbbs->telnet_cmd[4];
    						long rows = (sbbs->telnet_cmd[5]<<8) | sbbs->telnet_cmd[6];
    						lprintf(LOG_DEBUG,"Node %d %s telnet window size: %ux%u"
    	                		,sbbs->cfg.node_num
    							,sbbs->telnet_mode&TELNET_MODE_GATE ? "passed-through" : "received"
    							,cols
    							,rows);
    						if(rows && !sbbs->useron.rows)	/* auto-detect rows */
    							sbbs->rows=rows;
    						if(cols)
    							sbbs->cols=cols;
    
    					} else if(startup->options&BBS_OPT_DEBUG_TELNET)
                			lprintf(LOG_DEBUG,"Node %d %s unsupported telnet sub-negotiation cmd: %s, 0x%02X"
    	                		,sbbs->cfg.node_num
    							,sbbs->telnet_mode&TELNET_MODE_GATE ? "passed-through" : "received"
                    			,telnet_opt_desc(option)
    							,sbbs->telnet_cmd[3]);
    					sbbs->telnet_cmdlen=0;
    				}
    			}
                else if(sbbs->telnet_cmdlen==2 && inbuf[i]<TELNET_WILL) {
    				if(startup->options&BBS_OPT_DEBUG_TELNET)
                		lprintf(LOG_DEBUG,"Node %d %s telnet cmd: %s"
    	                	,sbbs->cfg.node_num
    						,sbbs->telnet_mode&TELNET_MODE_GATE ? "passed-through" : "received"
                    		,telnet_cmd_desc(option));
                    sbbs->telnet_cmdlen=0;
                }
                else if(sbbs->telnet_cmdlen>=3) {	/* telnet option negotiation */
    
    				if(startup->options&BBS_OPT_DEBUG_TELNET)
    					lprintf(LOG_DEBUG,"Node %d %s telnet cmd: %s %s"
    						,sbbs->cfg.node_num
    						,sbbs->telnet_mode&TELNET_MODE_GATE ? "passed-through" : "received"
    						,telnet_cmd_desc(command)
    						,telnet_opt_desc(option));
    
    				if(!(sbbs->telnet_mode&TELNET_MODE_GATE)) {
    					if(command==TELNET_DO || command==TELNET_DONT) {	/* local options */
    						if(sbbs->telnet_local_option[option]==command) 
    							SetEvent(sbbs->telnet_ack_event);
    						else {
    							sbbs->telnet_local_option[option]=command;
    							sbbs->send_telnet_cmd(telnet_opt_ack(command),option);
    						}
    					} else { /* WILL/WONT (remote options) */ 
    						if(sbbs->telnet_remote_option[option]==command)	
    							SetEvent(sbbs->telnet_ack_event);
    						else {
    							switch(option) {
    								case TELNET_BINARY_TX:
    								case TELNET_ECHO:
    								case TELNET_TERM_TYPE:
    								case TELNET_TERM_SPEED:
    								case TELNET_SUP_GA:
    								case TELNET_NEGOTIATE_WINDOW_SIZE:
    								case TELNET_SEND_LOCATION:
    #ifdef SBBS_TELNET_ENVIRON_SUPPORT
    								case TELNET_NEW_ENVIRON:
    #endif
    									sbbs->telnet_remote_option[option]=command;
    									sbbs->send_telnet_cmd(telnet_opt_ack(command),option);
    									break;
    								default: /* unsupported remote options */
    									if(command==TELNET_WILL) /* NAK */
    										sbbs->send_telnet_cmd(telnet_opt_nak(command),option);
    									break;
    							}
    						}
    
    						if(command==TELNET_WILL && option==TELNET_TERM_TYPE) {
    							if(startup->options&BBS_OPT_DEBUG_TELNET)
    								lprintf(LOG_DEBUG,"Node %d requesting telnet terminal type"
    									,sbbs->cfg.node_num);
    
    							char	buf[64];
    							sprintf(buf,"%c%c%c%c%c%c"
    								,TELNET_IAC,TELNET_SB
    								,TELNET_TERM_TYPE,TELNET_TERM_SEND
    								,TELNET_IAC,TELNET_SE);
    							sbbs->putcom(buf,6);
    						}
    						else if(command==TELNET_WILL && option==TELNET_TERM_SPEED) {
    							if(startup->options&BBS_OPT_DEBUG_TELNET)
    								lprintf(LOG_DEBUG,"Node %d requesting telnet terminal speed"
    									,sbbs->cfg.node_num);
    
    							char	buf[64];
    							sprintf(buf,"%c%c%c%c%c%c"
    								,TELNET_IAC,TELNET_SB
    								,TELNET_TERM_SPEED,TELNET_TERM_SEND
    								,TELNET_IAC,TELNET_SE);
    							sbbs->putcom(buf,6);
    						}
    #ifdef SBBS_TELNET_ENVIRON_SUPPORT
    						else if(command==TELNET_WILL && option==TELNET_NEW_ENVIRON) {
    							if(startup->options&BBS_OPT_DEBUG_TELNET)
    								lprintf(LOG_DEBUG,"Node %d requesting USER environment variable value"
    									,sbbs->cfg.node_num);
    
    							char	buf[64];
    							int len=sprintf(buf,"%c%c%c%c%c%c"
    								,TELNET_IAC,TELNET_SB
    								,TELNET_NEW_ENVIRON,TELNET_ENVIRON_SEND //,TELNET_ENVIRON_VAR
    								,TELNET_IAC,TELNET_SE);
    							sbbs->putcom(buf,len);
    						}
    #endif
    					}
    				}
    
                    sbbs->telnet_cmdlen=0;
    
                }
    			if(sbbs->telnet_mode&TELNET_MODE_GATE)	// Pass-through commads
    				outbuf[outlen++]=inbuf[i];
            } else
            	outbuf[outlen++]=inbuf[i];
        }
        return(outbuf);
    }
    
    void sbbs_t::send_telnet_cmd(uchar cmd, uchar opt)
    {
    	char buf[16];
    	
    	if(telnet_mode&TELNET_MODE_OFF)	
    		return;
    
    	if(cmd<TELNET_WILL) {
    		if(startup->options&BBS_OPT_DEBUG_TELNET)
                lprintf(LOG_DEBUG,"Node %d sending telnet cmd: %s"
    	            ,cfg.node_num
                    ,telnet_cmd_desc(cmd));
    		sprintf(buf,"%c%c",TELNET_IAC,cmd);
    		putcom(buf,2);
    	} else {
    		if(startup->options&BBS_OPT_DEBUG_TELNET)
    			lprintf(LOG_DEBUG,"Node %d sending telnet cmd: %s %s"
    				,cfg.node_num
    				,telnet_cmd_desc(cmd)
    				,telnet_opt_desc(opt));
    		sprintf(buf,"%c%c%c",TELNET_IAC,cmd,opt);
    		putcom(buf,3);
    	}
    }
    
    bool sbbs_t::request_telnet_opt(uchar cmd, uchar opt, unsigned waitforack)
    {
    	if(telnet_mode&TELNET_MODE_OFF)	
    		return false;
    
    	if(cmd==TELNET_DO || cmd==TELNET_DONT) {	/* remote option */
    		if(telnet_remote_option[opt]==telnet_opt_ack(cmd))
    			return true;	/* already set in this mode, do nothing */
    		telnet_remote_option[opt]=telnet_opt_ack(cmd);
    	} else {	/* local option */
    		if(telnet_local_option[opt]==telnet_opt_ack(cmd))
    			return true;	/* already set in this mode, do nothing */
    		telnet_local_option[opt]=telnet_opt_ack(cmd);
    	}
    	if(waitforack)
    		ResetEvent(telnet_ack_event);
    	send_telnet_cmd(cmd,opt);
    	if(waitforack)
    		return WaitForEvent(telnet_ack_event, waitforack)==WAIT_OBJECT_0;
    	return true;
    }
    
    void input_thread(void *arg)
    {
    	BYTE		inbuf[4000];
       	BYTE		telbuf[sizeof(inbuf)];
        BYTE		*wrbuf;
        int			i,rd,wr,avail;
    	ulong		total_recv=0;
    	ulong		total_pkts=0;
    	fd_set		socket_set;
    	sbbs_t*		sbbs = (sbbs_t*) arg;
    	struct timeval	tv;
    	SOCKET		high_socket;
    	SOCKET		sock;
    
    	SetThreadName("sbbs/termInput");
    	thread_up(TRUE /* setuid */);
    
    #ifdef _DEBUG
    	lprintf(LOG_DEBUG,"Node %d input thread started",sbbs->cfg.node_num);
    #endif
    
    	sbbs->console|=CON_R_INPUT;
    
    	while(sbbs->online && sbbs->client_socket!=INVALID_SOCKET
    		&& node_socket[sbbs->cfg.node_num-1]!=INVALID_SOCKET) {
    
    		if(pthread_mutex_lock(&sbbs->input_thread_mutex)!=0)
    			sbbs->errormsg(WHERE,ERR_LOCK,"input_thread_mutex",0);
    
    		FD_ZERO(&socket_set);
    		FD_SET(sbbs->client_socket,&socket_set);
    		high_socket=sbbs->client_socket;
    #ifdef __unix__
    		if(uspy_socket[sbbs->cfg.node_num-1]!=INVALID_SOCKET)  {
    			FD_SET(uspy_socket[sbbs->cfg.node_num-1],&socket_set);
    			if(uspy_socket[sbbs->cfg.node_num-1] > high_socket)
    				high_socket=uspy_socket[sbbs->cfg.node_num-1];
    		}
    #endif
    
    		tv.tv_sec=1;
    		tv.tv_usec=0;
    
    		if((i=select(high_socket+1,&socket_set,NULL,NULL,&tv))<1) {
    			if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0)
    				sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0);
    			if(i==0) {
    				YIELD();	/* This kludge is necessary on some Linux distros */
    				continue;	/* to allow other threads to lock the input_thread_mutex */
    			}
    
    			if(sbbs->client_socket==INVALID_SOCKET)
    				break;
    #ifdef __unix__
    			if(uspy_socket[sbbs->cfg.node_num-1]!=INVALID_SOCKET) {
    				if(!socket_check(uspy_socket[sbbs->cfg.node_num-1],NULL,NULL,0)) {
    					close_socket(uspy_socket[sbbs->cfg.node_num-1]);
    					lprintf(LOG_NOTICE,"Closing local spy socket: %d",uspy_socket[sbbs->cfg.node_num-1]);
    					uspy_socket[sbbs->cfg.node_num-1]=INVALID_SOCKET;
    					continue;
    				}
    			}
    #endif
    	       	if(ERROR_VALUE == ENOTSOCK)
        	        lprintf(LOG_NOTICE,"Node %d socket closed by peer on input->select", sbbs->cfg.node_num);
    			else if(ERROR_VALUE==ESHUTDOWN)
    				lprintf(LOG_NOTICE,"Node %d socket shutdown on input->select", sbbs->cfg.node_num);
    			else if(ERROR_VALUE==EINTR)
    				lprintf(LOG_DEBUG,"Node %d input thread interrupted",sbbs->cfg.node_num);
                else if(ERROR_VALUE==ECONNRESET) 
    				lprintf(LOG_NOTICE,"Node %d connection reset by peer on input->select", sbbs->cfg.node_num);
    	        else if(ERROR_VALUE==ECONNABORTED) 
    				lprintf(LOG_NOTICE,"Node %d connection aborted by peer on input->select", sbbs->cfg.node_num);
    			else
    				lprintf(LOG_WARNING,"Node %d !ERROR %d input->select socket %d"
                   		,sbbs->cfg.node_num, ERROR_VALUE, sbbs->client_socket);
    			break;
    		}
    
    		if(sbbs->client_socket==INVALID_SOCKET) {
    			if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0)
    				sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0);
    			break;
    		}
    
    /*         ^          ^
     *		\______    ______/
     *       \  * \   / *   /
     *        -----   ------           /----\
     *              ||               -< Boo! |
     *             /__\                \----/
     *       \______________/
     *        \/\/\/\/\/\/\/
     *         ------------
     */
    
    		if(FD_ISSET(sbbs->client_socket,&socket_set))
    			sock=sbbs->client_socket;
    #ifdef __unix__
    		else if(uspy_socket[sbbs->cfg.node_num-1]!=INVALID_SOCKET
    				&& FD_ISSET(uspy_socket[sbbs->cfg.node_num-1],&socket_set))  {
    			if(!socket_check(uspy_socket[sbbs->cfg.node_num-1],NULL,NULL,0)) {
    				close_socket(uspy_socket[sbbs->cfg.node_num-1]);
    				lprintf(LOG_NOTICE,"Closing local spy socket: %d",uspy_socket[sbbs->cfg.node_num-1]);
    				uspy_socket[sbbs->cfg.node_num-1]=INVALID_SOCKET;
    				if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0)
    					sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0);
    				continue;
    			}
    			sock=uspy_socket[sbbs->cfg.node_num-1];
    		}
    #endif
    		else {
    			if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0)
    				sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0);
    			continue;
    		}
    
        	rd=RingBufFree(&sbbs->inbuf);
    
    		if(rd==0) { // input buffer full
    			lprintf(LOG_WARNING,"Node %d !WARNING input buffer full", sbbs->cfg.node_num);
            	// wait up to 5 seconds to empty (1 byte min)
    			time_t start=time(NULL);
                while((rd=RingBufFree(&sbbs->inbuf))==0 && time(NULL)-start<5) {
                    YIELD();
                }
    			if(rd==0) {	/* input buffer still full */
    				if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0)
    					sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0);
    				continue;
    			}
    		}
    		
    	    if(rd > (int)sizeof(inbuf))
            	rd=sizeof(inbuf);
    
    #ifdef USE_CRYPTLIB
    		if(sbbs->ssh_mode && sock==sbbs->client_socket) {
    			int err;
    			pthread_mutex_lock(&sbbs->ssh_mutex);
    			if(!cryptStatusOK((err=cryptPopData(sbbs->ssh_session, (char*)inbuf, rd, &i)))) {
    				pthread_mutex_unlock(&sbbs->ssh_mutex);
    				if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0)
    					sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0);
    				if(err==CRYPT_ERROR_TIMEOUT)
    					continue;
    				/* Handle the SSH error here... */
    				lprintf(LOG_WARNING,"Node %d !ERROR %d receiving on Cryptlib session", sbbs->cfg.node_num, err);
    				break;
    			}
    			else {
    				pthread_mutex_unlock(&sbbs->ssh_mutex);
    				if(!i) {
    					if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0)
    						sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0);
    					continue;
    				}
    				rd=i;
    			}
    		}
    		else
    #endif
        	rd = recv(sock, (char*)inbuf, rd, 0);
    
    		if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0)
    			sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0);
    
    		if(rd == SOCKET_ERROR)
    		{
    #ifdef __unix__
    			if(sock==sbbs->client_socket)  {
    #endif
    				if(!sbbs->online)	// sbbs_t::hangup() called?
    					break;
    	        	if(ERROR_VALUE == ENOTSOCK)
        	            lprintf(LOG_NOTICE,"Node %d socket closed by peer on receive", sbbs->cfg.node_num);
            	    else if(ERROR_VALUE==ECONNRESET) 
    					lprintf(LOG_NOTICE,"Node %d connection reset by peer on receive", sbbs->cfg.node_num);
    				else if(ERROR_VALUE==ESHUTDOWN)
    					lprintf(LOG_NOTICE,"Node %d socket shutdown on receive", sbbs->cfg.node_num);
            	    else if(ERROR_VALUE==ECONNABORTED) 
    					lprintf(LOG_NOTICE,"Node %d connection aborted by peer on receive", sbbs->cfg.node_num);
    				else
    					lprintf(LOG_WARNING,"Node %d !ERROR %d receiving from socket %d"
            	        	,sbbs->cfg.node_num, ERROR_VALUE, sock);
    				break;
    #ifdef __unix__
    			} else  {
    				if(ERROR_VALUE != EAGAIN)  {
    					lprintf(LOG_ERR,"Node %d !ERROR %d on local spy socket %d receive"
    						, sbbs->cfg.node_num, errno, sock);
    					close_socket(uspy_socket[sbbs->cfg.node_num-1]);
    					uspy_socket[sbbs->cfg.node_num-1]=INVALID_SOCKET;
    				}
    				continue;
    			}
    #endif
    		}
    
    		if(rd == 0 && sock==sbbs->client_socket)
    		{
    			lprintf(LOG_NOTICE,"Node %d disconnected", sbbs->cfg.node_num);
    			break;
    		}
    
    		total_recv+=rd;
    		total_pkts++;
    
            // telbuf and wr are modified to reflect telnet escaped data
    		wr=rd;
    #ifdef __unix__
    		if(sock!=sbbs->client_socket)
    			wrbuf=inbuf;
    		else
    #endif
    		if(sbbs->telnet_mode&TELNET_MODE_OFF)
    			wrbuf=inbuf;
    		else
    			wrbuf=telnet_interpret(sbbs, inbuf, rd, telbuf, wr);
    		if(wr > (int)sizeof(telbuf)) 
    			lprintf(LOG_ERR,"!TELBUF OVERFLOW (%d>%d)",wr,sizeof(telbuf));
    
    		/* First level Ctrl-C checking */
    		if(!(sbbs->cfg.ctrlkey_passthru&(1<<CTRL_C))
    			&& sbbs->rio_abortable 
    			&& !(sbbs->telnet_mode&TELNET_MODE_GATE)
    			&& sbbs->telnet_remote_option[TELNET_BINARY_TX]!=TELNET_WILL
    			&& memchr(wrbuf, CTRL_C, wr)) {	
    			if(RingBufFull(&sbbs->inbuf))
        			lprintf(LOG_DEBUG,"Node %d Ctrl-C hit with %lu bytes in input buffer"
    					,sbbs->cfg.node_num,RingBufFull(&sbbs->inbuf));
    			if(RingBufFull(&sbbs->outbuf))
        			lprintf(LOG_DEBUG,"Node %d Ctrl-C hit with %lu bytes in output buffer"
    					,sbbs->cfg.node_num,RingBufFull(&sbbs->outbuf));
    			sbbs->sys_status|=SS_ABORT;
    			RingBufReInit(&sbbs->inbuf);	/* Purge input buffer */
        		RingBufReInit(&sbbs->outbuf);	/* Purge output buffer */
    			sem_post(&sbbs->inbuf.sem);
    			continue;	// Ignore the entire buffer
    		}
    
    		avail=RingBufFree(&sbbs->inbuf);
    
            if(avail<wr)
    			lprintf(LOG_ERR,"!INPUT BUFFER FULL (%d free)", avail);
            else
    			RingBufWrite(&sbbs->inbuf, wrbuf, wr);
    //		if(wr>100)
    //			mswait(500);	// Throttle sender
    	}
    	sbbs->online=FALSE;
    	sbbs->sys_status|=SS_ABORT;	/* as though Ctrl-C were hit */
    
        sbbs->input_thread_running = false;
    	if(node_socket[sbbs->cfg.node_num-1]==INVALID_SOCKET)	// Shutdown locally
    		sbbs->terminated = true;	// Signal JS to stop execution
    
    	thread_down();
    	lprintf(LOG_DEBUG,"Node %d input thread terminated (received %lu bytes in %lu blocks)"
    		,sbbs->cfg.node_num, total_recv, total_pkts);
    }
    
    #ifdef USE_CRYPTLIB
    /*
     * This thread copies anything received from the client to the passthru_socket
     * It can only do that when the input thread is locked.
     * Luckily, the input thread is currently locked exactly when we want it to be.
     * Since the passthru socket is 8-bit clean and does NOT use a protocol,
     * we must handle telnet stuff HERE.
     * However, for JS stuff, direct operations on client_socket should generally
     * be done to the passthru_socket instead... THIS is the biggest problem here.
     */
    void passthru_output_thread(void* arg)
    {
    	fd_set	socket_set;
    	sbbs_t	*sbbs = (sbbs_t*) arg;
    	struct	timeval	tv;
    	int		i;
    	BYTE	inbuf[4000];
       	BYTE	telbuf[sizeof(inbuf)];
    	BYTE	*wrbuf;
    	int		rd;
    	int		wr;
    
    	SetThreadName("sbbs/ptOutput");
    	thread_up(FALSE /* setuid */);
    
    	while(sbbs->client_socket!=INVALID_SOCKET && sbbs->passthru_socket!=INVALID_SOCKET && !terminate_server) {
    		if(!sbbs->input_thread_mutex_locked) {
    			SLEEP(1);
    			continue;
    		}
    		
    		FD_ZERO(&socket_set);
    		FD_SET(sbbs->client_socket,&socket_set);
    
    		tv.tv_sec=1;
    		tv.tv_usec=0;
    
    		if((i=select(sbbs->client_socket+1,&socket_set,NULL,NULL,&tv))<1) {
    			if(i==0) {
    				YIELD();	/* This kludge is necessary on some Linux distros */
    				continue;	/* to allow other threads to lock the input_thread_mutex */
    			}
    
    			if(sbbs->client_socket==INVALID_SOCKET)
    				break;
    	       	if(ERROR_VALUE == ENOTSOCK)
        	        lprintf(LOG_NOTICE,"Node %d socket closed by peer on input->select", sbbs->cfg.node_num);
    			else if(ERROR_VALUE==ESHUTDOWN)
    				lprintf(LOG_NOTICE,"Node %d socket shutdown on input->select", sbbs->cfg.node_num);
    			else if(ERROR_VALUE==EINTR)
    				lprintf(LOG_DEBUG,"Node %d passthru output thread interrupted",sbbs->cfg.node_num);
                else if(ERROR_VALUE==ECONNRESET) 
    				lprintf(LOG_NOTICE,"Node %d connection reset by peer on input->select", sbbs->cfg.node_num);
    	        else if(ERROR_VALUE==ECONNABORTED) 
    				lprintf(LOG_NOTICE,"Node %d connection aborted by peer on input->select", sbbs->cfg.node_num);
    			else
    				lprintf(LOG_WARNING,"Node %d !ERROR %d ->select socket %d"
                   		,sbbs->cfg.node_num, ERROR_VALUE, sbbs->client_socket);
    			break;
    		}
    
    		if(sbbs->client_socket==INVALID_SOCKET)
    			break;
    
        	rd=sizeof(inbuf);
    
    #ifdef USE_CRYPTLIB
    		if(sbbs->ssh_mode) {
    			pthread_mutex_lock(&sbbs->ssh_mutex);
    			if(!cryptStatusOK(cryptPopData(sbbs->ssh_session, (char*)inbuf, rd, &i)))
    				rd=0;
    			else {
    				if(!i) {
    					pthread_mutex_unlock(&sbbs->ssh_mutex);
    					continue;
    				}
    				rd=i;
    			}
    			pthread_mutex_unlock(&sbbs->ssh_mutex);
    		}
    		else
    #endif
        	rd = recv(sbbs->client_socket, (char*)inbuf, rd, 0);
    
    		if(rd == SOCKET_ERROR)
    		{
    	       	if(ERROR_VALUE == ENOTSOCK)
        	        lprintf(LOG_NOTICE,"Node %d socket closed by peer on receive", sbbs->cfg.node_num);
                else if(ERROR_VALUE==ECONNRESET) 
    				lprintf(LOG_NOTICE,"Node %d connection reset by peer on receive", sbbs->cfg.node_num);
    			else if(ERROR_VALUE==ESHUTDOWN)
    				lprintf(LOG_NOTICE,"Node %d socket shutdown on receive", sbbs->cfg.node_num);
                else if(ERROR_VALUE==ECONNABORTED) 
    				lprintf(LOG_NOTICE,"Node %d connection aborted by peer on receive", sbbs->cfg.node_num);
    			else
    				lprintf(LOG_WARNING,"Node %d !ERROR %d receiving from socket %d"
                    	,sbbs->cfg.node_num, ERROR_VALUE, sbbs->client_socket);
    			break;
    		}
    
    		if(rd == 0)
    		{
    			lprintf(LOG_DEBUG,"Node %d passthru input socket disconnected", sbbs->cfg.node_num);
    			break;
    		}
    
            // telbuf and wr are modified to reflect telnet escaped data
    		wr=rd;
    		if(sbbs->telnet_mode&TELNET_MODE_OFF)
    			wrbuf=inbuf;
    		else
    			wrbuf=telnet_interpret(sbbs, inbuf, rd, telbuf, wr);
    		if(wr > (int)sizeof(telbuf)) 
    			lprintf(LOG_ERR,"!TELBUF OVERFLOW (%d>%d)",wr,sizeof(telbuf));
    
    		/*
    		 * TODO: This should check for writability etc.
    		 */
    		sendsocket(sbbs->passthru_socket, (char*)wrbuf, wr);
    	}
    
    	sbbs->passthru_output_thread_running = false;
    }
    
    /*
     * This thread simply copies anything it manages to read from the
     * passthru_socket into the output ringbuffer.
     */
    void passthru_input_thread(void* arg)
    {
    	fd_set	r_set;
    	sbbs_t	*sbbs = (sbbs_t*) arg;
    	struct	timeval	tv;
    	BYTE	ch;
    	int		i;
    
    	SetThreadName("sbbs/ptInput");
    	thread_up(FALSE /* setuid */);
    
    	while(sbbs->passthru_socket!=INVALID_SOCKET && !terminate_server) {
    		tv.tv_sec=1;
    		tv.tv_usec=0;
    
    		FD_ZERO(&r_set);
    		FD_SET(sbbs->passthru_socket,&r_set);
    		if((i=select(sbbs->passthru_socket+1,&r_set,NULL,NULL,&tv))<1) {
    			if(i==0) {
    				YIELD();	/* This kludge is necessary on some Linux distros */
    				continue;	/* to allow other threads to lock the input_thread_mutex */
    			}
    
    			if(sbbs->passthru_socket==INVALID_SOCKET)
    				break;
    	       	if(ERROR_VALUE == ENOTSOCK)
        	        lprintf(LOG_NOTICE,"Node %d socket closed by peer on passthru->select", sbbs->cfg.node_num);
    			else if(ERROR_VALUE==ESHUTDOWN)
    				lprintf(LOG_NOTICE,"Node %d socket shutdown on passthru->select", sbbs->cfg.node_num);
    			else if(ERROR_VALUE==EINTR)
    				lprintf(LOG_DEBUG,"Node %d passthru thread interrupted",sbbs->cfg.node_num);
                else if(ERROR_VALUE==ECONNRESET) 
    				lprintf(LOG_NOTICE,"Node %d connection reset by peer on passthru->select", sbbs->cfg.node_num);
    	        else if(ERROR_VALUE==ECONNABORTED) 
    				lprintf(LOG_NOTICE,"Node %d connection aborted by peer on passthru->select", sbbs->cfg.node_num);
    			else
    				lprintf(LOG_WARNING,"Node %d !ERROR %d passthru->select socket %d"
                   		,sbbs->cfg.node_num, ERROR_VALUE, sbbs->passthru_socket);
    			break;
    		}
    		if(!RingBufFree(&sbbs->outbuf))
    			continue;
    
        	i = recv(sbbs->passthru_socket, (char*)(&ch), 1, 0);
    
    		if(i == SOCKET_ERROR)
    		{
    	        	if(ERROR_VALUE == ENOTSOCK)
        	            lprintf(LOG_NOTICE,"Node %d passthru socket closed by peer on receive", sbbs->cfg.node_num);
            	    else if(ERROR_VALUE==ECONNRESET) 
    					lprintf(LOG_NOTICE,"Node %d passthru connection reset by peer on receive", sbbs->cfg.node_num);
    				else if(ERROR_VALUE==ESHUTDOWN)
    					lprintf(LOG_NOTICE,"Node %d passthru socket shutdown on receive", sbbs->cfg.node_num);
            	    else if(ERROR_VALUE==ECONNABORTED) 
    					lprintf(LOG_NOTICE,"Node %d passthru connection aborted by peer on receive", sbbs->cfg.node_num);
    				else
    					lprintf(LOG_WARNING,"Node %d !ERROR %d receiving from passthru socket %d"
            	        	,sbbs->cfg.node_num, ERROR_VALUE, sbbs->passthru_socket);
    				break;
    		}
    
    		if(i == 0)
    		{
    			lprintf(LOG_NOTICE,"Node %d SSH passthru disconnected", sbbs->cfg.node_num);
    			break;
    		}
    
        	if(!RingBufWrite(&sbbs->outbuf, &ch, 1)) {
    			lprintf(LOG_ERR,"Cannot pass from passthru socket to outbuf");
    			break;
    		}
    	}
    	if(sbbs->passthru_socket!=INVALID_SOCKET) {
    		close_socket(sbbs->passthru_socket);
    		sbbs->passthru_socket=INVALID_SOCKET;
    	}
    	thread_down();
    
    	sbbs->passthru_input_thread_running = false;
    }
    #endif
    
    void output_thread(void* arg)
    {
    	char		node[128];
    	char		stats[128];
        BYTE		buf[IO_THREAD_BUF_SIZE];
    	int			i=0;	// Assignment to silence Valgrind
        ulong		avail;
    	ulong		total_sent=0;
    	ulong		total_pkts=0;
    	ulong		short_sends=0;
        ulong		bufbot=0;
        ulong		buftop=0;
    	sbbs_t*		sbbs = (sbbs_t*) arg;
    	fd_set		socket_set;
    	struct timeval tv;
    	ulong		mss=IO_THREAD_BUF_SIZE;
    
    	SetThreadName("sbbs/termOutput");
    	thread_up(TRUE /* setuid */);
    
        if(sbbs->cfg.node_num)
        	SAFEPRINTF(node,"Node %d",sbbs->cfg.node_num);
        else
        	SAFECOPY(node,sbbs->client_name);
    #ifdef _DEBUG
    	lprintf(LOG_DEBUG,"%s output thread started",node);
    #endif
    
    	sbbs->console|=CON_R_ECHO;
    
    #ifdef TCP_MAXSEG
    	/*
    	 * Auto-tune the highwater mark to be the negotiated MSS for the
         * socket (when possible)
    	 */
    	if(!sbbs->outbuf.highwater_mark) {
    		socklen_t	sl;
    		sl=sizeof(i);
    		if(!getsockopt(sbbs->client_socket, IPPROTO_TCP, TCP_MAXSEG, 
    #ifdef _WIN32
    			(char *)
    #endif
    			&i, &sl)) {
    			/* Check for sanity... */
    			if(i>100) {
    				sbbs->outbuf.highwater_mark=i;
    				lprintf(LOG_DEBUG,"Autotuning outbuf highwater mark to %d based on MSS",i);
    				mss=sbbs->outbuf.highwater_mark;
    				if(mss>IO_THREAD_BUF_SIZE) {
    					mss=IO_THREAD_BUF_SIZE;
    					lprintf(LOG_DEBUG,"MSS (%d) is higher than IO_THREAD_BUF_SIZE (%d)",i,IO_THREAD_BUF_SIZE);
    				}
    			}
    		}
    	}
    #endif
    
    	/* Note: do not terminate when online==FALSE, that is expected for the terminal server output_thread */
    	while(sbbs->client_socket!=INVALID_SOCKET && !terminate_server) {
    		/*
    		 * I'd like to check the linear buffer against the highwater
    		 * at this point, but it would get too clumsy imho - Deuce
    		 *
    		 * Actually, another option would just be to have the size
    		 * of the linear buffer equal to the MSS... any larger and
    		 * you could have small sends off the end.  this would
    		 * probobly be even clumbsier
    		 */
    		if(bufbot == buftop) {
    			/* Wait for something to output in the RingBuffer */
    			if((avail=RingBufFull(&sbbs->outbuf))==0) {	/* empty */
    				if(sem_trywait_block(&sbbs->outbuf.sem,1000))
    					continue;
    				/* Check for spurious sem post... */
    				if((avail=RingBufFull(&sbbs->outbuf))==0)
    					continue;
    			}
    			else
    				sem_trywait(&sbbs->outbuf.sem);
    
    			/* Wait for full buffer or drain timeout */
    			if(sbbs->outbuf.highwater_mark) {
    				if(avail<sbbs->outbuf.highwater_mark) {
    					sem_trywait_block(&sbbs->outbuf.highwater_sem,startup->outbuf_drain_timeout);
    					/* We (potentially) blocked, so get fill level again */
    		    		avail=RingBufFull(&sbbs->outbuf);
    				} else
    					sem_trywait(&sbbs->outbuf.highwater_sem);	
    			}
    
    			/*
    			 * At this point, there's something to send and,
    			 * if the highwater mark is set, the timeout has
    			 * passed or we've hit highwater.  Read ring buffer
    			 * into linear buffer.
    			 */
               	if(avail>sizeof(buf)) {
                   	lprintf(LOG_WARNING,"%s !Insufficient linear output buffer (%lu > %lu)"
    					,node, avail, sizeof(buf));
                   	avail=sizeof(buf);
               	}
    			/* If we know the MSS, use it as the max send() size. */
    			if(avail>mss)
    				avail=mss;
               	buftop=RingBufRead(&sbbs->outbuf, buf, avail);
               	bufbot=0;
    			if (buftop == 0)
    				continue;
    		}
    
    		/* Check socket for writability (using select) */
    		tv.tv_sec=0;
    		tv.tv_usec=1000;
    
    		FD_ZERO(&socket_set);
    		if(sbbs->client_socket==INVALID_SOCKET)		// Make the race condition less likely to actually happen... TODO: Fix race
    			continue;
    		FD_SET(sbbs->client_socket,&socket_set);
    
    		i=select(sbbs->client_socket+1,NULL,&socket_set,NULL,&tv);
    		if(i==SOCKET_ERROR) {
    			if(sbbs->client_socket!=INVALID_SOCKET)
    				lprintf(LOG_WARNING,"%s !ERROR %d selecting socket %u for send"
    					,node,ERROR_VALUE,sbbs->client_socket);
    			if(sbbs->cfg.node_num)	/* Only break if node output (not server) */
    				break;
    			RingBufReInit(&sbbs->outbuf);	/* Purge output ring buffer */
    			bufbot=buftop=0;				/* Purge linear buffer */
    			continue;
    		}
    		if(i<1) {
    			continue;
    		}
    
    #ifdef USE_CRYPTLIB
    		if(sbbs->ssh_mode) {
    			int err;
    			pthread_mutex_lock(&sbbs->ssh_mutex);	/* exception here May-22-2013:
     	sbbs.dll!pthread_mutex_lock(_RTL_CRITICAL_SECTION * mutex=0x0a45e95c)  Line 147 + 0xc bytes	C
    >	sbbs.dll!output_thread(void * arg=0x0a458c68)  Line 2126 + 0x11 bytes	C++
     	sbbs.dll!_callthreadstart()  Line 259 + 0xf bytes	C
     	sbbs.dll!_threadstart(void * ptd=0x04b20c40)  Line 243	C
    
    -		sbbs->ssh_mutex	{DebugInfo=0x00000000 LockCount=-4 RecursionCount=0 ...}	_RTL_CRITICAL_SECTION
    +		DebugInfo	0x00000000 {Type=??? CreatorBackTraceIndex=??? CriticalSection=??? ...}	_RTL_CRITICAL_SECTION_DEBUG *
    		LockCount	-4	long
    		RecursionCount	0	long
    		OwningThread	0x00000000	void *
    		LockSemaphore	0x00006354	void *
    		SpinCount	0	unsigned long
    */
    			if(!cryptStatusOK((err=cryptPushData(sbbs->ssh_session, (char*)buf+bufbot, buftop-bufbot, &i)))) {
    				/* Handle the SSH error here... */
    				lprintf(LOG_WARNING,"%s !ERROR %d sending on Cryptlib session", node, err);
    				sbbs->online=FALSE;
    				i=buftop-bufbot;	// Pretend we sent it all
    			}
    			else {
    				if(!cryptStatusOK((err=cryptFlushData(sbbs->ssh_session)))) {
    					lprintf(LOG_WARNING,"%s !ERROR %d flushing Cryptlib session", node, err);
    					sbbs->online=FALSE;
    					i=buftop-bufbot;	// Pretend we sent it all
    				}
    			}
    			pthread_mutex_unlock(&sbbs->ssh_mutex);
    		}
    		else
    #endif
    			i=sendsocket(sbbs->client_socket, (char*)buf+bufbot, buftop-bufbot);
    		if(i==SOCKET_ERROR) {
            	if(ERROR_VALUE == ENOTSOCK)
                    lprintf(LOG_NOTICE,"%s client socket closed on send", node);
                else if(ERROR_VALUE==ECONNRESET) 
    				lprintf(LOG_NOTICE,"%s connection reset by peer on send", node);
                else if(ERROR_VALUE==ECONNABORTED) 
    				lprintf(LOG_NOTICE,"%s connection aborted by peer on send", node);
    			else
    				lprintf(LOG_WARNING,"%s !ERROR %d sending on socket %d"
                    	,node, ERROR_VALUE, sbbs->client_socket);
    			sbbs->online=FALSE;
    			/* was break; on 4/7/00 */
    			i=buftop-bufbot;	// Pretend we sent it all
    		}
    
    		if(sbbs->cfg.node_num>0 && !(sbbs->sys_status&SS_FILEXFER)) {
    			/* Spy on the user locally */
    			if(startup->node_spybuf!=NULL 
    				&& startup->node_spybuf[sbbs->cfg.node_num-1]!=NULL) {
    				RingBufWrite(startup->node_spybuf[sbbs->cfg.node_num-1],buf+bufbot,i);
    				/* Signal spy output semaphore? */
    				if(startup->node_spysem!=NULL 
    					&& startup->node_spysem[sbbs->cfg.node_num-1]!=NULL)
    					sem_post(startup->node_spysem[sbbs->cfg.node_num-1]);
    			}
    			/* Spy on the user remotely */
    			if(spy_socket[sbbs->cfg.node_num-1]!=INVALID_SOCKET) 
    				sendsocket(spy_socket[sbbs->cfg.node_num-1],(char*)buf+bufbot,i);
    #ifdef __unix__
    			if(uspy_socket[sbbs->cfg.node_num-1]!=INVALID_SOCKET)
    				sendsocket(uspy_socket[sbbs->cfg.node_num-1],(char*)buf+bufbot,i);
    #endif
    		}
    
    		if(i!=(int)(buftop-bufbot)) {
    			lprintf(LOG_WARNING,"%s !Short socket send (%u instead of %u)"
    				,node, i ,buftop-bufbot);
    			short_sends++;
    		}
    		bufbot+=i;
    		total_sent+=i;
    		total_pkts++;
        }
    
    	sbbs->spymsg("Disconnected");
    
        sbbs->output_thread_running = false;
    
    	if(total_sent)
    		safe_snprintf(stats,sizeof(stats),"(sent %lu bytes in %lu blocks, %lu average, %lu short)"
    			,total_sent, total_pkts, total_sent/total_pkts, short_sends);
    	else
    		stats[0]=0;
    
    	thread_down();
    	lprintf(LOG_DEBUG,"%s output thread terminated %s", node, stats);
    }
    
    void event_thread(void* arg)
    {
    	ulong		stack_frame;
    	char		str[MAX_PATH+1];
    	char		bat_list[MAX_PATH+1];
    	char		semfile[MAX_PATH+1];
    	int			i,j,k;
    	int			file;
    	int			offset;
    	bool		check_semaphores;
    	bool		packed_rep;
    	ulong	l;
    	/* TODO: This is a silly hack... */
    	uint32_t	l32;
    	time_t		now;
    	time_t		start;
    	time_t		lastsemchk=0;
    	time_t		lastnodechk=0;
    	time32_t	lastprepack=0;
    	time_t		tmptime;
    	node_t		node;
    	glob_t		g;
    	sbbs_t*		sbbs = (sbbs_t*) arg;
    	struct tm	now_tm;
    	struct tm	tm;
    
    	eprintf(LOG_INFO,"BBS Events thread started");
    
    	sbbs_srand();	/* Seed random number generator */
    
    	SetThreadName("sbbs/events");
    	thread_up(TRUE /* setuid */);
    
    #ifdef JAVASCRIPT
    	if(!(startup->options&BBS_OPT_NO_JAVASCRIPT)) {
    		if(!sbbs->js_init(&stack_frame)) /* This must be done in the context of the events thread */
    			lprintf(LOG_ERR,"!JavaScript Initialization FAILURE");
    	}
    #endif
    
    	// Read TIME.DAB
    	SAFEPRINTF(str,"%stime.dab",sbbs->cfg.ctrl_dir);
    	if((file=sbbs->nopen(str,O_RDWR|O_CREAT))==-1)
    		sbbs->errormsg(WHERE,ERR_OPEN,str,0);
    	else {
    		for(i=0;i<sbbs->cfg.total_events;i++) {
    			sbbs->cfg.event[i]->last=0;
    			if(filelength(file)<(long)(sizeof(time32_t)*(i+1))) {
    				eprintf(LOG_WARNING,"Initializing last run time for event: %s"
    					,sbbs->cfg.event[i]->code);
    				write(file,&sbbs->cfg.event[i]->last,sizeof(sbbs->cfg.event[i]->last));
    			} else {
    				if(read(file,&sbbs->cfg.event[i]->last,sizeof(sbbs->cfg.event[i]->last))!=sizeof(sbbs->cfg.event[i]->last))
    					sbbs->errormsg(WHERE,ERR_READ,str,sizeof(time32_t));
    			}
    			/* Event always runs after initialization? */
    			if(sbbs->cfg.event[i]->misc&EVENT_INIT)
    				sbbs->cfg.event[i]->last=-1;
    		}
    		lastprepack=0;
    		read(file,&lastprepack,sizeof(lastprepack));	/* expected to fail first time */
    		close(file);
    	}
    
    	// Read QNET.DAB
    	SAFEPRINTF(str,"%sqnet.dab",sbbs->cfg.ctrl_dir);
    	if((file=sbbs->nopen(str,O_RDWR|O_CREAT))==-1)
    		sbbs->errormsg(WHERE,ERR_OPEN,str,0);
    	else {
    		for(i=0;i<sbbs->cfg.total_qhubs;i++) {
    			sbbs->cfg.qhub[i]->last=0;
    			if(filelength(file)<(long)(sizeof(time32_t)*(i+1))) {
    				eprintf(LOG_WARNING,"Initializing last call-out time for QWKnet hub: %s"
    					,sbbs->cfg.qhub[i]->id);
    				write(file,&sbbs->cfg.qhub[i]->last,sizeof(sbbs->cfg.qhub[i]->last));
    			} else {
    				if(read(file,&sbbs->cfg.qhub[i]->last,sizeof(sbbs->cfg.qhub[i]->last))!=sizeof(sbbs->cfg.qhub[i]->last))
    					sbbs->errormsg(WHERE,ERR_READ,str,sizeof(sbbs->cfg.qhub[i]->last));
    			}
    		}
    		close(file);
    	}
    
    	// Read PNET.DAB
    	SAFEPRINTF(str,"%spnet.dab",sbbs->cfg.ctrl_dir);
    	if((file=sbbs->nopen(str,O_RDWR|O_CREAT))==-1)
    		sbbs->errormsg(WHERE,ERR_OPEN,str,0);
    	else {
    		for(i=0;i<sbbs->cfg.total_phubs;i++) {
    			sbbs->cfg.phub[i]->last=0;
    			if(filelength(file)<(long)(sizeof(time32_t)*(i+1)))
    				write(file,&sbbs->cfg.phub[i]->last,sizeof(sbbs->cfg.phub[i]->last));
    			else
    				read(file,&sbbs->cfg.phub[i]->last,sizeof(sbbs->cfg.phub[i]->last)); 
    		}
    		close(file);
    	}
    
    	while(!sbbs->terminated && !terminate_server) {
    
    		if(startup->options&BBS_OPT_NO_EVENTS) {
    			SLEEP(1000);
    			continue;
    		}
    
    		now=time(NULL);
    		localtime_r(&now,&now_tm);
    
    		if(now-lastsemchk>=sbbs->cfg.node_sem_check) {
    			check_semaphores=true;
    			lastsemchk=now;
    		} else
    			check_semaphores=false;
    
    		sbbs->online=FALSE;	/* reset this from ON_LOCAL */
    
    		/* QWK events */
    		if(check_semaphores && !(startup->options&BBS_OPT_NO_QWK_EVENTS)) {
    			/* Import any REP files that have magically appeared (via FTP perhaps) */
    			SAFEPRINTF(str,"%sfile/",sbbs->cfg.data_dir);
    			offset=strlen(str);
    			strcat(str,"*.rep");
    			glob(str,0,NULL,&g);
    			for(i=0;i<(int)g.gl_pathc && !sbbs->terminated;i++) {
    				sbbs->useron.number=atoi(g.gl_pathv[i]+offset);
    				getuserdat(&sbbs->cfg,&sbbs->useron);
    				if(sbbs->useron.number && flength(g.gl_pathv[i])>0) {
    					SAFEPRINTF(semfile,"%s.lock",g.gl_pathv[i]);
    					if(!fmutex(semfile,startup->host_name,TIMEOUT_MUTEX_FILE)) {
    						eprintf(LOG_INFO,"%s exists (unpack in progress?)", semfile);
    						continue;
    					}
    					sbbs->online=ON_LOCAL;
    					eprintf(LOG_INFO,"Un-packing QWK Reply packet from %s",sbbs->useron.alias);
    					sbbs->getusrsubs();
    					sbbs->unpack_rep(g.gl_pathv[i]);
    					delfiles(sbbs->cfg.temp_dir,ALLFILES);		/* clean-up temp_dir after unpacking */
    					sbbs->batch_create_list();	/* FREQs? */
    					sbbs->batdn_total=0;
    					
    					/* putuserdat? */
    					remove(g.gl_pathv[i]);
    					remove(semfile);
    				}
    			}
    			globfree(&g);
    
    			/* Create any QWK files that have magically appeared (via FTP perhaps) */
    			SAFEPRINTF(str,"%spack*.now",sbbs->cfg.data_dir);
    			offset=strlen(sbbs->cfg.data_dir)+4;
    			glob(str,0,NULL,&g);
    			for(i=0;i<(int)g.gl_pathc && !sbbs->terminated;i++) {
    				eprintf(LOG_INFO,"QWK pack semaphore signaled: %s", g.gl_pathv[i]);
    				sbbs->useron.number=atoi(g.gl_pathv[i]+offset);
    				SAFEPRINTF2(semfile,"%spack%04u.lock",sbbs->cfg.data_dir,sbbs->useron.number);
    				if(!fmutex(semfile,startup->host_name,TIMEOUT_MUTEX_FILE)) {
    					eprintf(LOG_INFO,"%s exists (pack in progress?)", semfile);
    					continue;
    				}
    				getuserdat(&sbbs->cfg,&sbbs->useron);
    				if(sbbs->useron.number && !(sbbs->useron.misc&(DELETED|INACTIVE))) {
    					eprintf(LOG_INFO,"Packing QWK Message Packet for %s",sbbs->useron.alias);
    					sbbs->online=ON_LOCAL;
    					sbbs->getmsgptrs();
    					sbbs->getusrsubs();
    					sbbs->batdn_total=0;
    
    					sbbs->last_ns_time=sbbs->ns_time=sbbs->useron.ns_time;
    					SAFEPRINTF2(bat_list,"%sfile/%04u.dwn",sbbs->cfg.data_dir,sbbs->useron.number);
    					sbbs->batch_add_list(bat_list);
    
    					SAFEPRINTF3(str,"%sfile%c%04u.qwk"
    						,sbbs->cfg.data_dir,PATH_DELIM,sbbs->useron.number);
    					if(sbbs->pack_qwk(str,&l,true /* pre-pack/off-line */)) {
    						eprintf(LOG_INFO,"Packing completed");
    						sbbs->qwk_success(l,0,1);
    						sbbs->putmsgptrs(); 
    						remove(bat_list);
    					} else
    						eprintf(LOG_INFO,"No packet created (no new messages)");
    					delfiles(sbbs->cfg.temp_dir,ALLFILES);
    					sbbs->online=FALSE;
    				}
    				remove(g.gl_pathv[i]);
    				remove(semfile);
    			}
    			globfree(&g);
    
    			/* Create (pre-pack) QWK files for users configured as such */
    			SAFEPRINTF(semfile,"%sprepack.now",sbbs->cfg.data_dir);
    			if(sbbs->cfg.preqwk_ar[0] 
    				&& (fexistcase(semfile) || (now-lastprepack)/60>(60*24))) {
    				j=lastuser(&sbbs->cfg);
    				eprintf(LOG_INFO,"Pre-packing QWK Message packets...");
    				int userfile = openuserdat(&sbbs->cfg, /* for_modify: */FALSE);
    				for(i=1;i<=j;i++) {
    
    					SAFEPRINTF2(str,"%5u of %-5u",i,j);
    					//status(str);
    					sbbs->useron.number=i;
    					fgetuserdat(&sbbs->cfg,&sbbs->useron, userfile);
    
    					if(sbbs->useron.number
    						&& !(sbbs->useron.misc&(DELETED|INACTIVE))	 /* Pre-QWK */
    						&& sbbs->chk_ar(sbbs->cfg.preqwk_ar,&sbbs->useron,/* client: */NULL)) { 
    						for(k=1;k<=sbbs->cfg.sys_nodes;k++) {
    							if(sbbs->getnodedat(k,&node,0)!=0)
    								continue;
    							if((node.status==NODE_INUSE || node.status==NODE_QUIET
    								|| node.status==NODE_LOGON) && node.useron==i)
    								break; 
    						}
    						if(k<=sbbs->cfg.sys_nodes)	/* Don't pre-pack with user online */
    							continue;
    						eprintf(LOG_INFO,"Pre-packing QWK for %s",sbbs->useron.alias);
    						sbbs->online=ON_LOCAL;
    						sbbs->getmsgptrs();
    						sbbs->getusrsubs();
    						sbbs->batdn_total=0;
    						SAFEPRINTF3(str,"%sfile%c%04u.qwk"
    							,sbbs->cfg.data_dir,PATH_DELIM,sbbs->useron.number);
    						if(sbbs->pack_qwk(str,&l,true /* pre-pack */)) {
    							sbbs->qwk_success(l,0,1);
    							sbbs->putmsgptrs(); 
    						}
    						delfiles(sbbs->cfg.temp_dir,ALLFILES);
    						sbbs->online=FALSE;
    					} 
    				}
    				close(userfile);
    				lastprepack=(time32_t)now;
    				SAFEPRINTF(str,"%stime.dab",sbbs->cfg.ctrl_dir);
    				if((file=sbbs->nopen(str,O_WRONLY))==-1) {
    					sbbs->errormsg(WHERE,ERR_OPEN,str,O_WRONLY);
    					break; 
    				}
    				lseek(file,(long)sbbs->cfg.total_events*4L,SEEK_SET);
    				write(file,&lastprepack,sizeof(lastprepack));
    				close(file);
    
    				remove(semfile);
    				//status(STATUS_WFC);
    			}
    		}
    
    		if(check_semaphores) {
    
    			/* Run daily maintenance? */
    			sbbs->cfg.node_num=0;
    			sbbs->logonstats();
    			if(sbbs->sys_status&SS_DAILY)
    				sbbs->daily_maint();
    
    			/* Node Daily Events */
    			for(i=first_node;i<=last_node;i++) {
    				// Node Daily Event
    				node.status=NODE_INVALID_STATUS;
    				if(sbbs->getnodedat(i,&node,0)!=0)
    					continue;
    				if(node.misc&NODE_EVENT && node.status==NODE_WFC) {
    					sbbs->getnodedat(i,&node,1);
    					node.status=NODE_EVENT_RUNNING;
    					sbbs->putnodedat(i,&node);
    					if(sbbs->cfg.node_daily[0]) {
    						sbbs->cfg.node_num=i;
    						strcpy(sbbs->cfg.node_dir, sbbs->cfg.node_path[i-1]);
    
    						eprintf(LOG_INFO,"Running node %d daily event",i);
    						sbbs->online=ON_LOCAL;
    						sbbs->logentry("!:","Run node daily event");
    						sbbs->external(
    							 sbbs->cmdstr(sbbs->cfg.node_daily,nulstr,nulstr,NULL)
    							,EX_OFFLINE);
    					}
    					sbbs->getnodedat(i,&node,1);
    					node.misc&=~NODE_EVENT;
    					node.status=NODE_WFC;
    					node.useron=0;
    					sbbs->putnodedat(i,&node); 
    				}
    			}
    
    			/* QWK Networking Call-out sempahores */
    			for(i=0;i<sbbs->cfg.total_qhubs;i++) {
    				if(sbbs->cfg.qhub[i]->node<first_node 
    					|| sbbs->cfg.qhub[i]->node>last_node)
    					continue;
    				if(sbbs->cfg.qhub[i]->last==-1) // already signaled
    					continue;
    				SAFEPRINTF2(str,"%sqnet/%s.now",sbbs->cfg.data_dir,sbbs->cfg.qhub[i]->id);
    				if(fexistcase(str)) {
    					strcpy(str,sbbs->cfg.qhub[i]->id);
    					eprintf(LOG_INFO,"Semaphore signaled for QWK Network Hub: %s",strupr(str));
    					sbbs->cfg.qhub[i]->last=-1; 
    				}
    			}
    
    			/* Timed Event sempahores */
    			for(i=0;i<sbbs->cfg.total_events;i++) {
    				if((sbbs->cfg.event[i]->node<first_node
    					|| sbbs->cfg.event[i]->node>last_node)
    					&& !(sbbs->cfg.event[i]->misc&EVENT_EXCL))
    					continue;	// ignore non-exclusive events for other instances
    				if(sbbs->cfg.event[i]->misc&EVENT_DISABLED)
    					continue;
    				if(sbbs->cfg.event[i]->last==-1) // already signaled
    					continue;
    				SAFEPRINTF2(str,"%s%s.now",sbbs->cfg.data_dir,sbbs->cfg.event[i]->code);
    				if(fexistcase(str)) {
    					strcpy(str,sbbs->cfg.event[i]->code);
    					eprintf(LOG_INFO,"Semaphore signaled for Timed Event: %s",strupr(str));
    					sbbs->cfg.event[i]->last=-1; 
    				}
    			}
    		}
    
    		/* QWK Networking Call-out Events */
    		for(i=0;i<sbbs->cfg.total_qhubs && !sbbs->terminated;i++) {
    			if(sbbs->cfg.qhub[i]->node<first_node ||
    				sbbs->cfg.qhub[i]->node>last_node)
    				continue;
    
    			if(check_semaphores) {
    				// See if any packets have come in
    				SAFEPRINTF2(str,"%s%s.q??",sbbs->cfg.data_dir,sbbs->cfg.qhub[i]->id);
    				glob(str,GLOB_NOSORT,NULL,&g);
    				for(j=0;j<(int)g.gl_pathc;j++) {
    					SAFECOPY(str,g.gl_pathv[j]);
    					if(flength(str)>0) {	/* silently ignore 0-byte QWK packets */
    						eprintf(LOG_DEBUG,"Inbound QWK Packet detected: %s", str);
    						sbbs->online=ON_LOCAL;
    						sbbs->console|=CON_L_ECHO;
    						if(sbbs->unpack_qwk(str,i)==false) {
    							char newname[MAX_PATH+1];
    							SAFEPRINTF2(newname,"%s.%lx.bad",str,(long)now);
    							remove(newname);
    							if(rename(str,newname)==0) {
    								char logmsg[MAX_PATH*3];
    								SAFEPRINTF2(logmsg,"%s renamed to %s",str,newname);
    								sbbs->logline(LOG_NOTICE,"Q!",logmsg);
    							}
    						}
    						delfiles(sbbs->cfg.temp_dir,ALLFILES);
    						sbbs->console&=~CON_L_ECHO;
    						sbbs->online=FALSE;
    						remove(str);
    					}
    				}
    				globfree(&g);
    			}
    
    			/* Qnet call out based on time */
    			tmptime=sbbs->cfg.qhub[i]->last;
    			if(localtime_r(&tmptime,&tm)==NULL)
    				memset(&tm,0,sizeof(tm));
    			if(sbbs->cfg.qhub[i]->last==-1L					/* or frequency */
    				|| (((sbbs->cfg.qhub[i]->freq && (now-sbbs->cfg.qhub[i]->last)/60>=sbbs->cfg.qhub[i]->freq)
    					|| (sbbs->cfg.qhub[i]->time
    						&& (now_tm.tm_hour*60)+now_tm.tm_min>=sbbs->cfg.qhub[i]->time
    						&& (now_tm.tm_mday!=tm.tm_mday || now_tm.tm_mon!=tm.tm_mon)))
    					&& sbbs->cfg.qhub[i]->days&(1<<now_tm.tm_wday))) {
    				SAFEPRINTF2(str,"%sqnet/%s.now"
    					,sbbs->cfg.data_dir,sbbs->cfg.qhub[i]->id);
    				if(fexistcase(str))
    					remove(str);					/* Remove semaphore file */
    				SAFEPRINTF2(str,"%sqnet/%s.ptr"
    					,sbbs->cfg.data_dir,sbbs->cfg.qhub[i]->id);
    				file=sbbs->nopen(str,O_RDONLY);
    				for(j=0;j<sbbs->cfg.qhub[i]->subs;j++) {
    					sbbs->subscan[sbbs->cfg.qhub[i]->sub[j]].ptr=0;
    					if(file!=-1) {
    						lseek(file,sbbs->cfg.sub[sbbs->cfg.qhub[i]->sub[j]]->ptridx*sizeof(int32_t),SEEK_SET);
    						read(file,&sbbs->subscan[sbbs->cfg.qhub[i]->sub[j]].ptr,sizeof(sbbs->subscan[sbbs->cfg.qhub[i]->sub[j]].ptr)); 
    					}
    				}
    				if(file!=-1)
    					close(file);
    				sbbs->console|=CON_L_ECHO;
    				packed_rep=sbbs->pack_rep(i);
    				sbbs->console&=~CON_L_ECHO;
    				if(packed_rep) {
    					if((file=sbbs->nopen(str,O_WRONLY|O_CREAT))==-1)
    						sbbs->errormsg(WHERE,ERR_OPEN,str,O_WRONLY|O_CREAT);
    					else {
    						for(j=l=0;j<sbbs->cfg.qhub[i]->subs;j++) {
    							while(filelength(file)<
    								sbbs->cfg.sub[sbbs->cfg.qhub[i]->sub[j]]->ptridx*4L) {
    								l32=l;
    								write(file,&l32,4);		/* initialize ptrs to null */
    							}
    							lseek(file
    								,sbbs->cfg.sub[sbbs->cfg.qhub[i]->sub[j]]->ptridx*sizeof(int32_t)
    								,SEEK_SET);
    							write(file,&sbbs->subscan[sbbs->cfg.qhub[i]->sub[j]].ptr,sizeof(sbbs->subscan[sbbs->cfg.qhub[i]->sub[j]].ptr)); 
    						}
    						close(file); 
    					} 
    				}
    				delfiles(sbbs->cfg.temp_dir,ALLFILES);
    
    				sbbs->cfg.qhub[i]->last=time32(NULL);
    				SAFEPRINTF(str,"%sqnet.dab",sbbs->cfg.ctrl_dir);
    				if((file=sbbs->nopen(str,O_WRONLY))==-1) {
    					sbbs->errormsg(WHERE,ERR_OPEN,str,O_WRONLY);
    					break; 
    				}
    				lseek(file,sizeof(time32_t)*i,SEEK_SET);
    				write(file,&sbbs->cfg.qhub[i]->last,sizeof(sbbs->cfg.qhub[i]->last));
    				close(file);
    
    				if(sbbs->cfg.qhub[i]->call[0]) {
    					sbbs->cfg.node_num=sbbs->cfg.qhub[i]->node;
    					if(sbbs->cfg.node_num<1) 
    						sbbs->cfg.node_num=1;
    					strcpy(sbbs->cfg.node_dir, sbbs->cfg.node_path[sbbs->cfg.node_num-1]);
    					eprintf(LOG_INFO,"QWK Network call-out: %s",sbbs->cfg.qhub[i]->id); 
    					sbbs->online=ON_LOCAL;
    					sbbs->external(
    						 sbbs->cmdstr(sbbs->cfg.qhub[i]->call
    							,sbbs->cfg.qhub[i]->id,sbbs->cfg.qhub[i]->id,NULL)
    						,EX_OFFLINE|EX_SH);	/* sh for Unix perl scripts */
    				}
    			} 
    		}
    
    		/* PostLink Networking Call-out Events */
    		for(i=0;i<sbbs->cfg.total_phubs && !sbbs->terminated;i++) {
    			if(sbbs->cfg.phub[i]->node<first_node 
    				|| sbbs->cfg.phub[i]->node>last_node)
    				continue;
    			/* PostLink call out based on time */
    			tmptime=sbbs->cfg.phub[i]->last;
    			if(localtime_r(&tmptime,&tm)==NULL)
    				memset(&tm,0,sizeof(tm));
    			if(sbbs->cfg.phub[i]->last==-1
    				|| (((sbbs->cfg.phub[i]->freq								/* or frequency */
    					&& (now-sbbs->cfg.phub[i]->last)/60>=sbbs->cfg.phub[i]->freq)
    				|| (sbbs->cfg.phub[i]->time
    					&& (now_tm.tm_hour*60)+now_tm.tm_min>=sbbs->cfg.phub[i]->time
    				&& (now_tm.tm_mday!=tm.tm_mday || now_tm.tm_mon!=tm.tm_mon)))
    				&& sbbs->cfg.phub[i]->days&(1<<now_tm.tm_wday))) {
    
    				sbbs->cfg.phub[i]->last=time32(NULL);
    				SAFEPRINTF(str,"%spnet.dab",sbbs->cfg.ctrl_dir);
    				if((file=sbbs->nopen(str,O_WRONLY))==-1) {
    					sbbs->errormsg(WHERE,ERR_OPEN,str,O_WRONLY);
    					break; 
    				}
    				lseek(file,sizeof(time32_t)*i,SEEK_SET);
    				write(file,&sbbs->cfg.phub[i]->last,sizeof(sbbs->cfg.phub[i]->last));
    				close(file);
    
    				if(sbbs->cfg.phub[i]->call[0]) {
    					sbbs->cfg.node_num=sbbs->cfg.phub[i]->node;
    					if(sbbs->cfg.node_num<1) 
    						sbbs->cfg.node_num=1;
    					strcpy(sbbs->cfg.node_dir, sbbs->cfg.node_path[sbbs->cfg.node_num-1]);
    					eprintf(LOG_INFO,"PostLink Network call-out: %s",sbbs->cfg.phub[i]->name); 
    					sbbs->online=ON_LOCAL;
    					sbbs->external(
    						 sbbs->cmdstr(sbbs->cfg.phub[i]->call,nulstr,nulstr,NULL)
    						,EX_OFFLINE|EX_SH);	/* sh for Unix perl scripts */
    				} 
    			}
    		}
    
    		/* Timed Events */
    		for(i=0;i<sbbs->cfg.total_events && !sbbs->terminated;i++) {
    			if(!sbbs->cfg.event[i]->node 
    				|| sbbs->cfg.event[i]->node>sbbs->cfg.sys_nodes)
    				continue;	// ignore events for invalid nodes
    
    			if(sbbs->cfg.event[i]->misc&EVENT_DISABLED)
    				continue;
    
    			if((sbbs->cfg.event[i]->node<first_node
    				|| sbbs->cfg.event[i]->node>last_node)
    				&& !(sbbs->cfg.event[i]->misc&EVENT_EXCL))
    				continue;	// ignore non-exclusive events for other instances
    
    			tmptime=sbbs->cfg.event[i]->last;
    			if(localtime_r(&tmptime,&tm)==NULL)
    				memset(&tm,0,sizeof(tm));
    			if(sbbs->cfg.event[i]->last==-1 ||
    				(((sbbs->cfg.event[i]->freq 
    					&& (now-sbbs->cfg.event[i]->last)/60>=sbbs->cfg.event[i]->freq)
    				|| 	(!sbbs->cfg.event[i]->freq 
    					&& (now_tm.tm_hour*60)+now_tm.tm_min>=sbbs->cfg.event[i]->time
    				&& (now_tm.tm_mday!=tm.tm_mday || now_tm.tm_mon!=tm.tm_mon)))
    				&& sbbs->cfg.event[i]->days&(1<<now_tm.tm_wday)
    				&& (sbbs->cfg.event[i]->mdays==0 
    					|| sbbs->cfg.event[i]->mdays&(1<<now_tm.tm_mday))
    				&& (sbbs->cfg.event[i]->months==0
    					|| sbbs->cfg.event[i]->months&(1<<now_tm.tm_mon)))) 
    			{
    				if(sbbs->cfg.event[i]->misc&EVENT_EXCL) { /* exclusive event */
    
    					if(sbbs->cfg.event[i]->node<first_node
    						|| sbbs->cfg.event[i]->node>last_node) {
    						eprintf(LOG_INFO,"Waiting for node %d to run timed event: %s"
    							,sbbs->cfg.event[i]->node,sbbs->cfg.event[i]->code);
    						eprintf(LOG_DEBUG,"%s event last run: %s (0x%08lx)"
    							,sbbs->cfg.event[i]->code
    							,timestr(&sbbs->cfg, sbbs->cfg.event[i]->last, str)
    							,sbbs->cfg.event[i]->last);
    						lastnodechk=0;	 /* really last event time check */
    						start=time(NULL);
    						while(!sbbs->terminated) {
    							mswait(1000);
    							now=time(NULL);
    							if(now-start>10 && now-lastnodechk<10)
    								continue;
    							for(j=first_node;j<=last_node;j++) {
    								if(sbbs->getnodedat(j,&node,1)!=0)
    									continue;
    								if(node.status==NODE_WFC)
    									node.status=NODE_EVENT_LIMBO;
    								node.aux=sbbs->cfg.event[i]->node;
    								sbbs->putnodedat(j,&node);
    							}
    
    							lastnodechk=now;
    							SAFEPRINTF(str,"%stime.dab",sbbs->cfg.ctrl_dir);
    							if((file=sbbs->nopen(str,O_RDONLY))==-1) {
    								sbbs->errormsg(WHERE,ERR_OPEN,str,O_RDONLY);
    								sbbs->cfg.event[i]->last=(time32_t)now;
    								continue; 
    							}
    							lseek(file,(long)i*4L,SEEK_SET);
    							read(file,&sbbs->cfg.event[i]->last,sizeof(sbbs->cfg.event[i]->last));
    							close(file);
    							if(now-sbbs->cfg.event[i]->last<(60*60))	/* event is done */
    								break; 
    							if(now-start>(90*60)) {
    								eprintf(LOG_WARNING,"!TIMEOUT waiting for event to complete");
    								break;
    							}
    						}
    						SAFEPRINTF2(str,"%s%s.now",sbbs->cfg.data_dir,sbbs->cfg.event[i]->code);
    						if(fexistcase(str))
    							remove(str);
    						sbbs->cfg.event[i]->last=(time32_t)now;
    					} else {	// Exclusive event to run on a node under our control
    						eprintf(LOG_INFO,"Waiting for all nodes to become inactive before "
    							"running timed event: %s",sbbs->cfg.event[i]->code);
    						lastnodechk=0;
    						start=time(NULL);
    						while(!sbbs->terminated) {
    							mswait(1000);
    							now=time(NULL);
    							if(now-start>10 && now-lastnodechk<10)
    								continue;
    							lastnodechk=now;
    							// Check/change the status of the nodes that we're in control of
    							for(j=first_node;j<=last_node;j++) {
    								if(sbbs->getnodedat(j,&node,1)!=0)
    									continue;
    								if(node.status==NODE_WFC) {
    									if(j==sbbs->cfg.event[i]->node)
    										node.status=NODE_EVENT_WAITING;
    									else
    										node.status=NODE_EVENT_LIMBO;
    									node.aux=sbbs->cfg.event[i]->node;
    								}
    								sbbs->putnodedat(j,&node);
    							}
    
    							for(j=1;j<=sbbs->cfg.sys_nodes;j++) {
    								if(sbbs->getnodedat(j,&node,0)!=0)
    									continue;
    								if(j==sbbs->cfg.event[i]->node) {
    									if(node.status!=NODE_EVENT_WAITING)
    										break;
    								} else {
    									if(node.status!=NODE_OFFLINE
    										&& node.status!=NODE_EVENT_LIMBO)
    										break; 
    								}
    							}
    							if(j>sbbs->cfg.sys_nodes) /* all nodes either offline or in limbo */
    								break;
    							eprintf(LOG_DEBUG,"Waiting for node %d (status=%d)",j,node.status);
    							if(now-start>(90*60)) {
    								eprintf(LOG_WARNING,"!TIMEOUT waiting for node %d to become inactive",j);
    								break;
    							}
    						} 
    					} 
    				}
    #if 0 // removed Jun-23-2002
    				else {	/* non-exclusive */
    					sbbs->getnodedat(sbbs->cfg.event[i]->node,&node,0);
    					if(node.status!=NODE_WFC)
    						continue;
    				}
    #endif
    				if(sbbs->cfg.event[i]->node<first_node 
    					|| sbbs->cfg.event[i]->node>last_node) {
    					eprintf(LOG_NOTICE,"Changing node status for nodes %d through %d to WFC"
    						,first_node,last_node);
    					sbbs->cfg.event[i]->last=(time32_t)now;
    					for(j=first_node;j<=last_node;j++) {
    						node.status=NODE_INVALID_STATUS;
    						if(sbbs->getnodedat(j,&node,1)!=0)
    							continue;
    						node.status=NODE_WFC;
    						sbbs->putnodedat(j,&node);
    					}
    				}
    				else {
    					sbbs->cfg.node_num=sbbs->cfg.event[i]->node;
    					if(sbbs->cfg.node_num<1) 
    						sbbs->cfg.node_num=1;
    					strcpy(sbbs->cfg.node_dir, sbbs->cfg.node_path[sbbs->cfg.node_num-1]);
    				
    					SAFEPRINTF2(str,"%s%s.now",sbbs->cfg.data_dir,sbbs->cfg.event[i]->code);
    					if(fexistcase(str))
    						remove(str);
    					if(sbbs->cfg.event[i]->misc&EVENT_EXCL) {
    						sbbs->getnodedat(sbbs->cfg.event[i]->node,&node,1);
    						node.status=NODE_EVENT_RUNNING;
    						sbbs->putnodedat(sbbs->cfg.event[i]->node,&node);
    					}
    					int ex_mode = EX_OFFLINE;
    					if(!(sbbs->cfg.event[i]->misc&EVENT_EXCL)
    						&& sbbs->cfg.event[i]->misc&EX_BG)
    						ex_mode |= EX_BG;
    					if(sbbs->cfg.event[i]->misc&XTRN_SH)
    						ex_mode |= EX_SH;
    					ex_mode|=(sbbs->cfg.event[i]->misc&EX_NATIVE);
    					sbbs->online=ON_LOCAL;
    					strcpy(str,sbbs->cfg.event[i]->code);
    					eprintf(LOG_INFO,"Running %s%stimed event: %s"
    						,(ex_mode&EX_NATIVE)	? "native ":""
    						,(ex_mode&EX_BG)		? "background ":""
    						,strupr(str));
    					{
    						int result=
    						sbbs->external(
    							 sbbs->cmdstr(sbbs->cfg.event[i]->cmd,nulstr,sbbs->cfg.event[i]->dir,NULL)
    							,ex_mode
    							,sbbs->cfg.event[i]->dir);
    						if(!(ex_mode&EX_BG))
    							eprintf(result ? LOG_ERR : LOG_INFO,"Timed event: %s returned %d",strupr(str), result);
    					}
    					sbbs->cfg.event[i]->last=time32(NULL);
    					SAFEPRINTF(str,"%stime.dab",sbbs->cfg.ctrl_dir);
    					if((file=sbbs->nopen(str,O_WRONLY))==-1) {
    						sbbs->errormsg(WHERE,ERR_OPEN,str,O_WRONLY);
    						break; 
    					}
    					lseek(file,(long)i*4L,SEEK_SET);
    					write(file,&sbbs->cfg.event[i]->last,sizeof(sbbs->cfg.event[i]->last));
    					close(file);
    
    					if(sbbs->cfg.event[i]->misc&EVENT_EXCL) { /* exclusive event */
    						// Check/change the status of the nodes that we're in control of
    						for(j=first_node;j<=last_node;j++) {
    							node.status=NODE_INVALID_STATUS;
    							if(sbbs->getnodedat(j,&node,1)!=0)
    								continue;
    							node.status=NODE_WFC;
    							sbbs->putnodedat(j,&node);
    						}
    					}
    				} 
    			} 
    		}
    		mswait(1000);
    	}
    	sbbs->cfg.node_num=0;
    	sbbs->js_cleanup(sbbs->client_name);
    
        sbbs->event_thread_running = false;
    
    	thread_down();
    	eprintf(LOG_INFO,"BBS Events thread terminated");
    }
    
    
    //****************************************************************************
    sbbs_t::sbbs_t(ushort node_num, union xp_sockaddr *addr, size_t addr_len, const char* name, SOCKET sd,
    			   scfg_t* global_cfg, char* global_text[], client_t* client_info)
    {
    	char	nodestr[32];
    	char	path[MAX_PATH+1];
    	uint	i;
    
        if(node_num)
        	SAFEPRINTF(nodestr,"Node %d",node_num);
        else
        	SAFECOPY(nodestr,name);
    
    	lprintf(LOG_DEBUG,"%s constructor using socket %d (settings=%lx)"
    		,nodestr, sd, global_cfg->node_misc);
    
    	startup = ::startup;	// Convert from global to class member
    
    	memcpy(&cfg, global_cfg, sizeof(cfg));
    
    	cfg.node_num=node_num;
    	if(node_num>0) {
    		strcpy(cfg.node_dir, cfg.node_path[node_num-1]);
    		prep_dir(cfg.node_dir, cfg.temp_dir, sizeof(cfg.temp_dir));
    	} else {	/* event thread needs exclusive-use temp_dir */
    		if(startup->temp_dir[0])
    			SAFECOPY(cfg.temp_dir,startup->temp_dir);
    		else
    			SAFECOPY(cfg.temp_dir,"../temp");
        	prep_dir(cfg.ctrl_dir, cfg.temp_dir, sizeof(cfg.temp_dir));
    		md(cfg.temp_dir);
    		if(sd==INVALID_SOCKET) {	/* events thread */
    			if(startup->first_node==1)
    				SAFEPRINTF(path,"%sevent",cfg.temp_dir);
    			else
    				SAFEPRINTF2(path,"%sevent%u",cfg.temp_dir,startup->first_node);
    			backslash(path);
    			SAFECOPY(cfg.temp_dir,path);
    		}
    	}
    	lprintf(LOG_DEBUG,"%s temporary file directory: %s", nodestr, cfg.temp_dir);
    
    	terminated = false;
    	event_thread_running = false;
        input_thread_running = false;
        output_thread_running = false;
    	input_thread_mutex_locked = false;
    
    	if(client_info==NULL)
    		memset(&client,0,sizeof(client));
    	else
    		memcpy(&client,client_info,sizeof(client));
    	client_addr.store = addr->store;
    	client_socket = sd;
    	SAFECOPY(client_name, name);
    	client_socket_dup=INVALID_SOCKET;
    	client_ident[0]=0;
    
    	telnet_location[0]=0;
    	terminal[0]=0;
    	rlogin_name[0]=0;
    	rlogin_pass[0]=0;
    	rlogin_term[0]=0;
    
    	/* Init some important variables */
    
    	input_thread_mutex_created = false;
    #ifdef USE_CRYPTLIB
    	ssh_mode=false;
    	ssh_mutex_created=false;
        passthru_input_thread_running = false;
        passthru_output_thread_running = false;
    #endif
    
    	rio_abortable=false;
    
    	console = 0;
    	online = 0;
    	outchar_esc = 0;
    	nodemsg_inside = 0;	/* allows single nest */
    	hotkey_inside = 0;	/* allows single nest */
    	event_time = 0;
    	event_code = nulstr;
    	nodesync_inside = false;
    	errormsg_inside = false;
    	gettimeleft_inside = false;
    	readmail_inside = false;
    	scanposts_inside = false;
    	scansubs_inside = false;
    	timeleft = 60*10;	/* just incase this is being used for calling gettimeleft() */
    	uselect_total = 0;
    	lbuflen = 0;
    	keybufbot=keybuftop=0;	/* initialize [unget]keybuf pointers */
    	SAFECOPY(connection,"Telnet");
    	node_connection=NODE_CONNECTION_TELNET;
    
    	ZERO_VAR(telnet_local_option);
    	ZERO_VAR(telnet_remote_option);
    
        telnet_cmdlen=0;
    	telnet_mode=0;
    	telnet_last_rxch=0;
    	telnet_ack_event=CreateEvent(NULL, /* Manual Reset: */FALSE,/* InitialState */FALSE,NULL);
    
    	sys_status=lncntr=tos=criterrs=slcnt=0L;
    	column=0;
    	curatr=LIGHTGRAY;
    	attr_sp=0;	/* attribute stack pointer */
    	errorlevel=0;
    	logcol=1;
    	logfile_fp=NULL;
    	nodefile=-1;
    	pthread_mutex_init(&nodefile_mutex, NULL);
    	node_ext=-1;
    	nodefile_fp=NULL;
    	node_ext_fp=NULL;
    	current_msg=NULL;
    	mnestr=NULL;
    
    #ifdef JAVASCRIPT
    	js_runtime=NULL;	/* runtime */
    	js_cx=NULL;			/* context */
    #endif
    
    	for(i=0;i<TOTAL_TEXT;i++)
    		text[i]=text_sav[i]=global_text[i];
    
    	ZERO_VAR(main_csi);
    	ZERO_VAR(thisnode);
    	ZERO_VAR(useron);
    	ZERO_VAR(inbuf);
    	ZERO_VAR(outbuf);
    	ZERO_VAR(smb);
    	ZERO_VAR(nodesync_user);
    
    	action=NODE_MAIN;
    	global_str_vars=0;
    	global_str_var=NULL;
    	global_str_var_name=NULL;
    	global_int_vars=0;
    	global_int_var=NULL;
    	global_int_var_name=NULL;
    	sysvar_li=0;
    	sysvar_pi=0;
    
    	cursub=NULL;
    	cursubnum=INVALID_SUB;
    	usrgrp=NULL;
    	usrsubs=NULL;
    	usrsub=NULL;
    	usrgrp_total=0;
    
    	subscan=NULL;
    
    	curdir=NULL;
    	curdirnum=INVALID_SUB;
    	usrlib=NULL;
    	usrdirs=NULL;
    	usrdir=NULL;
    	usrlib_total=0;
    
    	batup_desc=NULL;
    	batup_name=NULL;
    	batup_misc=NULL;
    	batup_dir=NULL;
    	batup_alt=NULL;
    
    	batdn_name=NULL;
    	batdn_dir=NULL;
    	batdn_offset=NULL;
    	batdn_size=NULL;
    	batdn_alt=NULL;
    	batdn_cdt=NULL;
    
    	/* used by update_qwkroute(): */
    	qwknode=NULL;	
    	total_qwknodes=0;
    
    	spymsg("Connected");
    }
    
    //****************************************************************************
    bool sbbs_t::init()
    {
    	char		str[MAX_PATH+1];
    	char		tmp[128];
    	int			result;
    	uint		i,j,k,l;
    	node_t		node;
    	socklen_t	addr_len;
    	union xp_sockaddr	addr;
    
    	RingBufInit(&inbuf, IO_THREAD_BUF_SIZE);
    	if(cfg.node_num>0)
    		node_inbuf[cfg.node_num-1]=&inbuf;
    
        RingBufInit(&outbuf, IO_THREAD_BUF_SIZE);
    	outbuf.highwater_mark=startup->outbuf_highwater_mark;
    
    	if(cfg.node_num && client_socket!=INVALID_SOCKET) {
    
    #ifdef _WIN32
    		if(!DuplicateHandle(GetCurrentProcess(),
    			(HANDLE)client_socket,
    			GetCurrentProcess(),
    			(HANDLE*)&client_socket_dup,
    			0,
    			TRUE, // Inheritable
    			DUPLICATE_SAME_ACCESS)) {
    			errormsg(WHERE,ERR_CREATE,"duplicate socket handle",client_socket);
    			return(false);
    		}
    		lprintf(LOG_DEBUG,"Node %d socket %u duplicated as %u", cfg.node_num, client_socket, client_socket_dup);
    #else
    		client_socket_dup = client_socket;
    #endif
    
    		addr_len=sizeof(addr);
    		if((result=getsockname(client_socket, &addr.addr, &addr_len))!=0) {
    			lprintf(LOG_ERR,"Node %d !ERROR %d (%d) getting address/port"
    				,cfg.node_num, result, ERROR_VALUE);
    			return(false);
    		} 
    		inet_addrtop(&addr, local_addr, sizeof(local_addr));
    		inet_addrtop(&client_addr, client_ipaddr, sizeof(client_ipaddr));
    		lprintf(LOG_INFO,"Node %d attached to local interface %s port %u"
    			,cfg.node_num, local_addr, inet_addrport(&addr));
    
    	}
    
    	if((comspec=os_cmdshell())==NULL) {
    		errormsg(WHERE, ERR_CHK, OS_CMD_SHELL_ENV_VAR" environment variable", 0);
    		return(false);
    	}
    
    	md(cfg.temp_dir);
    
    	/* Shared NODE files */
    	SAFEPRINTF2(str,"%s%s",cfg.ctrl_dir,"node.dab");
    	pthread_mutex_lock(&nodefile_mutex);
    	if((nodefile=nopen(str,O_DENYNONE|O_RDWR|O_CREAT))==-1) {
    		errormsg(WHERE, ERR_OPEN, str, cfg.node_num);
    		pthread_mutex_unlock(&nodefile_mutex);
    		return(false); 
    	}
    	memset(&node,0,sizeof(node_t));  /* write NULL to node struct */
    	node.status=NODE_OFFLINE;
    	while(filelength(nodefile)<(long)(cfg.sys_nodes*sizeof(node_t))) {
    		lseek(nodefile,0L,SEEK_END);
    		if(write(nodefile,&node,sizeof(node_t))!=sizeof(node_t)) {
    			errormsg(WHERE,ERR_WRITE,str,sizeof(node_t));
    			break; 
    		}
    	}
    	for(i=0; cfg.node_num>0 && i<LOOP_NODEDAB; i++) {
    		if(lock(nodefile,(cfg.node_num-1)*sizeof(node_t),sizeof(node_t))==0) {
    			unlock(nodefile,(cfg.node_num-1)*sizeof(node_t),sizeof(node_t));
    			break;
    		}
    		mswait(100);
    	}
    	if(cfg.node_misc&NM_CLOSENODEDAB) {
    		close(nodefile);
    		nodefile=-1;
    	}
    	pthread_mutex_unlock(&nodefile_mutex);
    
    	if(i>=LOOP_NODEDAB) {
    		errormsg(WHERE, ERR_LOCK, str, cfg.node_num);
    		return(false); 
    	}
    
    	if(cfg.node_num) {
    		SAFEPRINTF(str,"%snode.log",cfg.node_dir);
    		if((logfile_fp=fopen(str,"a+b"))==NULL) {
    			errormsg(WHERE, ERR_OPEN, str, 0);
    			lprintf(LOG_ERR,"Perhaps this node is already running");
    			return(false); 
    		}
    
    		if(filelength(fileno(logfile_fp))) {
    			log(crlf);
    			now=time(NULL);
    			struct tm tm;
    			localtime_r(&now,&tm);
    			safe_snprintf(str,sizeof(str),"%s  %s %s %02d %u  "
    				"End of preexisting log entry (possible crash)"
    				,hhmmtostr(&cfg,&tm,tmp)
    				,wday[tm.tm_wday]
    				,mon[tm.tm_mon],tm.tm_mday,tm.tm_year+1900);
    			logline(LOG_NOTICE,"L!",str);
    			log(crlf);
    			catsyslog(1); 
    		}
    
    		getnodedat(cfg.node_num,&thisnode,1);
    		/* thisnode.status=0; */
    		thisnode.action=0;
    		thisnode.useron=0;
    		thisnode.aux=0;
    		thisnode.misc&=(NODE_EVENT|NODE_LOCK|NODE_RRUN);
    		criterrs=thisnode.errors;
    		putnodedat(cfg.node_num,&thisnode);
    	}
    
    /** Put in if(cfg.node_num) ? (not needed for server and event threads) */
    	backout();
    
    	/* Reset COMMAND SHELL */
    
    	main_csi.str=(char *)malloc(1024);
    	if(main_csi.str==NULL) {
    		errormsg(WHERE,ERR_ALLOC,"main_csi.str",1024);
    		return(false); 
    	}
    	memset(main_csi.str,0,1024);
    /***/
    
    	if(cfg.total_grps) {
    
    		usrgrp_total = cfg.total_grps;
    
    		if((cursub=(uint *)malloc(sizeof(uint)*usrgrp_total))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "cursub", sizeof(uint)*usrgrp_total);
    			return(false);
    		}
    
    		if((usrgrp=(uint *)malloc(sizeof(uint)*usrgrp_total))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "usrgrp", sizeof(uint)*usrgrp_total);
    			return(false);
    		}
    
    		if((usrsubs=(uint *)malloc(sizeof(uint)*usrgrp_total))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "usrsubs", sizeof(uint)*usrgrp_total);
    			return(false);
    		}
    
    		if((usrsub=(uint **)calloc(usrgrp_total,sizeof(uint *)))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "usrsub", sizeof(uint)*usrgrp_total);
    			return(false);
    		}
     
    		if((subscan=(subscan_t *)malloc(sizeof(subscan_t)*cfg.total_subs))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "subscan", sizeof(subscan_t)*cfg.total_subs);
    			return(false);
    		}
    	}
    
    	for(i=l=0;i<(uint)cfg.total_grps;i++) {
    		for(j=k=0;j<cfg.total_subs;j++)
    			if(cfg.sub[j]->grp==i)
    				k++;	/* k = number of subs per grp[i] */
    		if(k>l) l=k;  	/* l = the largest number of subs per grp */
    	}
    	if(l)
    		for(i=0;i<cfg.total_grps;i++)
    			if((usrsub[i]=(uint *)malloc(sizeof(uint)*l))==NULL) {
    				errormsg(WHERE, ERR_ALLOC, "usrsub[x]", sizeof(uint)*l);
    				return(false);
    			}
    
    	if(cfg.total_libs) {
    
    		usrlib_total = cfg.total_libs;
    
    		if((curdir=(uint *)malloc(sizeof(uint)*usrlib_total))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "curdir", sizeof(uint)*usrlib_total);
    			return(false);
    		}
    
    		if((usrlib=(uint *)malloc(sizeof(uint)*usrlib_total))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "usrlib", sizeof(uint)*usrlib_total);
    			return(false);
    		}
    
    		if((usrdirs=(uint *)malloc(sizeof(uint)*usrlib_total))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "usrdirs", sizeof(uint)*usrlib_total);
    			return(false);
    		}
    
    		if((usrdir=(uint **)calloc(usrlib_total,sizeof(uint *)))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "usrdir", sizeof(uint)*usrlib_total);
    			return(false);
    		}
    	}
    
    	for(i=l=0;i<cfg.total_libs;i++) {
    		for(j=k=0;j<cfg.total_dirs;j++)
    			if(cfg.dir[j]->lib==i)
    				k++;
    		if(k>l) l=k; 	/* l = largest number of dirs in a lib */
    	}
    	if(l) {
    		l++;	/* for temp dir */
    		for(i=0;i<cfg.total_libs;i++)
    			if((usrdir[i]=(uint *)malloc(sizeof(uint)*l))==NULL) {
    				errormsg(WHERE, ERR_ALLOC, "usrdir[x]", sizeof(uint)*l);
    				return(false);
    			}
    	}
     
    	if(cfg.max_batup) {
    
    		if((batup_desc=(char **)malloc(sizeof(char *)*cfg.max_batup))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "batup_desc", sizeof(char *)*cfg.max_batup);
    			return(false);
    		}
    		if((batup_name=(char **)malloc(sizeof(char *)*cfg.max_batup))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "batup_name", sizeof(char *)*cfg.max_batup);
    			return(false);
    		}
    		if((batup_misc=(long *)malloc(sizeof(long)*cfg.max_batup))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "batup_misc", sizeof(char *)*cfg.max_batup);
    			return(false);
    		}
    		if((batup_dir=(uint *)malloc(sizeof(uint)*cfg.max_batup))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "batup_dir", sizeof(char *)*cfg.max_batup);
    			return(false);
    		}
    		if((batup_alt=(ushort *)malloc(sizeof(ushort)*cfg.max_batup))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "batup_alt", sizeof(char *)*cfg.max_batup);
    			return(false);
    		}
    		for(i=0;i<cfg.max_batup;i++) {
    			if((batup_desc[i]=(char *)malloc(LEN_FDESC+1))==NULL) {
    				errormsg(WHERE, ERR_ALLOC, "batup_desc[x]", LEN_FDESC+1);
    				return(false);
    			}
    			if((batup_name[i]=(char *)malloc(13))==NULL) {
    				errormsg(WHERE, ERR_ALLOC, "batup_name[x]", 13);
    				return(false);
    			} 
    		} 
    	}
    
    	if(cfg.max_batdn) {
    
    		if((batdn_name=(char **)malloc(sizeof(char *)*cfg.max_batdn))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "batdn_name", sizeof(char *)*cfg.max_batdn);
    			return(false);
    		}
    		if((batdn_dir=(uint *)malloc(sizeof(uint)*cfg.max_batdn))==NULL)  {
    			errormsg(WHERE, ERR_ALLOC, "batdn_dir", sizeof(uint)*cfg.max_batdn);
    			return(false);
    		}
    		if((batdn_offset=(long *)malloc(sizeof(long)*cfg.max_batdn))==NULL)  {
    			errormsg(WHERE, ERR_ALLOC, "batdn_offset", sizeof(long)*cfg.max_batdn);
    			return(false);
    		}
    		if((batdn_size=(ulong *)malloc(sizeof(ulong)*cfg.max_batdn))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "batdn_size", sizeof(ulong)*cfg.max_batdn);
    			return(false);
    		}
    		if((batdn_cdt=(ulong *)malloc(sizeof(ulong)*cfg.max_batdn))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "batdn_cdt", sizeof(long)*cfg.max_batdn);
    			return(false);
    		}
    		if((batdn_alt=(ushort *)malloc(sizeof(ushort)*cfg.max_batdn))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "batdn_alt", sizeof(ushort)*cfg.max_batdn);
    			return(false);
    		}
    		for(i=0;i<cfg.max_batdn;i++)
    			if((batdn_name[i]=(char *)malloc(13))==NULL) {
    				errormsg(WHERE, ERR_ALLOC, "batdn_name[x]", 13);
    				return(false);
    			} 
    	}
    
    #ifdef USE_CRYPTLIB
    	pthread_mutex_init(&ssh_mutex,NULL);
    	ssh_mutex_created = true;
    #endif
    	pthread_mutex_init(&input_thread_mutex,NULL);
    	input_thread_mutex_created = true;
    
    	reset_logon_vars();
    
    	online=ON_REMOTE;
    
    	return(true);
    }
    
    //****************************************************************************
    sbbs_t::~sbbs_t()
    {
    	uint i;
    	char node[32];
    
        if(cfg.node_num)
        	SAFEPRINTF(node,"Node %d", cfg.node_num);
        else
        	SAFECOPY(node,client_name);
    #ifdef _DEBUG
    	lprintf(LOG_DEBUG,"%s destructor begin", node);
    #endif
    
    //	if(!cfg.node_num)
    //		rmdir(cfg.temp_dir);
    
    	if(client_socket_dup!=INVALID_SOCKET && client_socket_dup!=client_socket)
    		closesocket(client_socket_dup);	/* close duplicate handle */
    
    	if(cfg.node_num>0)
    		node_inbuf[cfg.node_num-1]=NULL;
    	if(!input_thread_running)
    		RingBufDispose(&inbuf);
    	if(!output_thread_running)
    		RingBufDispose(&outbuf);
    
    	if(telnet_ack_event!=NULL)
    		CloseEvent(telnet_ack_event);
    
    	/* Close all open files */
    	pthread_mutex_lock(&nodefile_mutex);
    	if(nodefile!=-1) {
    		close(nodefile);
    		nodefile=-1;
    		pthread_mutex_unlock(&nodefile_mutex);
    	}
    	if(node_ext!=-1) {
    		close(node_ext);
    		node_ext=-1;
    	}
    	if(logfile_fp!=NULL) {
    		fclose(logfile_fp);
    		logfile_fp=NULL;
    	}
    
    	/********************************/
    	/* Free allocated class members */
    	/********************************/
    
    	js_cleanup(node);
    
    	/* Reset text.dat */
    
    	for(i=0;i<TOTAL_TEXT;i++)
    		if(text[i]!=text_sav[i]) {
    			if(text[i]!=nulstr)
    				free(text[i]); 
    		}
    
    	/* Global command shell vars */
    
    	freevars(&main_csi);
    	clearvars(&main_csi);
    	FREE_AND_NULL(main_csi.str);	/* crash */
    	FREE_AND_NULL(main_csi.cs);
    
    	for(i=0;i<global_str_vars && global_str_var!=NULL;i++)
    		FREE_AND_NULL(global_str_var[i]);
    
    	FREE_AND_NULL(global_str_var);
    	FREE_AND_NULL(global_str_var_name);
    	global_str_vars=0;
    
    	FREE_AND_NULL(global_int_var);
    	FREE_AND_NULL(global_int_var_name);
    	global_int_vars=0;
    
    	/* Sub-board variables */
    	for(i=0;i<usrgrp_total && usrsub!=NULL;i++)
    		FREE_AND_NULL(usrsub[i]);	/* exception here (ptr=0xfdfdfdfd) on exit July-10-2002 */
    
    	FREE_AND_NULL(cursub);
    	FREE_AND_NULL(usrgrp);
    	FREE_AND_NULL(usrsubs);
    	FREE_AND_NULL(usrsub);
    	FREE_AND_NULL(subscan);
    
    	/* File Directory variables */
    	for(i=0;i<usrlib_total && usrdir!=NULL;i++)
    		FREE_AND_NULL(usrdir[i]);
    
    	FREE_AND_NULL(curdir);
    	FREE_AND_NULL(usrlib);
    	FREE_AND_NULL(usrdirs);
    	FREE_AND_NULL(usrdir);
    
    	/* Batch upload vars */
    	for(i=0;i<cfg.max_batup && batup_desc!=NULL && batup_name!=NULL;i++) {
    		FREE_AND_NULL(batup_desc[i]);
    		FREE_AND_NULL(batup_name[i]);
    	}
    
    	FREE_AND_NULL(batup_desc);
    	FREE_AND_NULL(batup_name);
    	FREE_AND_NULL(batup_misc);
    	FREE_AND_NULL(batup_dir);
    	FREE_AND_NULL(batup_alt);
    
    	/* Batch download vars */
    	for(i=0;i<cfg.max_batdn && batdn_name!=NULL;i++)
    		FREE_AND_NULL(batdn_name[i]); 
    
    	FREE_AND_NULL(batdn_name);
    	FREE_AND_NULL(batdn_dir);
    	FREE_AND_NULL(batdn_offset);
    	FREE_AND_NULL(batdn_size);
    	FREE_AND_NULL(batdn_cdt);
    	FREE_AND_NULL(batdn_alt);
    
    #ifdef USE_CRYPTLIB
    	while(ssh_mutex_created && pthread_mutex_destroy(&ssh_mutex)==EBUSY)
    		mswait(1);
    #endif
    	while(input_thread_mutex_created && pthread_mutex_destroy(&input_thread_mutex)==EBUSY)
    		mswait(1);
    
    #if 0 && defined(_WIN32) && defined(_DEBUG) && defined(_MSC_VER)
    	if(!_CrtCheckMemory())
    		lprintf(LOG_ERR,"!MEMORY ERRORS REPORTED IN DATA/DEBUG.LOG!");
    #endif
    
    #ifdef _DEBUG
    	lprintf(LOG_DEBUG,"%s destructor end", node);
    #endif
    }
    
    /****************************************************************************/
    /* Network open function. Opens all files DENYALL and retries LOOP_NOPEN    */
    /* number of times if the attempted file is already open or denying access  */
    /* for some other reason.													*/
    /* All files are opened in BINARY mode, unless O_TEXT access bit is set.	*/
    /****************************************************************************/
    int sbbs_t::nopen(char *str, int access)
    {
    	char logstr[256];
    	int file,share,count=0;
    
        if(access&O_DENYNONE) {
            share=SH_DENYNO;
            access&=~O_DENYNONE; 
    	}
        else if(access==O_RDONLY) share=SH_DENYWR;
        else share=SH_DENYRW;
    	if(!(access&O_TEXT))
    		access|=O_BINARY;
        while(((file=sopen(str,access,share,DEFFILEMODE))==-1)
            && (errno==EACCES || errno==EAGAIN) && count++<LOOP_NOPEN)
    	    mswait(100);
        if(count>(LOOP_NOPEN/2) && count<=LOOP_NOPEN) {
            SAFEPRINTF2(logstr,"NOPEN COLLISION - File: \"%s\" Count: %d"
                ,str,count);
            logline(LOG_WARNING,"!!",logstr); 
    	}
        if(file==-1 && (errno==EACCES || errno==EAGAIN)) {
            SAFEPRINTF2(logstr,"NOPEN ACCESS DENIED - File: \"%s\" errno: %d"
    			,str,errno);
    		logline(LOG_WARNING,"!!",logstr);
    		bputs("\7\r\nNOPEN: ACCESS DENIED\r\n\7");
    	}
        return(file);
    }
    
    void sbbs_t::spymsg(const char* msg)
    {
    	char str[512];
    
    	if(cfg.node_num<1)
    		return;
    
    	SAFEPRINTF4(str,"\r\n\r\n*** Spy Message ***\r\nNode %d: %s [%s]\r\n*** %s ***\r\n\r\n"
    		,cfg.node_num,client_name,client_ipaddr,msg);
    	if(startup->node_spybuf!=NULL 
    		&& startup->node_spybuf[cfg.node_num-1]!=NULL) {
    		RingBufWrite(startup->node_spybuf[cfg.node_num-1],(uchar*)str,strlen(str));
    		/* Signal spy output semaphore? */
    		if(startup->node_spysem!=NULL 
    			&& startup->node_spysem[sbbs->cfg.node_num-1]!=NULL)
    			sem_post(startup->node_spysem[sbbs->cfg.node_num-1]);
    	}
    
    	if(cfg.node_num && spy_socket[cfg.node_num-1]!=INVALID_SOCKET) 
    		sendsocket(spy_socket[cfg.node_num-1],str,strlen(str));
    #ifdef __unix__
    	if(cfg.node_num && uspy_socket[cfg.node_num-1]!=INVALID_SOCKET) 
    		sendsocket(uspy_socket[cfg.node_num-1],str,strlen(str));
    #endif
    }
    
    #define MV_BUFLEN	4096
    
    /****************************************************************************/
    /* Moves or copies a file from one dir to another                           */
    /* both 'src' and 'dest' must contain full path and filename                */
    /* returns 0 if successful, -1 if error                                     */
    /****************************************************************************/
    int sbbs_t::mv(char *src, char *dest, char copy)
    {
    	char	str[MAX_PATH+1],*buf,atr=curatr;
    	int		ind,outd;
    	uint	chunk=MV_BUFLEN;
    	ulong	length,l;
    	time_t	ftime;
    	FILE *inp,*outp;
    
        if(!stricmp(src,dest))	 /* source and destination are the same! */
            return(0);
        if(!fexistcase(src)) {
            bprintf("\r\n\7MV ERROR: Source doesn't exist\r\n'%s'\r\n"
                ,src);
            return(-1); 
    	}
        if(!copy && fexistcase(dest)) {
            bprintf("\r\n\7MV ERROR: Destination already exists\r\n'%s'\r\n"
                ,dest);
            return(-1); 
    	}
    #ifndef __unix__	/* need to determine if on same mount device */
        if(!copy && ((src[1]!=':' && dest[1]!=':')
            || (src[1]==':' && dest[1]==':' && toupper(src[0])==toupper(dest[0])))) {
            if(rename(src,dest)) {						/* same drive, so move */
                bprintf("\r\nMV ERROR: Error renaming '%s'"
                        "\r\n                      to '%s'\r\n\7",src,dest);
                return(-1); 
    		}
            return(0); 
    	}
    #endif
        attr(WHITE);
        if((ind=nopen(src,O_RDONLY))==-1) {
            errormsg(WHERE,ERR_OPEN,src,O_RDONLY);
            return(-1); 
    	}
        if((inp=fdopen(ind,"rb"))==NULL) {
            close(ind);
            errormsg(WHERE,ERR_FDOPEN,str,O_RDONLY);
            return(-1); 
    	}
        setvbuf(inp,NULL,_IOFBF,32*1024);
        if((outd=nopen(dest,O_WRONLY|O_CREAT|O_TRUNC))==-1) {
            fclose(inp);
            errormsg(WHERE,ERR_OPEN,dest,O_WRONLY|O_CREAT|O_TRUNC);
            return(-1); 
    	}
        if((outp=fdopen(outd,"wb"))==NULL) {
            close(outd);
            fclose(inp);
            errormsg(WHERE,ERR_FDOPEN,dest,O_WRONLY|O_CREAT|O_TRUNC);
            return(-1); 
    	}
        setvbuf(outp,NULL,_IOFBF,8*1024);
    	ftime=filetime(ind);
        length=(long)filelength(ind);
        if(length) {	/* Something to copy */
    		if((buf=(char *)malloc(MV_BUFLEN))==NULL) {
    			fclose(inp);
    			fclose(outp);
    			errormsg(WHERE,ERR_ALLOC,nulstr,MV_BUFLEN);
    			return(-1); 
    		}
    		l=0L;
    		while(l<length) {
    			bprintf("%2lu%%",l ? (long)(100.0/((float)length/l)) : 0L);
    			if(l+chunk>length)
    				chunk=length-l;
    			if(fread(buf,1,chunk,inp)!=chunk) {
    				free(buf);
    				fclose(inp);
    				fclose(outp);
    				errormsg(WHERE,ERR_READ,src,chunk);
    				return(-1); 
    			}
    			if(fwrite(buf,1,chunk,outp)!=chunk) {
    				free(buf);
    				fclose(inp);
    				fclose(outp);
    				errormsg(WHERE,ERR_WRITE,dest,chunk);
    				return(-1); 
    			}
    			l+=chunk;
    			bputs("\b\b\b"); 
    		}
    		bputs("   \b\b\b");  /* erase it */
    		attr(atr);
    		free(buf);
    	}
        fclose(inp);
        fclose(outp);
    	setfdate(dest,ftime);	/* Would be nice if we could use futime() instead */
        if(!copy && remove(src)) {
            errormsg(WHERE,ERR_REMOVE,src,0);
            return(-1); 
    	}
        return(0);
    }
    
    void sbbs_t::hangup(void)
    {
    	if(online) {
    		lprintf(LOG_DEBUG,"Node %d disconnecting client", cfg.node_num);
    		online=FALSE;	// moved from the bottom of this function on Jan-25-2009
    	}
    	if(client_socket_dup!=INVALID_SOCKET && client_socket_dup!=client_socket)
    		closesocket(client_socket_dup);
    	client_socket_dup=INVALID_SOCKET;
    
    	if(client_socket!=INVALID_SOCKET) {
    		mswait(1000);	/* Give socket output buffer time to flush */
    		client_off(client_socket);
    		if(ssh_mode) {
    			pthread_mutex_lock(&sbbs->ssh_mutex);
    			cryptDestroySession(sbbs->ssh_session);
    			pthread_mutex_unlock(&sbbs->ssh_mutex);
    		}
    		close_socket(client_socket);
    		client_socket=INVALID_SOCKET;
    	}
    	sem_post(&outbuf.sem);
    }
    
    int sbbs_t::incom(unsigned long timeout)
    {
    	uchar	ch;
    
    #if 0	/* looping version */
    	while(!RingBufRead(&inbuf, &ch, 1))
    		if(sem_trywait_block(&inbuf.sem,timeout)!=0 || sys_status&SS_ABORT)
    			return(NOINP);
    #else
    	if(!RingBufRead(&inbuf, &ch, 1)) {
    		if(sem_trywait_block(&inbuf.sem,timeout)!=0)
    			return(NOINP);
    		if(!RingBufRead(&inbuf, &ch, 1))
    			return(NOINP);
    	}
    #endif
    	return(ch);
    }
    
    int sbbs_t::outcom(uchar ch)
    {
    	if(!RingBufFree(&outbuf))
    		return(TXBOF);
        if(!RingBufWrite(&outbuf, &ch, 1))
    		return(TXBOF);
    	return(0);
    }
    
    int sbbs_t::putcom(const char *str, size_t len)
    {
    	size_t i;
    
    	if(!len)
    		len=strlen(str);
        for(i=0;i<len && online;i++)
            if(outcom(str[i])!=0)
    			break;
    	return i;
    }
    
    /* Legacy Remote I/O Control Interface:
     * This function mimics the RCIOL MS-DOS library written in 8086 assembler by Steven B. Deppe (1958-2014).
     * This function prototype shall remain the same in tribute to Steve (Ille Homine Albe).
     */
    int sbbs_t::rioctl(ushort action)
    {
    	int		mode;
    	int		state;
    
    	switch(action) {
    		case GVERS: 	/* Get version */
    			return(0x200);
    		case GUART: 	/* Get UART I/O address, not available */
    			return(0xffff);
    		case GIRQN: 	/* Get IRQ number, not available */
    			return((int)client_socket);
    		case GBAUD: 	/* Get current bit rate */
    			return(0xffff);
    		case RXBC:		/* Get receive buffer count */
    			// ulong	cnt;
    			// ioctlsocket (client_socket,FIONREAD,&cnt);
     			return(/* cnt+ */RingBufFull(&inbuf));
    		case RXBS:		/* Get receive buffer size */
    			return(inbuf.size);
    		case TXBC:		/* Get transmit buffer count */
    			return(RingBufFull(&outbuf));
    		case TXBS:		/* Get transmit buffer size */
    			return(outbuf.size);
    		case TXBF:		/* Get transmit buffer free space */
     			return(RingBufFree(&outbuf));
    		case IOMODE:
    			mode=0;
    			if(rio_abortable)
    				mode|=ABORT;
    			return(mode);
    		case IOSTATE:
    			state=0;
    			if(sys_status&SS_ABORT)
    				state|=ABORT;
    			return(state);
    		case IOFI:		/* Flush input buffer */
    			RingBufReInit(&inbuf);
    			break;
    		case IOFO:		/* Flush output buffer */
        		RingBufReInit(&outbuf);
    			break;
    		case IOFB:		/* Flush both buffers */
    			RingBufReInit(&inbuf);
    			RingBufReInit(&outbuf);
    			break;
    		case LFN81:
    		case LFE71:
    		case FIFOCTL:
    			return(0);
    		}
    
    	if((action&0xff)==IOSM) {	/* Get/Set/Clear mode */
    		if(action&ABORT)
    			rio_abortable=true;
    		return(0); 
    	}
    
    	if((action&0xff)==IOCM) {	/* Get/Set/Clear mode */
    		if(action&ABORT)
    			rio_abortable=false;
    		return(0); 
    	}
    
    	if((action&0xff)==IOSS) {	/* Set state */
    		if(action&ABORT)
    			sys_status|=SS_ABORT;
    		return(0); 
    	}
    
    	if((action&0xff)==IOCS) {	/* Clear state */
    		if(action&ABORT)
    			sys_status&=~SS_ABORT;
    		return(0); 
    	}
    
    	return(0);
    }
    
    void sbbs_t::reset_logon_vars(void)
    {
    	int i;
    
        /* bools */
        qwklogon=false;
    
        sys_status&=~(SS_USERON|SS_TMPSYSOP|SS_LCHAT|SS_ABORT
            |SS_PAUSEON|SS_PAUSEOFF|SS_EVENT|SS_NEWUSER|SS_NEWDAY);
        cid[0]=0;
        wordwrap[0]=0;
        question[0]=0;
        menu_dir[0]=0;
        menu_file[0]=0;
        rows=24;
    	cols=80;
        lncntr=0;
        autoterm=0;
        lbuflen=0;
        slcnt=0;
        altul=0;
        timeleft_warn=0;
    	keybufbot=keybuftop=0;
        logon_uls=logon_ulb=logon_dls=logon_dlb=0;
        logon_posts=logon_emails=logon_fbacks=0;
        batdn_total=batup_total=0;
        usrgrps=usrlibs=0;
        curgrp=curlib=0;
        for(i=0;i<cfg.total_libs;i++)
            curdir[i]=0;
        for(i=0;i<cfg.total_grps;i++)
            cursub[i]=0;
    	cur_cps=3000;
        cur_rate=30000;
        dte_rate=38400;
    	main_cmds=xfer_cmds=posts_read=0;
    	lastnodemsg=0;
    	lastnodemsguser[0]=0;
    }
    
    /****************************************************************************/
    /* Writes NODE.LOG at end of SYSTEM.LOG										*/
    /****************************************************************************/
    void sbbs_t::catsyslog(int crash)
    {
    	char str[MAX_PATH+1];
    	char *buf;
    	int  i,file;
    	long length;
    	struct tm tm;
    
    	if(logfile_fp==NULL) {
    		SAFEPRINTF(str,"%snode.log",cfg.node_dir);
    		if((logfile_fp=fopen(str,"rb"))==NULL) {
    			errormsg(WHERE,ERR_OPEN,str,O_RDONLY);
    			return; 
    		}
    	}
    	length=(long)ftell(logfile_fp);
    	if(length) {
    		if((buf=(char *)malloc(length))==NULL) {
    			errormsg(WHERE,ERR_ALLOC,str,length);
    			return; 
    		}
    		rewind(logfile_fp);
    		if(fread(buf,1,length,logfile_fp)!=(size_t)length) {
    			errormsg(WHERE,ERR_READ,"log file",length);
    			free((char *)buf);
    			return; 
    		}
    		now=time(NULL);
    		localtime_r(&now,&tm);
    		SAFEPRINTF4(str,"%slogs/%2.2d%2.2d%2.2d.log",cfg.logs_dir,tm.tm_mon+1,tm.tm_mday
    			,TM_YEAR(tm.tm_year));
    		if((file=nopen(str,O_WRONLY|O_APPEND|O_CREAT))==-1) {
    			errormsg(WHERE,ERR_OPEN,str,O_WRONLY|O_APPEND|O_CREAT);
    			free((char *)buf);
    			return; 
    		}
    		if(lwrite(file,buf,length)!=length) {
    			close(file);
    			errormsg(WHERE,ERR_WRITE,str,length);
    			free((char *)buf);
    			return; 
    		}
    		close(file);
    		if(crash) {
    			for(i=0;i<2;i++) {
    				SAFEPRINTF(str,"%scrash.log",i ? cfg.logs_dir : cfg.node_dir);
    				if((file=nopen(str,O_WRONLY|O_APPEND|O_CREAT))==-1) {
    					errormsg(WHERE,ERR_OPEN,str,O_WRONLY|O_APPEND|O_CREAT);
    					free((char *)buf);
    					return; 
    				}
    				if(lwrite(file,buf,length)!=length) {
    					close(file);
    					errormsg(WHERE,ERR_WRITE,str,length);
    					free((char *)buf);
    					return; 
    				}
    				close(file); 
    			} 
    		}
    		free((char *)buf); 
    	}
    
    	fclose(logfile_fp);
    
    	SAFEPRINTF(str,"%snode.log",cfg.node_dir);
    	if((logfile_fp=fopen(str,"w+b"))==NULL) /* Truncate NODE.LOG */
    		errormsg(WHERE,ERR_OPEN,str,O_WRONLY|O_TRUNC);
    }
    
    
    void sbbs_t::logoffstats()
    {
        char str[MAX_PATH+1];
        int i,file;
        stats_t stats;
    
    	if(REALSYSOP && !(cfg.sys_misc&SM_SYSSTAT))
    		return;
    	
    	for(i=0;i<2;i++) {
    		SAFEPRINTF(str,"%sdsts.dab",i ? cfg.ctrl_dir : cfg.node_dir);
    		if((file=nopen(str,O_RDWR))==-1) {
    			errormsg(WHERE,ERR_OPEN,str,O_RDWR);
    			return; 
    		}
    		memset(&stats,0,sizeof(stats));
    		lseek(file,4L,SEEK_SET);   /* Skip timestamp, logons and logons today */
    		read(file,&stats,sizeof(stats));  
    
    		if(!(useron.rest&FLAG('Q'))) {	/* Don't count QWKnet nodes */
    			stats.timeon+=(uint32_t)(now-logontime)/60;
    			stats.ttoday+=(uint32_t)(now-logontime)/60;
    			stats.ptoday+=logon_posts;
    		}
    		stats.uls+=logon_uls;
    		stats.ulb+=logon_ulb;
    		stats.dls+=logon_dls;
    		stats.dlb+=logon_dlb;
    		stats.etoday+=logon_emails;
    		stats.ftoday+=logon_fbacks;
    
    #if 0 // This is now handled in newuserdat()
    		if(sys_status&SS_NEWUSER)
    			stats.nusers++;
    #endif
    
    		lseek(file,4L,SEEK_SET);
    		write(file,&stats,sizeof(stats));
    		close(file); 
    	}
    }
    
    void node_thread(void* arg)
    {
    	ulong			stack_frame;
    	char			str[128];
    	int				file;
    	uint			curshell=0;
    	node_t			node;
    	ulong			login_attempts;
    	sbbs_t*			sbbs = (sbbs_t*) arg;
    
    	update_clients();
    	SetThreadName("sbbs/termNode");
    	thread_up(TRUE /* setuid */);
    
    #ifdef _DEBUG
    	lprintf(LOG_DEBUG,"Node %d thread started",sbbs->cfg.node_num);
    #endif
    
    	sbbs_srand();		/* Seed random number generator */
    
    #ifdef JAVASCRIPT
    	if(!(startup->options&BBS_OPT_NO_JAVASCRIPT)) {
    		if(!sbbs->js_init(&stack_frame)) /* This must be done in the context of the node thread */
    			lprintf(LOG_ERR,"Node %d !JavaScript Initialization FAILURE",sbbs->cfg.node_num);
    	}
    #endif
    
    	if(startup->login_attempt.throttle
    		&& (login_attempts=loginAttempts(startup->login_attempt_list, &sbbs->client_addr)) > 1) {
    		lprintf(LOG_DEBUG,"Node %d Throttling suspicious connection from: %s (%u login attempts)"
    			,sbbs->cfg.node_num, sbbs->client_ipaddr, login_attempts);
    		mswait(login_attempts*startup->login_attempt.throttle);
    	}
    
    	if(sbbs->answer()) {
    
    		if(sbbs->qwklogon) {
    			sbbs->getsmsg(sbbs->useron.number);
    			sbbs->qwk_sec();
    		} else while(sbbs->useron.number 
    			&& (sbbs->main_csi.misc&CS_OFFLINE_EXEC || sbbs->online)) {
    
    			if(!sbbs->main_csi.cs || curshell!=sbbs->useron.shell) {
    				if(sbbs->useron.shell>=sbbs->cfg.total_shells)
    					sbbs->useron.shell=0;
    				SAFEPRINTF2(str,"%s%s.bin",sbbs->cfg.mods_dir
    					,sbbs->cfg.shell[sbbs->useron.shell]->code);
    				if(sbbs->cfg.mods_dir[0]==0 || !fexistcase(str))
    					SAFEPRINTF2(str,"%s%s.bin",sbbs->cfg.exec_dir
    						,sbbs->cfg.shell[sbbs->useron.shell]->code);
    				if((file=sbbs->nopen(str,O_RDONLY))==-1) {
    					sbbs->errormsg(WHERE,ERR_OPEN,str,O_RDONLY);
    					sbbs->hangup();
    					break; 
    				}
    				FREE_AND_NULL(sbbs->main_csi.cs);
    				sbbs->freevars(&sbbs->main_csi);
    				sbbs->clearvars(&sbbs->main_csi);
    
    				sbbs->main_csi.length=(long)filelength(file);
    				if((sbbs->main_csi.cs=(uchar *)malloc(sbbs->main_csi.length))==NULL) {
    					close(file);
    					sbbs->errormsg(WHERE,ERR_ALLOC,str,sbbs->main_csi.length);
    					sbbs->hangup();
    					break; 
    				}
    
    				if(lread(file,sbbs->main_csi.cs,sbbs->main_csi.length)
    					!=(int)sbbs->main_csi.length) {
    					sbbs->errormsg(WHERE,ERR_READ,str,sbbs->main_csi.length);
    					close(file);
    					free(sbbs->main_csi.cs);
    					sbbs->main_csi.cs=NULL;
    					sbbs->hangup();
    					break; 
    				}
    				close(file);
    
    				curshell=sbbs->useron.shell;
    				sbbs->main_csi.ip=sbbs->main_csi.cs;
    				sbbs->menu_dir[0]=0;
    				sbbs->menu_file[0]=0;
    				}
    			if(sbbs->exec(&sbbs->main_csi))
    				break;
    		}
    	}
    
    #ifdef _WIN32
    	if(startup->hangup_sound[0] && !(startup->options&BBS_OPT_MUTE)) 
    		PlaySound(startup->hangup_sound, NULL, SND_ASYNC|SND_FILENAME);
    #endif
    
    	sbbs->hangup();	/* closes sockets, calls client_off, and shuts down the output_thread */
        node_socket[sbbs->cfg.node_num-1]=INVALID_SOCKET;
    
    	sbbs->logout();
    	sbbs->logoffstats();	/* Updates both system and node dsts.dab files */
    
    	if(sbbs->sys_status&SS_DAILY) {	// New day, run daily events/maintenance
    		sbbs->daily_maint();
    	}
    
    #if 0	/* this is handled in the event_thread now */
    	// Node Daily Event
    	sbbs->getnodedat(sbbs->cfg.node_num,&node,0);
    	if(node.misc&NODE_EVENT) {
    		sbbs->getnodedat(sbbs->cfg.node_num,&node,1);
    		node.status=NODE_EVENT_RUNNING;
    		sbbs->putnodedat(sbbs->cfg.node_num,&node);
    		if(sbbs->cfg.node_daily[0]) {
    			sbbs->logentry("!:","Run node daily event");
    			sbbs->external(
    				 sbbs->cmdstr(sbbs->cfg.node_daily,nulstr,nulstr,NULL)
    				,EX_OFFLINE);
    		}
    		sbbs->getnodedat(sbbs->cfg.node_num,&node,1);
    		node.misc&=~NODE_EVENT;
    		sbbs->putnodedat(sbbs->cfg.node_num,&node); 
    	}
    #endif
    
        // Wait for all node threads to terminate
    	if(sbbs->input_thread_running || sbbs->output_thread_running
    #ifdef USE_CRYPTLIB
    		|| sbbs->passthru_input_thread_running || sbbs->passthru_output_thread_running
    #endif
    		) {
    		lprintf(LOG_DEBUG,"Node %d Waiting for %s to terminate..."
    			,sbbs->cfg.node_num
    			,(sbbs->input_thread_running && sbbs->output_thread_running) ?
                   	"I/O threads" : sbbs->input_thread_running
    				? "input thread" : "output thread");
    		time_t start=time(NULL);
    		while(sbbs->input_thread_running
        		|| sbbs->output_thread_running
    #ifdef USE_CRYPTLIB
    			|| sbbs->passthru_input_thread_running || sbbs->passthru_output_thread_running
    #endif
    			) {
    			if(time(NULL)-start>TIMEOUT_THREAD_WAIT) {
    				lprintf(LOG_NOTICE,"Node %d !TIMEOUT waiting for %s to terminate"
    					, sbbs->cfg.node_num
    					,(sbbs->input_thread_running && sbbs->output_thread_running) ?
                      		"I/O threads"
    					: sbbs->input_thread_running
                    		? "input thread" : "output thread");
    				break;
    			}
    			mswait(100);
    		}
    	}
    
    	sbbs->catsyslog(0);
    
    	status(STATUS_WFC);
    
    	sbbs->getnodedat(sbbs->cfg.node_num,&node,1);
    	if(node.misc&NODE_DOWN)
    		node.status=NODE_OFFLINE;
    	else
    		node.status=NODE_WFC;
    	node.misc&=~(NODE_DOWN|NODE_INTR|NODE_MSGW|NODE_NMSG
    				|NODE_UDAT|NODE_POFF|NODE_AOFF|NODE_EXT);
    /*	node.useron=0; needed for hang-ups while in multinode chat */
    	sbbs->putnodedat(sbbs->cfg.node_num,&node);
    
    	{
    		/* crash here on Aug-4-2015:
    		node_thread_running already destroyed
    		bbs_thread() timed out waiting for 1 node thread(s) to terminate */
    		int32_t remain = protected_uint32_adjust(&node_threads_running, -1);
    		lprintf(LOG_INFO,"Node %d thread terminated (%u node threads remain, %lu clients served)"
    			,sbbs->cfg.node_num, remain, served);
    	}
        if(!sbbs->input_thread_running && !sbbs->output_thread_running)
    		delete sbbs;
        else
    		lprintf(LOG_WARNING,"Node %d !ORPHANED I/O THREAD(s)",sbbs->cfg.node_num);
    
    	update_clients();
    	thread_down();
    }
    
    void sbbs_t::daily_maint(void)
    {
    	char			str[128];
    	char			uname[LEN_ALIAS+1];
    	uint			i;
    	uint			usernum;
    	uint			lastusernum;
    	user_t			user;
    
    	now=time(NULL);
    
    	if(sbbs->cfg.node_num) {
    		if((i=sbbs->getnodedat(sbbs->cfg.node_num,&sbbs->thisnode,true)) != 0)
    			sbbs->errormsg(WHERE,ERR_LOCK,"node file",i);
    		else {
    			sbbs->thisnode.status=NODE_EVENT_RUNNING;
    			sbbs->putnodedat(sbbs->cfg.node_num,&sbbs->thisnode);
    		}
    	}
    	sbbs->logentry("!:","Ran system daily maintenance");
    
    	if(sbbs->cfg.user_backup_level) {
    		lputs(LOG_INFO,"Backing-up user data...");
    		SAFEPRINTF(str,"%suser/user.dat",sbbs->cfg.data_dir);
    		backup(str,sbbs->cfg.user_backup_level,FALSE);
    		SAFEPRINTF(str,"%suser/name.dat",sbbs->cfg.data_dir);
    		backup(str,sbbs->cfg.user_backup_level,FALSE);
    	}
    
    	if(sbbs->cfg.mail_backup_level) {
    		lputs(LOG_INFO,"Backing-up mail data...");
    		SAFEPRINTF(str,"%smail.shd",sbbs->cfg.data_dir);
    		backup(str,sbbs->cfg.mail_backup_level,FALSE);
    		SAFEPRINTF(str,"%smail.sha",sbbs->cfg.data_dir);
    		backup(str,sbbs->cfg.mail_backup_level,FALSE);
    		SAFEPRINTF(str,"%smail.sdt",sbbs->cfg.data_dir);
    		backup(str,sbbs->cfg.mail_backup_level,FALSE);
    		SAFEPRINTF(str,"%smail.sda",sbbs->cfg.data_dir);
    		backup(str,sbbs->cfg.mail_backup_level,FALSE);
    		SAFEPRINTF(str,"%smail.sid",sbbs->cfg.data_dir);
    		backup(str,sbbs->cfg.mail_backup_level,FALSE);
    		SAFEPRINTF(str,"%smail.sch",sbbs->cfg.data_dir);
    		backup(str,sbbs->cfg.mail_backup_level,FALSE);
    	}
    
    	lputs(LOG_INFO,status("Checking for inactive/expired user records..."));
    	lastusernum=lastuser(&sbbs->cfg);
    	int userfile=openuserdat(&sbbs->cfg, /* for_modify: */FALSE);
    	for(usernum=1;usernum<=lastusernum;usernum++) {
    		SAFEPRINTF2(str,"%5u of %-5u",usernum,lastusernum);
    		status(str);
    		user.number = usernum;
    		if((i=fgetuserdat(&sbbs->cfg, &user, userfile)) != 0) {
    			SAFEPRINTF(str,"user record %u",usernum);
    			sbbs->errormsg(WHERE, ERR_READ, str, i);
    			continue;
    		}
    
    		/***********************************************/
    		/* Fix name (name.dat and user.dat) mismatches */
    		/***********************************************/
    		username(&sbbs->cfg,user.number,uname);
    		if(user.misc&DELETED) {
    			if(strcmp(uname,"DELETED USER"))
    				putusername(&sbbs->cfg,user.number,nulstr);
    			continue; 
    		}
    
    		if(strcmp(user.alias,uname))
    			putusername(&sbbs->cfg,user.number,user.alias);
    
    		if(user.number==1)
    			continue;	/* skip expiration/inactivity checks for user #1 */
    
    		if(!(user.misc&(DELETED|INACTIVE))
    			&& user.expire && (ulong)user.expire<=(ulong)now) {
    			putsmsg(&sbbs->cfg,user.number,sbbs->text[AccountHasExpired]);
    			SAFEPRINTF2(str,"%s #%u Expired",user.alias,user.number);
    			sbbs->logentry("!%",str);
    			if(sbbs->cfg.level_misc[user.level]&LEVEL_EXPTOVAL
    				&& sbbs->cfg.level_expireto[user.level]<10) {
    				user.flags1=sbbs->cfg.val_flags1[sbbs->cfg.level_expireto[user.level]];
    				user.flags2=sbbs->cfg.val_flags2[sbbs->cfg.level_expireto[user.level]];
    				user.flags3=sbbs->cfg.val_flags3[sbbs->cfg.level_expireto[user.level]];
    				user.flags4=sbbs->cfg.val_flags4[sbbs->cfg.level_expireto[user.level]];
    				user.exempt=sbbs->cfg.val_exempt[sbbs->cfg.level_expireto[user.level]];
    				user.rest=sbbs->cfg.val_rest[sbbs->cfg.level_expireto[user.level]];
    				if(sbbs->cfg.val_expire[sbbs->cfg.level_expireto[user.level]])
    					user.expire=(time32_t)now
    						+(sbbs->cfg.val_expire[sbbs->cfg.level_expireto[user.level]]*24*60*60);
    				else
    					user.expire=0;
    				user.level=sbbs->cfg.val_level[sbbs->cfg.level_expireto[user.level]]; 
    			}
    			else {
    				if(sbbs->cfg.level_misc[user.level]&LEVEL_EXPTOLVL)
    					user.level=sbbs->cfg.level_expireto[user.level];
    				else
    					user.level=sbbs->cfg.expired_level;
    				user.flags1&=~sbbs->cfg.expired_flags1; /* expired status */
    				user.flags2&=~sbbs->cfg.expired_flags2; /* expired status */
    				user.flags3&=~sbbs->cfg.expired_flags3; /* expired status */
    				user.flags4&=~sbbs->cfg.expired_flags4; /* expired status */
    				user.exempt&=~sbbs->cfg.expired_exempt;
    				user.rest|=sbbs->cfg.expired_rest;
    				user.expire=0; 
    			}
    			putuserrec(&sbbs->cfg,user.number,U_LEVEL,2,ultoa(user.level,str,10));
    			putuserrec(&sbbs->cfg,user.number,U_FLAGS1,8,ultoa(user.flags1,str,16));
    			putuserrec(&sbbs->cfg,user.number,U_FLAGS2,8,ultoa(user.flags2,str,16));
    			putuserrec(&sbbs->cfg,user.number,U_FLAGS3,8,ultoa(user.flags3,str,16));
    			putuserrec(&sbbs->cfg,user.number,U_FLAGS4,8,ultoa(user.flags4,str,16));
    			putuserrec(&sbbs->cfg,user.number,U_EXPIRE,8,ultoa((ulong)user.expire,str,16));
    			putuserrec(&sbbs->cfg,user.number,U_EXEMPT,8,ultoa(user.exempt,str,16));
    			putuserrec(&sbbs->cfg,user.number,U_REST,8,ultoa(user.rest,str,16));
    			if(sbbs->cfg.expire_mod[0]) {
    				sbbs->useron=user;
    				sbbs->online=ON_LOCAL;
    				sbbs->exec_bin(sbbs->cfg.expire_mod,&sbbs->main_csi);
    				sbbs->online=FALSE; 
    			}
    		}
    
    		/***********************************************************/
    		/* Auto deletion based on expiration date or days inactive */
    		/***********************************************************/
    		if(!(user.exempt&FLAG('P'))     /* Not a permanent account */
    			&& !(user.misc&(DELETED|INACTIVE))	 /* alive */
    			&& (sbbs->cfg.sys_autodel && (now-user.laston)/(long)(24L*60L*60L)
    			> sbbs->cfg.sys_autodel)) {			/* Inactive too long */
    			SAFEPRINTF2(str,"Auto-Deleted %s #%u",user.alias,user.number);
    			sbbs->logentry("!*",str);
    			sbbs->delallmail(user.number, MAIL_ANY);
    			putusername(&sbbs->cfg,user.number,nulstr);
    			putuserrec(&sbbs->cfg,user.number,U_MISC,8,ultoa(user.misc|DELETED,str,16)); 
    		}
    	}
    	close(userfile);
    
    	lputs(LOG_INFO,status("Purging deleted/expired e-mail"));
    	SAFEPRINTF(sbbs->smb.file,"%smail",sbbs->cfg.data_dir);
    	sbbs->smb.retry_time=sbbs->cfg.smb_retry_time;
    	sbbs->smb.subnum=INVALID_SUB;
    	if((i=smb_open(&sbbs->smb))!=0)
    		sbbs->errormsg(WHERE,ERR_OPEN,sbbs->smb.file,i,sbbs->smb.last_error);
    	else {
    		if(filelength(fileno(sbbs->smb.shd_fp))>0) {
    			if((i=smb_locksmbhdr(&sbbs->smb))!=0)
    				sbbs->errormsg(WHERE,ERR_LOCK,sbbs->smb.file,i,sbbs->smb.last_error);
    			else
    				sbbs->delmail(0,MAIL_ALL);
    		}
    		smb_close(&sbbs->smb); 
    	}
    
    	sbbs->sys_status&=~SS_DAILY;
    	if(sbbs->cfg.sys_daily[0]) {
    //			status("Running system daily event");
    		sbbs->logentry("!:","Ran system daily event");
    		sbbs->external(sbbs->cmdstr(sbbs->cfg.sys_daily,nulstr,nulstr,NULL)
    			,EX_OFFLINE); 
    	}
    	status(STATUS_WFC);
    }
    
    const char* DLLCALL js_ver(void)
    {
    #ifdef JAVASCRIPT
    	return(JS_GetImplementationVersion());
    #else
    	return("");
    #endif
    }
    
    /* Returns char string of version and revision */
    const char* DLLCALL bbs_ver(void)
    {
    	static char ver[256];
    	char compiler[32];
    
    	if(ver[0]==0) {	/* uninitialized */
    		DESCRIBE_COMPILER(compiler);
    
    		safe_snprintf(ver,sizeof(ver),"%s %s%c%s  SMBLIB %s  Compiled %s %s with %s"
    			,TELNET_SERVER
    			,VERSION, REVISION
    #ifdef _DEBUG
    			," Debug"
    #else
    			,""
    #endif
    			,smb_lib_ver()
    			,__DATE__, __TIME__, compiler
    			);
    	}
    	return(ver);
    }
    
    /* Returns binary-coded version and revision (e.g. 0x31000 == 3.10a) */
    long DLLCALL bbs_ver_num(void)
    {
    	return(VERSION_HEX);
    }
    
    void DLLCALL bbs_terminate(void)
    {
       	lprintf(LOG_INFO,"BBS Server terminate");
    	terminate_server=true;
    }
    
    extern bbs_startup_t bbs_startup;
    static void cleanup(int code)
    {
        lputs(LOG_INFO,"Terminal Server thread terminating");
    
    	xpms_destroy(ts_set, sock_close_cb, startup);
    
    #ifdef _WINSOCKAPI_
    	if(WSAInitialized && WSACleanup()!=0) 
    		lprintf(LOG_ERR,"!WSACleanup ERROR %d",ERROR_VALUE);
    #endif
    
    	free_cfg(&scfg);
    	free_text(text);
    
    	semfile_list_free(&recycle_semfiles);
    	semfile_list_free(&shutdown_semfiles);
    
    	protected_uint32_destroy(node_threads_running);
    
    #ifdef _WIN32
    	if(exec_mutex!=NULL) {
    		CloseHandle(exec_mutex);
    		exec_mutex=NULL;
    	}
    
    	if(hK32!=NULL) {
    		FreeLibrary(hK32);
    		hK32=NULL;
    	}
    
    #if 0 && defined(_DEBUG) && defined(_MSC_VER)
    	_CrtMemDumpAllObjectsSince(&mem_chkpoint);
    
    	if(debug_log!=INVALID_HANDLE_VALUE) {
    		CloseHandle(debug_log);
    		debug_log=INVALID_HANDLE_VALUE;
    	}
    #endif // _DEBUG && _MSC_VER
    #endif // _WIN32
    
    	status("Down");
    	thread_down();
    	if(terminate_server || code)
    		lprintf(LOG_INFO,"Terminal Server thread terminated (%lu clients served)", served);
    	if(startup->terminated!=NULL)
    		startup->terminated(startup->cbdata,code);
    }
    
    void DLLCALL bbs_thread(void* arg)
    {
    	char			host_name[256];
    	char*			identity;
    	char*			p;
        char			str[MAX_PATH+1];
    	char			logstr[256];
    	union xp_sockaddr	server_addr={0};
    	union xp_sockaddr	client_addr;
    	socklen_t		client_addr_len;
    	SOCKET			client_socket=INVALID_SOCKET;
    	int				i;
        int				file;
    	int				result;
    	time_t			t;
    	time_t			start;
    	time_t			initialized=0;
    	node_t			node;
    	sbbs_t*			events=NULL;
    	client_t		client;
    	startup=(bbs_startup_t*)arg;
    	BOOL			is_client=FALSE;
    #ifdef __unix__
    	SOCKET	uspy_listen_socket[MAX_NODES];
    	struct main_sock_cb_data	uspy_cb[MAX_NODES]={};
    	union xp_sockaddr uspy_addr;
    #endif
    #ifdef USE_CRYPTLIB
    	CRYPT_CONTEXT	ssh_context;
    #endif
    	struct main_sock_cb_data	telnet_cb;
    	struct main_sock_cb_data	ssh_cb;
    	struct main_sock_cb_data	rlogin_cb;
    	void						*ts_cb;
    	int							err;
    
        if(startup==NULL) {
        	sbbs_beep(100,500);
        	fprintf(stderr, "No startup structure passed!\n");
        	return;
        }
    
    	if(startup->size!=sizeof(bbs_startup_t)) {	// verify size
    		sbbs_beep(100,500);
    		sbbs_beep(300,500);
    		sbbs_beep(100,500);
    		fprintf(stderr, "Invalid startup structure!\n");
    		return;
    	}
    
    #ifdef _THREAD_SUID_BROKEN
    	if(thread_suid_broken)
    		startup->seteuid(TRUE);
    #endif
    
    	ZERO_VAR(js_server_props);
    	SAFEPRINTF3(js_server_props.version,"%s %s%c",TELNET_SERVER,VERSION,REVISION);
    	js_server_props.version_detail=bbs_ver();
    	js_server_props.clients=&node_threads_running.value;
    	js_server_props.options=&startup->options;
    	js_server_props.interfaces=&startup->telnet_interfaces;
    
    	uptime=0;
    	served=0;
    
    	startup->recycle_now=FALSE;
    	startup->shutdown_now=FALSE;
    	terminate_server=false;
    
    	SetThreadName("sbbs/termServer");
    
    	do {
    	/* Setup intelligent defaults */
    	if(startup->telnet_port==0)				startup->telnet_port=IPPORT_TELNET;
    	if(startup->rlogin_port==0)				startup->rlogin_port=513;
    #ifdef USE_CRYPTLIB
    	if(startup->ssh_port==0)				startup->ssh_port=22;
    #endif
    	if(startup->outbuf_drain_timeout==0)	startup->outbuf_drain_timeout=10;
    	if(startup->sem_chk_freq==0)			startup->sem_chk_freq=DEFAULT_SEM_CHK_FREQ;
    	if(startup->temp_dir[0])				backslash(startup->temp_dir);
    
    	protected_uint32_init(&node_threads_running,0);
    
    	thread_up(FALSE /* setuid */);
    	if(startup->seteuid!=NULL)
    		startup->seteuid(TRUE);
    
    	status("Initializing");
    
    	/* Defeat the lameo hex0rs - the name and copyright must remain intact */
    	if(crc32(COPYRIGHT_NOTICE,0)!=COPYRIGHT_CRC 
    		|| crc32(VERSION_NOTICE,10)!=SYNCHRONET_CRC) {
    		lprintf(LOG_CRIT,"!CORRUPTED LIBRARY FILE");
    		cleanup(1);
    		return;
    	}
    
    	memset(text, 0, sizeof(text));
        memset(&scfg, 0, sizeof(scfg));
    
    	lastuseron[0]=0;
    
    	char compiler[32];
    	DESCRIBE_COMPILER(compiler);
    
    	lprintf(LOG_INFO,"%s Version %s Revision %c%s"
    		,TELNET_SERVER
    		,VERSION
    		,toupper(REVISION)
    #ifdef _DEBUG
    		," Debug"
    #else
    		,""
    #endif
    		);
    	lprintf(LOG_INFO,"Compiled %s %s with %s", __DATE__, __TIME__, compiler);
    	lprintf(LOG_DEBUG,"SMBLIB %s (format %x.%02x)",smb_lib_ver(),smb_ver()>>8,smb_ver()&0xff);
    
        if(startup->first_node<1 || startup->first_node>startup->last_node) {
        	lprintf(LOG_CRIT,"!ILLEGAL node configuration (first: %d, last: %d)"
            	,startup->first_node, startup->last_node);
    		cleanup(1);
            return;
        }
    
    #ifdef __BORLANDC__
    	#pragma warn -8008	/* Disable "Condition always false" warning */
    	#pragma warn -8066	/* Disable "Unreachable code" warning */
    #endif
    	if(sizeof(node_t)!=SIZEOF_NODE_T) {
    		lprintf(LOG_CRIT,"!COMPILER ERROR: sizeof(node_t)=%d instead of %d"
    			,sizeof(node_t),SIZEOF_NODE_T);
    		cleanup(1);
    		return;
    	}
    
    #ifdef _WIN32
        if((exec_mutex=CreateMutex(NULL,false,NULL))==NULL) {
        	lprintf(LOG_CRIT,"!ERROR %d creating exec_mutex", GetLastError());
    		cleanup(1);
            return;
        }
    	hK32 = LoadLibrary("KERNEL32");
    #endif // _WIN32
    
    	if(!winsock_startup()) {
    		cleanup(1);
    		return;
    	}
    
    	t=time(NULL);
    	lprintf(LOG_INFO,"Initializing on %.24s with options: %lx"
    		,ctime_r(&t,str),startup->options);
    
    	if(chdir(startup->ctrl_dir)!=0)
    		lprintf(LOG_ERR,"!ERROR %d changing directory to: %s", errno, startup->ctrl_dir);
    
    	/* Initial configuration and load from CNF files */
        SAFECOPY(scfg.ctrl_dir,startup->ctrl_dir);
        lprintf(LOG_INFO,"Loading configuration files from %s", scfg.ctrl_dir);
    	scfg.size=sizeof(scfg);
    	scfg.node_num=startup->first_node;
    	SAFECOPY(logstr,UNKNOWN_LOAD_ERROR);
    	if(!load_cfg(&scfg, text, TRUE, logstr)) {
    		lprintf(LOG_CRIT,"!ERROR %s",logstr);
    		lprintf(LOG_CRIT,"!FAILED to load configuration files");
    		cleanup(1);
    		return;
    	}
    	
    	if(startup->host_name[0]==0)
    		SAFECOPY(startup->host_name,scfg.sys_inetaddr);
    
    	if((t=checktime())!=0) {   /* Check binary time */
    		lprintf(LOG_ERR,"!TIME PROBLEM (%ld)",t);
    	}
    
    	if(smb_tzutc(sys_timezone(&scfg)) != xpTimeZone_local()) { 
    		lprintf(LOG_WARNING,"Configured time zone (%s, 0x%04hX, offset: %d) does not match system-local time zone offset: %d"
    			,smb_zonestr(scfg.sys_timezone,str), scfg.sys_timezone, smb_tzutc(scfg.sys_timezone), xpTimeZone_local());
    	}
    	if(uptime==0)
    		uptime=time(NULL);	/* this must be done *after* setting the timezone */
    
        if(startup->last_node>scfg.sys_nodes) {
        	lprintf(LOG_NOTICE,"Specified last_node (%d) > sys_nodes (%d), auto-corrected"
            	,startup->last_node, scfg.sys_nodes);
            startup->last_node=scfg.sys_nodes;
        }
    
    	/* Create missing directories */
    	lprintf(LOG_INFO,"Verifying/creating data directories");
    	make_data_dirs(&scfg);
    
    	/* Create missing node directories and dsts.dab files */
    	lprintf(LOG_INFO,"Verifying/creating node directories");
    	for(i=0;i<=scfg.sys_nodes;i++) {
    		if(i)
    			md(scfg.node_path[i-1]);
    		SAFEPRINTF(str,"%sdsts.dab",i ? scfg.node_path[i-1] : scfg.ctrl_dir);
    		if(flength(str)<DSTSDABLEN) {
    			if((file=sopen(str,O_WRONLY|O_CREAT|O_APPEND, SH_DENYNO, DEFFILEMODE))==-1) {
    				lprintf(LOG_CRIT,"!ERROR %d creating %s",errno, str);
    				cleanup(1);
    				return; 
    			}
    			while(filelength(file)<DSTSDABLEN)
    				if(write(file,"\0",1)!=1)
    					break;				/* Create NULL system dsts.dab */
    			close(file); 
    		} 
    	}
    
    	/* Initial global node variables */
    	for(i=0;i<MAX_NODES;i++) {
    		node_inbuf[i]=NULL;
        	node_socket[i]=INVALID_SOCKET;
    		spy_socket[i]=INVALID_SOCKET;
    #ifdef __unix__
    		uspy_socket[i]=INVALID_SOCKET;
    		uspy_listen_socket[i]=INVALID_SOCKET;
    #endif
    	}
    
    	startup->node_inbuf=node_inbuf;
    
        /* open a socket and wait for a client */
        ts_set = xpms_create(startup->bind_retry_count, startup->bind_retry_delay, lprintf);
        if(ts_set==NULL) {
    		lprintf(LOG_CRIT,"!ERROR %d creating Terminal Server socket set", ERROR_VALUE);
    		cleanup(1);
    		return;
    	}
        telnet_cb.protocol="telnet";
        telnet_cb.startup=startup;
    
    	/*
    	 * Add interfaces
    	 */
    	xpms_add_list(ts_set, PF_UNSPEC, SOCK_STREAM, 0, startup->telnet_interfaces, startup->telnet_port, "Telnet Server", sock_cb, startup->seteuid, &telnet_cb);
    
    	lprintf(LOG_INFO,"Telnet Server listening");
    
    	if(startup->options&BBS_OPT_ALLOW_RLOGIN) {
    		/* open a socket and wait for a client */
    		rlogin_cb.protocol="rlogin";
    		rlogin_cb.startup=startup;
    		xpms_add_list(ts_set, PF_UNSPEC, SOCK_STREAM, 0, startup->rlogin_interfaces, startup->rlogin_port, "RLogin Server", sock_cb, startup->seteuid, &rlogin_cb);
    		lprintf(LOG_INFO,"RLogin Server listening");
    	}
    
    #ifdef USE_CRYPTLIB
    #if CRYPTLIB_VERSION < 3300
    	#warning This version of Cryptlib is known to crash Synchronet.  Upgrade to at least version 3.3 or do not build with Cryptlib support.
    #endif
    	if(startup->options&BBS_OPT_ALLOW_SSH) {
    		CRYPT_KEYSET	ssh_keyset;
    
    		if(!do_cryptInit()) {
    			lprintf(LOG_ERR, "SSH cryptInit failure");
    			goto NO_SSH;
    		}
    		/* Get the private key... first try loading it from a file... */
    		SAFEPRINTF2(str,"%s%s",scfg.ctrl_dir,"cryptlib.key");
    		if(cryptStatusOK(cryptKeysetOpen(&ssh_keyset, CRYPT_UNUSED, CRYPT_KEYSET_FILE, str, CRYPT_KEYOPT_NONE))) {
    			if(!cryptStatusOK(cryptGetPrivateKey(ssh_keyset, &ssh_context, CRYPT_KEYID_NAME, "ssh_server", scfg.sys_pass))) {
    				lprintf(LOG_ERR, "SSH cryptGetPrivateKey failure");
    				goto NO_SSH;
    			}
    		}
    		else {
    			/* Couldn't do that... create a new context and use the key from there... */
    
    			if(!cryptStatusOK(i=cryptCreateContext(&ssh_context, CRYPT_UNUSED, CRYPT_ALGO_RSA))) {
    				lprintf(LOG_ERR,"SSH Cryptlib error %d creating context",i);
    				goto NO_SSH;
    			}
    			if(!cryptStatusOK(i=cryptSetAttributeString(ssh_context, CRYPT_CTXINFO_LABEL, "ssh_server", 10))) {
    				lprintf(LOG_ERR,"SSH Cryptlib error %d setting key label",i);
    				goto NO_SSH;
    			}
    			if(!cryptStatusOK(i=cryptGenerateKey(ssh_context))) {
    				lprintf(LOG_ERR,"SSH Cryptlib error %d generating key",i);
    				goto NO_SSH;
    			}
    
    			/* Ok, now try saving this one... use the syspass to enctrpy it. */
    			if(cryptStatusOK(cryptKeysetOpen(&ssh_keyset, CRYPT_UNUSED, CRYPT_KEYSET_FILE, str, CRYPT_KEYOPT_CREATE))) {
    				if(!cryptStatusOK(cryptAddPrivateKey(ssh_keyset, ssh_context, scfg.sys_pass)))
    					lprintf(LOG_ERR,"SSH Cryptlib error %d saving key",i);
    				if(!cryptStatusOK(cryptKeysetClose(ssh_keyset)))
    					lprintf(LOG_ERR,"SSH Cryptlib error %d closing keyset",i);
    			}
    		}
    
    		/* open a socket and wait for a client */
    		ssh_cb.protocol="ssh";
    		ssh_cb.startup=startup;
    		xpms_add_list(ts_set, PF_UNSPEC, SOCK_STREAM, 0, startup->ssh_interfaces, startup->ssh_port, "SSH Server", sock_cb, startup->seteuid, &ssh_cb);
    		lprintf(LOG_INFO,"SSH Server listening");
    	}
    NO_SSH:
    #endif
    
    	sbbs = new sbbs_t(0, &server_addr, sizeof(server_addr)
    		,"Terminal Server", ts_set->socks[0].sock, &scfg, text, NULL);
    	if(sbbs->init()==false) {
    		lputs(LOG_CRIT,"!BBS initialization failed");
    		cleanup(1);
    		return;
    	}
        sbbs->output_thread_running = true;
    	_beginthread(output_thread, 0, sbbs);
    
    	if(!(startup->options&BBS_OPT_NO_EVENTS)) {
    		events = new sbbs_t(0, &server_addr, sizeof(server_addr)
    			,"BBS Events", INVALID_SOCKET, &scfg, text, NULL);
    		if(events->init()==false) {
    			lputs(LOG_CRIT,"!Events initialization failed");
    			cleanup(1);
    			return;
    		}
    		events->event_thread_running = true;
    		_beginthread(event_thread, 0, events);
    	}
    
    	/* Save these values incase they're changed dynamically */
    	first_node=startup->first_node;
    	last_node=startup->last_node;
    
    	for(i=first_node;i<=last_node;i++) {
    		sbbs->getnodedat(i,&node,1);
    		node.status=NODE_WFC;
    		node.misc&=NODE_EVENT;	/* Note: Turns-off NODE_RRUN flag (and others) */
    		node.action=0;
    		sbbs->putnodedat(i,&node);
    	}
    
    	status(STATUS_WFC);
    
    #if defined(_WIN32) && defined(_DEBUG) && defined(_MSC_VER)
    	
    	SAFEPRINTF(str,"%sDEBUG.LOG",scfg.logs_dir);
    	if((debug_log=CreateFile(
    		str,				// pointer to name of the file
    		GENERIC_READ|GENERIC_WRITE,
    		FILE_SHARE_READ|FILE_SHARE_WRITE,
    		NULL,               // pointer to security attributes
    		OPEN_ALWAYS,		// how to create
    		FILE_ATTRIBUTE_NORMAL, // file attributes
    		NULL				// handle to file with attributes to 
    		))==INVALID_HANDLE_VALUE) {
    		lprintf(LOG_CRIT,"!ERROR %ld creating %s",GetLastError(),str);
    		cleanup(1);
    		return;
    	}
    
    	_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
    	_CrtSetReportFile(_CRT_WARN, debug_log);
    	_CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE|_CRTDBG_MODE_WNDW);
    	_CrtSetReportFile(_CRT_ERROR, debug_log);
    	_CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE|_CRTDBG_MODE_WNDW);
    	_CrtSetReportFile(_CRT_ASSERT, debug_log);
    
    	/* Turns on memory leak checking during program termination */
    //	_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
    
    	/* Save this allocation point for comparison */
    	_CrtMemCheckpoint(&mem_chkpoint);
    
    #endif // _WIN32 && _DEBUG && _MSC_VER
    
    	/* Setup recycle/shutdown semaphore file lists */
    	shutdown_semfiles=semfile_list_init(scfg.ctrl_dir,"shutdown","telnet");
    	recycle_semfiles=semfile_list_init(scfg.ctrl_dir,"recycle","telnet");
    	SAFEPRINTF(str,"%stelnet.rec",scfg.ctrl_dir);	/* legacy */
    	semfile_list_add(&recycle_semfiles,str);
    	SAFEPRINTF(str,"%stext.dat",scfg.ctrl_dir);
    	semfile_list_add(&recycle_semfiles,str);
    	SAFEPRINTF(str,"%sattr.cfg",scfg.ctrl_dir);
    	semfile_list_add(&recycle_semfiles,str);
    	if(!initialized)
    		semfile_list_check(&initialized,shutdown_semfiles);
    	semfile_list_check(&initialized,recycle_semfiles);
    
    #ifdef __unix__	//	unix-domain spy sockets
    	for(i=first_node;i<=last_node && !(startup->options&BBS_OPT_NO_SPY_SOCKETS);i++)  {
    	    if((unsigned int)snprintf(str,sizeof(uspy_addr.un.sun_path),
    	            "%slocalspy%d.sock", startup->temp_dir, i)
    	            >=sizeof(uspy_addr.un.sun_path)) {
    			lprintf(LOG_ERR,"Node %d !ERROR local spy socket path \"%slocalspy%d.sock\" too long."
    				, i, startup->temp_dir, i);
    			continue;
    		}
    	    else  {
    			if(xpms_add(ts_set, PF_UNIX, SOCK_STREAM, 0, str, 0, "Spy Socket", sock_cb, NULL, &uspy_cb[i-1]))
    				lprintf(LOG_INFO,"Node %d local spy using socket %s", i, str);
    			else
    				lprintf(LOG_ERR,"Node %d !ERROR %d creating local spy socket %s"
    					, i, errno, str);
    	    }
    	}
    #endif // __unix__ (unix-domain spy sockets)
    
    	/* signal caller that we've started up successfully */
        if(startup->started!=NULL)
        	startup->started(startup->cbdata);
    
    	lprintf(LOG_INFO,"Terminal Server thread started for nodes %d through %d", first_node, last_node);
    
    	while(!terminate_server) {
    		YIELD();
    		if(protected_uint32_value(node_threads_running)==0) {	/* check for re-run flags and recycle/shutdown sem files */
    			if(!(startup->options&BBS_OPT_NO_RECYCLE)) {
    
    				bool rerun=false;
    				for(i=first_node;i<=last_node;i++) {
    					if(sbbs->getnodedat(i,&node,0)!=0)
    						continue;
    					if(node.misc&NODE_RRUN) {
    						sbbs->getnodedat(i,&node,1);
    						if(!rerun)
    							lprintf(LOG_INFO,"Node %d flagged for re-run",i);
    						rerun=true;
    						node.misc&=~NODE_RRUN;
    						sbbs->putnodedat(i,&node);
    					}
    				}
    				if(rerun)
    					break;
    
    				if((p=semfile_list_check(&initialized,recycle_semfiles))!=NULL) {
    					lprintf(LOG_INFO,"Recycle semaphore file (%s) detected"
    						,p);
    					break;
    				}
    				if(startup->recycle_now==TRUE) {
    					lprintf(LOG_INFO,"Recycle semaphore signaled");
    					startup->recycle_now=FALSE;
    					break;
    				}
    			}
    			if(((p=semfile_list_check(&initialized,shutdown_semfiles))!=NULL
    					&& lprintf(LOG_INFO,"Shutdown semaphore file (%s) detected"
    						,p))
    				|| (startup->shutdown_now==TRUE
    					&& lprintf(LOG_INFO,"Shutdown semaphore signaled"))) {
    				startup->shutdown_now=FALSE;
    				terminate_server=TRUE;
    				break;
    			}
    		}
    
        	sbbs->online=FALSE;
    //		sbbs->client_socket=INVALID_SOCKET;
    #ifdef USE_CRYPTLIB
    		sbbs->ssh_mode=false;
    #endif
    
    		/* now wait for connection */
    		client_addr_len = sizeof(client_addr);
    		client_socket=xpms_accept(ts_set, &client_addr
    	        	,&client_addr_len, startup->sem_chk_freq*1000, &ts_cb);
    
    		if(terminate_server)	/* terminated */
    			break;
    
    		if(client_socket == INVALID_SOCKET)
    			continue;
    
    		bool rlogin = false;
    #ifdef USE_CRYPTLIB
    		bool ssh = false;
    #endif
    
    		is_client=FALSE;
    		if(ts_cb == &telnet_cb) {
    	        is_client=TRUE;
    		} else if(ts_cb == &rlogin_cb) {
    			rlogin = true;
    			is_client=TRUE;
    #ifdef USE_CRYPTLIB
    		} else if(ts_cb == &ssh_cb) {
    			ssh = true;
    			is_client=TRUE;
    			sbbs->ssh_mode=true;
    #endif
    		} else {
    #ifdef __unix__
    			for(i=first_node;i<=last_node;i++)  {
    				if(&uspy_cb[i-1] == ts_cb) {
    					if(node_socket[i-1]==INVALID_SOCKET)
    						read(uspy_socket[i-1],str,sizeof(str));
    					if(!socket_check(uspy_socket[i-1],NULL,NULL,0)) {
    						lprintf(LOG_NOTICE,"Spy socket for node %d disconnected",i);
    						close_socket(uspy_socket[i-1]);
    						uspy_socket[i-1]=INVALID_SOCKET;
    					}
    				}
    				if(&uspy_cb[i-1] == ts_cb) {
    					BOOL already_connected=(uspy_socket[i-1]!=INVALID_SOCKET);
    					SOCKET new_socket=client_socket;
    					fcntl(new_socket,F_SETFL,fcntl(new_socket,F_GETFL)|O_NONBLOCK);
    					if(already_connected)  {
    						lprintf(LOG_ERR,"!ERROR Spy socket %s already in use",uspy_addr.un.sun_path);
    						send(new_socket,"Spy socket already in use.\r\n",27,0);
    						close_socket(new_socket);
    					}
    					else  {
    						lprintf(LOG_ERR,"!Spy socket %s (%d) connected",uspy_addr.un.sun_path,new_socket);
    						uspy_socket[i-1]=new_socket;
    						SAFEPRINTF(str,"Spy connection established to node %d\r\n",i);
    						send(uspy_socket[i-1],str,strlen(str),0);
    					}
    				}
    			}
    #else
    			lprintf(LOG_ERR,"!NO SOCKETS set by select");
    #endif
    		}
    
    		if(!is_client) {
    			/* Do not need to close_socket(client_socket) here */
    			continue;
    		}
    
    		if(client_socket == INVALID_SOCKET)	{
    #if 0	/* is this necessary still? */
    			if(ERROR_VALUE == ENOTSOCK || ERROR_VALUE == EINTR || ERROR_VALUE == EINVAL) {
                	lputs(LOG_NOTICE,"BBS socket closed");
    				break;
    			}
    #endif
    			lprintf(LOG_ERR,"!ERROR %d accepting connection", ERROR_VALUE);
    #ifdef _WIN32
    			if(WSAGetLastError()==WSAENOBUFS) {	/* recycle (re-init WinSock) on this error */
    				break;
    			}
    #endif
    			continue;
    		}
    		char host_ip[INET6_ADDRSTRLEN];
    
    		inet_addrtop(&client_addr, host_ip, sizeof(host_ip));
    
    		if(trashcan(&scfg,host_ip,"ip-silent")) {
    			SSH_END();
    			close_socket(client_socket);
    			continue;
    		}
    
    		lprintf(LOG_INFO,"%04d %s connection accepted from: %s port %u"
    			,client_socket
    #ifdef USE_CRYPTLIB
    			,rlogin ? "RLogin" : (ssh ? "SSH" : "Telnet")
    #else
    			,rlogin ? "RLogin" : "Telnet"
    #endif
    			, host_ip, inet_addrport(&client_addr));
    
    		login_attempt_t attempted;
    		ulong banned = loginBanned(&scfg, startup->login_attempt_list, client_socket, /* host_name: */NULL, startup->login_attempt, &attempted);
    		if(banned || sbbs->trashcan(host_ip,"ip")) {
    			if(banned) {
    				char ban_duration[128];
    				lprintf(LOG_NOTICE, "%04d !TEMPORARY BAN of %s (%u login attempts, last: %s) - remaining: %s"
    					,client_socket, host_ip, attempted.count, attempted.user, seconds_to_str(banned, ban_duration));
    			} else
    				lprintf(LOG_NOTICE,"%04d !CLIENT BLOCKED in ip.can: %s", client_socket, host_ip);
    			SSH_END();
    			close_socket(client_socket);
    			SAFEPRINTF(logstr, "Blocked IP: %s",host_ip);
    			sbbs->syslog("@!",logstr);
    			continue;
    		}
    
    #ifdef _WIN32
    		if(startup->answer_sound[0] && !(startup->options&BBS_OPT_MUTE)) 
    			PlaySound(startup->answer_sound, NULL, SND_ASYNC|SND_FILENAME);
    #endif
    
    		/* Purge (flush) any pending input or output data */
    		sbbs->rioctl(IOFB);
    
    		/* Do SSH stuff here */
    #ifdef USE_CRYPTLIB
    		if(ssh) {
    			int	ssh_failed=0;
    			if(!cryptStatusOK(i=cryptCreateSession(&sbbs->ssh_session, CRYPT_UNUSED, CRYPT_SESSION_SSH_SERVER))) {
    				lprintf(LOG_WARNING,"%04d SSH Cryptlib error %d creating session", client_socket, i);
    				close_socket(client_socket);
    				continue;
    			}
    			if(!cryptStatusOK(i=cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_PRIVATEKEY, ssh_context))) {
    				lprintf(LOG_WARNING,"%04d SSH Cryptlib error %d setting private key",client_socket, i);
    				cryptDestroySession(sbbs->ssh_session);
    				close_socket(client_socket);
    				continue;
    			}
    			if(!cryptStatusOK(i=cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_NETWORKSOCKET, client_socket))) {
    				lprintf(LOG_WARNING,"%04d SSH Cryptlib error %d setting socket",client_socket, i);
    				cryptDestroySession(sbbs->ssh_session);
    				close_socket(client_socket);
    				continue;
    			}
    			for(ssh_failed=0; ssh_failed < 2; ssh_failed++) {
    				/* Accept any credentials */
    				if(!cryptStatusOK(i=cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_AUTHRESPONSE, 1))) {
    					ssh_failed=1;
    					break;
    				}
    				if(!cryptStatusOK(i=cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_ACTIVE, 1))) {
    					if(i != CRYPT_ENVELOPE_RESOURCE) {
    						ssh_failed=2;
    						break;
    					}
    				}
    				else {
    					ssh_failed=0;
    					break;
    				}
    			}
    			switch(ssh_failed) {
    				case 1:
    					lprintf(LOG_WARNING,"%04d SSH Cryptlib error %d setting AUTHRESPONSE",client_socket, i);
    					break;
    				case 2:
    					switch(i) {
    						case CRYPT_ERROR_BADDATA:
    							lprintf(LOG_NOTICE,"%04d SSH Bad/unrecognized data format", client_socket);
    							break;
    						case CRYPT_ERROR_READ:
    							lprintf(LOG_WARNING,"%04d SSH Read failure", client_socket);
    							break;
    						case CRYPT_ERROR_WRITE:
    							lprintf(LOG_WARNING,"%04d SSH Write failure", client_socket);
    							break;
    						default:
    							lprintf(LOG_WARNING,"%04d SSH Cryptlib error %d setting session active",client_socket, i);
    							break;
    					}
    					break;
    			}
    			if(ssh_failed) {
    				cryptDestroySession(sbbs->ssh_session);
    				close_socket(client_socket);
    				continue;
    			}
    			if(!cryptStatusOK(err=cryptPopData(sbbs->ssh_session, str, sizeof(str), &i))) {
    				lprintf(LOG_WARNING,"Node %d !ERROR %d receiving on Cryptlib session", sbbs->cfg.node_num, err);
    				i=0;
    			}
    		}
    #endif
       		sbbs->client_socket=client_socket;	// required for output to the user
            sbbs->online=ON_REMOTE;
    
    		if(rlogin)
    			sbbs->outcom(0); /* acknowledge RLogin per RFC 1282 */
    
    		sbbs->putcom(crlf);
    		sbbs->putcom(VERSION_NOTICE);
    		sbbs->putcom(crlf);
    
    		sbbs->bprintf("Connection from: %s\r\n", host_ip);
    
    		SAFECOPY(host_name, "<no name>");
    		if(!(startup->options&BBS_OPT_NO_HOST_LOOKUP)) {
    			sbbs->bprintf("Resolving hostname...");
    			getnameinfo(&client_addr.addr, client_addr_len, host_name, sizeof(host_name), NULL, 0, NI_NAMEREQD);
    			sbbs->putcom(crlf);
    			lprintf(LOG_INFO,"%04d Hostname: %s", client_socket, host_name);
    		}
    
    		if(sbbs->trashcan(host_name,"host")) {
    			SSH_END();
    			close_socket(client_socket);
    			lprintf(LOG_NOTICE,"%04d !CLIENT BLOCKED in host.can: %s"
    				,client_socket, host_name);
    			SAFEPRINTF(logstr, "Blocked Hostname: %s",host_name);
    			sbbs->syslog("@!",logstr);
    			continue;
    		}
    
    		identity=NULL;
    		if(startup->options&BBS_OPT_GET_IDENT) {
    			sbbs->bprintf("Resolving identity...");
    			/* ToDo: Make ident timeout configurable */
    			if(identify(&client_addr, inet_addrport(&client_addr), str, sizeof(str)-1, /* timeout: */1)) {
    				lprintf(LOG_DEBUG,"%04d Ident Response: %s",client_socket, str);
    				identity=strrchr(str,':');
    				if(identity!=NULL) {
    					identity++;	/* skip colon */
    					SKIP_WHITESPACE(identity);
    					if(*identity)
    						lprintf(LOG_INFO,"%04d Identity: %s",client_socket, identity);
    				}
    			}
    			sbbs->putcom(crlf);
    		}
    		/* Initialize client display */
    		client.size=sizeof(client);
    		client.time=time32(NULL);
    		SAFECOPY(client.addr,host_ip);
    		SAFECOPY(client.host,host_name);
    		client.port=inet_addrport(&client_addr);
    #ifdef USE_CRYPTLIB
    		client.protocol=rlogin ? "RLogin":(ssh ? "SSH" : "Telnet");
    #else
    		client.protocol=rlogin ? "RLogin":"Telnet";
    #endif
    		client.user="<unknown>";
    		client_on(client_socket,&client,FALSE /* update */);
    
    		for(i=first_node;i<=last_node;i++) {
    			/* paranoia: make sure node.status!=NODE_WFC by default */
    			node.status=NODE_INVALID_STATUS;	
    			if(sbbs->getnodedat(i,&node,1)!=0)
    				continue;
    			if(node.status==NODE_WFC) {
    				node.status=NODE_LOGON;
    #ifdef USE_CRYPTLIB
    				if(ssh)
    					node.connection=NODE_CONNECTION_SSH;
    				else
    #endif
    				if(rlogin)
    					node.connection=NODE_CONNECTION_RLOGIN;
    				else
    					node.connection=NODE_CONNECTION_TELNET;
    
    				sbbs->putnodedat(i,&node);
    				break;
    			}
    			sbbs->putnodedat(i,&node);
    		}
    
    		if(i>last_node) {
    			lprintf(LOG_WARNING,"%04d !No nodes available for login.",client_socket);
    			SAFEPRINTF(str,"%snonodes.txt",scfg.text_dir);
    			if(fexist(str))
    				sbbs->printfile(str,P_NOABORT);
    			else {
    				sbbs->putcom("\r\nSorry, all telnet nodes are in use or otherwise unavailable.\r\n");
    				sbbs->putcom("Please try again later.\r\n");
    			}
    			mswait(3000);
    			client_off(client_socket);
    			SSH_END();
    			close_socket(client_socket);
    			continue;
    		}
    
            node_socket[i-1]=client_socket;
    
    		sbbs_t* new_node = new sbbs_t(i, &client_addr, client_addr_len, host_name
            	,client_socket
    			,&scfg, text, &client);
    
    		new_node->client=client;
    #ifdef USE_CRYPTLIB
    		if(ssh) {
    			new_node->ssh_session=sbbs->ssh_session;
    			new_node->ssh_mode=true;
    		}
    #endif
    
    		/* copy the IDENT response, if any */
    		if(identity!=NULL)
    			SAFECOPY(new_node->client_ident,identity);
    
    		if(new_node->init()==false) {
    			lprintf(LOG_INFO,"%04d Node %d !Initialization failure"
    				,client_socket,new_node->cfg.node_num);
    			SAFEPRINTF(str,"%snonodes.txt",scfg.text_dir);
    			if(fexist(str))
    				sbbs->printfile(str,P_NOABORT);
    			else 
    				sbbs->putcom("\r\nSorry, initialization failed. Try again later.\r\n");
    			mswait(3000);
    			sbbs->getnodedat(new_node->cfg.node_num,&node,1);
    			node.status=NODE_WFC;
    			sbbs->putnodedat(new_node->cfg.node_num,&node);
    			delete new_node;
    			node_socket[i-1]=INVALID_SOCKET;
    			client_off(client_socket);
    			SSH_END();
    			close_socket(client_socket);
    			continue;
    		}
    
    		if(rlogin==true) {
    			SAFECOPY(new_node->connection,"RLogin");
    			new_node->node_connection=NODE_CONNECTION_RLOGIN;
    			new_node->sys_status|=SS_RLOGIN;
    			new_node->telnet_mode|=TELNET_MODE_OFF; // RLogin does not use Telnet commands
    		}
    #ifdef USE_CRYPTLIB
    		if(ssh) {
    			/* TODO: IPv6? */
    			SOCKET	tmp_sock;
    			SOCKADDR_IN		tmp_addr={0};
    			socklen_t		tmp_addr_len;
    
        		/* open a socket and connect to yourself */
    
        		tmp_sock = open_socket(SOCK_STREAM, "passthru");
    
    			if(tmp_sock == INVALID_SOCKET) {
    				lprintf(LOG_ERR,"!ERROR %d creating passthru listen socket", ERROR_VALUE);
    				goto NO_PASSTHRU;
    			}
    
        		lprintf(LOG_DEBUG,"passthru listen socket %d opened",tmp_sock);
    
    			/*****************************/
    			/* Listen for incoming calls */
    			/*****************************/
        		memset(&tmp_addr, 0, sizeof(tmp_addr));
    
    			tmp_addr.sin_addr.s_addr = htonl(IPv4_LOCALHOST);
        		tmp_addr.sin_family = AF_INET;
        		tmp_addr.sin_port   = 0;
    
        		result = bind(tmp_sock,(struct sockaddr *)&tmp_addr,sizeof(tmp_addr));
    			if(result != 0) {
    				lprintf(LOG_NOTICE,"%s",BIND_FAILURE_HELP);
    				close_socket(tmp_sock);
    				goto NO_PASSTHRU;
    			}
    
    		    result = listen(tmp_sock, 1);
    
    			if(result != 0) {
    				lprintf(LOG_ERR,"!ERROR %d (%d) listening on passthru socket", result, ERROR_VALUE);
    				close_socket(tmp_sock);
    				goto NO_PASSTHRU;
    			}
    			lprintf(LOG_INFO,"Listening passthru socket listening on port %u",htons(tmp_addr.sin_port));
    
        		new_node->passthru_socket = open_socket(SOCK_STREAM, "passthru");
    
    			if(new_node->passthru_socket == INVALID_SOCKET) {
    				lprintf(LOG_ERR,"!ERROR %d creating passthru connecting socket", ERROR_VALUE);
    				close_socket(tmp_sock);
    				goto NO_PASSTHRU;
    			}
    
        		lprintf(LOG_DEBUG,"passthru connect socket %d opened",new_node->passthru_socket);
    
    			tmp_addr_len=sizeof(tmp_addr);
    			if(getsockname(tmp_sock, (struct sockaddr *)&tmp_addr, &tmp_addr_len)) {
    				lprintf(LOG_ERR,"!ERROR %d getting passthru listener address", ERROR_VALUE);
    				close_socket(tmp_sock);
    				close_socket(new_node->passthru_socket);
    				new_node->passthru_socket=INVALID_SOCKET;
    				goto NO_PASSTHRU;
    			}
    
    		    result = connect(new_node->passthru_socket, (struct sockaddr *)&tmp_addr, tmp_addr_len);
    
    			if(result != 0) {
    				lprintf(LOG_ERR,"!ERROR %d (%d) connecting to passthru socket", result, ERROR_VALUE);
    				close_socket(new_node->passthru_socket);
    				new_node->passthru_socket=INVALID_SOCKET;
    				close_socket(tmp_sock);
    				goto NO_PASSTHRU;
    			}
    
    			new_node->client_socket_dup=accept(tmp_sock, (struct sockaddr *)&tmp_addr, &tmp_addr_len);
    
    			if(new_node->client_socket_dup == INVALID_SOCKET) {
    				lprintf(LOG_ERR,"!ERROR (%d) connecting accept()ing on passthru socket", ERROR_VALUE);
    				lprintf(LOG_WARNING,"!WARNING native doors which use sockets will not function");
    				close_socket(new_node->passthru_socket);
    				new_node->passthru_socket=INVALID_SOCKET;
    				close_socket(tmp_sock);
    				goto NO_PASSTHRU;
    			}
    			close_socket(tmp_sock);
    			new_node->passthru_output_thread_running = true;
    			_beginthread(passthru_output_thread, 0, new_node);
    			new_node->passthru_input_thread_running = true;
    			_beginthread(passthru_input_thread, 0, new_node);
    
    NO_PASSTHRU:
    			SAFECOPY(new_node->connection,"SSH");
    			new_node->node_connection=NODE_CONNECTION_SSH;
    			new_node->sys_status|=SS_SSH;
    			new_node->telnet_mode|=TELNET_MODE_OFF; // SSH does not use Telnet commands
    			new_node->ssh_session=sbbs->ssh_session;
    			/* Wait for pending data to be sent then turn off ssh_mode for uber-output */
    			while(sbbs->output_thread_running && RingBufFull(&sbbs->outbuf))
    				SLEEP(1);
    			if(!cryptStatusOK(err=cryptPopData(sbbs->ssh_session, str, sizeof(str), &i))) {
    				lprintf(LOG_WARNING,"Node %d !ERROR %d receiving on Cryptlib session", sbbs->cfg.node_num, err);
    				i=0;
    			}
    			sbbs->ssh_mode=false;
    		}
    #endif
    
    	    protected_uint32_adjust(&node_threads_running, 1);
    	    new_node->input_thread_running = true;
    		new_node->input_thread=(HANDLE)_beginthread(input_thread,0, new_node);
    	    new_node->output_thread_running = true;
    		_beginthread(output_thread, 0, new_node);
    		_beginthread(node_thread, 0, new_node);
    		served++;
    	}
    
        // Close all open sockets
        for(i=0;i<MAX_NODES;i++)  {
        	if(node_socket[i]!=INVALID_SOCKET) {
            	lprintf(LOG_INFO,"Closing node %d socket %d", i+1, node_socket[i]);
            	close_socket(node_socket[i]);
    			node_socket[i]=INVALID_SOCKET;
            }
    #ifdef __unix__
    		if(uspy_listen_socket[i]!=INVALID_SOCKET) {
    			close_socket(uspy_listen_socket[i]);
    			uspy_listen_socket[i]=INVALID_SOCKET;
    			snprintf(str,sizeof(uspy_addr.un.sun_path),"%slocalspy%d.sock", startup->temp_dir, i+1);
    			if(fexist(str))
    				unlink(str);
    		}
    		if(uspy_socket[i]!=INVALID_SOCKET) {
    			close_socket(uspy_socket[i]);
    			uspy_socket[i]=INVALID_SOCKET;
    		}		
    #endif
    	}
    
    	sbbs->client_socket=INVALID_SOCKET;
    	if(events!=NULL)
    		events->terminated=true;
        // Wake-up BBS output thread so it can terminate
        sem_post(&sbbs->outbuf.sem);
    
        // Wait for all node threads to terminate
    	if(protected_uint32_value(node_threads_running)) {
    		lprintf(LOG_INFO,"Waiting for %d node threads to terminate...", protected_uint32_value(node_threads_running));
    		start=time(NULL);
    		while(protected_uint32_value(node_threads_running)) {
    			if(time(NULL)-start>TIMEOUT_THREAD_WAIT) {
    				lprintf(LOG_ERR,"!TIMEOUT waiting for %d node thread(s) to "
                		"terminate", protected_uint32_value(node_threads_running));
    				break;
    			}
    			mswait(100);
    		}
    	}
    
    	// Wait for Events thread to terminate
    	if(events!=NULL && events->event_thread_running) {
    		lprintf(LOG_INFO,"Waiting for events thread to terminate...");
    		start=time(NULL);
    		while(events->event_thread_running) {
    #if 0 /* the events thread can/will segfault if it continues to run and dereference sbbs->cfg */
    			if(time(NULL)-start>TIMEOUT_THREAD_WAIT) {
    				lprintf(LOG_ERR,"!TIMEOUT waiting for BBS events thread to "
                		"terminate");
    				break;
    			}
    #endif
    			mswait(100);
    		}
    	}
    
        // Wait for BBS output thread to terminate
    	if(sbbs->output_thread_running) {
    		lprintf(LOG_INFO,"Waiting for system output thread to terminate...");
    		start=time(NULL);
    		while(sbbs->output_thread_running) {
    			if(time(NULL)-start>TIMEOUT_THREAD_WAIT) {
    				lprintf(LOG_ERR,"!TIMEOUT waiting for BBS output thread to "
                		"terminate");
    				break;
    			}
    			mswait(100);
    		}
    	}
    
        // Set all nodes' status to OFFLINE
        for(i=first_node;i<=last_node;i++) {
            sbbs->getnodedat(i,&node,1);
            node.status=NODE_OFFLINE;
            sbbs->putnodedat(i,&node);
        }
    
        if(events!=NULL) {
    		if(events->event_thread_running)
    			lprintf(LOG_ERR,"!Events thread still running, can't delete");
    		else
    		    delete events; 
    	}
    
        if(sbbs->output_thread_running)
    		lprintf(LOG_ERR,"!Output thread still running, can't delete");
    	else
    	    delete sbbs;
    
    	cleanup(0);
    
    	if(!terminate_server) {
    		lprintf(LOG_INFO,"Recycling server...");
    		mswait(2000);
    		if(startup->recycle!=NULL)
    			startup->recycle(startup->cbdata);
    	}
    
    	} while(!terminate_server);
    
    }