From 9b3be7dc63b3bb6a4b8a3e4e6b2e7f799fdaa5ee Mon Sep 17 00:00:00 2001
From: "Rob Swindell (on Windows 11)" <rob@synchro.net>
Date: Thu, 14 Nov 2024 18:24:46 -0800
Subject: [PATCH] Only allow one FTP session per QWKnet user account

Vertrauen's FTP server gets abused by QWKnet logins sometimes and handling
the race conditions around QWK packet creation attempts is silly - there's
no legit reason why a QWKnet account needs to be logged-in multiple times
concurrently to the hub's FTP server, so reject the subsequent logins even
when they're on different hosts (as is the case with Vertrauen).

As part of this change:
- fmutex() now takes an new time_t* argument to (optionally) store the
  time of the mutex file for helping logging (locked since when?).
- time_as_hhmm() created to format a string as either HH:MM or HH:MM[a|p]
  (depending on system configuration for 12 or 24 hour time formatting).
- renamed the old hhmmtostr ()to tm_as_hhmm() (since it takes a struct tm arg)
  and have it return a non-padded string (useful in more situations without
  requiring truncation) when the sysop prefers 24-hour time.
---
 src/sbbs3/answer.cpp  |  4 ++--
 src/sbbs3/date_str.c  | 20 +++++++++++++++++---
 src/sbbs3/date_str.h  |  5 +++--
 src/sbbs3/ftpsrvr.c   | 16 +++++++++++++++-
 src/sbbs3/js_global.c |  2 +-
 src/sbbs3/logout.cpp  | 10 +++-------
 src/sbbs3/main.cpp    | 10 ++++++----
 src/sbbs3/nopen.c     | 13 +++++++++----
 src/sbbs3/nopen.h     |  2 +-
 src/sbbs3/sbbsecho.c  | 11 +++++++----
 10 files changed, 64 insertions(+), 29 deletions(-)

diff --git a/src/sbbs3/answer.cpp b/src/sbbs3/answer.cpp
index 34ea886662..52a6085388 100644
--- a/src/sbbs3/answer.cpp
+++ b/src/sbbs3/answer.cpp
@@ -114,8 +114,8 @@ bool sbbs_t::answer()
 	memset(&tm,0,sizeof(tm));
     localtime_r(&now,&tm); 
 
-	safe_snprintf(str,sizeof(str),"%s  %s %s %02d %u            Node %3u"
-		,hhmmtostr(&cfg,&tm,str2)
+	safe_snprintf(str,sizeof(str),"%-6s  %s %s %02d %u            Node %3u"
+		,tm_as_hhmm(&cfg, &tm, str2)
 		,wday[tm.tm_wday]
         ,mon[tm.tm_mon],tm.tm_mday,tm.tm_year+1900,cfg.node_num);
 	logline("@ ",str);
diff --git a/src/sbbs3/date_str.c b/src/sbbs3/date_str.c
index c147a0f6ff..7afd30cc01 100644
--- a/src/sbbs3/date_str.c
+++ b/src/sbbs3/date_str.c
@@ -218,11 +218,12 @@ char* minutes_to_str(uint min, char* str, size_t size)
 }
 
 /****************************************************************************/
+/* Returns 5 or 6 character string, depending on configuration				*/
 /****************************************************************************/
-char* hhmmtostr(scfg_t* cfg, struct tm* tm, char* str)
+char* tm_as_hhmm(scfg_t* cfg, struct tm* tm, char* str)
 {
-	if(cfg->sys_misc&SM_MILITARY)
-		sprintf(str,"%02d:%02d "
+	if(cfg != NULL && (cfg->sys_misc & SM_MILITARY))
+		sprintf(str,"%02d:%02d"
 	        ,tm->tm_hour,tm->tm_min);
 	else
 		sprintf(str,"%02d:%02d%c"
@@ -231,6 +232,19 @@ char* hhmmtostr(scfg_t* cfg, struct tm* tm, char* str)
 	return(str);
 }
 
+/****************************************************************************/
+/* Returns 5 or 6 character string, depending on configuration				*/
+/****************************************************************************/
+char* time_as_hhmm(scfg_t* cfg, time_t t, char* str)
+{
+	struct tm tm;
+	if(localtime_r(&t, &tm) == NULL) {
+		strcpy(str,"??:??");
+		return str;
+	}
+	return tm_as_hhmm(cfg, &tm, str);
+}
+
 /****************************************************************************/
 /* Generates a 24 character ASCII string that represents the time_t pointer */
 /* Used as a replacement for ctime()                                        */
diff --git a/src/sbbs3/date_str.h b/src/sbbs3/date_str.h
index 993750d032..04b0bffd66 100644
--- a/src/sbbs3/date_str.h
+++ b/src/sbbs3/date_str.h
@@ -41,8 +41,9 @@ DLLEXPORT char *	datestr(scfg_t*, time_t, char* str);
 DLLEXPORT char *	verbal_datestr(scfg_t*, time_t, char* str);
 DLLEXPORT char *	sectostr(uint sec, char *str);
 DLLEXPORT char *	seconds_to_str(uint, char*);
-DLLEXPORT char *	hhmmtostr(scfg_t* cfg, struct tm* tm, char* str);
-DLLEXPORT char *	timestr(scfg_t* cfg, time32_t intime, char* str);
+DLLEXPORT char *	tm_as_hhmm(scfg_t* cfg, struct tm*, char* buf);
+DLLEXPORT char *	time_as_hhmm(scfg_t* cfg, time_t, char* buf);
+DLLEXPORT char *	timestr(scfg_t* cfg, time32_t, char* str);
 DLLEXPORT char*		minutes_to_str(uint min, char* str, size_t);
 
 #ifdef __cplusplus
diff --git a/src/sbbs3/ftpsrvr.c b/src/sbbs3/ftpsrvr.c
index 216752481f..0e5ef8d16f 100644
--- a/src/sbbs3/ftpsrvr.c
+++ b/src/sbbs3/ftpsrvr.c
@@ -2134,6 +2134,7 @@ static void ctrl_thread(void* arg)
 	char		aliasfile[MAX_PATH+1];
 	char		aliaspath[MAX_PATH+1];
 	char		mls_path[MAX_PATH+1];
+	char		mutex_file[MAX_PATH+1]="";
 	char		*mls_fname;
 	char		permstr[11];
 	char		aliasline[512];
@@ -2595,6 +2596,17 @@ static void ctrl_thread(void* arg)
 				continue;
 			}
 
+			snprintf(mutex_file, sizeof mutex_file, "%suser/%04u.ftp", scfg.data_dir, user.number);
+			if(user.rest & FLAG('Q')) { // QWKnet accont
+				if(!fmutex(mutex_file, startup->host_name, /* max_age: */60 * 60, &t)) {
+					lprintf(LOG_NOTICE, "%04d <%s> QWKnet account already logged-in to FTP server: %s (since %s)"
+						,sock, user.alias, mutex_file, time_as_hhmm(&scfg, t, str));
+					sockprintf(sock, sess, "421 QWKnet accounts are limited to one concurrent FTP session");
+					user.number = 0;
+					break;
+				}
+			}
+
 			/* Update client display */
 			if(user.pass[0]) {
 				SAFECOPY(client.user, user.alias);
@@ -4254,7 +4266,7 @@ static void ctrl_thread(void* arg)
 				if(!fexistcase(qwkfile)) {
 					lprintf(LOG_INFO,"%04d <%s> creating QWK packet...",sock,user.alias);
 					sprintf(str,"%spack%04u.now",scfg.data_dir,user.number);
-					if(!fmutex(str, startup->host_name, /* max_age: */60 * 60)) {
+					if(!fmutex(str, startup->host_name, /* max_age: */60 * 60, /* time: */NULL)) {
 						lprintf(LOG_WARNING, "%04d <%s> !ERROR %d (%s) creating mutex-semaphore file: %s"
 							,sock, user.alias, errno, strerror(errno), str);
 						sockprintf(sock,sess,"451 Packet creation already in progress (are you logged-in concurrently?)");
@@ -4909,6 +4921,8 @@ static void ctrl_thread(void* arg)
 	}
 
 	if(user.number) {
+		if(*mutex_file != '\0')
+			ftp_remove(sock, __LINE__, mutex_file, user.alias, LOG_ERR);
 		/* Update User Statistics */
 		if(!logoutuserdat(&scfg, &user, time(NULL), logintime))
 			lprintf(LOG_ERR,"%04d <%s> !ERROR in logoutuserdat", sock, user.alias);
diff --git a/src/sbbs3/js_global.c b/src/sbbs3/js_global.c
index a6a611de6d..d22907967d 100644
--- a/src/sbbs3/js_global.c
+++ b/src/sbbs3/js_global.c
@@ -3548,7 +3548,7 @@ js_fmutex(JSContext *cx, uintN argc, jsval *arglist)
 	}
 
 	rc=JS_SUSPENDREQUEST(cx);
-	ret=fmutex(fname,text,max_age);
+	ret=fmutex(fname,text,max_age, /* time: */NULL);
 	free(fname);
 	if(text)
 		free(text);
diff --git a/src/sbbs3/logout.cpp b/src/sbbs3/logout.cpp
index 11e997b018..27234c1e8c 100644
--- a/src/sbbs3/logout.cpp
+++ b/src/sbbs3/logout.cpp
@@ -33,16 +33,13 @@ void sbbs_t::logout(bool logged_in)
 	int 	i,j;
 	ushort	ttoday;
 	node_t	node;
-	struct	tm tm;
 
 	now=time(NULL);
-	if(localtime_r(&now,&tm)==NULL)
-		errormsg(WHERE,ERR_CHK,"localtime",(ulong)now);
 
 	if(!useron.number) {				 /* Not logged in, so do nothing */
 		if(!online) {
-			SAFEPRINTF2(str,"%s  T:%3u sec\r\n"
-				,hhmmtostr(&cfg,&tm,tmp)
+			SAFEPRINTF2(str,"%-6s  T:%3u sec\r\n"
+				,time_as_hhmm(&cfg, now, tmp)
 				,(uint)(now-answertime));
 			logline("@-",str); 
 		}
@@ -133,8 +130,7 @@ void sbbs_t::logout(bool logged_in)
 		putuserstr(useron.number, USER_CURSUB, cfg.sub[usrsub[curgrp][cursub[curgrp]]]->code);
 	if(usrlibs>0)
 		putuserstr(useron.number, USER_CURDIR, cfg.dir[usrdir[curlib][curdir[curlib]]]->code);
-	hhmmtostr(&cfg,&tm,str);
-	SAFECAT(str,"  ");
+	snprintf(str, sizeof str, "%-6s  ", time_as_hhmm(&cfg, now, tmp));
 	if(sys_status&SS_USERON) {
 		char ulb[64];
 		char dlb[64];
diff --git a/src/sbbs3/main.cpp b/src/sbbs3/main.cpp
index bd60e89e2f..aea3e414ac 100644
--- a/src/sbbs3/main.cpp
+++ b/src/sbbs3/main.cpp
@@ -2921,9 +2921,10 @@ void event_thread(void* arg)
 				sbbs->useron.number = atoi(g.gl_pathv[i]+offset);
 				getuserdat(&sbbs->cfg,&sbbs->useron);
 				if(sbbs->useron.number != 0 && !(sbbs->useron.misc&(DELETED|INACTIVE))) {
+					time_t t;
 					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);
+					if(!fmutex(semfile,startup->host_name,TIMEOUT_MUTEX_FILE, &t)) {
+						sbbs->lprintf(LOG_INFO," %s exists (unpack in progress?) since %s", semfile, time_as_hhmm(&sbbs->cfg, t, str));
 						continue;
 					}
 					sbbs->online=ON_LOCAL;
@@ -2973,8 +2974,9 @@ void event_thread(void* arg)
 				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);
+				time_t t;
+				if(!fmutex(semfile,startup->host_name,TIMEOUT_MUTEX_FILE, &t)) {
+					sbbs->lprintf(LOG_INFO,"%s exists (pack in progress?) since %s", semfile, time_as_hhmm(&sbbs->cfg, t, str));
 					continue;
 				}
 				getuserdat(&sbbs->cfg,&sbbs->useron);
diff --git a/src/sbbs3/nopen.c b/src/sbbs3/nopen.c
index b190de58bc..f6d196132f 100644
--- a/src/sbbs3/nopen.c
+++ b/src/sbbs3/nopen.c
@@ -109,7 +109,7 @@ bool ftouch(const char* fname)
 	return true;
 }
 
-bool fmutex(const char* fname, const char* text, long max_age)
+bool fmutex(const char* fname, const char* text, long max_age, time_t* tp)
 {
 	int file;
 	time_t t;
@@ -120,9 +120,14 @@ bool fmutex(const char* fname, const char* text, long max_age)
 		text=hostname;
 #endif
 
-	if(max_age && (t=fdate(fname)) >= 0 && (time(NULL)-t) > max_age) {
-		if(remove(fname)!=0)
-			return false;
+	if(max_age > 0 || tp != NULL) {
+		if(tp == NULL)
+			tp = &t;
+		*tp = fdate(fname);
+		if(max_age > 0 && *tp != -1 && (time(NULL) - *tp) > max_age) {
+			if(remove(fname)!=0)
+				return false;
+		}
 	}
 	if((file=open(fname,O_CREAT|O_WRONLY|O_EXCL,DEFFILEMODE))<0)
 		return false;
diff --git a/src/sbbs3/nopen.h b/src/sbbs3/nopen.h
index 9306406704..cbdd4a7cb8 100644
--- a/src/sbbs3/nopen.h
+++ b/src/sbbs3/nopen.h
@@ -36,7 +36,7 @@ extern "C" {
 int		nopen(const char* str, uint access);
 FILE *	fnopen(int* file, const char* str, uint access);
 bool	ftouch(const char* fname);
-bool	fmutex(const char* fname, const char* text, long max_age);
+bool	fmutex(const char* fname, const char* text, long max_age, time_t*);
 bool	fcompare(const char* fn1, const char* fn2);
 bool	backup(const char* org, int backup_level, bool ren);
 
diff --git a/src/sbbs3/sbbsecho.c b/src/sbbs3/sbbsecho.c
index 12f9efc124..6a0ea1591f 100644
--- a/src/sbbs3/sbbsecho.c
+++ b/src/sbbs3/sbbsecho.c
@@ -685,9 +685,11 @@ bool bso_lock_node(fidoaddr_t dest)
 	if(strListFind(locked_bso_nodes, fname, /* case_sensitive: */true) >= 0)
 		return true;
 	for(unsigned attempt=0;;) {
-		if(fmutex(fname, program_id(), cfg.bsy_timeout))
+		char tmp[128];
+		time_t t;
+		if(fmutex(fname, program_id(), cfg.bsy_timeout, &t))
 			break;
-		lprintf(LOG_NOTICE, "Node (%s) externally locked via: %s", smb_faddrtoa(&dest, NULL), fname);
+		lprintf(LOG_NOTICE, "Node (%s) externally locked via: %s (since %s)", smb_faddrtoa(&dest, NULL), fname, time_as_hhmm(&scfg, t, tmp));
 		if(++attempt >= cfg.bso_lock_attempts) {
 			lprintf(LOG_WARNING, "Giving up after %u attempts to lock node %s", attempt, smb_faddrtoa(&dest, NULL));
 			return false;
@@ -6428,8 +6430,9 @@ int main(int argc, char **argv)
 	}
 
 	SAFEPRINTF(path,"%ssbbsecho.bsy", scfg.ctrl_dir);
-	if(!fmutex(path, program_id(), cfg.bsy_timeout)) {
-		lprintf(LOG_WARNING, "Mutex file exists (%s): SBBSecho appears to be already running", path);
+	time_t t;
+	if(!fmutex(path, program_id(), cfg.bsy_timeout, &t)) {
+		lprintf(LOG_WARNING, "Mutex file exists (%s): SBBSecho appears to be already running since %s", path, time_as_hhmm(&scfg, t, str));
 		bail(1);
 	}
 	mtxfile_locked = true;
-- 
GitLab