From a104bab10da71f22e5ccda741ff4043feeda4f19 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Deuc=D0=B5?= <shurd@sasktel.net>
Date: Wed, 17 Jan 2024 20:24:48 -0500
Subject: [PATCH] Move SSH authentication into answer()

This should fix a long-standing issue where someone could connect to
the SSH port and do nothing, which would prevent other incoming
terminal sessions from being accepted until it times out.

Unfortunately, this means that Synchronet can't send any data until
authentication is completed, which means useful messages about why
you're being disconnected (ie: "Sorry, all terminal nodes are in use or otherwise unavailable.")
as well as usless information nobody ever cares about (ie: The IP
you're connecting from, that it is resolving your hostname, etc).
can no longer be sent to the user.
---
 src/sbbs3/answer.cpp | 164 ++++++++++++++++++++++++++++---------------
 src/sbbs3/main.cpp   | 106 ++++++++++------------------
 src/sbbs3/sbbs.h     |   3 +
 src/sbbs3/ssl.c      |   8 ++-
 4 files changed, 150 insertions(+), 131 deletions(-)

diff --git a/src/sbbs3/answer.cpp b/src/sbbs3/answer.cpp
index 4da4569825..9049256e1e 100644
--- a/src/sbbs3/answer.cpp
+++ b/src/sbbs3/answer.cpp
@@ -25,6 +25,20 @@
 
 extern "C" void client_on(SOCKET sock, client_t* client, BOOL update);
 
+bool
+sbbs_t::set_authresponse(bool activate_ssh)
+{
+	int status;
+
+	lprintf(LOG_DEBUG, "%04d SSH Setting attribute: SESSINFO_AUTHRESPONSE", client_socket);
+	status = cryptSetAttribute(ssh_session, CRYPT_SESSINFO_AUTHRESPONSE, activate_ssh);
+	if(cryptStatusError(status)) {
+		log_crypt_error_status_sock(status, "setting auth response");
+		return false;
+	}
+	return true;
+}
+
 bool sbbs_t::answer()
 {
 	char	str[MAX_PATH+1],str2[MAX_PATH+1],c;
@@ -177,29 +191,47 @@ bool sbbs_t::answer()
 	}
 #ifdef USE_CRYPTLIB
 	if(sys_status&SS_SSH) {
+		int  ssh_failed=0;
+		bool activate_ssh = false;
+
 		tmp[0]=0;
 		pthread_mutex_lock(&ssh_mutex);
-		ctmp = get_crypt_attribute(ssh_session, CRYPT_SESSINFO_USERNAME);
-		if (ctmp) {
-			SAFECOPY(rlogin_name, parse_login(ctmp));
-			free_crypt_attrstr(ctmp);
-			ctmp = get_crypt_attribute(ssh_session, CRYPT_SESSINFO_PASSWORD);
+		for(ssh_failed=0; ssh_failed < 3; ssh_failed++) {
+			lprintf(LOG_DEBUG, "%04d SSH Setting attribute: SESSINFO_ACTIVE", client_socket);
+			if(cryptStatusError(i=cryptSetAttribute(ssh_session, CRYPT_SESSINFO_ACTIVE, 1))) {
+				log_crypt_error_status_sock(i, "setting session active");
+				activate_ssh = false;
+				// TODO: Add private key here...
+				if(i != CRYPT_ENVELOPE_RESOURCE) {
+					break;
+				}
+			}
+			else {
+				break;
+			}
+			ctmp = get_crypt_attribute(ssh_session, CRYPT_SESSINFO_USERNAME);
 			if (ctmp) {
-				SAFECOPY(tmp, ctmp);
+				SAFECOPY(rlogin_name, parse_login(ctmp));
 				free_crypt_attrstr(ctmp);
+				ctmp = get_crypt_attribute(ssh_session, CRYPT_SESSINFO_PASSWORD);
+				if (ctmp) {
+					SAFECOPY(tmp, ctmp);
+					free_crypt_attrstr(ctmp);
+				}
+				lprintf(LOG_DEBUG,"SSH login: '%s'", rlogin_name);
 			}
-			pthread_mutex_unlock(&ssh_mutex);
-			lprintf(LOG_DEBUG,"SSH login: '%s'", rlogin_name);
-		}
-		else {
-			rlogin_name[0] = 0;
-			pthread_mutex_unlock(&ssh_mutex);
-		}
-		useron.number = find_login_id(&cfg, rlogin_name);
-		if(useron.number) {
-			getuserdat(&cfg,&useron);
-			for(i=0;i<3 && online;i++) {
-				if(stricmp(tmp,useron.pass)) {
+			else {
+				rlogin_name[0] = 0;
+				continue;
+			}
+			useron.number = find_login_id(&cfg, rlogin_name);
+			if(useron.number) {
+				getuserdat(&cfg,&useron);
+				if (stricmp(tmp, useron.pass) == 0) {
+					SAFECOPY(rlogin_pass, tmp);
+					activate_ssh = set_authresponse(true);
+				}
+				else if(ssh_failed) {
 					if(cfg.sys_misc&SM_ECHO_PW)
 						safe_snprintf(str,sizeof(str),"(%04u)  %-25s  FAILED Password attempt: '%s'"
 							,useron.number,useron.alias,tmp);
@@ -208,53 +240,68 @@ bool sbbs_t::answer()
 							,useron.number,useron.alias);
 					logline(LOG_NOTICE,"+!",str);
 					badlogin(useron.alias, tmp);
-					rioctl(IOFI);       /* flush input buffer */
-					bputs(text[InvalidLogon]);
-					bputs(text[PasswordPrompt]);
-					console|=CON_R_ECHOX;
-					getstr(tmp,LEN_PASS*2,K_UPPER|K_LOWPRIO|K_TAB);
-					console&=~(CON_R_ECHOX|CON_L_ECHOX);
-				}
-				else {
-					SAFECOPY(rlogin_pass, tmp);
-					if(REALSYSOP && (cfg.sys_misc&SM_SYSPASSLOGIN) && (cfg.sys_misc&SM_R_SYSOP)) {
-						rioctl(IOFI);       /* flush input buffer */
-						if(!chksyspass())
-							bputs(text[InvalidLogon]);
-						else {
-							i=0;
-							break;
-						}
-					}
-					else {
-						i = 0;
-						break;
-					}
+					useron.number=0;
 				}
 			}
-			if(i) {
-				if(stricmp(tmp,useron.pass)) {
-					if(cfg.sys_misc&SM_ECHO_PW)
-						safe_snprintf(str,sizeof(str),"(%04u)  %-25s  FAILED Password attempt: '%s'"
-							,useron.number,useron.alias,tmp);
+			else {
+				if(cfg.sys_misc&SM_ECHO_PW)
+					lprintf(LOG_NOTICE, "SSH !UNKNOWN USER: '%s' (password: %s)", rlogin_name, truncsp(tmp));
+				else
+					lprintf(LOG_NOTICE, "SSH !UNKNOWN USER: '%s'", rlogin_name);
+				badlogin(rlogin_name, tmp);
+				// Enable SSH so we can create a new user...
+				activate_ssh = set_authresponse(true);
+			}
+		}
+		if (activate_ssh) {
+			int cid;
+			char tname[1024];
+			int tnamelen;
+
+			ssh_failed=0;
+			// Check the channel ID and name...
+			if (cryptStatusOK(i=cryptGetAttribute(ssh_session, CRYPT_SESSINFO_SSH_CHANNEL, &cid))) {
+				if (i == CRYPT_OK) {
+					tnamelen = 0;
+					i=cryptGetAttributeString(ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_TYPE, tname, &tnamelen);
+					log_crypt_error_status_sock(i, "getting channel type");
+					if (tnamelen != 7 || strnicmp(tname, "session", 7)) {
+						lprintf(LOG_NOTICE, "%04d SSH [%s] active channel '%.*s' is not 'session', disconnecting.", client_socket, client_ipaddr, tnamelen, tname);
+						badlogin(/* user: */NULL, /* passwd: */NULL, "SSH", &client_addr, /* delay: */false);
+						// Fail because there's no session.
+						activate_ssh = false;
+					}
 					else
-						safe_snprintf(str,sizeof(str),"(%04u)  %-25s  FAILED Password attempt"
-							,useron.number,useron.alias);
-					logline(LOG_NOTICE,"+!",str);
-					badlogin(useron.alias, tmp);
-					bputs(text[InvalidLogon]);
+						session_channel = cid;
 				}
-				useron.number=0;
-				hangup();
+			}
+			else {
+				log_crypt_error_status_sock(i, "getting channel id");
+				if (i == CRYPT_ERROR_PERMISSION)
+					lprintf(LOG_CRIT, "!Your cryptlib build is obsolete, please update");
 			}
 		}
-		else {
-			if(cfg.sys_misc&SM_ECHO_PW)
-				lprintf(LOG_NOTICE, "SSH !UNKNOWN USER: '%s' (password: %s)", rlogin_name, truncsp(tmp));
-			else
-				lprintf(LOG_NOTICE, "SSH !UNKNOWN USER: '%s'", rlogin_name);
-			badlogin(rlogin_name, tmp);
+		if (activate_ssh) {
+			if(cryptStatusError(i=cryptSetAttribute(ssh_session, CRYPT_PROPERTY_OWNER, CRYPT_UNUSED))) {
+				log_crypt_error_status_sock(i, "clearing owner");
+				activate_ssh = false;
+			}
+		}
+		if(!activate_ssh) {
+			int status;
+			lprintf(LOG_NOTICE, "%04d SSH [%s] session establishment failed", client_socket, client_ipaddr);
+			if (cryptStatusError(status = cryptDestroySession(ssh_session))) {
+				lprintf(LOG_ERR, "%04d SSH ERROR %d destroying Cryptlib Session %d from %s line %d"
+					, client_socket, status, ssh_session, __FILE__, __LINE__);
+			}
+			ssh_mode = false;
+			pthread_mutex_unlock(&ssh_mutex);
+			close_socket(client_socket);
+			useron.number = 0;
+			return false;
 		}
+		SetEvent(ssh_active);
+
 		if (cryptStatusOK(cryptGetAttribute(ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_WIDTH, &l)) && l > 0) {
 			cols = l;
 			lprintf(LOG_DEBUG, "%04d SSH [%s] height %d", client_socket, client.addr, cols);
@@ -271,6 +318,7 @@ bool sbbs_t::answer()
 				terminal[sizeof(terminal)-1] = 0;
 			lprintf(LOG_DEBUG, "%04d SSH [%s] term: %s", client_socket, client.addr, terminal);
 		}
+		pthread_mutex_unlock(&ssh_mutex);
 	}
 #endif
 
diff --git a/src/sbbs3/main.cpp b/src/sbbs3/main.cpp
index c6302f9c1a..d4b698ecd8 100644
--- a/src/sbbs3/main.cpp
+++ b/src/sbbs3/main.cpp
@@ -274,6 +274,20 @@ int lprintf(int level, const char *fmt, ...)
 	return(lputs(level,sbuf));
 }
 
+void
+sbbs_t::log_crypt_error_status_sock(int status, const char *action)
+{
+	char *estr;
+	int level;
+	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)
@@ -1929,7 +1943,7 @@ static int crypt_pop_channel_data(sbbs_t *sbbs, char *inbuf, int want, int *got)
 					continue;
 				if (cid != sbbs->session_channel) {
 					if (cryptStatusError(status = cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL, cid))) {
-						GCESS(status, sbbs->client_socket, sbbs->ssh_session, "setting channel");
+						sbbs->log_crypt_error_status_sock(status, "setting channel");
 						return status;
 					}
 					cname = get_crypt_attribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_TYPE);
@@ -1939,7 +1953,7 @@ static int crypt_pop_channel_data(sbbs_t *sbbs, char *inbuf, int want, int *got)
 						free_crypt_attrstr(cname);
 					closing_channel = cid;
 					if (cryptStatusError(status = cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_ACTIVE, 0))) {
-						GCESS(status, sbbs->client_socket, sbbs->ssh_session, "closing channel");
+						sbbs->log_crypt_error_status_sock(status, "closing channel");
 						return status;
 					}
 					continue;
@@ -1951,7 +1965,7 @@ static int crypt_pop_channel_data(sbbs_t *sbbs, char *inbuf, int want, int *got)
 				 * and it was destroyed, so it's no longer possible to get the channel id.
 				 */
 				if (status != CRYPT_ERROR_NOTFOUND)
-					GCESS(status, sbbs->client_socket, sbbs->ssh_session, "getting channel id");
+					sbbs->log_crypt_error_status_sock(status, "getting channel id");
 				closing_channel = -1;
 			}
 		}
@@ -2090,6 +2104,10 @@ void input_thread(void *arg)
 #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);
@@ -3661,6 +3679,7 @@ bool sbbs_t::init()
 #ifdef USE_CRYPTLIB
 	pthread_mutex_init(&ssh_mutex,NULL);
 	ssh_mutex_created = true;
+	ssh_active = CreateEvent(NULL, TRUE, FALSE, (void *)"ssh_active");
 #endif
 	pthread_mutex_init(&input_thread_mutex,NULL);
 	input_thread_mutex_created = true;
@@ -3779,6 +3798,12 @@ sbbs_t::~sbbs_t()
 #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);
@@ -5389,7 +5414,6 @@ NO_SSH:
 		/* Do SSH stuff here */
 #ifdef USE_CRYPTLIB
 		if(ssh) {
-			int	ssh_failed=0;
 			BOOL nodelay = TRUE;
 			ulong nb = 0;
 
@@ -5403,10 +5427,10 @@ NO_SSH:
 			sbbs->ssh_mode = true;
 
 			if(cryptStatusError(i=cryptSetAttribute(sbbs->ssh_session, CRYPT_OPTION_NET_CONNECTTIMEOUT, startup->ssh_connect_timeout)))
-				GCESS(i, client_socket, sbbs->ssh_session, "setting connect timeout");
+				sbbs->log_crypt_error_status_sock(i, "setting connect timeout");
 
 			if(cryptStatusError(i=cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_PRIVATEKEY, ssh_context))) {
-				GCESS(i, client_socket, sbbs->ssh_session, "setting private key");
+				sbbs->log_crypt_error_status_sock(i, "setting private key");
 				SSH_END(client_socket);
 				close_socket(client_socket);
 				continue;
@@ -5414,81 +5438,22 @@ NO_SSH:
 			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))) {
-				GCESS(i, client_socket, sbbs->ssh_session, "setting network socket");
-				SSH_END(client_socket);
-				close_socket(client_socket);
-				continue;
-			}
-			for(ssh_failed=0; ssh_failed < 2; ssh_failed++) {
-				/* Accept any credentials */
-				lprintf(LOG_DEBUG, "%04d SSH Setting attribute: SESSINFO_AUTHRESPONSE", client_socket);
-				if(cryptStatusError(i=cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_AUTHRESPONSE, 1))) {
-					GCESS(i, client_socket, sbbs->ssh_session, "setting auth response");
-					ssh_failed=1;
-					break;
-				}
-				lprintf(LOG_DEBUG, "%04d SSH Setting attribute: SESSINFO_ACTIVE", client_socket);
-				if(cryptStatusError(i=cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_ACTIVE, 1))) {
-					GCESS(i, client_socket, sbbs->ssh_session, "setting session active");
-					if(i != CRYPT_ENVELOPE_RESOURCE) {
-						ssh_failed=2;
-						break;
-					}
-				}
-				else {
-					int cid;
-					char tname[1024];
-					int tnamelen;
-
-					ssh_failed=0;
-					// Check the channel ID and name...
-					if (cryptStatusOK(i=cryptGetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL, &cid))) {
-						if (i == CRYPT_OK) {
-							tnamelen = 0;
-							i=cryptGetAttributeString(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_TYPE, tname, &tnamelen);
-							GCESS(i, client_socket, sbbs->ssh_session, "getting channel type");
-							if (tnamelen != 7 || strnicmp(tname, "session", 7)) {
-								lprintf(LOG_NOTICE, "%04d SSH [%s] active channel '%.*s' is not 'session', disconnecting.", client_socket, host_ip, tnamelen, tname);
-								sbbs->badlogin(/* user: */NULL, /* passwd: */NULL, "SSH", &client_addr, /* delay: */false);
-								// Fail because there's no session.
-								ssh_failed = 3;
-							}
-							else
-								sbbs->session_channel = cid;
-						}
-					}
-					else {
-						GCESS(i, client_socket, sbbs->ssh_session, "getting channel id");
-						if (i == CRYPT_ERROR_PERMISSION)
-							lprintf(LOG_CRIT, "!Your cryptlib build is obsolete, please update");
-					}
-					break;
-				}
-			}
-			if (!ssh_failed) {
-				if(cryptStatusError(i=cryptSetAttribute(sbbs->ssh_session, CRYPT_PROPERTY_OWNER, CRYPT_UNUSED))) {
-					GCESS(i, client_socket, sbbs->ssh_session, "clearing owner");
-					ssh_failed = 2;
-				}
-			}
-			if(ssh_failed) {
-				lprintf(LOG_NOTICE, "%04d SSH [%s] session establishment failed", client_socket, host_ip);
+				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)))
-				GCESS(i, sbbs->client_socket, sbbs->ssh_session, "setting read timeout");
+				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)))
-				GCESS(i, sbbs->client_socket, sbbs->ssh_session, "setting write timeout");
+				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
-			sbbs->online=ON_REMOTE;
 		}
 #endif
 
@@ -5780,6 +5745,7 @@ NO_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))
@@ -5791,8 +5757,8 @@ NO_SSH:
 		}
 
 	    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->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;
diff --git a/src/sbbs3/sbbs.h b/src/sbbs3/sbbs.h
index 58462a8f84..7a848b7237 100644
--- a/src/sbbs3/sbbs.h
+++ b/src/sbbs3/sbbs.h
@@ -451,6 +451,7 @@ public:
 	bool	input_thread_mutex_created = false;
 	pthread_mutex_t	ssh_mutex;
 	bool	ssh_mutex_created = false;
+	xpevent_t ssh_active = nullptr;
 
 	#define OUTCOM_RETRY_DELAY		80		// milliseconds
 	#define OUTCOM_RETRY_ATTEMPTS	1000	// 80 seconds
@@ -1020,6 +1021,7 @@ public:
 	const char*	parse_login(const char*);
 
 	/* answer.cpp */
+	bool    set_authresponse(bool activate_ssh);
 	bool	answer(void);
 
 	/* logon.ccp */
@@ -1066,6 +1068,7 @@ public:
 	int		getnodetopage(int all, int telegram);
 
 	/* main.cpp */
+	void    log_crypt_error_status_sock(int status, const char *action);
 	int		lputs(int level, const char* str);
 	int		lprintf(int level, const char *fmt, ...)
 #if defined(__GNUC__)   // Catch printf-format errors
diff --git a/src/sbbs3/ssl.c b/src/sbbs3/ssl.c
index e6b81825ee..2eb68917a0 100644
--- a/src/sbbs3/ssl.c
+++ b/src/sbbs3/ssl.c
@@ -15,10 +15,12 @@ void free_crypt_attrstr(char *attr)
 
 char* get_crypt_attribute(CRYPT_HANDLE sess, C_IN CRYPT_ATTRIBUTE_TYPE attr)
 {
-	int		len = 0;
-	char	*estr = NULL;
+	int   len = 0;
+	char *estr = NULL;
+	int   status;
 
-	if (cryptStatusOK(cryptGetAttributeString(sess, attr, NULL, &len))) {
+	status = cryptGetAttributeString(sess, attr, NULL, &len);
+	if (cryptStatusOK(status)) {
 		estr = malloc(len + 1);
 		if (estr) {
 			if (cryptStatusError(cryptGetAttributeString(sess, attr, estr, &len))) {
-- 
GitLab