Skip to content
Snippets Groups Projects
Select Git revision
  • 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
  • new_user_dat
  • 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
    • Deucе's avatar
      346849ac
      Unlock ssh_mutex before calling sftps_recv() · 346849ac
      Deucе authored
      This should fix the sftp hangs...
      While FreeBSD will return EDEADLOCK when a mutex would deadlock,
      Linux will do what I ask and deadlock.
      
      Since we're not checking the return value of pthread_mutex_lock(),
      this would go completely unnoticed on FreeBSD, resulting in the
      mutex being unlocked early and all sorts of unprotected accesses
      would happen, potentially doing crazy things.  On Linux it just
      deadlocks, and taking a peek at the thread backtraces makes the
      problem obvious.
      
      So, point to Linux for making life more correct for people who
      do incorrect things (locking without checking the return value).
      
      I'd also like to thank nelgin, for sticking with me on this issue
      and being my gdb puppet.
      
      And really, this commit should be shared by all the contributors
      who... 𝆺𝅥𝅮𝆺𝅥𝅮𝆹𝅥𝅯𝆹𝅥𝅯
      346849ac
      History
      Unlock ssh_mutex before calling sftps_recv()
      Deucе authored
      This should fix the sftp hangs...
      While FreeBSD will return EDEADLOCK when a mutex would deadlock,
      Linux will do what I ask and deadlock.
      
      Since we're not checking the return value of pthread_mutex_lock(),
      this would go completely unnoticed on FreeBSD, resulting in the
      mutex being unlocked early and all sorts of unprotected accesses
      would happen, potentially doing crazy things.  On Linux it just
      deadlocks, and taking a peek at the thread backtraces makes the
      problem obvious.
      
      So, point to Linux for making life more correct for people who
      do incorrect things (locking without checking the return value).
      
      I'd also like to thank nelgin, for sticking with me on this issue
      and being my gdb puppet.
      
      And really, this commit should be shared by all the contributors
      who... 𝆺𝅥𝅮𝆺𝅥𝅮𝆹𝅥𝅯𝆹𝅥𝅯
    main.cpp 178.59 KiB
    /* Synchronet terminal server thread and related functions */
    
    /****************************************************************************
     * @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										*
     *																			*
     * For Synchronet coding style and modification guidelines, see				*
     * http://www.synchro.net/source.html										*
     *																			*
     * 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 "petdefs.h"
    #include "filedat.h"
    #include "js_rtpool.h"
    #include "js_request.h"
    #include "ssl.h"
    #include "ver.h"
    #include "eventwrap.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"
    static const char* server_abbrev = "term";
    
    #define TIMEOUT_THREAD_WAIT		60			// Seconds (was 15)
    #define IO_THREAD_BUF_SIZE	   	20000		// Bytes
    #define TIMEOUT_MUTEX_FILE		12*60*60
    
    #ifdef USE_CRYPTLIB
    
    	static	protected_uint32_t	ssh_sessions;
    
    	void ssh_session_destroy(SOCKET sock, CRYPT_SESSION session, int line)
    	{
    		int result = cryptDestroySession(session);
    
    		if(result != 0)
    			lprintf(LOG_ERR, "%04d SSH ERROR %d destroying Cryptlib Session %d from line %d"
    				, sock, result, session, line);
    		else {
    			uint32_t remain = protected_uint32_adjust_fetch(&ssh_sessions, -1);
    			lprintf(LOG_DEBUG, "%04d SSH Cryptlib Session: %d destroyed from line %d (%u remain)"
    				, sock, session, line, remain);
    		}
    	}
    
    	#define SSH_END(sock) do {										\
    		if(ssh) {													\
    			pthread_mutex_lock(&sbbs->ssh_mutex);					\
    			ssh_session_destroy(sock, sbbs->ssh_session, __LINE__);	\
    			sbbs->ssh_mode = false;									\
    			pthread_mutex_unlock(&sbbs->ssh_mutex);					\
    		}															\
    	} while(0)
    #else
    	#define	SSH_END(x)
    #endif
    
    volatile time_t	uptime=0;
    volatile uint	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	scfg_t	scfg;
    static	struct mqtt mqtt;
    static	char *	text[TOTAL_TEXT];
    static	scfg_t	node_scfg[MAX_NODES];
    static	char *	node_text[MAX_NODES][TOTAL_TEXT];
    static	WORD	first_node;
    static	WORD	last_node;
    static	bool	terminate_server=false;
    static	str_list_t pause_semfiles;
    static	str_list_t recycle_semfiles;
    static	str_list_t shutdown_semfiles;
    static	str_list_t clear_attempts_semfiles;
    static	link_list_t current_logins;
    static	link_list_t current_connections;
    #ifdef _THREAD_SUID_BROKEN
    int	thread_suid_broken=true;			/* NPTL is no longer broken */
    #endif
    
    /* convenient space-saving global variables */
    extern "C" {
    const char* crlf="\r\n";
    const char* nulstr="";
    };
    
    #define GCES(status, node, sess, action) do {                          \
    	char *GCES_estr;                                                    \
    	int GCES_level;                                                      \
    	get_crypt_error_string(status, sess, &GCES_estr, action, &GCES_level);\
    	if (GCES_estr) {                                                       \
    		if (GCES_level < startup->ssh_error_level)							\
    			GCES_level = startup->ssh_error_level;							 \
    		lprintf(GCES_level, "Node %d SSH %s from %s", node, GCES_estr, __FUNCTION__);             \
    		free_crypt_attrstr(GCES_estr);                                       \
    	}                                                                         \
    } while (0)
    
    #define GCESNN(status, sess, action) do {                              \
    	char *GCES_estr;                                                    \
    	int GCES_level;                                                      \
    	get_crypt_error_string(status, sess, &GCES_estr, action, &GCES_level);\
    	if (GCES_estr) {                                                       \
    		if (GCES_level < startup->ssh_error_level)							\
    			GCES_level = startup->ssh_error_level;							 \
    		lprintf(GCES_level, "SSH %s from %s", GCES_estr, __FUNCTION__);     \
    		free_crypt_attrstr(GCES_estr);                                       \
    	}                                                                         \
    } while (0)
    
    #define GCESS(status, sock, sess, action) do {                         \
    	char *GCES_estr;                                                    \
    	int GCES_level;                                                      \
    	get_crypt_error_string(status, sess, &GCES_estr, action, &GCES_level);\
    	if (GCES_estr) {                                                       \
    		if (GCES_level < startup->ssh_error_level)							\
    			GCES_level = startup->ssh_error_level;							 \
    		lprintf(GCES_level, "%04d SSH %s from %s", sock, GCES_estr, __FUNCTION__);                \
    		free_crypt_attrstr(GCES_estr);                                       \
    	}                                                                         \
    } while (0)
    
    #define GCESSTR(status, str, sess, action) do {                         \
    	char *GCES_estr;                                                    \
    	int GCES_level;                                                      \
    	get_crypt_error_string(status, sess, &GCES_estr, action, &GCES_level);\
    	if (GCES_estr) {                                                       \
    		if (GCES_level < startup->ssh_error_level)							\
    			GCES_level = startup->ssh_error_level;							 \
    		lprintf(GCES_level, "%s SSH %s from %s (session %d)", str, GCES_estr, __FUNCTION__, sess);                \
    		free_crypt_attrstr(GCES_estr);                                       \
    	}                                                                         \
    } while (0)
    
    
    extern "C" {
    
    static bbs_startup_t* startup=NULL;
    
    static void set_state(enum server_state state)
    {
    	static int curr_state;
    
    	if(state == curr_state)
    		return;
    
    	if(startup != NULL) {
    		if(startup->set_state != NULL)
    			startup->set_state(startup->cbdata, state);
    		mqtt_server_state(&mqtt, state);
    	}
    	curr_state = state;
    }
    
    static void update_clients()
    {
    	if(startup != NULL) {
    		if(startup->clients != NULL)
    			startup->clients(startup->cbdata,protected_uint32_value(node_threads_running));
    	}
    }
    
    void client_on(SOCKET sock, client_t* client, BOOL update)
    {
    	if(!update)
    		listAddNodeData(&current_connections, client->addr, strlen(client->addr)+1, sock, LAST_NODE);
    	if(startup!=NULL && startup->client_on!=NULL)
    		startup->client_on(startup->cbdata,true,sock,client,update);
    	mqtt_client_on(&mqtt, true, sock, client, update);
    }
    
    static void client_off(SOCKET sock)
    {
    	listRemoveTaggedNode(&current_connections, sock, /* free_data */true);
    	if(startup!=NULL && startup->client_on!=NULL)
    		startup->client_on(startup->cbdata,false,sock,NULL,false);
    	mqtt_client_on(&mqtt, 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)
    {
    	mqtt_lputs(&mqtt, TOPIC_SERVER, level, str);
    	if(level <= LOG_ERR) {
    		char errmsg[1024];
    		SAFEPRINTF2(errmsg, "%-4s %s", server_abbrev, str);
    		errorlog(&scfg, &mqtt, level, startup==NULL ? NULL:startup->host_name, errmsg);
    		if(startup!=NULL && startup->errormsg!=NULL)
    			startup->errormsg(startup->cbdata,level,errmsg);
    	}
    
    	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 eputs(int level, const char *str)
    {
    	if(*str == 0)
    		return 0;
    
    	mqtt_lputs(&mqtt, TOPIC_HOST_EVENT, level, str);
    
    	if(level <= LOG_ERR) {
    		char errmsg[1024];
    		SAFEPRINTF(errmsg, "evnt %s", str);
    		errorlog(&scfg, &mqtt, level, startup==NULL ? NULL:startup->host_name, errmsg);
    		if(startup!=NULL && startup->errormsg!=NULL)
    			startup->errormsg(startup->cbdata, level, errmsg);
    	}
    
    	if(startup==NULL || startup->event_lputs==NULL || level > startup->log_level)
    		return(0);
    
    	return(startup->event_lputs(startup->event_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));
    }
    
    void
    sbbs_t::log_crypt_error_status_sock(int status, const char *action)
    {
    	char *estr;
    	int level;
    	if (cryptStatusError(status)) {
    		get_crypt_error_string(status, ssh_session, &estr, action, &level);
    		if (estr) {
    			if (level < startup->ssh_error_level)
    				level = startup->ssh_error_level;
    			lprintf(level, "%04d SSH %s", client_socket, estr);
    			free_crypt_attrstr(estr);
    		}
    	}
    }
    
    /* Picks the right log callback function (event or term) based on the sbbs->cfg.node_num value */
    /* Prepends the current node number and user alias (if applicable) */
    int sbbs_t::lputs(int level, const char* str)
    {
    	char msg[2048];
    	char prefix[32] = "";
    	char user_str[64] = "";
    
    	if(is_event_thread && event_code != NULL && *event_code)
    		SAFEPRINTF(prefix, "%s ", event_code);
    	else if(cfg.node_num && !is_event_thread)
    		SAFEPRINTF(prefix, "Node %d ", cfg.node_num);
    	else if(client_name[0])
    		SAFEPRINTF(prefix, "%s ", client_name);
    	if(useron.number)
    		SAFEPRINTF(user_str, "<%s> ", useron.alias);
    	SAFEPRINTF3(msg, "%s%s%s", prefix, user_str, str);
    	strip_ctrl(msg, msg);
    	if(is_event_thread)
    		return ::eputs(level, msg);
    	return ::lputs(level, msg);
    }
    
    int sbbs_t::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));
    }
    
    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);
    }
    
    void call_socket_open_callback(bool open)
    {
    	if(startup!=NULL && startup->socket_open!=NULL)
    		startup->socket_open(startup->cbdata, open);
    }
    
    SOCKET open_socket(int domain, int type, const char* protocol)
    {
    	SOCKET	sock;
    	char	error[256];
    
    	sock=socket(domain, type, IPPROTO_IP);
    	if(sock!=INVALID_SOCKET)
    		call_socket_open_callback(true);
    	if(sock!=INVALID_SOCKET && set_socket_options(&scfg, sock, protocol, error, sizeof(error)))
    		lprintf(LOG_ERR,"%04d !ERROR %s",sock,error);
    
    	return(sock);
    }
    
    // Used by sbbs_t::ftp_put() and js_accept()
    SOCKET accept_socket(SOCKET s, union xp_sockaddr* addr, socklen_t* addrlen)
    {
    	SOCKET	sock;
    
    	sock=accept(s,&addr->addr,addrlen);
    	if(sock!=INVALID_SOCKET)
    		call_socket_open_callback(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);
    	call_socket_open_callback(false);
    	if(result!=0 && ERROR_VALUE!=ENOTSOCK)
    		lprintf(LOG_WARNING,"!ERROR %d closing socket %d",ERROR_VALUE,sock);
    	return(result);
    }
    
    /* TODO: IPv6 */
    in_addr_t resolve_ip(char *addr)
    {
    	HOSTENT*	host;
    	char*		p;
    
    	if(*addr==0)
    		return INADDR_NONE;
    
    	for(p=addr;*p;p++)
    		if(*p!='.' && !IS_DIGIT(*p))
    			break;
    	if(!(*p))
    		return(inet_addr(addr));
    	if((host=gethostbyname(addr))==NULL)
    		return INADDR_NONE;
    	if(host->h_addr_list[0] == NULL)
    		return INADDR_NONE;
    	return *((in_addr_t*)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 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 sbbs_random(int n)
    {
    	return(xp_random(n));
    }
    
    #ifdef JAVASCRIPT
    
    static js_server_props_t js_server_props;
    
    void* js_GetClassPrivate(JSContext *cx, JSObject *obj, JSClass* cls)
    {
    	void *ret = JS_GetInstancePrivate(cx, obj, cls, NULL);
    
    	/*
    	 * NOTE: Any changes here should also be added to the same function in jsdoor.c
    	 *       (ie: anything not Synchronet specific).
    	 */
    	if(ret == NULL)
    		JS_ReportError(cx, "'%s' instance: No Private Data or Class Mismatch"
    			, cls == NULL ? "???" : cls->name);
    	return ret;
    }
    
    JSBool
    js_CreateArrayOfStrings(JSContext* cx, JSObject* parent, const char* name, const 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
    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_FALSE
    		&& JS_DefineProperty(cx,obj,"_ver"
    			,INT_TO_JSVAL(ver),NULL,NULL,JSPROP_READONLY) != JS_FALSE);
    }
    
    JSBool
    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* property_array_name = "_property_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
    js_DefineSyncProperties(JSContext *cx, JSObject *obj, jsSyncPropertySpec* props)
    {
    	uint		i;
    	int			ver;
    	jsval		val;
    	JSString*	js_str;
    	JSObject*	array;
    	JSObject*	prop;
    
    	if((array=JS_NewObject(cx, NULL, NULL, obj))==NULL)
    		return(JS_FALSE);
    
    	for(i=0; props[i].name != NULL; ++i) {
    		if (props[i].tinyid < 256 && props[i].tinyid > -129) {
    			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);
    		}
    		else {
    			if(!JS_DefineProperty(cx, obj, props[i].name, JSVAL_VOID, NULL, NULL, props[i].flags|JSPROP_SHARED))
    				return(JS_FALSE);
    		}
    		if (!(props[i].flags & JSPROP_ENUMERATE))	/* No need to document invisible props */
    			continue;
    
    		prop = JS_NewObject(cx, NULL, NULL, array);
    		if (prop == NULL)
    			return JS_FALSE;
    
    		if((ver=props[i].ver) < 10000)		/* auto convert 313 to 31300 */
    			ver*=100;
    		val = INT_TO_JSVAL(ver);
    		JS_SetProperty(cx, prop, "ver", &val);
    
    		if (props[i].desc != NULL) {
    			if ((js_str = JS_NewStringCopyZ(cx, props[i].desc)) == NULL)
    				return JS_FALSE;
    			val = STRING_TO_JSVAL(js_str);
    			JS_SetProperty(cx, prop, "desc", &val);
    		}
    		if (!JS_DefineProperty(cx, array, props[i].name, OBJECT_TO_JSVAL(prop), NULL, NULL, JSPROP_READONLY | JSPROP_ENUMERATE))
    			return JS_FALSE;
    	}
    
    	return JS_DefineProperty(cx, obj, property_array_name, OBJECT_TO_JSVAL(array), NULL, NULL, JSPROP_READONLY);
    }
    
    JSBool
    js_DefineSyncMethods(JSContext* cx, JSObject* obj, jsSyncMethodSpec *funcs)
    {
    	int			i;
    	jsuint		len=0;
    	int			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 < (int)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 != NULL; ++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);
    		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
    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
    js_DefineSyncProperties(JSContext *cx, JSObject *obj, jsSyncPropertySpec* props)
    {
    	uint i;
    
    	/*
    	 * NOTE: Any changes here should also be added to the same function in jsdoor.c
    	 *       (ie: anything not Synchronet specific).
    	 */
    	for(i=0;props[i].name;i++) {
    		if (props[i].tinyid < 256 && props[i].tinyid > -129) {
    			if(!JS_DefinePropertyWithTinyId(cx, obj,
    			    props[i].name,props[i].tinyid, JSVAL_VOID, NULL, NULL, props[i].flags|JSPROP_SHARED))
    				return(JS_FALSE);
    		}
    		else {
    			if(!JS_DefineProperty(cx, obj, props[i].name, JSVAL_VOID, NULL, NULL, props[i].flags|JSPROP_SHARED))
    				return(JS_FALSE);
    		}
    	}
    
    	return(JS_TRUE);
    }
    
    
    JSBool
    js_DefineSyncMethods(JSContext* cx, JSObject* obj, jsSyncMethodSpec *funcs)
    {
    	uint i;
    
    	/*
    	 * NOTE: Any changes here should also be added to the same function in jsdoor.c
    	 *       (ie: anything not Synchronet specific).
    	 */
    	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
    js_SyncResolve(JSContext* cx, JSObject* obj, char *name, jsSyncPropertySpec* props, jsSyncMethodSpec* funcs, jsConstIntSpec* consts, int flags)
    {
    	uint i;
    	jsval	val;
    
    	/*
    	 * NOTE: Any changes here should also be added to the same function in jsdoor.c
    	 *       (ie: anything not Synchronet specific).
    	 */
    	if(props) {
    		for(i=0;props[i].name;i++) {
    			if(name==NULL || strcmp(name, props[i].name)==0) {
    				if (props[i].tinyid < 256 && props[i].tinyid > -129) {
    					if(!JS_DefinePropertyWithTinyId(cx, obj,
    					    props[i].name,props[i].tinyid, JSVAL_VOID, NULL, NULL, props[i].flags|JSPROP_SHARED))
    						return(JS_FALSE);
    				}
    				else {
    					if(!JS_DefineProperty(cx, obj, props[i].name, 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
    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) {
    			FREE_AND_NULL(line);
    			return(JS_FALSE);
    		}
    		JSSTRING_TO_RASTRING(cx, str, line, &line_sz, NULL);
    		if(line==NULL)
    		    return(JS_FALSE);
    		rc=JS_SUSPENDREQUEST(cx);
    		sbbs->lputs(level, line);
    		JS_RESUMEREQUEST(cx, rc);
    	}
    	if(line != NULL)
    		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) {
    			FREE_AND_NULL(cstr);
    			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_REMOTE)
    			sbbs->lputs(LOG_INFO, cstr);
    		else
    			sbbs->bputs(cstr);
    		JS_RESUMEREQUEST(cx, rc);
    	}
    	FREE_AND_NULL(cstr);
    
    	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);
    		if(len < 1)
    			continue;
    		rc=JS_SUSPENDREQUEST(cx);
    		sbbs->putcom(str, len);
    		JS_RESUMEREQUEST(cx, rc);
    	}
    	if (str != NULL)
    		free(str);
    
        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_REMOTE)
    		sbbs->lputs(LOG_INFO, 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);
    	if(sbbs->online != ON_REMOTE)
    		sbbs->lputs(LOG_WARNING, cstr);
    	else {
    		sbbs->attr(sbbs->cfg.color[clr_err]);
    		sbbs->bputs(cstr);
    		sbbs->attr(LIGHTGRAY);
    		sbbs->bputs(crlf);
    	}
    	free(cstr);
    	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[128] = "";
        JSString *	str;
    	sbbs_t*		sbbs;
    	jsrefcount	rc;
        char*		prompt=NULL;
    	int32		mode = K_EDIT;
    	size_t		result;
    
    	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
    
    	if((sbbs=(sbbs_t*)JS_GetContextPrivate(cx))==NULL)
    		return(JS_FALSE);
    
    	uintN argn = 0;
    	if(argc > argn && JSVAL_IS_STRING(argv[argn])) {
    		JSVALUE_TO_MSTRING(cx, argv[argn], prompt, NULL);
    		if(prompt==NULL)
    			return(JS_FALSE);
    		argn++;
    	}
    	if(argc > argn && JSVAL_IS_STRING(argv[argn])) {
    		JSVALUE_TO_STRBUF(cx, argv[argn], instr, sizeof(instr), NULL);
    		argn++;
    	}
    	if(argc > argn && JSVAL_IS_NUMBER(argv[argn])) {
    		if(!JS_ValueToInt32(cx,argv[argn], &mode)) {
    			free(prompt);
    			return JS_FALSE;
    		}
    		argn++;
    	}
    
    	rc=JS_SUSPENDREQUEST(cx);
    	if(prompt != NULL) {
    		sbbs->bprintf("\1n\1y\1h%s\1w: ",prompt);
    		free(prompt);
    	}
    
    	result = sbbs->getstr(instr, sizeof(instr)-1, mode);
    	sbbs->attr(LIGHTGRAY);
    	if(!result) {
    		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("[<i>number</i> level=LOG_INFO,] value [,value]")
    	,JSDOCSTR("Add a line of text to the server and/or system log.<br>"
    		"<i>values</i> are typically string constants or variables (each logged as a separate log message),<br>"
    		"<i>level</i> is the severity of the message to be logged, one of the globally-defined values, in decreasing severity:<br>"
    		"<tt>LOG_EMERG, LOG_ALERT, LOG_CRIT, LOG_ERR, LOG_WARNING, LOG_NOTICE, LOG_INFO, and LOG_DEBUG</tt> (default: <tt>LOG_INFO</tt>)")
    	,311
    	},
    	{"read",			js_read,			0,	JSTYPE_STRING,	JSDOCSTR("[count=128]")
    	,JSDOCSTR("Read up to <tt>count</tt> characters from input stream and return as a string or <tt>undefined</tt> upon error")
    	,311
    	},
    	{"readln",			js_readln,			0,	JSTYPE_STRING,	JSDOCSTR("[count=128]")
    	,JSDOCSTR("Read a single line, up to <tt>count</tt> characters, from input stream and return as a string or <tt>undefined</tt> upon error")
    	,311
    	},
    	{"write",			js_write,			0,	JSTYPE_VOID,	JSDOCSTR("value [,value]")
    	,JSDOCSTR("Send one or more values (typically strings) to the output stream")
    	,311
    	},
    	{"write_raw",		js_write_raw,			0,	JSTYPE_VOID,	JSDOCSTR("value [,value]")
    	,JSDOCSTR("Send a stream of bytes (possibly containing NUL or special control code sequences) to the output stream")
    	,314
    	},
    	{"print",			js_writeln,			0,	JSTYPE_ALIAS },
        {"writeln",         js_writeln,         0,	JSTYPE_VOID,	JSDOCSTR("value [,value]")
    	,JSDOCSTR("Send a line of text to the output stream with automatic line termination (CRLF), "
    		"<i>values</i> are typically string constants or variables (AKA print)")
    	,311
    	},
        {"printf",          js_printf,          1,	JSTYPE_STRING,	JSDOCSTR("<i>string</i> format [,value][,value]")
    	,JSDOCSTR("Send a C-style formatted string of text to the output stream.  See also the <tt>format()</tt> function.")
    	,310
    	},
    	{"alert",			js_alert,			1,	JSTYPE_VOID,	JSDOCSTR("value")
    	,JSDOCSTR("Send an alert message (ala client-side JS) to the output stream")
    	,310
    	},
    	{"prompt",			js_prompt,			1,	JSTYPE_STRING,	JSDOCSTR("[<i>string</i> text] [,<i>string</i> value] [,<i>number</i> k_mode=K_EDIT]")
    	,JSDOCSTR("Display a prompt (<tt>text</tt>) and return a string of user input (ala client-side JS) or <tt>null</tt> upon no-input<br>"
    		"<tt>value</tt> is an optional default string to be edited (used with the <tt>k_mode K_EDIT</tt> flag)<br>"
    		"See <tt>sbbsdefs.js</tt> for all valid <tt>K_</tt> (keyboard-input) mode flags.")
    	,310
    	},
    	{"confirm",			js_confirm,			1,	JSTYPE_BOOLEAN,	JSDOCSTR("value")
    	,JSDOCSTR("Display a Yes/No prompt and return <tt>true</tt> or <tt>false</tt> "
    		"based on user's confirmation (ala client-side JS, <tt>true</tt> = yes)<br>"
    		"see also <tt>console.yesno()<tt>")
    	,310
    	},
    	{"deny",			js_deny,			1,	JSTYPE_BOOLEAN,	JSDOCSTR("value")
    	,JSDOCSTR("Display a No/Yes prompt and returns <tt>true</tt> or <tt>false</tt> "
    		"based on user's denial (<tt>true</tt> = no)<br>"
    		"see also <tt>console.noyes()<tt>")
    	,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;
    
    	if((sbbs=(sbbs_t*)JS_GetContextPrivate(cx))==NULL)
    		return;
    
    	if(report==NULL) {
    		sbbs->lprintf(LOG_ERR,"!JavaScript: %s", 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);
    	sbbs->lprintf(log_level, "!JavaScript %s%s%s: %s",warning,file,line,message);
    	if(sbbs->online==ON_REMOTE)
    		sbbs->bprintf("!JavaScript %s%s%s: %s\r\n",warning,getfname(file),line,message);
    	JS_RESUMEREQUEST(cx, rc);
    }
    
    JSContext* sbbs_t::js_init(JSRuntime** runtime, JSObject** glob, const char* desc)
    {
    	JSContext* js_cx;
    
    	if(startup->js.max_bytes==0)			startup->js.max_bytes=JAVASCRIPT_MAX_BYTES;
    
    	lprintf(LOG_DEBUG,"JavaScript: Creating %s runtime: %u bytes"
    		,desc, startup->js.max_bytes);
    	if((*runtime = jsrt_GetNew(startup->js.max_bytes, 1000, __FILE__, __LINE__))==NULL) {
    		lprintf(LOG_NOTICE, "Failed to created new JavaScript runtime");
    		return NULL;
    	}
    
        if((js_cx = JS_NewContext(*runtime, JAVASCRIPT_CONTEXT_STACK))==NULL) {
    		lprintf(LOG_NOTICE, "Failed to create new JavaScript context");
    		return NULL;
    	}
    	JS_SetOptions(js_cx, startup->js.options);
    	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;
    	js_callback.events_supported = 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, server_host_name(), SOCKLIB_DESC	/* system */
    					,&js_callback								/* js */
    					,&startup->js
    					,client_socket == INVALID_SOCKET ? NULL : &client, client_socket, -1 /* client */
    					,&js_server_props							/* server */
    					,glob
    					,mqtt
    			))
    			break;
    		rooted=true;
    
    #ifdef BUILD_JSDOCS
    		js_CreateUifcObject(js_cx, *glob);
    		js_CreateConioObject(js_cx, *glob);
    #endif
    
    		/* BBS Object */
    		if(js_CreateBbsObject(js_cx, *glob)==NULL)
    			break;
    
    		/* Console Object */
    		if(js_CreateConsoleObject(js_cx, *glob)==NULL)
    			break;
    
    		success=true;
    
    	} while(0);
    
    	if(!success) {
    		if(rooted)
    			JS_RemoveObjectRoot(js_cx, glob);
    		JS_ENDREQUEST(js_cx);
    		JS_DestroyContext(js_cx);
    		js_cx=NULL;
    		return NULL;
    	}
    	else
    		JS_ENDREQUEST(js_cx);
    
    	return js_cx;
    }
    
    void sbbs_t::js_cleanup(void)
    {
    	/* Free Context */
    	if(js_cx!=NULL) {
    		lprintf(LOG_DEBUG,"JavaScript: Destroying context");
    		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,"JavaScript: Destroying runtime");
    		jsrt_Release(js_runtime);
    		js_runtime=NULL;
    	}
    
    	if(js_hotkey_cx!=NULL) {
    		lprintf(LOG_DEBUG,"JavaScript: Destroying HotKey context");
    		JS_BEGINREQUEST(js_hotkey_cx);
    		JS_RemoveObjectRoot(js_hotkey_cx, &js_hotkey_glob);
    		JS_ENDREQUEST(js_hotkey_cx);
    		JS_DestroyContext(js_hotkey_cx);
    		js_hotkey_cx=NULL;
    	}
    
    	if(js_hotkey_runtime!=NULL) {
    		lprintf(LOG_DEBUG,"JavaScript: Destroying HotKey runtime");
    		jsrt_Release(js_hotkey_runtime);
    		js_hotkey_runtime=NULL;
    	}
    
    }
    
    bool sbbs_t::js_create_user_objects(JSContext* cx, JSObject* glob)
    {
    	bool result = false;
    	if(cx != NULL) {
    		JS_BEGINREQUEST(cx);
    		if(!js_CreateUserObjects(cx, glob, &cfg, &useron, &client, startup == NULL ? NULL :startup->web_file_vpath_prefix, subscan, mqtt))
    			lprintf(LOG_ERR,"!JavaScript ERROR creating user objects");
    		else
    			result = true;
    		JS_ENDREQUEST(cx);
    	}
    	return result;
    }
    
    extern "C" bool 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
    										,struct mqtt* mqtt
    										)
    {
    	bool	success=false;
    
    	if(node_cfg==NULL)
    		node_cfg=cfg;
    
    	/* Global Object */
    	if(!js_CreateGlobalObject(js_cx, node_cfg, methods, js_startup, glob))
    		return(false);
    
    	do {
    		/*
    		 * NOTE: Where applicable, anything added here should also be added to
    		 *       the same function in jsdoor.c (ie: anything not Synchronet
    		 *       specific).
    		 */
    
    		/* System Object */
    		if(js_CreateSystemObject(js_cx, *glob, node_cfg, uptime, host_name, socklib_desc, mqtt)==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)==NULL)
    			break;
    
    		/* FileBase Class */
    		if(js_CreateFileBaseClass(js_cx, *glob)==NULL)
    			break;
    
    		/* File Class */
    		if(js_CreateFileClass(js_cx, *glob)==NULL)
    			break;
    
    		/* Archive Class */
    		if(js_CreateArchiveClass(js_cx, *glob)==NULL)
    			break;
    
    		/* User class */
    		if(js_CreateUserClass(js_cx, *glob)==NULL)
    			break;
    
    		/* COM Class */
    		if(js_CreateCOMClass(js_cx, *glob)==NULL)
    			break;
    
    		/* CryptContext Class */
    		if(js_CreateCryptContextClass(js_cx, *glob)==NULL)
    			break;
    
    		/* CryptKeyset Class */
    		if(js_CreateCryptKeysetClass(js_cx, *glob)==NULL)
    			break;
    
    		/* CryptCert Class */
    		if(js_CreateCryptCertClass(js_cx, *glob)==NULL)
    			break;
    
    #if defined USE_MOSQUITTO
    		if(js_CreateMQTTClass(js_cx, *glob) == NULL)
    			break;
    #endif
    		/* Area Objects */
    		if(!js_CreateUserObjects(js_cx, *glob, cfg, /* user: */NULL, client, startup == NULL ? NULL :startup->web_file_vpath_prefix, /* subscan: */NULL, mqtt))
    			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;
    
    	outlen=0;
    
    	if(inlen<1) {
    		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) {
    		if(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);
    		}
    	}
    
        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];
    			else {
    				lprintf(LOG_WARNING, "Node %d telnet command (%d, %d) buffer limit reached (%u bytes)"
    					,sbbs->cfg.node_num, sbbs->telnet_cmd[1], sbbs->telnet_cmd[2], sbbs->telnet_cmdlen);
    				sbbs->telnet_cmdlen = 0;
    			}
    
    			uchar command	= sbbs->telnet_cmd[1];
    			uchar option	= sbbs->telnet_cmd[2];
    
    			if(sbbs->telnet_cmdlen == 2 && command == TELNET_SE) {
    				lprintf(LOG_WARNING, "Node %d unexpected telnet sub-negotiation END command"
    					,sbbs->cfg.node_num);
    				sbbs->telnet_cmdlen = 0;
    			}
    			else if(sbbs->telnet_cmdlen>=2 && command==TELNET_SB) {
    				if(inbuf[i]==TELNET_SE
    					&& sbbs->telnet_cmd[sbbs->telnet_cmdlen-2]==TELNET_IAC) {
    					sbbs->telnet_cmds_received++;
    					if(startup->options&BBS_OPT_DEBUG_TELNET)
    						lprintf(LOG_DEBUG,"Node %d %s telnet sub-negotiation command: %s (%u bytes)"
    	                		,sbbs->cfg.node_num
    							,sbbs->telnet_mode&TELNET_MODE_GATE ? "passed-through" : "received"
    							,telnet_opt_desc(option)
    							,sbbs->telnet_cmdlen);
    
    					/* sub-option terminated */
    					if(option==TELNET_TERM_TYPE
    						&& sbbs->telnet_cmd[3]==TELNET_TERM_IS) {
    						safe_snprintf(sbbs->telnet_terminal,sizeof(sbbs->telnet_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->telnet_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->telnet_speed=atoi(speed);
    #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_TERM_LOCATION_NUMBER && sbbs->telnet_cmd[3] == 0) {
    						SAFEPRINTF4(sbbs->telnet_location, "%u.%u.%u.%u"
    							,sbbs->telnet_cmd[4]
    							,sbbs->telnet_cmd[5]
    							,sbbs->telnet_cmd[6]
    							,sbbs->telnet_cmd[7]
    						);
    						lprintf(LOG_DEBUG,"Node %d %s telnet location number (IP address): %s"
    	                		,sbbs->cfg.node_num
    							,sbbs->telnet_mode&TELNET_MODE_GATE ? "passed-through" : "received"
    							,sbbs->telnet_location);
    					} else if(option==TELNET_NEGOTIATE_WINDOW_SIZE) {
    						int cols = (sbbs->telnet_cmd[3]<<8) | sbbs->telnet_cmd[4];
    						int rows = (sbbs->telnet_cmd[5]<<8) | sbbs->telnet_cmd[6];
    						lprintf(LOG_DEBUG,"Node %d %s telnet window size: %dx%d"
    	                		,sbbs->cfg.node_num
    							,sbbs->telnet_mode&TELNET_MODE_GATE ? "passed-through" : "received"
    							,cols
    							,rows);
    						sbbs->telnet_cols = cols;
    						sbbs->telnet_rows = rows;
    					} 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;
    				}
    			} // Sub-negotiation command
                else if(sbbs->telnet_cmdlen==2 && inbuf[i]<TELNET_WILL) {
    				sbbs->telnet_cmds_received++;
    				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 */
    				sbbs->telnet_cmds_received++;
    				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:
    								case TELNET_TERM_LOCATION_NUMBER:
    #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 commands
    				outbuf[outlen++]=inbuf[i];
            } else
            	outbuf[outlen++]=inbuf[i];
        }
        return(outbuf);
    }
    
    void sbbs_t::send_telnet_cmd(uchar cmd, uchar opt)
    {
    	char buf[16];
    	int sz;
    	int result;
    
    	if(telnet_mode&TELNET_MODE_OFF)
    		return;
    
    	if(cmd<TELNET_WILL) {
    		if(startup->options&BBS_OPT_DEBUG_TELNET)
                lprintf(LOG_DEBUG,"sending telnet cmd: %s"
                    ,telnet_cmd_desc(cmd));
    		sprintf(buf,"%c%c",TELNET_IAC,cmd);
    		result = sendsocket(client_socket, buf, sz = 2);
    	} else {
    		if(startup->options&BBS_OPT_DEBUG_TELNET)
    			lprintf(LOG_DEBUG,"sending telnet cmd: %s %s"
    				,telnet_cmd_desc(cmd)
    				,telnet_opt_desc(opt));
    		sprintf(buf,"%c%c%c",TELNET_IAC,cmd,opt);
    		result = sendsocket(client_socket, buf, sz = 3);
    	}
    	if(result != sz)
    		lprintf(LOG_DEBUG, "ERROR sending telnet command (%s): send returned %d instead of %d"
    			,telnet_cmd_desc(cmd)
    			,result
    			,sz);
    }
    
    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;
    }
    
    static bool channel_open(sbbs_t *sbbs, int channel)
    {
    	int rval;
    
    	if (cryptStatusError(cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL, sbbs->session_channel)))
    		return false;
    	if (cryptStatusError(cryptGetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_OPEN, &rval)))
    		return false;
    	return rval;
    }
    
    static int crypt_pop_channel_data(sbbs_t *sbbs, char *inbuf, int want, int *got)
    {
    	int status;
    	int cid;
    	char *cname = nullptr;
    	char *ssname = nullptr;
    	int ret;
    	int closing_channel = -1;
    	int tgot;
    
    	*got=0;
    	while(sbbs->online && sbbs->client_socket!=INVALID_SOCKET
    	    && node_socket[sbbs->cfg.node_num-1]!=INVALID_SOCKET) {
    		ret = cryptPopData(sbbs->ssh_session, inbuf, want, &tgot);
    		if (ret == CRYPT_OK) {
    			status = cryptGetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL, &cid);
    			if (status == CRYPT_OK) {
    				if (cid == closing_channel)
    					continue;
    				if (cid == sbbs->sftp_channel) {
    					pthread_mutex_unlock(&sbbs->ssh_mutex);
    					if (!sftps_recv(sbbs->sftp_state, reinterpret_cast<uint8_t *>(inbuf), tgot))
    						sbbs->sftp_end();
    					pthread_mutex_lock(&sbbs->ssh_mutex);
    				}
    				else if (cid == sbbs->session_channel) {
    					*got = tgot;
    				}
    				else {
    					if (cryptStatusError(status = cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL, cid))) {
    						sbbs->log_crypt_error_status_sock(status, "setting channel");
    						return status;
    					}
    					cname = get_crypt_attribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_TYPE);
    					if (strcmp(cname, "subsystem") == 0) {
    						ssname = get_crypt_attribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_ARG1);
    					}
    					if (((startup->options & (BBS_OPT_ALLOW_SFTP | BBS_OPT_SSH_ANYAUTH)) == BBS_OPT_ALLOW_SFTP) && ssname && cname && sbbs->sftp_channel == -1 && strcmp(ssname, "sftp") == 0) {
    						if (sbbs->init_sftp(cid)) {
    							if (tgot > 0) {
    								pthread_mutex_lock(&sbbs->ssh_mutex);
    								if (!sftps_recv(sbbs->sftp_state, reinterpret_cast<uint8_t *>(inbuf), tgot))
    									sbbs->sftp_end();
    								pthread_mutex_lock(&sbbs->ssh_mutex);
    							}
    							sbbs->sftp_channel = cid;
    						}
    					}
    					if (cname && sbbs->session_channel == -1 && strcmp(cname, "shell") == 0) {
    						sbbs->session_channel = cid;
    					}
    
    					if (cid != sbbs->sftp_channel && cid != sbbs->session_channel) {
    						if (cname == nullptr || strcmp(cname, "session") != 0) {
    							lprintf(LOG_WARNING, "Node %d SSH WARNING: attempt to use channel '%s' (%d != %d or %d)"
    								, sbbs->cfg.node_num, cname ? cname : "<unknown>", cid, sbbs->session_channel, sbbs->sftp_channel);
    							if (cname) {
    								free_crypt_attrstr(cname);
    								cname = nullptr;
    							}
    							if (ssname) {
    								free_crypt_attrstr(ssname);
    								ssname = nullptr;
    							}
    							closing_channel = cid;
    							if (cryptStatusError(status = cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_ACTIVE, 0))) {
    								sbbs->log_crypt_error_status_sock(status, "closing channel");
    								return status;
    							}
    						}
    					}
    					free_crypt_attrstr(cname);
    					cname = nullptr;
    					free_crypt_attrstr(ssname);
    					ssname = nullptr;
    
    					continue;
    				}
    				if (cid != -1) {
    					if (cryptStatusOK(cryptGetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_OPEN, &ret))) {
    						if (!ret) {
    							closing_channel = -1;
    							ret = CRYPT_ERROR_NOTFOUND;
    						}
    					}
    				}
    			}
    			else {
    				/*
    				 * CRYPT_ERROR_NOTFOUND indicates this is the last data on the channel (whatever it was)
    				 * and it was destroyed, so it's no longer possible to get the channel id.
    				 */
    				bool closed {false};
    				if (status != CRYPT_ERROR_NOTFOUND)
    					sbbs->log_crypt_error_status_sock(status, "getting channel id");
    				closing_channel = -1;
    				if (sbbs->sftp_channel != -1) {
    					if (!channel_open(sbbs, sbbs->sftp_channel)) {
    						if (cryptStatusOK(cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL, sbbs->sftp_channel)))
    							cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_ACTIVE, 0);
    						sbbs->sftp_channel = -1;
    						closed = true;
    					}
    				}
    				if (sbbs->session_channel != -1) {
    					if (!channel_open(sbbs, sbbs->session_channel)) {
    						if (cryptStatusOK(cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL, sbbs->session_channel)))
    							cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_ACTIVE, 0);
    						sbbs->session_channel = -1;
    						closed = true;
    					}
    				}
    				// All channels are now closed.
    				if (closed && sbbs->sftp_channel == -1 && sbbs->session_channel == -1)
    					return CRYPT_ERROR_COMPLETE;
    			}
    		}
    		if (ret == CRYPT_ENVELOPE_RESOURCE)
    			return CRYPT_ERROR_TIMEOUT;
    		return ret;
    	}
    	return CRYPT_ERROR_TIMEOUT;
    }
    
    void input_thread(void *arg)
    {
    	BYTE		inbuf[4000];
       	BYTE		telbuf[sizeof(inbuf)];
        BYTE		*wrbuf;
        int			i,rd,wr,avail;
    	uint		total_recv=0;
    	uint		total_pkts=0;
    	sbbs_t*		sbbs = (sbbs_t*) arg;
    	SOCKET sock;
    #ifdef PREFER_POLL
    	struct pollfd fds[2];
    	int nfds;
    #endif
    
    	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(sbbs->max_socket_inactivity && !(sbbs->console & CON_NO_INACT)) {
    			if(sbbs->cfg.inactivity_warn
    				&& sbbs->socket_inactive >= sbbs->max_socket_inactivity * (sbbs->cfg.inactivity_warn / 100.0)
    				&& !sbbs->socket_inactivity_warning_sent) {
    				sbbs->bputs(text[InactivityAlert]);
    				sbbs->socket_inactivity_warning_sent = true;
    			}
    			if(sbbs->socket_inactive > sbbs->max_socket_inactivity) {
    				lprintf(LOG_NOTICE, "Node %d maximum socket inactivity exceeded: %u seconds"
    					,sbbs->cfg.node_num, sbbs->max_socket_inactivity);
    				sbbs->bputs(text[CallBackWhenYoureThere]);
    				break;
    			}
    		}
    
    #ifdef _WIN32	// No spy sockets
    		if (!socket_readable(sbbs->client_socket, 1000)) {
    			++sbbs->socket_inactive;
    			continue;
    		}
    #else
    #ifdef PREFER_POLL
    		fds[0].fd = sbbs->client_socket;
    		fds[0].events = POLLIN;
    		nfds = 1;
    		if (uspy_socket[sbbs->cfg.node_num-1] != INVALID_SOCKET) {
    			fds[1].fd = uspy_socket[sbbs->cfg.node_num-1];
    			fds[1].events = POLLIN;
    			nfds++;
    		}
    
    		if (poll(fds, nfds, 1000) < 1) {
    			++sbbs->socket_inactive;
    			continue;
    		}
    #else
    #error Spy sockets without poll() was removed in commit 3971ef4dcc3db19f400a648b6110718e56a64cf3
    #endif
    #endif
    
    		if(sbbs->client_socket==INVALID_SOCKET)
    			break;
    
    		sbbs->socket_inactive = 0;
    		sbbs->socket_inactivity_warning_sent = false;
    
    /*         ^          ^
     *      \______    ______/
     *       \  * \   / *   /
     *        -----   ------           /----\
     *              ||               -< Boo! |
     *             /__\                \----/
     *       \______________/
     *        \/\/\/\/\/\/\/
     *         ------------
     */
    
    #ifdef _WIN32	// No spy sockets
    		sock=sbbs->client_socket;
    #else
    #ifdef PREFER_POLL
    		if (fds[0].revents & POLLIN)
    			sock = sbbs->client_socket;
    		else if(uspy_socket[sbbs->cfg.node_num - 1] != INVALID_SOCKET && fds[1].revents & POLLIN) {
    			if(socket_recvdone(uspy_socket[sbbs->cfg.node_num-1], 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;
    			}
    			sock=uspy_socket[sbbs->cfg.node_num-1];
    		}
    		else {
    			continue;
    		}
    #else
    #error Spy sockets without poll() was removed in commit 3971ef4dcc3db19f400a648b6110718e56a64cf3
    #endif
    #endif
        	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 */
    				continue;
    		}
    
    		if(rd > (int)sizeof(inbuf))
    			rd=sizeof(inbuf);
    
    		if(pthread_mutex_lock(&sbbs->input_thread_mutex)!=0)
    			sbbs->errormsg(WHERE,ERR_LOCK,"input_thread_mutex",0);
    
    #ifdef USE_CRYPTLIB
    		if(sbbs->ssh_mode && sock==sbbs->client_socket) {
    			int err;
    			if (WaitForEvent(sbbs->ssh_active, 1000) == WAIT_TIMEOUT) {
    				pthread_mutex_unlock(&sbbs->input_thread_mutex);
    				continue;
    			}
    			pthread_mutex_lock(&sbbs->ssh_mutex);
    			if(cryptStatusError((err=crypt_pop_channel_data(sbbs, (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_COMPLETE)
    					break;
    				if(err==CRYPT_ERROR_TIMEOUT)
    					continue;
    				/* Handle the SSH error here... */
    				GCES(err, sbbs->cfg.node_num, sbbs->ssh_session, "popping data");
    				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 == 0 && !socket_recvdone(sock, 0))
    			continue;
    
    		if(rd == SOCKET_ERROR)
    		{
    #ifdef __unix__
    			if(sock==sbbs->client_socket)  {
    #endif
    				if(!sbbs->online)	// sbbs_t::hangup() called?
    					break;
    				if(ERROR_VALUE == EAGAIN)
    					continue;
    	        	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 (%s) on local spy socket %d receive"
    						, sbbs->cfg.node_num, errno, strerror(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,(int)sizeof(telbuf));
    			wr = sizeof(telbuf);
    		}
    
    		if(!(sbbs->console & CON_RAW_IN))
    			sbbs->translate_input(wrbuf, wr);
    
    		if(sbbs->passthru_socket_active == true) {
    			bool writable = false;
    			if(socket_check(sbbs->passthru_socket, NULL, &writable, 1000) && writable) {
    				if(sendsocket(sbbs->passthru_socket, (char*)wrbuf, wr) != wr)
    					lprintf(LOG_ERR, "Node %d ERROR %d writing to passthru socket"
    						,sbbs->cfg.node_num, ERROR_VALUE);
    			} else
    				lprintf(LOG_WARNING, "Node %d could not write to passthru socket (writable=%d)"
    					, sbbs->cfg.node_num, (int)writable);
    			continue;
    		}
    
    		/* 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 %u bytes in input buffer"
    					,sbbs->cfg.node_num,RingBufFull(&sbbs->inbuf));
    			if(RingBufFull(&sbbs->outbuf))
        			lprintf(LOG_DEBUG,"Node %d Ctrl-C hit with %u 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 */
    			SetEvent(sbbs->inbuf.data_event);
    			SetEvent(sbbs->inbuf.highwater_event);
    			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 */
    	SetEvent(sbbs->inbuf.data_event); // terminate incom() wait
    
        sbbs->input_thread_running = false;
    	sbbs->terminate_output_thread = true;
    	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 %u bytes in %u blocks)"
    		,sbbs->cfg.node_num, total_recv, total_pkts);
    }
    
    // Flush the duplicate client_socket when activating the passthru socket
    // to eliminate any stale data from the previous passthru session
    void sbbs_t::passthru_socket_activate(bool activate)
    {
    	time_t timeout = time(NULL) + 60;
    
    	if(activate) {
    		bool rd = false;
    		while(socket_check(client_socket_dup, &rd, /* wr_p */NULL, /* timeout */0) && rd && time(NULL) < timeout) {
    			char ch;
    			if(recv(client_socket_dup, &ch, sizeof(ch), /* flags: */0) != sizeof(ch))
    				break;
    		}
    	} else {
    		/* Re-enable blocking (in case disabled by external program) */
    		u_long l=0;
    		ioctlsocket(client_socket_dup, FIONBIO, &l);
    
    		/* Re-set socket options */
    		char err[512];
    		if(set_socket_options(&cfg, client_socket_dup, "passthru", err, sizeof(err)))
    			lprintf(LOG_ERR,"%04d !ERROR %s setting passthru socket options", client_socket, err);
    
    		do { // Allow time for the passthru_thread to move any pending socket data to the outbuf
    			SLEEP(100); // Before the node_thread starts sending its own data to the outbuf
    		} while(RingBufFull(&outbuf) && time(NULL) < timeout);
    	}
    	passthru_socket_active = activate;
    }
    
    /*
     * This thread simply copies anything it manages to read from the
     * passthru_socket into the output ringbuffer.
     */
    void passthru_thread(void* arg)
    {
    	sbbs_t	*sbbs = (sbbs_t*) arg;
    	int		rd;
    	char	inbuf[IO_THREAD_BUF_SIZE / 2];
    
    	SetThreadName("sbbs/passthru");
    	thread_up(false /* setuid */);
    
    	while(sbbs->online && sbbs->passthru_socket!=INVALID_SOCKET && !terminate_server) {
    		if (!socket_readable(sbbs->passthru_socket, 1000))
    			continue;
    
    		if(sbbs->passthru_socket==INVALID_SOCKET)
    			break;
    
    		rd = RingBufFree(&sbbs->outbuf) / 2;
    		if(rd > (int)sizeof(inbuf))
    			rd = sizeof(inbuf);
    
        	rd = recv(sbbs->passthru_socket, inbuf, rd, 0);
    
    		if(rd == 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(rd == 0)
    		{
    			char ch;
    			if(recv(sbbs->passthru_socket, &ch, sizeof(ch), MSG_PEEK) == 0) {
    				lprintf(sbbs->online ? LOG_WARNING : LOG_DEBUG
    					,"Node %d passthru socket disconnected", sbbs->cfg.node_num);
    				break;
    			}
    			YIELD();
    			continue;
    		}
    
    		if(sbbs->xtrn_mode & EX_BIN) {
    			BYTE	telnet_buf[sizeof(inbuf) * 2];
    			BYTE*	bp = (BYTE*)inbuf;
    
    			if(!(sbbs->telnet_mode&TELNET_MODE_OFF))
    				rd = telnet_expand((BYTE*)inbuf, rd, telnet_buf, sizeof(telnet_buf), /* expand_cr */false, &bp);
    
    			int wr = RingBufWrite(&sbbs->outbuf, bp, rd);
        		if(wr != rd) {
    				lprintf(LOG_ERR,"Short-write (%d of %d bytes) from passthru socket to outbuf"
    					,wr, rd);
    				break;
    			}
    		} else {
    			sbbs->rputs(inbuf, rd);
    		}
    	}
    	if(sbbs->passthru_socket!=INVALID_SOCKET) {
    		close_socket(sbbs->passthru_socket);
    		sbbs->passthru_socket=INVALID_SOCKET;
    	}
    	thread_down();
    
    	sbbs->passthru_thread_running = false;
    	sbbs->passthru_socket_active = false;
    }
    
    void output_thread(void* arg)
    {
    	char		node[128];
    	char		stats[128];
    	char		spy_topic[128];
    	BYTE		buf[IO_THREAD_BUF_SIZE];
    	int			i=0;	// Assignment to silence Valgrind
    	uint		avail;
    	uint		total_sent=0;
    	uint		total_pkts=0;
    	uint		short_sends=0;
    	uint		bufbot=0;
    	uint		buftop=0;
    	sbbs_t*		sbbs = (sbbs_t*) arg;
    	uint		mss=IO_THREAD_BUF_SIZE;
    	uint		ssh_errors = 0;
    
    	SetThreadName("sbbs/termOutput");
    	thread_up(true /* setuid */);
    
    	if(sbbs->cfg.node_num) {
    		SAFEPRINTF(node,"Node %d",sbbs->cfg.node_num);
    		SAFEPRINTF(spy_topic, "node/%d/output", 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)
    	 */
    	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) {
    #ifdef _WIN32
    #ifdef TCP_TIMESTAMPS
    			DWORD ts;
    			sl = sizeof(ts);
    			if (!getsockopt(sbbs->client_socket, IPPROTO_TCP, TCP_TIMESTAMPS, (char *)&ts, &sl)) {
    				if (ts)
    					i -= 12;
    			}
    #endif
    #else
    #if (defined(TCP_INFO) && defined(TCPI_OPT_TIMESTAMPS))
    			struct tcp_info tcpi;
    
    			sl = sizeof(tcpi);
    			if (!getsockopt(sbbs->client_socket, IPPROTO_TCP, TCP_INFO,&tcpi, &sl)) {
    				if (tcpi.tcpi_options & TCPI_OPT_TIMESTAMPS)
    					i -= 12;
    			}
    #endif
    #endif
    			if(i>IO_THREAD_BUF_SIZE)
    				i=IO_THREAD_BUF_SIZE;
    			sbbs->outbuf.highwater_mark=i;
    			mss=sbbs->outbuf.highwater_mark;
    			lprintf(LOG_DEBUG, "%s outbuf highwater mark tuned to %d based on MSS", node, sbbs->outbuf.highwater_mark);
    		}
    	}
    #endif
    	sbbs->terminate_output_thread = false;
    
    	/* Note: do not terminate when online==false, that is expected for the terminal server output_thread */
    	while (sbbs->client_socket != INVALID_SOCKET && !terminate_server && !sbbs->terminate_output_thread) {
    		/*
    		 * 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
    		 * probably be even clumsier
    		 */
    		if(bufbot == buftop) {
    			/* Wait for something to output in the RingBuffer */
    			if (WaitForEvent(sbbs->outbuf.data_event, 1000) != WAIT_OBJECT_0)
    				continue;
    
    			/* Wait for full buffer or drain timeout */
    			if(sbbs->outbuf.highwater_mark)
    				WaitForEvent(sbbs->outbuf.highwater_event, startup->outbuf_drain_timeout);
    
    			/* Get fill level */
    			avail = RingBufFull(&sbbs->outbuf);
    
    			// If flushing or terminating, there will be nothing available
    			if (avail == 0) {
    				// Reset data/highwater events
    				RingBufRead(&sbbs->outbuf, NULL, 0);
    				continue;
    			}
    
    			/*
    			 * 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 (%u > %d)"
    					,node, avail, (int)sizeof(buf));
    				avail = sizeof(buf);
    			}
    			/* If we know the MSS, use it as the max linear buffer size. */
    			if (avail > mss)
    				avail = mss;
    			buftop = RingBufRead(&sbbs->outbuf, buf, avail);
    			bufbot = 0;
    			if (buftop == 0)
    				continue;
    		}
    
    		/* Check socket for writability */
    		if (!socket_writable(sbbs->client_socket, 1)) {
    			continue;
    		}
    
    #ifdef USE_CRYPTLIB
    		if(sbbs->ssh_mode) {
    			int err;
    			pthread_mutex_lock(&sbbs->ssh_mutex);
    			if(sbbs->terminate_output_thread) {
    				pthread_mutex_unlock(&sbbs->ssh_mutex);
    				break;
    			}
    			if(!sbbs->ssh_mode) {
    				pthread_mutex_unlock(&sbbs->ssh_mutex);
    				continue;
    			}
    			if (sbbs->session_channel == -1) {
    				i=buftop-bufbot;	// Pretend we sent it all
    			}
    			else {
    				if (cryptStatusError((err=cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL, sbbs->session_channel)))) {
    					GCESSTR(err, node, sbbs->ssh_session, "setting channel");
    					ssh_errors++;
    					sbbs->online=false;
    					i=buftop-bufbot;	// Pretend we sent it all
    				}
    				else {
    					/*
    					 * Limit as per js_socket.c.
    					 * Sure, this is TLS, not SSH, but we see weird stuff here in sz file transfers.
    					 */
    					size_t sendbytes = buftop-bufbot;
    					if (sendbytes > 0x2000)
    						sendbytes = 0x2000;
    					if(cryptStatusError((err=cryptPushData(sbbs->ssh_session, (char*)buf+bufbot, buftop-bufbot, &i)))) {
    						/* Handle the SSH error here... */
    						GCESSTR(err, node, sbbs->ssh_session, "pushing data");
    						ssh_errors++;
    						sbbs->online=false;
    						i=buftop-bufbot;	// Pretend we sent it all
    					}
    					else {
    						// READ = WRITE TIMEOUT HACK... REMOVE WHEN FIXED
    						/* This sets the write timeout for the flush, then sets it to zero
    						 * afterward... presumably because the read timeout gets set to
    						 * what the current write timeout is.
    						 */
    						if(cryptStatusError(err=cryptSetAttribute(sbbs->ssh_session, CRYPT_OPTION_NET_WRITETIMEOUT, 5)))
    							GCESSTR(err, node, sbbs->ssh_session, "setting write timeout");
    						if(cryptStatusError((err=cryptFlushData(sbbs->ssh_session)))) {
    							GCESSTR(err, node, sbbs->ssh_session, "flushing data");
    							ssh_errors++;
    							if (err != CRYPT_ERROR_TIMEOUT) {
    								sbbs->online=false;
    								i=buftop-bufbot;	// Pretend we sent it all
    							}
    						}
    						// READ = WRITE TIMEOUT HACK... REMOVE WHEN FIXED
    						if(cryptStatusError(err=cryptSetAttribute(sbbs->ssh_session, CRYPT_OPTION_NET_WRITETIMEOUT, 0)))
    							GCESSTR(err, node, sbbs->ssh_session, "setting write timeout");
    					}
    				}
    			}
    			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) {
    				int result = RingBufWrite(startup->node_spybuf[sbbs->cfg.node_num-1],buf+bufbot,i);
    				if(result != i)
    					lprintf(LOG_WARNING, "%s SHORT WRITE to spy ring buffer (%u < %u)"
    						,node, result, i);
    			}
    			/* Spy on the user remotely */
    			if(sbbs->cfg.mqtt.enabled && mqtt.handle != NULL) {
    				int result = mqtt_pub_message(&mqtt, TOPIC_BBS, spy_topic, buf+bufbot, i, /* retain: */false);
    				if(result != MQTT_SUCCESS)
    					lprintf(LOG_WARNING, "%s ERROR %d (%d) publishing node output (%u bytes): %s"
    						,node, result, errno, i, spy_topic);
    			}
    			if(spy_socket[sbbs->cfg.node_num-1]!=INVALID_SOCKET)
    				if(sendsocket(spy_socket[sbbs->cfg.node_num-1],(char*)buf+bufbot,i) != i && ERROR_VALUE != EPIPE)
    					lprintf(LOG_ERR, "%s ERROR %d writing to spy socket", node, ERROR_VALUE);
    #ifdef __unix__
    			if(uspy_socket[sbbs->cfg.node_num-1]!=INVALID_SOCKET)
    				if(sendsocket(uspy_socket[sbbs->cfg.node_num-1],(char*)buf+bufbot,i) != i)
    					lprintf(LOG_ERR, "%s ERROR %d writing to UNIX spy socket", node, ERROR_VALUE);
    #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 %u bytes in %u blocks, %u average, %u 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);
    }
    
    static bool is_day_to_run(const struct tm* now, uint8_t days, uint16_t months = 0, uint32_t mdays = 0)
    {
    	if(!(days & (1 << now->tm_wday)))
    		return false;
    
    	if((mdays > 1) && !(mdays & (1 << now->tm_mday)))
    		return false;
    
    	if((months != 0) && !(months & (1 << now->tm_mon)))
    		return false;
    
    	return true;
    }
    
    static bool is_time_to_run(time_t now, const struct tm* now_tm, time_t last, uint16_t freq, uint16_t time)
    {
    	if(freq != 0) {
    		if((now - last) / 60 < freq)
    			return false;
    	} else {
    		struct tm last_tm = {};
    		localtime_r(&last, &last_tm);
    		if(now_tm->tm_mday == last_tm.tm_mday && now_tm->tm_mon == last_tm.tm_mon) // Already ran today
    			return false;
    		if(((now_tm->tm_hour * 60) + now_tm->tm_min) < time)
    			return false;
    	}
    	return true;
    }
    
    static bool is_time_to_run(time_t now, const event_t* event)
    {
    	if(event->last == -1)	// Always Run after init/re-init or signaled via semaphore
    		return true;
    
    	struct tm now_tm = {};
    	localtime_r(&now, &now_tm);
    
    	if(!is_day_to_run(&now_tm, event->days, event->months, event->mdays))
    		return false;
    
    	if(!is_time_to_run(now, &now_tm, event->last, event->freq, event->time))
    		return false;
    
    	return true;
    }
    
    static bool is_time_to_run(time_t now, const qhub_t* hub)
    {
    	if(hub->last == -1)	// Always Run when signaled via semaphore
    		return true;
    
    	struct tm now_tm = {};
    	localtime_r(&now, &now_tm);
    
    	if(!is_day_to_run(&now_tm, hub->days))
    		return false;
    
    	if(!is_time_to_run(now, &now_tm, hub->last, hub->freq, hub->time))
    		return false;
    
    	return true;
    }
    
    void event_thread(void* arg)
    {
    	char		str[MAX_PATH+1];
    	char		semfile[MAX_PATH+1];
    	int			i,j,k;
    	int			file;
    	int			offset;
    	bool		check_semaphores;
    	bool		packed_rep;
    	uint	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;
    	node_t		node;
    	glob_t		g;
    	sbbs_t*		sbbs = (sbbs_t*) arg;
    	char		event_code[LEN_CODE+1];
    
    	sbbs->lprintf(LOG_INFO,"BBS Events thread started");
    
    	sbbs_srand();	/* Seed random number generator */
    
    	SetThreadName("sbbs/events");
    	thread_up(true /* setuid */);
    
    	mqtt_pub_strval(&mqtt, TOPIC_HOST, "event", "thread started");
    
    #ifdef JAVASCRIPT
    	if(!(startup->options&BBS_OPT_NO_JAVASCRIPT)) {
    		if((sbbs->js_cx = sbbs->js_init(&sbbs->js_runtime, &sbbs->js_glob, "event")) == NULL) /* This must be done in the context of the events thread */
    			sbbs->lprintf(LOG_CRIT,"!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)<(int)(sizeof(time32_t)*(i+1))) {
    				sbbs->lprintf(LOG_WARNING,"Initializing last run time for event: %s"
    					,sbbs->cfg.event[i]->code);
    				if(write(file,&sbbs->cfg.event[i]->last,sizeof(sbbs->cfg.event[i]->last)) != sizeof sbbs->cfg.event[i]->last)
    					sbbs->errormsg(WHERE, ERR_WRITE, str, 4);
    			} 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;
    		}
    		if(read(file,&lastprepack,sizeof(lastprepack)) != sizeof lastprepack)	/* expected to fail first time */
    			lastprepack = 0;
    		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)<(int)(sizeof(time32_t)*(i+1))) {
    				sbbs->lprintf(LOG_WARNING,"Initializing last call-out time for QWKnet hub: %s"
    					,sbbs->cfg.qhub[i]->id);
    				if(write(file,&sbbs->cfg.qhub[i]->last,sizeof(sbbs->cfg.qhub[i]->last)) != sizeof sbbs->cfg.qhub[i]->last)
    					sbbs->errormsg(WHERE, ERR_WRITE, str, 4);
    			} 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);
    	}
    
    	while(!sbbs->terminated && !terminate_server) {
    
    		if(startup->options&BBS_OPT_NO_EVENTS) {
    			SLEEP(1000);
    			continue;
    		}
    
    		now=time(NULL);
    
    		if(now-lastsemchk >= startup->sem_chk_freq) {
    			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)) {
    			sbbs->event_code = "unpackREP";
    			/* 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++) {
    				if(flength(g.gl_pathv[i]) < 1)
    					continue;
    				sbbs->useron.number = 0;
    				sbbs->lprintf(LOG_DEBUG, "Inbound QWK Reply Packet detected: %s", g.gl_pathv[i]);
    				sbbs->useron.number = atoi(g.gl_pathv[i]+offset);
    				getuserdat(&sbbs->cfg,&sbbs->useron);
    				if(sbbs->useron.number != 0 && !(sbbs->useron.misc&(DELETED|INACTIVE))) {
    					SAFEPRINTF(semfile,"%s.lock",g.gl_pathv[i]);
    					if(!fmutex(semfile,startup->host_name,TIMEOUT_MUTEX_FILE)) {
    						sbbs->lprintf(LOG_INFO," %s exists (unpack in progress?)", semfile);
    						continue;
    					}
    					sbbs->online=ON_LOCAL;
    					sbbs->console|=CON_L_ECHO;
    					sbbs->getusrsubs();
    					bool success = sbbs->unpack_rep(g.gl_pathv[i]);
    					sbbs->delfiles(sbbs->cfg.temp_dir,ALLFILES);		/* clean-up temp_dir after unpacking */
    					sbbs->online=false;
    					sbbs->console&=~CON_L_ECHO;
    
    					/* putuserdat? */
    					if(success) {
    						if(remove(g.gl_pathv[i]))
    							sbbs->errormsg(WHERE, ERR_REMOVE, g.gl_pathv[i], 0);
    					} else {
    						char badpkt[MAX_PATH+1];
    						SAFEPRINTF2(badpkt, "%s.%" PRIx64 ".bad", g.gl_pathv[i], (uint64_t)time(NULL));
    						(void)remove(badpkt);
    						if(rename(g.gl_pathv[i], badpkt) == 0)
    							sbbs->lprintf(LOG_NOTICE, "%s renamed to %s", g.gl_pathv[i], badpkt);
    						else
    							sbbs->lprintf(LOG_ERR, "!ERROR %d (%s) renaming %s to %s"
    								,errno, strerror(errno), g.gl_pathv[i], badpkt);
    						SAFEPRINTF(badpkt, "%u.rep.*.bad", sbbs->useron.number);
    						SAFEPRINTF(str,"%sfile/", sbbs->cfg.data_dir);
    						sbbs->delfiles(str, badpkt, /* keep: */10);
    					}
    					if(remove(semfile))
    						sbbs->errormsg(WHERE, ERR_REMOVE, semfile, 0);
    				}
    				else {
    					sbbs->lprintf(LOG_INFO, "Removing: %s", g.gl_pathv[i]);
    					if(remove(g.gl_pathv[i]))
    						sbbs->errormsg(WHERE, ERR_REMOVE, g.gl_pathv[i], 0);
    				}
    			}
    			globfree(&g);
    			sbbs->useron.number = 0;
    
    			/* Create any QWK files that have magically appeared (via FTP perhaps) */
    			sbbs->event_code = "packQWK";
    			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++) {
    				sbbs->useron.number = 0;
    				sbbs->lprintf(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)) {
    					sbbs->lprintf(LOG_INFO,"%s exists (pack in progress?)", semfile);
    					continue;
    				}
    				getuserdat(&sbbs->cfg,&sbbs->useron);
    				if(sbbs->useron.number != 0 && !(sbbs->useron.misc&(DELETED|INACTIVE))) {
    					sbbs->lprintf(LOG_INFO, "Packing QWK Message Packet");
    					sbbs->online=ON_LOCAL;
    					sbbs->console|=CON_L_ECHO;
    					sbbs->getmsgptrs();
    					sbbs->getusrsubs();
    
    					sbbs->last_ns_time=sbbs->ns_time=sbbs->useron.ns_time;
    
    					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 */)) {
    						sbbs->lprintf(LOG_INFO, "Packing completed: %s", str);
    						sbbs->qwk_success(l,0,1);
    						sbbs->putmsgptrs();
    						batch_list_clear(&sbbs->cfg, sbbs->useron.number, XFER_BATCH_DOWNLOAD);
    					} else
    						sbbs->lputs(LOG_INFO, "No packet created (no new messages)");
    					sbbs->delfiles(sbbs->cfg.temp_dir,ALLFILES);
    					sbbs->console&=~CON_L_ECHO;
    					sbbs->online=false;
    				}
    				if(fexist(g.gl_pathv[i]) && remove(g.gl_pathv[i]) != 0)
    					sbbs->errormsg(WHERE, ERR_REMOVE, g.gl_pathv[i], 0);
    				if(remove(semfile))
    					sbbs->errormsg(WHERE, ERR_REMOVE, semfile, 0);
    			}
    			globfree(&g);
    			sbbs->useron.number = 0;
    
    			/* Create (pre-pack) QWK files for users configured as such */
    			sbbs->event_code = "prepackQWK";
    			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);
    				sbbs->lputs(LOG_INFO,"Pre-packing QWK Message packets...");
    				int userfile = openuserdat(&sbbs->cfg, /* for_modify: */false);
    				for(i=1;i<=j;i++) {
    
    					sbbs->useron.number=i;
    					if(fgetuserdat(&sbbs->cfg,&sbbs->useron, userfile) != 0)
    						continue;
    
    					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;
    						sbbs->lprintf(LOG_INFO, "Pre-packing QWK");
    						sbbs->online=ON_LOCAL;
    						sbbs->console|=CON_L_ECHO;
    						sbbs->getmsgptrs();
    						sbbs->getusrsubs();
    						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();
    							batch_list_clear(&sbbs->cfg, sbbs->useron.number, XFER_BATCH_DOWNLOAD);
    						}
    						sbbs->delfiles(sbbs->cfg.temp_dir,ALLFILES);
    						sbbs->console&=~CON_L_ECHO;
    						sbbs->online=false;
    					}
    				}
    				closeuserdat(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,(int)sbbs->cfg.total_events*4L,SEEK_SET);
    				if(write(file,&lastprepack,sizeof(lastprepack)) != sizeof lastprepack)
    					sbbs->errormsg(WHERE, ERR_WRITE, str, sizeof lastprepack);
    				close(file);
    
    				remove(semfile);
    			}
    			sbbs->useron.number = 0;
    		}
    
    		if(check_semaphores) {
    
    			/* Run daily maintenance? */
    			sbbs->cfg.node_num=0;
    			if(!(startup->options&BBS_OPT_NO_NEWDAY_EVENTS)) {
    				sbbs->event_code = "";
    				sbbs->logonstats();
    				if(sbbs->sys_status&SS_DAILY)
    					sbbs->daily_maint();
    			}
    			/* Node Daily Events */
    			sbbs->event_code = "DAILY";
    			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;
    						SAFECOPY(sbbs->cfg.node_dir, sbbs->cfg.node_path[i-1]);
    
    						sbbs->lprintf(LOG_INFO,"Running node %d daily event",i);
    						sbbs->online=ON_LOCAL;
    						sbbs->console|=CON_L_ECHO;
    						sbbs->logentry("!:","Run node daily event");
    						const char* cmd = sbbs->cmdstr(sbbs->cfg.node_daily,nulstr,nulstr,NULL);
    						int result = sbbs->external(cmd, EX_OFFLINE);
    						sbbs->lprintf(result ? LOG_ERR : LOG_INFO, "Node daily event: '%s' returned %d", cmd, result);
    						sbbs->console&=~CON_L_ECHO;
    						sbbs->online=false;
    					}
    					sbbs->getnodedat(i,&node,1);
    					node.misc&=~NODE_EVENT;
    					node.status=NODE_WFC;
    					node.useron=0;
    					sbbs->putnodedat(i,&node);
    				}
    			}
    			sbbs->event_code = nulstr;
    
    			/* QWK Networking Call-out semaphores */
    			for(i=0;i<sbbs->cfg.total_qhubs;i++) {
    				if(!sbbs->cfg.qhub[i]->enabled)
    					continue;
    				if(sbbs->cfg.qhub[i]->node != NODE_ANY
    					&& (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)) {
    					SAFECOPY(str,sbbs->cfg.qhub[i]->id);
    					sbbs->lprintf(LOG_INFO,"Semaphore signaled for QWK Network Hub: %s",strupr(str));
    					sbbs->cfg.qhub[i]->last=-1;
    				}
    			}
    
    			/* Timed Event semaphores */
    			for(i=0;i<sbbs->cfg.total_events;i++) {
    				if(sbbs->cfg.event[i]->node != NODE_ANY
    					&& (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)) {
    					SAFECOPY(str,sbbs->cfg.event[i]->code);
    					sbbs->lprintf(LOG_INFO,"Semaphore signaled for Timed Event: %s",strupr(str));
    					sbbs->cfg.event[i]->last=-1;
    				}
    			}
    		}
    
    		/* QWK Networking Call-out Events */
    		sbbs->event_code = "QNET";
    		for(i=0;i<sbbs->cfg.total_qhubs && !sbbs->terminated;i++) {
    			if(!sbbs->cfg.qhub[i]->enabled)
    				continue;
    			if(sbbs->cfg.qhub[i]->node != NODE_ANY
    				&& (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 */
    						sbbs->lprintf(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.%x.bad",str,(int)now);
    							remove(newname);
    							if(rename(str,newname)==0)
    								sbbs->lprintf(LOG_NOTICE, "%s renamed to %s", str, newname);
    							else
    								sbbs->lprintf(LOG_ERR, "!ERROR %d (%s) renaming %s to %s"
    									,errno, strerror(errno), str, newname);
    							SAFEPRINTF(newname, "%s.q??.*.bad", sbbs->cfg.qhub[i]->id);
    							sbbs->delfiles(sbbs->cfg.data_dir, newname, /* keep: */10);
    						}
    						sbbs->delfiles(sbbs->cfg.temp_dir,ALLFILES);
    						sbbs->console&=~CON_L_ECHO;
    						sbbs->online=false;
    						if(fexist(str) && remove(str))
    							sbbs->errormsg(WHERE, ERR_REMOVE, str, 0);
    					}
    				}
    				globfree(&g);
    			}
    
    			/* Qnet call out based on time */
    			if(is_time_to_run(now, sbbs->cfg.qhub[i])) {
    				SAFEPRINTF2(str,"%sqnet/%s.now"
    					,sbbs->cfg.data_dir,sbbs->cfg.qhub[i]->id);
    				if(fexistcase(str)) {
    					if(remove(str))					/* Remove semaphore file */
    						sbbs->errormsg(WHERE, ERR_REMOVE, str, 0);
    				}
    				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]->subnum].ptr=0;
    					if(file!=-1) {
    						lseek(file,sbbs->cfg.sub[sbbs->cfg.qhub[i]->sub[j]->subnum]->ptridx*sizeof(int32_t),SEEK_SET);
    						if(read(file,&sbbs->subscan[sbbs->cfg.qhub[i]->sub[j]->subnum].ptr
    							,sizeof(sbbs->subscan[sbbs->cfg.qhub[i]->sub[j]->subnum].ptr)) != sizeof(uint32_t))
    							sbbs->subscan[sbbs->cfg.qhub[i]->sub[j]->subnum].ptr = 0;
    					}
    				}
    				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]->subnum]->ptridx*4L) {
    								l32=l;
    								if(write(file,&l32,4) != 4)		/* initialize ptrs to null */
    									sbbs->errormsg(WHERE, ERR_WRITE, str, 4);
    							}
    							lseek(file
    								,sbbs->cfg.sub[sbbs->cfg.qhub[i]->sub[j]->subnum]->ptridx*sizeof(int32_t)
    								,SEEK_SET);
    							if(write(file,&sbbs->subscan[sbbs->cfg.qhub[i]->sub[j]->subnum].ptr,sizeof(sbbs->subscan[sbbs->cfg.qhub[i]->sub[j]->subnum].ptr)) != sizeof sbbs->subscan[sbbs->cfg.qhub[i]->sub[j]->subnum].ptr)
    								sbbs->errormsg(WHERE, ERR_WRITE, str, 4);
    						}
    						close(file);
    					}
    				}
    				sbbs->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);
    				if(write(file,&sbbs->cfg.qhub[i]->last,sizeof(sbbs->cfg.qhub[i]->last)) != sizeof sbbs->cfg.qhub[i]->last)
    					sbbs->errormsg(WHERE, ERR_WRITE, str, 4);
    				close(file);
    
    				if(sbbs->cfg.qhub[i]->call[0]) {
    					if(sbbs->cfg.qhub[i]->node == NODE_ANY)
    						sbbs->cfg.node_num = startup->first_node;
    					else
    						sbbs->cfg.node_num=sbbs->cfg.qhub[i]->node;
    					if(sbbs->cfg.node_num<1)
    						sbbs->cfg.node_num=1;
    					SAFECOPY(sbbs->cfg.node_dir, sbbs->cfg.node_path[sbbs->cfg.node_num-1]);
    					sbbs->lprintf(LOG_INFO,"Call-out: %s",sbbs->cfg.qhub[i]->id);
    					sbbs->online=ON_LOCAL;
    					sbbs->console|=CON_L_ECHO;
    					int ex_mode = EX_OFFLINE|EX_SH; /* sh for Unix perl scripts */
    					if(sbbs->cfg.qhub[i]->misc & QHUB_NATIVE)
    						ex_mode |= EX_NATIVE;
    					int result = sbbs->external(
    						 sbbs->cmdstr(sbbs->cfg.qhub[i]->call
    							,sbbs->cfg.qhub[i]->id,sbbs->cfg.qhub[i]->id,NULL)
    						,ex_mode);
    					sbbs->lprintf(result ? LOG_ERR : LOG_INFO, "Call-out to: %s returned %d", sbbs->cfg.qhub[i]->id, result);
    					sbbs->console&=~CON_L_ECHO;
    					sbbs->online=false;
    				}
    			}
    		}
    
    		/* Timed Events */
    		for(i=0;i<sbbs->cfg.total_events && !sbbs->terminated;i++) {
    			if(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 != NODE_ANY
    				&& (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
    
    			SAFECOPY(event_code, sbbs->cfg.event[i]->code);
    			strupr(event_code);
    			sbbs->event_code = event_code;
    
    			if(is_time_to_run(now, sbbs->cfg.event[i]))
    			{
    				if(sbbs->cfg.event[i]->node != NODE_ANY && (sbbs->cfg.event[i]->misc&EVENT_EXCL)) { /* exclusive event */
    
    					if(sbbs->cfg.event[i]->node<first_node
    						|| sbbs->cfg.event[i]->node>last_node) {
    						sbbs->lprintf(LOG_INFO,"Waiting for node %d to run timed event: %s"
    							,sbbs->cfg.event[i]->node, event_code);
    						sbbs->lprintf(LOG_DEBUG,"event last run: %s (0x%08x)"
    							,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,(int)i*4L,SEEK_SET);
    							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, 4);
    							close(file);
    							if(now-sbbs->cfg.event[i]->last<(60*60))	/* event is done */
    								break;
    							if(now-start>(90*60)) {
    								sbbs->lprintf(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)) {
    							if(remove(str))
    								sbbs->errormsg(WHERE, ERR_REMOVE, str, 0);
    						}
    						sbbs->cfg.event[i]->last=(time32_t)now;
    					} else {	// Exclusive event to run on a node under our control
    						sbbs->lprintf(LOG_INFO,"Waiting for all nodes to become inactive before running timed event");
    						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;
    							sbbs->lprintf(LOG_DEBUG,"Waiting for node %d (status=%d)"
    								,j, node.status);
    							if (now - start > (60 * 60) && node_socket[j - 1] != INVALID_SOCKET) {
    								sbbs->lprintf(LOG_WARNING, "!TIRED of waiting for node %d to become inactive (status=%d), closing socket %d"
    									,j, node.status, node_socket[j - 1]);
    								close_socket(node_socket[j - 1]);
    								node_socket[j - 1] = INVALID_SOCKET;
    							}
    							if(now-start>(90*60)) {
    								sbbs->lprintf(LOG_WARNING,"!TIMEOUT waiting for node %d to become inactive (status=%d), aborting wait"
    									,j, node.status);
    								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 != NODE_ANY
    					&& (sbbs->cfg.event[i]->node<first_node || sbbs->cfg.event[i]->node>last_node)) {
    					sbbs->lprintf(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 {
    					if(sbbs->cfg.event[i]->node == NODE_ANY)
    						sbbs->cfg.node_num = startup->first_node;
    					else
    						sbbs->cfg.node_num=sbbs->cfg.event[i]->node;
    					if(sbbs->cfg.node_num<1)
    						sbbs->cfg.node_num=1;
    					SAFECOPY(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)) {
    						if(remove(str))
    							sbbs->errormsg(WHERE, ERR_REMOVE, str, 0);
    					}
    					if(sbbs->cfg.event[i]->node != NODE_ANY && (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);
    					}
    					const char* cmd = sbbs->cfg.event[i]->cmd;
    					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;
    					sbbs->console|=CON_L_ECHO;
    					sbbs->lprintf(LOG_INFO,"Running %s%stimed event: %s"
    						,native_executable(&sbbs->cfg, cmd, ex_mode) ? "native ":"16-bit DOS "
    						,(ex_mode&EX_BG)		? "background ":""
    						,event_code);
    					{
    						int result=
    							sbbs->external(
    								 sbbs->cmdstr(cmd,nulstr,sbbs->cfg.event[i]->dir,NULL)
    								,ex_mode
    								,sbbs->cfg.event[i]->dir);
    						if(!(ex_mode&EX_BG))
    							sbbs->lprintf(result ? sbbs->cfg.event[i]->errlevel : LOG_INFO, "Timed event: %s returned %d", event_code, result);
    					}
    					sbbs->console&=~CON_L_ECHO;
    					sbbs->online=false;
    					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,(int)i*4L,SEEK_SET);
    					if(write(file,&sbbs->cfg.event[i]->last,sizeof(sbbs->cfg.event[i]->last)) != sizeof sbbs->cfg.event[i]->last)
    						sbbs->errormsg(WHERE, ERR_WRITE, str, 4);
    					close(file);
    
    					if(sbbs->cfg.event[i]->node != NODE_ANY
    						&& 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);
    						}
    					}
    				}
    			}
    		}
    		sbbs->event_code = nulstr;
    		mswait(1000);
    	}
    	sbbs->cfg.node_num=0;
    	sbbs->useron.number = 0;
    	sbbs->js_cleanup();
    
    	sbbs->event_thread_running = false;
    
    	mqtt_pub_strval(&mqtt, TOPIC_HOST, "event", "thread stopped");
    
    	thread_down();
    	sbbs->lprintf(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, bool is_event_thread)
    {
    	char	path[MAX_PATH+1];
    	uint	i;
    
    	// Initialize some member variables used by lputs:
    	this->is_event_thread = is_event_thread;
    	cfg.node_num = node_num;
    	SAFECOPY(client_name, name);
    
        lprintf(LOG_DEBUG,"constructor using socket %d (settings=%x)"
    		,sd, global_cfg->node_misc);
    
    	startup = ::startup;	// Convert from global to class member
    	mqtt = &::mqtt;
    
    	memcpy(&cfg, global_cfg, sizeof(cfg));
    	cfg.node_num = node_num;	// Restore the node_num passed to the constructor
    
    	if(node_num>0) {
    		SAFECOPY(cfg.node_dir, cfg.node_path[node_num-1]);
    		prep_dir(cfg.node_dir, cfg.temp_dir, sizeof(cfg.temp_dir));
    		SAFEPRINTF2(syspage_semfile, "%ssyspage.%u", cfg.ctrl_dir, node_num);
    		(void)remove(syspage_semfile);
    	} 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));
    		if((i = md(cfg.temp_dir)) != 0)
    			lprintf(LOG_CRIT,"!ERROR %d (%s) creating directory: %s", i, strerror(i), 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);
    		}
    		syspage_semfile[0] = 0;
    	}
    	lprintf(LOG_DEBUG, "temporary file directory: %s", cfg.temp_dir);
    
    	if(client_info != nullptr)
    		memcpy(&client,client_info,sizeof(client));
    	client_addr.store = addr->store;
    	client_socket = sd;
    	client_socket_dup=INVALID_SOCKET;
    
    	/* Init some important variables */
    
    	SAFECOPY(connection, "Telnet");
    	telnet_ack_event=CreateEvent(NULL, /* Manual Reset: */false,/* InitialState */false,NULL);
    
    	listInit(&savedlines, /* flags: */0);
    	listInit(&smb_list, /* flags: */0);
    	listInit(&mouse_hotspots, /* flags: */0);
    	pthread_mutex_init(&nodefile_mutex, NULL);
    
    	for(i=0;i<TOTAL_TEXT;i++)
    		text[i]=text_sav[i]=global_text[i];
    }
    
    //****************************************************************************
    bool sbbs_t::init()
    {
    	char		str[MAX_PATH+1];
    	char		tmp[128];
    	int			result;
    	int			i,j,k,l;
    	node_t		node;
    	socklen_t	addr_len;
    	union xp_sockaddr	addr;
    
    	if(cfg.total_shells < 1) {
    		errormsg(WHERE, ERR_CHK, "At least one command shell must be configured (e.g. in SCFG->Command Shells)"
    			,cfg.total_shells);
    		return false;
    	}
    
    	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) {
    
    		addr_len=sizeof(addr);
    		if((result=getsockname(client_socket, &addr.addr, &addr_len))!=0) {
    			lprintf(LOG_CRIT,"%04d %s !ERROR %d (%d) getting local address/port of socket"
    				,client_socket, client.protocol, result, ERROR_VALUE);
    			return(false);
    		}
    		inet_addrtop(&addr, local_addr, sizeof(local_addr));
    		inet_addrtop(&client_addr, client_ipaddr, sizeof(client_ipaddr));
    		SAFEPRINTF(str, "%sclient.ini", cfg.node_dir);
    		FILE* fp = fopen(str, "wt");
    		if(fp != NULL) {
    			fprintf(fp, "sock=%d\n", client_socket);
    			fprintf(fp, "addr=%s\n", client.addr);
    			fprintf(fp, "host=%s\n", client.host);
    			fprintf(fp, "port=%u\n", (uint)client.port);
    			fprintf(fp, "time=%u\n", (uint)client.time);
    			fprintf(fp, "prot=%s\n", client.protocol);
    			fprintf(fp, "local_addr=%s\n", local_addr);
    			fprintf(fp, "local_port=%u\n", (uint)inet_addrport(&addr));
    			fclose(fp);
    		}
    		spymsg("Connected");
    	}
    
    	if((comspec=os_cmdshell())==NULL) {
    		errormsg(WHERE, ERR_CHK, OS_CMD_SHELL_ENV_VAR" environment variable", 0);
    		return(false);
    	}
    
    	if((i = md(cfg.temp_dir)) != 0) {
    		lprintf(LOG_CRIT,"!ERROR %d (%s) creating directory: %s", i, strerror(i), cfg.temp_dir);
    		return false;
    	}
    
    	/* 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)<(int)(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;
    		}
    	}
    	if(chsize(nodefile, (off_t)(cfg.sys_nodes*sizeof(node_t))) != 0)
    		errormsg(WHERE, ERR_LEN, str, cfg.sys_nodes*sizeof(node_t));
    	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_NOTICE, "Perhaps this node is already running");
    			return(false);
    		}
    
    		if(filelength(fileno(logfile_fp))) {
    			log(crlf);
    			time_t ftime = fdate(str);
    			safe_snprintf(str,sizeof(str),
    				"End of preexisting log entry (possible crash on %.24s)"
    				,ctime_r(&ftime, tmp));
    			logline(LOG_NOTICE,"L!",str);
    			log(crlf);
    			catsyslog(true);
    		}
    
    		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);
    
    		// remove any pending node messages
    		safe_snprintf(str, sizeof(str), "%smsgs/n%3.3u.msg",cfg.data_dir,cfg.node_num);
    		(void)remove(str);
    		// Delete any stale temporary files (with potentially sensitive content)
    		delfiles(cfg.temp_dir,ALLFILES);
    		safe_snprintf(str, sizeof(str), "%sMSGTMP", cfg.node_dir);
    		removecase(str);
    		safe_snprintf(str, sizeof(str), "%sQUOTES.TXT", cfg.node_dir);
    		removecase(str);
    	}
    
    	/* 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=(int *)malloc(sizeof(int)*usrgrp_total))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "cursub", sizeof(int)*usrgrp_total);
    			return(false);
    		}
    
    		if((usrgrp=(int *)malloc(sizeof(int)*usrgrp_total))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "usrgrp", sizeof(int)*usrgrp_total);
    			return(false);
    		}
    
    		if((usrsubs=(int *)malloc(sizeof(int)*usrgrp_total))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "usrsubs", sizeof(int)*usrgrp_total);
    			return(false);
    		}
    
    		if((usrsub=(int **)calloc(usrgrp_total,sizeof(int *)))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "usrsub", sizeof(int)*usrgrp_total);
    			return(false);
    		}
    	}
    	if(cfg.total_subs) {
    		if((subscan=(subscan_t *)calloc(cfg.total_subs, sizeof(subscan_t)))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "subscan", sizeof(subscan_t)*cfg.total_subs);
    			return(false);
    		}
    	}
    
    	for(i=l=0;i<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]=(int *)malloc(sizeof(int)*l))==NULL) {
    				errormsg(WHERE, ERR_ALLOC, "usrsub[x]", sizeof(int)*l);
    				return(false);
    			}
    
    	if(cfg.total_libs) {
    
    		usrlib_total = cfg.total_libs;
    
    		if((curdir=(int *)malloc(sizeof(int)*usrlib_total))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "curdir", sizeof(int)*usrlib_total);
    			return(false);
    		}
    
    		if((usrlib=(int *)malloc(sizeof(int)*usrlib_total))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "usrlib", sizeof(int)*usrlib_total);
    			return(false);
    		}
    
    		if((usrdirs=(int *)malloc(sizeof(uint)*usrlib_total))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "usrdirs", sizeof(int)*usrlib_total);
    			return(false);
    		}
    
    		if((usrdir=(int **)calloc(usrlib_total,sizeof(int *)))==NULL) {
    			errormsg(WHERE, ERR_ALLOC, "usrdir", sizeof(int)*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]=(int *)malloc(sizeof(int)*l))==NULL) {
    				errormsg(WHERE, ERR_ALLOC, "usrdir[x]", sizeof(int)*l);
    				return(false);
    			}
    	}
    
    #ifdef USE_CRYPTLIB
    	pthread_mutex_init(&ssh_mutex,NULL);
    	ssh_mutex_created = true;
    	ssh_active = CreateEvent(nullptr, true, false, nullptr);
    #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()
    {
    	int i;
    
    	useron.number = 0;
    
    #ifdef _DEBUG
    	lprintf(LOG_DEBUG,"destructor begin");
    #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 && !passthru_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;
    	}
    
    	if(syspage_semfile[0])
    		remove(syspage_semfile);
    
    	/********************************/
    	/* Free allocated class members */
    	/********************************/
    
    	js_cleanup();
    
    	/* 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);
    	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]);
    
    	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);
    
    	FREE_AND_NULL(qwknode);
    	total_qwknodes = 0;
    
    	listFree(&savedlines);
    	listFree(&smb_list);
    	listFree(&mouse_hotspots);
    
    #ifdef USE_CRYPTLIB
    	while(ssh_mutex_created && pthread_mutex_destroy(&ssh_mutex)==EBUSY)
    		mswait(1);
    	if (ssh_active) {
    		SetEvent(ssh_active);
    		while ((!CloseEvent(ssh_active)) && errno == EBUSY)
    			mswait(1);
    		ssh_active = nullptr;
    	}
    #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, "destructor end");
    #endif
    }
    
    int sbbs_t::smb_stack(smb_t* smb, bool push)
    {
    	if(push) {
    		if(smb == NULL || !SMB_IS_OPEN(smb))
    			return SMB_SUCCESS;	  /* Msg base not open, do nothing */
    		if(listPushNodeData(&smb_list, smb, sizeof(*smb)) == NULL)
    			return SMB_FAILURE;
    		return SMB_SUCCESS;
    	}
    	/* pop */
    	smb_t* data = (smb_t*)listPopNode(&smb_list);
    	if(data == NULL)	/* Nothing on the stack, so do nothing */
    		return SMB_SUCCESS;
    
    	*smb = *data;
    	free(data);
    	return SMB_SUCCESS;
    }
    
    /****************************************************************************/
    /* 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));
    	}
    
    	if(cfg.node_num && spy_socket[cfg.node_num-1]!=INVALID_SOCKET)
    		if(sendsocket(spy_socket[cfg.node_num-1],str,strlen(str)) < 1)
    			lprintf(LOG_ERR, "Node %d ERROR %d writing to spy socket", cfg.node_num, ERROR_VALUE);
    #ifdef __unix__
    	if(cfg.node_num && uspy_socket[cfg.node_num-1]!=INVALID_SOCKET)
    		if(sendsocket(uspy_socket[cfg.node_num-1],str,strlen(str)) < 1)
    			lprintf(LOG_ERR, "Node %d ERROR %d writing to spy socket", cfg.node_num, ERROR_VALUE);
    #endif
    }
    
    /****************************************************************************/
    /* 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(const char* path, const char* dest, bool copy)
    {
    	char src[MAX_PATH + 1];
    
        if(!stricmp(path, dest))	 /* source and destination are the same! */
            return(0);
    
    	SAFECOPY(src, path);
        if(!fexistcase(src)) {
            bprintf("\r\n\7MV ERROR: Source doesn't exist\r\n'%s'\r\n"
                ,src);
            return(-1);
    	}
        if(!copy && fexist(dest)) {
            bprintf("\r\n\7MV ERROR: Destination already exists\r\n'%s'\r\n"
                ,dest);
            return(-1);
    	}
    	if(!copy && rename(src, dest) == 0)
    		return 0;
    	if(!CopyFile(src, dest, /* fail if exists: */!copy)) {
    		errormsg(WHERE, "CopyFile", src, 0, dest);
    		return -1;
    	}
        if(!copy && remove(src)) {
            errormsg(WHERE,ERR_REMOVE,src,0);
            return(-1);
    	}
        return(0);
    }
    
    void sbbs_t::hangup(void)
    {
    	if(online) {
    		clear_hotspots();
    		lprintf(LOG_DEBUG,"disconnecting client");
    		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(&ssh_mutex);
    			ssh_session_destroy(client_socket, ssh_session, __LINE__);
    			ssh_mode = false;
    			pthread_mutex_unlock(&ssh_mutex);
    		}
    		close_socket(client_socket);
    		client_socket=INVALID_SOCKET;
    	}
    	SetEvent(outbuf.data_event);
    	SetEvent(outbuf.highwater_event);
    }
    
    int sbbs_t::incom(unsigned int timeout)
    {
    	uchar	ch;
    
    	if(!online)
    		return NOINP;
    
    	// If we think we may have some input, send all our output
    	if (RingBufFull(&outbuf) != 0) {
    		SetEvent(outbuf.highwater_event);
    		SetEvent(outbuf.data_event);
    	}
    #if 0	/* looping version */
    	while(!RingBufRead(&inbuf, &ch, 1))
    		if(WaitForEvent(inbuf.data_event, timeout) != WAIT_OBJECT_0 || sys_status&SS_ABORT)
    			return(NOINP);
    #else
    	if(!RingBufRead(&inbuf, &ch, 1)) {
    		if (WaitForEvent(inbuf.data_event, timeout) != WAIT_OBJECT_0)
    			return(NOINP);
    		if(!RingBufRead(&inbuf, &ch, 1))
    			return(NOINP);
    	}
    #endif
    	return(ch);
    }
    
    // Steve's original implementation (in RCIOL) did not incorporate a retry
    // ... so this function does not either. :-P
    int sbbs_t::_outcom(uchar ch)
    {
    	if(!RingBufFree(&outbuf))
    		return(TXBOF);
        if(!RingBufWrite(&outbuf, &ch, 1))
    		return(TXBOF);
    	return(0);
    }
    
    // This outcom version retries - copied loop from sbbs_t::outchar()
    int sbbs_t::outcom(uchar ch, int max_attempts)
    {
    	int i = 0;
    	while(_outcom(ch) != 0) {
    		if(!online)
    			break;
    		i++;
    		if(i >= max_attempts) {			/* timeout - beep flush outbuf */
    			lprintf(LOG_NOTICE, "timeout(outcom) %04X %04X", rioctl(TXBC), rioctl(IOFO));
    			_outcom(BEL);
    			rioctl(IOCS|PAUSE);
    			return TXBOF;
    		}
    		if(sys_status&SS_SYSPAGE)
    			sbbs_beep(i, OUTCOM_RETRY_DELAY);
    		else
    			mswait(OUTCOM_RETRY_DELAY);
    	}
    	return 0;	// Success
    }
    
    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;
    
        sys_status&=~(SS_USERON|SS_TMPSYSOP|SS_LCHAT|SS_ABORT
            |SS_PAUSEON|SS_PAUSEOFF|SS_EVENT|SS_NEWUSER|SS_NEWDAY|SS_QWKLOGON|SS_FASTLOGON);
        cid[0]=0;
        wordwrap[0]=0;
        question[0]=0;
        menu_dir[0]=0;
        menu_file[0]=0;
    	row = 0;
    	rows = startup->default_term_height;
    	cols = startup->default_term_width;
        lncntr=0;
        autoterm=0;
    	cterm_version = 0;
        lbuflen=0;
        timeleft_warn=0;
    	keybufbot=keybuftop=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_rate=30000;
        dte_rate=38400;
    	cur_output_rate = output_rate_unlimited;
    	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] = "node.log";
    	char *buf;
    	FILE* fp;
    	int  i,file;
    	int 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 = ftell(logfile_fp);
    	if(length > 0) {
    		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(write(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((fp = fopenlog(&cfg, str)) == NULL) {
    					errormsg(WHERE,ERR_OPEN,str,O_WRONLY|O_APPEND|O_CREAT);
    					free((char *)buf);
    					return;
    				}
    				if(fwrite(buf, sizeof(uint8_t), length, fp) != (size_t)length) {
    					fcloselog(fp);
    					errormsg(WHERE,ERR_WRITE,str,length);
    					free((char *)buf);
    					return;
    				}
    				fcloselog(fp);
    			}
    		}
    		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()
    {
        int i;
        stats_t stats;
    
    	if(REALSYSOP && !(cfg.sys_misc&SM_SYSSTAT))
    		return;
    
    	if(useron.rest&FLAG('Q')) 	/* Don't count QWKnet nodes */
    		return;
    
    	now = time(NULL);
    	if(now <= logontime) {
    		char tmp[128];
    		lprintf(LOG_WARNING, "Logoff time (%u) <= logon time (%u): %s"
    			,(uint)now, (uint)logontime, ctime_r(&logontime, tmp));
    		return;
    	}
    	uint minutes_used = (uint)(now-logontime)/60;
    	if(minutes_used < 1)
    		return;
    
    	for(i=0;i<2;i++) {
    		FILE* fp = fopen_dstats(&cfg, i ? 0 : cfg.node_num, /* for_write: */true);
    		if(fp == NULL)
    			continue;
    		if(fread_dstats(fp, &stats)) {
    			stats.total.timeon += minutes_used;
    			stats.today.timeon += minutes_used;
    			fwrite_dstats(fp, &stats);
    		}
    		fclose_dstats(fp);
    	}
    }
    
    void node_thread(void* arg)
    {
    	char			str[MAX_PATH + 1];
    	int				file;
    	int				curshell=-1;
    	node_t			node;
    	uint			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_cx = sbbs->js_init(&sbbs->js_runtime, &sbbs->js_glob, "node")) == NULL) /* This must be done in the context of the node thread */
    			lprintf(LOG_CRIT,"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);
    	}
    
    	bool login_success = false;
    	if(sbbs->answer()) {
    
    		login_success = true;
    		if(sbbs->useron.pass[0])
    			listAddNodeData(&current_logins, sbbs->client.addr, strlen(sbbs->client.addr)+1, sbbs->cfg.node_num, LAST_NODE);
    		if(sbbs->sys_status&SS_QWKLOGON) {
    			sbbs->getsmsg(sbbs->useron.number);
    			sbbs->qwk_sec();
    		} else while(sbbs->useron.number
    			&& (sbbs->main_csi.misc&CS_OFFLINE_EXEC || sbbs->online)) {
    
    			if(curshell != sbbs->useron.shell) {
    				if(sbbs->useron.shell>=sbbs->cfg.total_shells)
    					sbbs->useron.shell=0;
    
    				if(sbbs->cfg.mods_dir[0]) {
    					SAFEPRINTF2(str,"%s%s.js",sbbs->cfg.mods_dir
    						,sbbs->cfg.shell[sbbs->useron.shell]->code);
    					if(fexistcase(str)) {
    						sbbs->js_execfile(str);
    						continue;
    					}
    				}
    				SAFEPRINTF2(str, "%s%s.js", sbbs->cfg.exec_dir
    					,sbbs->cfg.shell[sbbs->useron.shell]->code);
    				if(fexistcase(str)) {
    					sbbs->js_execfile(str);
    					continue;
    				}
    				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=(int)filelength(file);
    				if(sbbs->main_csi.length < 1) {
    					close(file);
    					sbbs->errormsg(WHERE,ERR_LEN, str, sbbs->main_csi.length);
    					sbbs->hangup();
    					break;
    				}
    
    				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(read(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;
    		}
    		listRemoveTaggedNode(&current_logins, sbbs->cfg.node_num, /* free_data */true);
    		sbbs->logoffstats();	/* Updates both system and node dsts.ini (daily statistics) files */
    	}
    
    #ifdef _WIN32
    	if(startup->sound.hangup[0] && !sound_muted(&scfg))
    		PlaySound(startup->sound.hangup, 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(login_success);
    
    	time_t now = time(NULL);
    	SAFEPRINTF(str, "%sclient.ini", sbbs->cfg.node_dir);
    	FILE* fp = fopen(str, "at");
    	if(fp != NULL) {
    		fprintf(fp, "user=%u\n", sbbs->useron.number);
    		fprintf(fp, "name=%s\n", sbbs->useron.alias);
    		fprintf(fp, "done=%u\n", (uint)now);
    		fclose(fp);
    	}
    
    	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
    		|| sbbs->passthru_thread_running
    		) {
    		lprintf(LOG_INFO,"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
    			|| sbbs->passthru_thread_running
    			) {
    			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);
    		}
    	}
    
    	if(login_success)
    		sbbs->catsyslog(/* Crash: */false);
    	else {
    		rewind(sbbs->logfile_fp);
    		if(chsize(fileno(sbbs->logfile_fp), 0) != 0)
    			sbbs->errormsg(WHERE, "truncating", "logfile", 0);
    	}
    
    	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 */
    		uint32_t remain = protected_uint32_adjust_fetch(&node_threads_running, -1);
    		lprintf(LOG_INFO,"Node %d thread terminated (%u node threads remain, %u 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);
    
    	/* crash here July-27-2018:
     	ntdll.dll!77282e19()	Unknown
     	[Frames below may be incorrect and/or missing, no symbols loaded for ntdll.dll]
     	[External Code]
     	sbbs.dll!pthread_mutex_lock(_RTL_CRITICAL_SECTION * mutex) Line 171	C
     	sbbs.dll!protected_uint32_adjust(protected_uint32_t * i, int adjustment) Line 244	C
     	sbbs.dll!update_clients() Line 185	C++
    >	sbbs.dll!node_thread(void * arg) Line 4568	C++
     	[External Code]
    
    	node_threads_running	{value=0 mutex={DebugInfo=0x00000000 <NULL> LockCount=-6 RecursionCount=0 ...} }	protected_uint32_t
    
    	and again on July-10-2019:
    
    	ntdll.dll!RtlpWaitOnCriticalSection()	Unknown
     	ntdll.dll!RtlpEnterCriticalSectionContended()	Unknown
     	ntdll.dll!_RtlEnterCriticalSection@4()	Unknown
     	sbbs.dll!pthread_mutex_lock(_RTL_CRITICAL_SECTION * mutex) Line 171	C
     	sbbs.dll!protected_uint32_adjust(protected_uint32_t * i, int adjustment) Line 244	C
     	sbbs.dll!update_clients() Line 187	C++
    >	sbbs.dll!node_thread(void * arg) Line 4668	C++
    	*/
    	update_clients();
    	thread_down();
    }
    
    bool sbbs_t::backup(const char* fname, int backup_level, bool rename)
    {
    	if(!fexist(fname))
    		return false;
    
    	lprintf(LOG_DEBUG, "Backing-up %s (%u bytes)", fname, (int)flength(fname));
    	return ::backup(fname, backup_level, rename) ? true : false;
    }
    
    void sbbs_t::daily_maint(void)
    {
    	char			str[128];
    	char			uname[LEN_ALIAS+1];
    	uint			i;
    	uint			usernum;
    	uint			lastusernum;
    	user_t			user;
    
    	lputs(LOG_INFO, "DAILY: System maintenance begun");
    	now=time(NULL);
    
    	if(cfg.node_num) {
    		if((i=getnodedat(cfg.node_num,&thisnode,true)) != 0)
    			errormsg(WHERE,ERR_LOCK,"node file",i);
    		else {
    			thisnode.status=NODE_EVENT_RUNNING;
    			putnodedat(cfg.node_num,&thisnode);
    		}
    	}
    
    	if(cfg.user_backup_level) {
    		lputs(LOG_INFO,"DAILY: Backing-up user data...");
    		SAFEPRINTF(str,"%suser/" USER_DATA_FILENAME, cfg.data_dir);
    		backup(str,cfg.user_backup_level,false);
    		SAFEPRINTF(str,"%suser/name.dat",cfg.data_dir);
    		backup(str,cfg.user_backup_level,false);
    	}
    
    	if(cfg.mail_backup_level) {
    		lputs(LOG_INFO,"DAILY: Backing-up mail data...");
    		smb_t mail;
    		int result = smb_open_sub(&cfg, &mail, INVALID_SUB);
    		if(result != SMB_SUCCESS)
    			lprintf(LOG_ERR, "ERROR %d (%s) opening mail base", result, mail.last_error);
    		else {
    			result = smb_lock(&mail);
    			if(result != SMB_SUCCESS)
    				lprintf(LOG_ERR, "ERROR %d (%s) locking mail base", result, mail.last_error);
    			else {
    				SAFEPRINTF(str,"%smail.shd",cfg.data_dir);
    				backup(str,cfg.mail_backup_level,false);
    				SAFEPRINTF(str,"%smail.sha",cfg.data_dir);
    				backup(str,cfg.mail_backup_level,false);
    				SAFEPRINTF(str,"%smail.sdt",cfg.data_dir);
    				backup(str,cfg.mail_backup_level,false);
    				SAFEPRINTF(str,"%smail.sda",cfg.data_dir);
    				backup(str,cfg.mail_backup_level,false);
    				SAFEPRINTF(str,"%smail.sid",cfg.data_dir);
    				backup(str,cfg.mail_backup_level,false);
    				SAFEPRINTF(str,"%smail.sch",cfg.data_dir);
    				backup(str,cfg.mail_backup_level,false);
    				SAFEPRINTF(str,"%smail.hash",cfg.data_dir);
    				backup(str,cfg.mail_backup_level,false);
    				SAFEPRINTF(str,"%smail.ini",cfg.data_dir);
    				backup(str,cfg.mail_backup_level,false);
    				result = smb_unlock(&mail);
    				if(result != SMB_SUCCESS)
    					lprintf(LOG_ERR, "ERROR %d (%s) unlocking mail base", result, mail.last_error);
    			}
    			smb_close(&mail);
    		}
    	}
    
    	lputs(LOG_INFO, "DAILY: Checking for inactive/expired user records...");
    	lastusernum=lastuser(&cfg);
    	int userfile=openuserdat(&cfg, /* for_modify: */false);
    	for(usernum=1;usernum<=lastusernum;usernum++) {
    		user.number = usernum;
    		if((i=fgetuserdat(&cfg, &user, userfile)) != 0) {
    			SAFEPRINTF(str,"user record %u",usernum);
    			errormsg(WHERE, ERR_READ, str, i);
    			continue;
    		}
    
    		/***********************************************/
    		/* Fix name (name.dat and user.tab) mismatches */
    		/***********************************************/
    		username(&cfg,user.number,uname);
    		if(user.misc&DELETED) {
    			if(strcmp(uname,"DELETED USER"))
    				putusername(&cfg,user.number,nulstr);
    			continue;
    		}
    
    		if(strcmp(user.alias,uname))
    			putusername(&cfg,user.number,user.alias);
    
    		if(user.number==1)
    			continue;	/* skip expiration/inactivity checks for user #1 */
    
    		if(!(user.misc&(DELETED|INACTIVE))
    			&& user.expire && (uint)user.expire<=(uint)now) {
    			putsmsg(user.number,text[AccountHasExpired]);
    			SAFEPRINTF2(str,"DAILY: %s #%u Expired",user.alias,user.number);
    			lputs(LOG_NOTICE, str);
    			if(cfg.level_misc[user.level]&LEVEL_EXPTOVAL
    				&& cfg.level_expireto[user.level]<10) {
    				user.flags1=cfg.val_flags1[cfg.level_expireto[user.level]];
    				user.flags2=cfg.val_flags2[cfg.level_expireto[user.level]];
    				user.flags3=cfg.val_flags3[cfg.level_expireto[user.level]];
    				user.flags4=cfg.val_flags4[cfg.level_expireto[user.level]];
    				user.exempt=cfg.val_exempt[cfg.level_expireto[user.level]];
    				user.rest=cfg.val_rest[cfg.level_expireto[user.level]];
    				if(cfg.val_expire[cfg.level_expireto[user.level]])
    					user.expire=(time32_t)now
    						+(cfg.val_expire[cfg.level_expireto[user.level]]*24*60*60);
    				else
    					user.expire=0;
    				user.level=cfg.val_level[cfg.level_expireto[user.level]];
    			}
    			else {
    				if(cfg.level_misc[user.level]&LEVEL_EXPTOLVL)
    					user.level=cfg.level_expireto[user.level];
    				else
    					user.level=cfg.expired_level;
    				user.flags1&=~cfg.expired_flags1; /* expired status */
    				user.flags2&=~cfg.expired_flags2; /* expired status */
    				user.flags3&=~cfg.expired_flags3; /* expired status */
    				user.flags4&=~cfg.expired_flags4; /* expired status */
    				user.exempt&=~cfg.expired_exempt;
    				user.rest|=cfg.expired_rest;
    				user.expire=0;
    			}
    			putuserdec32(user.number, USER_LEVEL, user.level);
    			putuserflags(user.number, USER_FLAGS1, user.flags1);
    			putuserflags(user.number, USER_FLAGS2, user.flags2);
    			putuserflags(user.number, USER_FLAGS3, user.flags3);
    			putuserflags(user.number, USER_FLAGS4, user.flags4);
    			putuserdatetime(user.number, USER_EXPIRE, user.expire);
    			putuserflags(user.number, USER_EXEMPT, user.exempt);
    			putuserflags(user.number, USER_REST, user.rest);
    			if(cfg.expire_mod[0]) {
    				useron=user;
    				online=ON_LOCAL;
    				exec_bin(cfg.expire_mod,&main_csi);
    				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 */
    			&& (cfg.sys_autodel && (now-user.laston)/(int)(24L*60L*60L)
    			> cfg.sys_autodel)) {			/* Inactive too long */
    			SAFEPRINTF4(str,"DAILY: Auto-Deleted %s (%s) #%u due to inactivity > %u days"
    				,user.alias, user.name, user.number, cfg.sys_autodel);
    			lputs(LOG_NOTICE, str);
    			delallmail(user.number, MAIL_ANY);
    			putusername(&cfg,user.number,nulstr);
    			putusermisc(user.number, user.misc | DELETED);
    		}
    	}
    	closeuserdat(userfile);
    
    	lputs(LOG_INFO,"DAILY: Purging deleted/expired e-mail");
    	SAFEPRINTF(smb.file,"%smail",cfg.data_dir);
    	smb.retry_time=cfg.smb_retry_time;
    	smb.subnum=INVALID_SUB;
    	if((i=smb_open(&smb))!=0)
    		errormsg(WHERE,ERR_OPEN,smb.file,i,smb.last_error);
    	else {
    		if(filelength(fileno(smb.shd_fp))>0) {
    			if((i=smb_locksmbhdr(&smb))!=0)
    				errormsg(WHERE,ERR_LOCK,smb.file,i,smb.last_error);
    			else
    				lprintf(LOG_INFO, "DAILY: Removed %d messages", delmail(0, MAIL_ALL));
    		}
    		smb_close(&smb);
    	}
    
    	if(cfg.sys_daily[0]) {
    		lputs(LOG_INFO, "DAILY: Running system event");
    		const char* cmd = cmdstr(cfg.sys_daily,nulstr,nulstr,NULL);
    		online = ON_LOCAL;
    		int result = external(cmd, EX_OFFLINE);
    		online = false;
    		lprintf(result ? LOG_ERR : LOG_INFO, "Daily event: '%s' returned %d", cmd, result);
    	}
    	lputs(LOG_INFO, "DAILY: System maintenance ended");
    	sys_status&=~SS_DAILY;
    }
    
    const char* js_ver(void)
    {
    #ifdef JAVASCRIPT
    	return(JS_GetImplementationVersion());
    #else
    	return("");
    #endif
    }
    
    /* Returns char string of version and revision */
    const char* 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  Compiled %s/%s %s %s with %s"
    			,TELNET_SERVER
    			,VERSION, REVISION
    #ifdef _DEBUG
    			," Debug"
    #else
    			,""
    #endif
    			,git_branch, git_hash
    			,__DATE__, __TIME__, compiler
    			);
    	}
    	return(ver);
    }
    
    /* Returns binary-coded version and revision (e.g. 0x31000 == 3.10a) */
    int bbs_ver_num(void)
    {
    	return(VERSION_HEX);
    }
    
    void 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);
    	ts_set = NULL;
    
    #ifdef _WINSOCKAPI_
    	if(WSAInitialized && WSACleanup()!=0)
    		lprintf(LOG_ERR,"!WSACleanup ERROR %d",ERROR_VALUE);
    #endif
    
    	free_cfg(&scfg);
    	free_text(text);
    
    	for(int i = 0; i < MAX_NODES; i++) {
    		free_text(node_text[i]);
    		free_cfg(&node_scfg[i]);
    		memset(&node_scfg[i], 0, sizeof(node_scfg[i]));
    	}
    
    	semfile_list_free(&pause_semfiles);
    	semfile_list_free(&recycle_semfiles);
    	semfile_list_free(&shutdown_semfiles);
    	semfile_list_free(&clear_attempts_semfiles);
    
    	listFree(&current_logins);
    	listFree(&current_connections);
    
    	protected_uint32_destroy(node_threads_running);
    	protected_uint32_destroy(ssh_sessions);
    
    	thread_down();
    	if(terminate_server || code)
    		lprintf(LOG_INFO,"Terminal Server thread terminated (%u clients served)", served);
    	set_state(SERVER_STOPPED);
    	mqtt_shutdown(&mqtt);
    	if(startup->terminated!=NULL)
    		startup->terminated(startup->cbdata,code);
    }
    
    void 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				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__
    	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;
    
        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;
    	}
    
    	set_state(SERVER_INIT);
    
    #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;
    	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);
    	protected_uint32_init(&ssh_sessions,0);
    
    	thread_up(false /* setuid */);
    	if(startup->seteuid!=NULL)
    		startup->seteuid(true);
    
    	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%c%s"
    		,TELNET_SERVER
    		,VERSION
    		,REVISION
    #ifdef _DEBUG
    		," Debug"
    #else
    		,""
    #endif
    		);
    	lprintf(LOG_INFO,"Compiled %s/%s %s %s with %s", git_branch, git_hash, __DATE__, __TIME__, compiler);
    
    #ifdef _DEBUG
    	lprintf(LOG_DEBUG, "sizeof: int=%d, long=%d, off_t=%d, time_t=%d"
    		,(int)sizeof(int), (int)sizeof(long), (int)sizeof(off_t), (int)sizeof(time_t));
    
    	memset(str, 0xff, sizeof(str));
    	snprintf(str, 2, "\x01\x02");
    	if(str[1] != '\0') {
    		lprintf(LOG_CRIT, "Non-terminating (unsafe) snprintf function detected (0x%02X instead of 0x00)", (uchar)str[1]);
    		cleanup(1);
    		return;
    	}
    #endif
    
        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"
    			,(int)sizeof(node_t),SIZEOF_NODE_T);
    		cleanup(1);
    		return;
    	}
    
    	if(!winsock_startup()) {
    		cleanup(1);
    		return;
    	}
    
    	t=time(NULL);
    	lprintf(LOG_INFO,"Initializing on %.24s with options: %x"
    		,ctime_r(&t,str),startup->options);
    
    	if(chdir(startup->ctrl_dir)!=0)
    		lprintf(LOG_ERR,"!ERROR %d (%s) changing directory to: %s", errno, strerror(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, /* prep: */true, /* node_req: */true, logstr, sizeof(logstr))) {
    		lprintf(LOG_CRIT,"!ERROR %s",logstr);
    		lprintf(LOG_CRIT,"!FAILED to load configuration files");
    		cleanup(1);
    		return;
    	}
    
    	mqtt_startup(&mqtt, &scfg, (struct startup*)startup, bbs_ver(), lputs);
    
    	if(scfg.total_shells < 1) {
    		lprintf(LOG_CRIT, "At least one command shell must be configured (e.g. in SCFG->Command Shells)");
    		cleanup(1);
    		return;
    	}
    
    	if((t=checktime())!=0) {   /* Check binary time */
    		lprintf(LOG_ERR,"!TIME PROBLEM (%" PRId64 ")",(int64_t)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;
        }
    
    	SAFEPRINTF(str, "%u total", scfg.sys_nodes);
    	mqtt_pub_strval(&mqtt, TOPIC_BBS, "node", str);
    
    	/* Create missing directories */
    	lprintf(LOG_INFO,"Verifying/creating data directories");
    	make_data_dirs(&scfg);
    
    	/* Create missing node directories */
    	lprintf(LOG_INFO,"Verifying/creating node directories");
    	for(i=0;i<=scfg.sys_nodes;i++) {
    		char newfn[MAX_PATH + 1];
    		char oldfn[MAX_PATH + 1];
    		if(i) {
    			int err;
    			if((err = md(scfg.node_path[i-1])) != 0) {
    				lprintf(LOG_CRIT,"!ERROR %d (%s) creating directory: %s", err, strerror(err), scfg.node_path[i-1]);
    				cleanup(1);
    				return;
    			}
    		}
    		// Convert old dsts.dab -> dsts.ini
    		SAFEPRINTF(oldfn, "%sdsts.dab", i ? scfg.node_path[i-1] : scfg.ctrl_dir);
    		if(fexist(oldfn) && !fexist(dstats_fname(&scfg, i, newfn, sizeof(newfn)))) {
    			lprintf(LOG_NOTICE, "Auto-upgrading daily statistics data file: %s", oldfn);
    			stats_t stats;
    			getstats(&scfg, i, &stats);
    			putstats(&scfg, i, &stats);
    		}
    		SAFEPRINTF(oldfn, "%scsts.dab", i ? scfg.node_path[i-1] : scfg.ctrl_dir);
    		if(fexist(oldfn) && !fexist(cstats_fname(&scfg, i, newfn, sizeof(newfn)))) {
    			uint record = 0;
    			lprintf(LOG_NOTICE, "Auto-upgrading cumulative statistics log file: %s", oldfn);
    			stats_t stats;
    			FILE* out = fopen_cstats(&scfg, i, /* write: */true);
    			if(out == NULL) {
    				lprintf(LOG_ERR, "!ERROR %d (%s) creating: %s", errno, strerror(errno), newfn);
    				continue;
    			}
    			FILE* in = fopen(oldfn, "rb");
    			if(in == NULL)
    				lprintf(LOG_ERR, "!ERROR %d (%s) opening: %s", errno, strerror(errno), oldfn);
    			else {
    				ZERO_VAR(stats);
    				while(!feof(in)) {
    					struct {									/* System/Node Statistics */
    						uint32_t	date,						/* When last rolled-over */
    									logons,						/* Total Logons on System */
    									timeon,						/* Total Time on System */
    									uls,						/* Total Uploads Today */
    									ulb,						/* Total Upload Bytes Today */
    									dls,						/* Total Downloads Today */
    									dlb,						/* Total Download Bytes Today */
    									ptoday,						/* Total Posts Today */
    									etoday,						/* Total Emails Today */
    									ftoday; 					/* Total Feedbacks Today */
    					} legacy_stats;
    					if(fread(&legacy_stats, sizeof(legacy_stats), 1, in) != 1)
    						break;
    					record++;
    					if(legacy_stats.logons > 1000
    						|| legacy_stats.timeon > 10000) {
    						lprintf(LOG_WARNING, "Skipped corrupted record #%u in %s", record, oldfn);
    						continue;
    					}
    					ZERO_VAR(stats);
    					stats.date         = legacy_stats.date;
    					stats.today.logons = legacy_stats.logons;
    					stats.today.timeon = legacy_stats.timeon;
    					stats.today.uls    = legacy_stats.uls;
    					stats.today.ulb    = legacy_stats.ulb;
    					stats.today.dls    = legacy_stats.dls;
    					stats.today.dlb    = legacy_stats.dlb;
    					stats.today.posts  = legacy_stats.ptoday;
    					stats.today.email  = legacy_stats.etoday;
    					stats.today.fbacks = legacy_stats.ftoday;
    					if(!fwrite_cstats(out, &stats)) {
    						lprintf(LOG_ERR, "!WRITE ERROR, %s line %d", __FILE__, __LINE__);
    						break;
    					}
    				}
    				fclose(in);
    			}
    			fclose_cstats(out);
    			lprintf(LOG_INFO, "Done (%u daily-statistics records converted)", record);
    		}
    	}
    
    	/* 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;
    #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;
    	}
    	if (!(startup->options & BBS_OPT_NO_TELNET)) {
    		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);
    	}
    
    	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);
    	}
    
    #ifdef USE_CRYPTLIB
    #if CRYPTLIB_VERSION < 3300 && CRYPTLIB_VERSION > 999
    	#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)) {
    			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_READONLY))) {
    			if(cryptStatusError(i=cryptGetPrivateKey(ssh_keyset, &ssh_context, CRYPT_KEYID_NAME, "ssh_server", scfg.sys_pass))) {
    				GCESNN(i, ssh_keyset, "getting private key");
    				goto NO_SSH;
    			}
    		}
    		else {
    			/* Couldn't do that... create a new context and use the key from there... */
    
    			if(cryptStatusError(i=cryptCreateContext(&ssh_context, CRYPT_UNUSED, CRYPT_ALGO_RSA))) {
    				GCESNN(i, CRYPT_UNUSED, "creating context");
    				goto NO_SSH;
    			}
    			if(cryptStatusError(i=cryptSetAttributeString(ssh_context, CRYPT_CTXINFO_LABEL, "ssh_server", 10))) {
    				GCESNN(i, ssh_context, "setting label");
    				goto NO_SSH;
    			}
    			if(cryptStatusError(i=cryptGenerateKey(ssh_context))) {
    				GCESNN(i, ssh_context, "generating key");
    				goto NO_SSH;
    			}
    
    			/* Ok, now try saving this one... use the syspass to encrypt it. */
    			if(cryptStatusOK(i=cryptKeysetOpen(&ssh_keyset, CRYPT_UNUSED, CRYPT_KEYSET_FILE, str, CRYPT_KEYOPT_CREATE))) {
    				if(cryptStatusError(i=cryptAddPrivateKey(ssh_keyset, ssh_context, scfg.sys_pass)))
    					GCESNN(i, ssh_keyset, "adding private key");
    				if(cryptStatusError(i=cryptKeysetClose(ssh_keyset)))
    					GCESNN(i, ssh_keyset, "closing keyset");
    			}
    			else
    				GCESNN(i, CRYPT_UNUSED, "creating keyset");
    		}
    
    		/* 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);
    	}
    NO_SSH:
    #endif
    
    	sbbs_t* 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, true);
    		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 in case 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);
    	}
    
    	/* Setup recycle/shutdown semaphore file lists */
    	shutdown_semfiles = semfile_list_init(scfg.ctrl_dir,"shutdown", server_abbrev);
    	recycle_semfiles = semfile_list_init(scfg.ctrl_dir,"recycle", server_abbrev);
    	pause_semfiles = semfile_list_init(scfg.ctrl_dir,"pause", server_abbrev);
    	clear_attempts_semfiles = semfile_list_init(scfg.ctrl_dir,"clear", server_abbrev);
    	semfile_list_add(&recycle_semfiles,startup->ini_fname);
    	strListAppendFormat(&recycle_semfiles, "%stext.dat", scfg.ctrl_dir);
    	strListAppendFormat(&recycle_semfiles, "%stext.ini", scfg.ctrl_dir);
    	strListAppendFormat(&recycle_semfiles, "%sattr.cfg", scfg.ctrl_dir);
    	if(!initialized)
    		semfile_list_check(&initialized,shutdown_semfiles);
    	semfile_list_check(&initialized,recycle_semfiles);
    	semfile_list_check(&initialized,clear_attempts_semfiles);
    
    	listInit(&current_logins, LINK_LIST_MUTEX);
    	listInit(&current_connections, LINK_LIST_MUTEX);
    
    #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 (%s) creating local spy socket %s"
    					, i, errno, strerror(errno), str);
    	    }
    	}
    #endif // __unix__ (unix-domain spy sockets)
    
    	lprintf(LOG_INFO,"Terminal Server thread started for nodes %d through %d", first_node, last_node);
    	mqtt_client_max(&mqtt, (last_node - first_node) + 1);
    
    	while(!terminate_server) {
    		YIELD();
    		/* check for re-run flags and recycle/shutdown sem files */
    		if(!(startup->options&BBS_OPT_NO_RECYCLE)
    			&& protected_uint32_value(node_threads_running) == 0) {
    			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;
    		}
    		if(((p = semfile_list_check(NULL, pause_semfiles)) != NULL
    				&& lprintf(LOG_INFO, "Pause semaphore file (%s) detected"
    					,p))
    			|| (startup->paused
    				&& lprintf(LOG_INFO, "Pause semaphore signaled"))) {
    			set_state(SERVER_PAUSED);
    			SLEEP(startup->sem_chk_freq * 1000);
    			continue;
    		}
    		/* signal caller that we've started up successfully */
    		set_state(SERVER_READY);
    
        	sbbs->online=false;
    #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, (startup->options & BBS_OPT_HAPROXY_PROTO) ? XPMS_ACCEPT_FLAG_HAPROXY : XPMS_FLAGS_NONE, &ts_cb);
    
    		if(terminate_server)	/* terminated */
    			break;
    
    		if((p=semfile_list_check(&initialized,clear_attempts_semfiles))!=NULL) {
    			lprintf(LOG_INFO,"Clear Failed Login Attempts semaphore file (%s) detected", p);
    			loginAttemptListClear(startup->login_attempt_list);
    		}
    
    		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;
    #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)
    						if(read(uspy_socket[i-1],str,sizeof(str)) < 1)
    							*str = '\0';
    					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_INFO, "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;
    		}
    
    		// Count the socket:
    		call_socket_open_callback(true);
    
    		char host_ip[INET6_ADDRSTRLEN];
    
    		inet_addrtop(&client_addr, host_ip, sizeof(host_ip));
    
    		if(trashcan(&scfg,host_ip,"ip-silent")) {
    			close_socket(client_socket);
    			continue;
    		}
    
    
    #ifdef USE_CRYPTLIB
    		client.protocol=rlogin ? "RLogin":(ssh ? "SSH" : "Telnet");
    #else
    		client.protocol=rlogin ? "RLogin":"Telnet";
    #endif
    		union xp_sockaddr local_addr;
    		memset(&local_addr, 0, sizeof(local_addr));
    		socklen_t addr_len = sizeof(local_addr);
    		if(getsockname(client_socket, (struct sockaddr *)&local_addr, &addr_len) != 0) {
    			lprintf(LOG_CRIT,"%04d %s [%s] !ERROR %d getting local address/port of socket"
    				,client_socket, client.protocol, host_ip, ERROR_VALUE);
    			close_socket(client_socket);
    			continue;
    		}
    		char local_ip[INET6_ADDRSTRLEN];
    		inet_addrtop(&local_addr, local_ip, sizeof local_ip);
    
    		lprintf(LOG_INFO,"%04d %s [%s] Connection accepted on %s port %u from port %u"
    			,client_socket, client.protocol, host_ip, local_ip, inet_addrport(&local_addr), inet_addrport(&client_addr));
    
    		if(startup->max_concurrent_connections > 0) {
    			int ip_len = strlen(host_ip)+1;
    			int connections = listCountMatches(&current_connections, host_ip, ip_len);
    			int logins = listCountMatches(&current_logins, host_ip, ip_len);
    
    			if(connections - logins >= (int)startup->max_concurrent_connections
    				&& !is_host_exempt(&scfg, host_ip, /* host_name */NULL)) {
    				lprintf(LOG_NOTICE, "%04d %s !Maximum concurrent connections without login (%u) reached from host: %s"
     					,client_socket, client.protocol, startup->max_concurrent_connections, host_ip);
    				close_socket(client_socket);
    				continue;
    			}
    		}
    
    		sbbs->client_socket=client_socket;	// required for output to the user
    		if(!ssh)
    			sbbs->online=ON_REMOTE;
    
    		login_attempt_t attempted;
    		uint banned = loginBanned(&scfg, startup->login_attempt_list, client_socket, /* host_name: */NULL, startup->login_attempt, &attempted);
    		if(banned) {
    			char ban_duration[128];
    			lprintf(LOG_NOTICE, "%04d %s [%s] !TEMPORARY BAN (%lu login attempts%s%s) - remaining: %s"
    				,client_socket, client.protocol, host_ip, attempted.count-attempted.dupes
    				,attempted.user[0] ? ", last: " : "", attempted.user, duration_estimate_to_vstr(banned, ban_duration, sizeof ban_duration, 1, 1));
    			close_socket(client_socket);
    			continue;
    		}
    		struct trash trash;
    		if(sbbs->trashcan(host_ip, "ip", &trash)) {
    			char details[128];
    			lprintf(LOG_NOTICE,"%04d %s [%s] !CLIENT BLOCKED in ip.can %s", client_socket, client.protocol, host_ip, trash_details(&trash, details, sizeof details));
    			close_socket(client_socket);
    			continue;
    		}
    
    #ifdef _WIN32
    		if(startup->sound.answer[0] && !sound_muted(&scfg))
    			PlaySound(startup->sound.answer, 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) {
    			BOOL nodelay = true;
    			ulong nb = 0;
    
    			if(cryptStatusError(i=cryptCreateSession(&sbbs->ssh_session, CRYPT_UNUSED, CRYPT_SESSION_SSH_SERVER))) {
    				GCESS(i, client_socket, CRYPT_UNUSED, "creating SSH session");
    				close_socket(client_socket);
    				continue;
    			}
    			lprintf(LOG_DEBUG, "%04d SSH Cryptlib Session: %d created", client_socket, sbbs->ssh_session);
    			protected_uint32_adjust(&ssh_sessions, 1);
    			sbbs->ssh_mode = true;
    
    			if(cryptStatusError(i=cryptSetAttribute(sbbs->ssh_session, CRYPT_OPTION_NET_CONNECTTIMEOUT, startup->ssh_connect_timeout)))
    				sbbs->log_crypt_error_status_sock(i, "setting connect timeout");
    
    			if (startup->options & BBS_OPT_SSH_ANYAUTH) {
    				if(cryptStatusError(i=cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_OPTIONS, CRYPT_SSHOPTION_NONE_AUTH)))
    					sbbs->log_crypt_error_status_sock(i, "setting none auth");
    			}
    
    			if(cryptStatusError(i=cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_PRIVATEKEY, ssh_context))) {
    				sbbs->log_crypt_error_status_sock(i, "setting private key");
    				SSH_END(client_socket);
    				close_socket(client_socket);
    				continue;
    			}
    			setsockopt(client_socket,IPPROTO_TCP,TCP_NODELAY,(char*)&nodelay,sizeof(nodelay));
    			ioctlsocket(client_socket,FIONBIO,&nb);
    			if(cryptStatusError(i=cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_NETWORKSOCKET, client_socket))) {
    				sbbs->log_crypt_error_status_sock(i, "setting network socket");
    				SSH_END(client_socket);
    				close_socket(client_socket);
    				continue;
    			}
    			if(cryptStatusError(i=cryptSetAttribute(sbbs->ssh_session, CRYPT_OPTION_NET_READTIMEOUT, 0)))
    				sbbs->log_crypt_error_status_sock(i, "setting read timeout");
    			// READ = WRITE TIMEOUT HACK... REMOVE WHEN FIXED
    			if(cryptStatusError(i=cryptSetAttribute(sbbs->ssh_session, CRYPT_OPTION_NET_WRITETIMEOUT, 0)))
    				sbbs->log_crypt_error_status_sock(i, "setting write timeout");
    #if 0
    			if(cryptStatusError(err=crypt_pop_channel_data(sbbs, str, sizeof(str), &i))) {
    				GCES(i, sbbs->cfg.node_num, sbbs->ssh_session, "popping data");
    				i=0;
    			}
    #endif
    		}
    #endif
    
    		if(rlogin)
    			sbbs->outcom(0); /* acknowledge RLogin per RFC 1282 */
    
    		sbbs->autoterm=0;
    		sbbs->cols = startup->default_term_width;
    		if (inet_addrport(&local_addr) == startup->pet40_port || inet_addrport(&local_addr) == startup->pet80_port) {
    			sbbs->autoterm = PETSCII;
    			sbbs->cols = inet_addrport(&local_addr) == startup->pet40_port ? 40 : 80;
    			sbbs->outcom(PETSCII_UPPERLOWER);
    		}
    
    		sbbs->bprintf("\r\n%s\r\n", VERSION_NOTICE);
    		sbbs->bprintf("%s connection from: %s\r\n", client.protocol, host_ip);
    
    		SAFECOPY(host_name, STR_NO_HOSTNAME);
    		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 %s [%s] Hostname: %s", client_socket, client.protocol, host_ip, host_name);
    		}
    
    		if(sbbs->trashcan(host_name,"host", &trash)) {
    			SSH_END(client_socket);
    			close_socket(client_socket);
    			char details[128];
    			lprintf(LOG_NOTICE,"%04d %s [%s] !CLIENT BLOCKED in host.can: %s %s"
    				,client_socket, client.protocol, host_ip, host_name, trash_details(&trash, details, sizeof details));
    			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 %s [%s] Ident Response: %s",client_socket, client.protocol, host_ip, str);
    				identity=strrchr(str,':');
    				if(identity!=NULL) {
    					identity++;	/* skip colon */
    					SKIP_WHITESPACE(identity);
    					if(*identity)
    						lprintf(LOG_INFO,"%04d %s [%s] Identity: %s",client_socket, client.protocol, host_ip, 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);
    		SAFECOPY(client.user, STR_UNKNOWN_USER);
    		client.usernum = 0;
    		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) {
    				if(node_socket[i - 1] != INVALID_SOCKET) {
    					lprintf(LOG_CRIT, "%04d !Node %d status is WFC, but the node socket (%d) and thread are still in use!"
    						,client_socket, i, node_socket[i - 1]);
    					sbbs->putnodedat(i, &node);
    					continue;
    				}
    				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 %s [%s] !No nodes available for login.", client_socket, client.protocol, host_ip);
    			SAFEPRINTF(str,"%snonodes.txt",scfg.text_dir);
    			if(fexist(str))
    				sbbs->printfile(str,P_NOABORT);
    			else {
    				sbbs->putcom("\r\nSorry, all terminal 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(client_socket);
    			close_socket(client_socket);
    			continue;
    		}
    
    		lprintf(LOG_INFO, "%04d %s [%s] Attaching to Node %d", client_socket, client.protocol, host_ip, i);
    
    		// Load the configuration files for this node, only if/when needed/updated
    		scfg_t* cfg = &node_scfg[i - 1];
    		if(cfg->size != sizeof(*cfg) || (node.misc & NODE_RRUN)) {
    			sbbs->bprintf("Loading configuration...");
    			cfg->size = sizeof(*cfg);
    			cfg->node_num = i;
    		    SAFECOPY(cfg->ctrl_dir, startup->ctrl_dir);
    			lprintf(LOG_INFO,"Node %d Loading configuration files from %s", cfg->node_num, cfg->ctrl_dir);
    			SAFECOPY(logstr,UNKNOWN_LOAD_ERROR);
    			if(!load_cfg(cfg, node_text[i - 1], /* prep: */true, /* node_req: */true, logstr, sizeof(logstr))) {
    				lprintf(LOG_WARNING, "Node %d LOAD ERROR: %s, falling back to Node %d", cfg->node_num, logstr, first_node);
    				cfg->node_num = first_node;
    				if(!load_cfg(cfg, node_text[i - 1], /* prep: */true, /* node: */true, logstr, sizeof(logstr))) {
    					lprintf(LOG_CRIT,"!ERROR %s",logstr);
    					lprintf(LOG_CRIT,"!FAILED to load configuration files");
    					sbbs->bprintf("\r\nFAILED: %s", logstr);
    					client_off(client_socket);
    					SSH_END(client_socket);
    					close_socket(client_socket);
    					sbbs->getnodedat(cfg->node_num,&node,true);
    					node.status = NODE_WFC;
    					sbbs->putnodedat(cfg->node_num,&node);
    					continue;
    				}
    			}
    			cfg->node_num = i; // correct the node number
    			if(node.misc & NODE_RRUN) {
    				sbbs->getnodedat(cfg->node_num,&node,true);
    				node.misc &= ~NODE_RRUN;
    				sbbs->putnodedat(cfg->node_num,&node);
    			}
    			sbbs->bputs(crlf);
    		}
    		// Copy event last-run info from global config
    		for(int e=0; e < cfg->total_events && e < scfg.total_events; e++)
    			cfg->event[e]->last = scfg.event[e]->last;
    
            node_socket[i-1]=client_socket;
    
    		sbbs_t* new_node = new sbbs_t(/* node_num: */i, &client_addr, client_addr_len, host_name
            	,client_socket
    			,cfg, node_text[i-1], &client);
    
    		new_node->client=client;
    #ifdef USE_CRYPTLIB
    		if(ssh) {
    			new_node->ssh_session=sbbs->ssh_session;	// This is done again later, after NO_PASSTHRU: Why?
    			new_node->ssh_mode=true;
    			new_node->session_channel = sbbs->session_channel;
    		}
    #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 %s Node %d !Initialization failure"
    				,client_socket, client.protocol, 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(client_socket);
    			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
    		}
    
    		// Passthru socket creation/connection
    		if(true) {
    			/* 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(PF_INET, SOCK_STREAM, "passthru");
    
    			if(tmp_sock == INVALID_SOCKET) {
    				lprintf(LOG_ERR,"Node %d !ERROR %d creating passthru listen socket"
    					,new_node->cfg.node_num, ERROR_VALUE);
    				goto NO_PASSTHRU;
    			}
    
        		lprintf(LOG_DEBUG,"Node %d passthru listen socket %d opened"
    				,new_node->cfg.node_num, 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,"Node %d !ERROR %d (%d) listening on passthru socket"
    					,new_node->cfg.node_num, result, ERROR_VALUE);
    				close_socket(tmp_sock);
    				goto NO_PASSTHRU;
    			}
    
    			tmp_addr_len=sizeof(tmp_addr);
    			if(getsockname(tmp_sock, (struct sockaddr *)&tmp_addr, &tmp_addr_len)) {
    				lprintf(LOG_CRIT,"Node %d !ERROR %d getting passthru listener address/port of socket"
    					,new_node->cfg.node_num, ERROR_VALUE);
    				close_socket(tmp_sock);
    				goto NO_PASSTHRU;
    			}
    			lprintf(LOG_DEBUG,"Node %d passthru socket listening on port %u"
    				,new_node->cfg.node_num, htons(tmp_addr.sin_port));
    
        		new_node->passthru_socket = open_socket(PF_INET, SOCK_STREAM, "passthru");
    
    			if(new_node->passthru_socket == INVALID_SOCKET) {
    				lprintf(LOG_ERR,"Node %d !ERROR %d creating passthru connecting socket"
    					,new_node->cfg.node_num, ERROR_VALUE);
    				close_socket(tmp_sock);
    				goto NO_PASSTHRU;
    			}
    
    			lprintf(LOG_DEBUG,"Node %d passthru connect socket %d opened"
    				,new_node->cfg.node_num, new_node->passthru_socket);
    
    			result = connect(new_node->passthru_socket, (struct sockaddr *)&tmp_addr, tmp_addr_len);
    
    			if(result != 0) {
    				lprintf(LOG_ERR,"Node %d !ERROR %d (%d) connecting to passthru socket: %s port %u"
    					,new_node->cfg.node_num, result, ERROR_VALUE, inet_ntoa(tmp_addr.sin_addr), htons(tmp_addr.sin_port));
    				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,"Node %d !ERROR (%d) accepting on passthru socket"
    					,new_node->cfg.node_num, ERROR_VALUE);
    				lprintf(LOG_WARNING,"Node %d !WARNING native doors which use sockets will not function"
    					,new_node->cfg.node_num);
    				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_thread_running = true;
    			_beginthread(passthru_thread, 0, new_node);
    
    	NO_PASSTHRU:
    			if(ssh) {
    				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;
    				new_node->online = ON_REMOTE;
    			}
    			/* 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);
    			pthread_mutex_lock(&sbbs->ssh_mutex);
    			sbbs->ssh_mode=false;
    			sbbs->ssh_session=0; // Don't allow subsequent SSH connections to affect this one (!)
    			pthread_mutex_unlock(&sbbs->ssh_mutex);
    		}
    
    	    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;
    		new_node->autoterm = sbbs->autoterm;
    		new_node->cols = sbbs->cols;
    		_beginthread(output_thread, 0, new_node);
    		_beginthread(node_thread, 0, new_node);
    		served++;
    	}
    
    	set_state(terminate_server ? SERVER_STOPPING : SERVER_RELOADING);
    
        // 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_socket[i]!=INVALID_SOCKET) {
    			close_socket(uspy_socket[i]);
    			uspy_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);
    #endif
    	}
    
    	sbbs->client_socket=INVALID_SOCKET;
    	if(events!=NULL)
    		events->terminated=true;
        // Wake-up BBS output thread so it can terminate
        SetEvent(sbbs->outbuf.data_event);
        SetEvent(sbbs->outbuf.highwater_event);
    
        // 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);
    		}
    		lprintf(LOG_INFO, "Done waiting for node threads to terminate");
    	}
    
    	// 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);
    		}
    		lprintf(LOG_INFO, "Done waiting for events thread to terminate");
    	}
    
        // 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);
    		}
    		lprintf(LOG_INFO, "Done waiting for system output thread to terminate");
    	}
    
        // Wait for BBS passthru thread to terminate
    	if(sbbs->passthru_thread_running) {
    		lprintf(LOG_INFO,"Waiting for passthru thread to terminate...");
    		start=time(NULL);
    		while(sbbs->passthru_thread_running) {
    			if(time(NULL)-start>TIMEOUT_THREAD_WAIT) {
    				lprintf(LOG_ERR,"!TIMEOUT waiting for passthru thread to terminate");
    				break;
    			}
    			mswait(100);
    		}
    		lprintf(LOG_INFO, "Done waiting for passthru threads to terminate");
    	}
    
        // 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->passthru_thread_running || sbbs->output_thread_running)
    		lprintf(LOG_ERR,"!System I/O 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);
    
    }