Skip to content
Snippets Groups Projects
ftpsrvr.c 177 KiB
Newer Older
deuce's avatar
deuce committed
		}
#if defined(SOCKET_DEBUG_RECV_BUF)
		socket_debug[xfer.ctrl_sock] &= ~SOCKET_DEBUG_RECV_BUF;
		if (rd < 1) {
			if (rd == 0) { /* Socket closed */
				if (startup->options & FTP_OPT_DEBUG_DATA)
					lprintf(LOG_DEBUG, "%04d <%s> DATA socket %d closed by client"
					        , xfer.ctrl_sock, xfer.user->alias, *xfer.data_sock);
			if (rd == SOCKET_ERROR) {
				if (SOCKET_ERRNO == EWOULDBLOCK) {
					/*lprintf(LOG_WARNING,"%04d DATA recv would block, retrying",xfer.ctrl_sock);*/
				else if (SOCKET_ERRNO == ECONNRESET)
					lprintf(LOG_WARNING, "%04d <%s> DATA Connection reset by peer, receiving on socket %d"
					        , xfer.ctrl_sock, xfer.user->alias, *xfer.data_sock);
				else if (SOCKET_ERRNO == ECONNABORTED)
					lprintf(LOG_WARNING, "%04d <%s> DATA Connection aborted by peer, receiving on socket %d"
					        , xfer.ctrl_sock, xfer.user->alias, *xfer.data_sock);
					lprintf(LOG_WARNING, "%04d <%s> !DATA ERROR %d receiving on data socket %d"
					        , xfer.ctrl_sock, xfer.user->alias, SOCKET_ERRNO, *xfer.data_sock);
				sockprintf(xfer.ctrl_sock, xfer.ctrl_sess, "426 Error %d receiving on DATA channel"
				           , SOCKET_ERRNO);
				error = TRUE;
			lprintf(LOG_ERR, "%04d <%s> !DATA ERROR recv returned %d on socket %d"
			        , xfer.ctrl_sock, xfer.user->alias, rd, *xfer.data_sock);
			sockprintf(xfer.ctrl_sock, xfer.ctrl_sess, "451 Unexpected socket error: %d", rd);
			error = TRUE;
		fwrite(buf, 1, rd, fp);
		total += rd;
		*xfer.lastactive = time(NULL);
	ftp_close_socket(xfer.data_sock, xfer.data_sess, __LINE__);
	if (error && startup->options & FTP_OPT_DEBUG_DATA)
		lprintf(LOG_DEBUG, "%04d <%s> DATA socket %d closed", xfer.ctrl_sock, xfer.user->alias, *xfer.data_sock);
	if (xfer.filepos + total < startup->min_fsize) {
		lprintf(LOG_WARNING, "%04d <%s> DATA received %" PRIdOFF " bytes for %s, less than minimum required (%" PRIu64 " bytes)"
		        , xfer.ctrl_sock, xfer.user->alias, xfer.filepos + total, xfer.filename, startup->min_fsize);
		sockprintf(xfer.ctrl_sock, xfer.ctrl_sess, "550 File size less than minimum required (%" PRIu64 " bytes)"
		           , startup->min_fsize);
		error = TRUE;
	if (error) {
		if (!xfer.append)
			ftp_remove(xfer.ctrl_sock, __LINE__, xfer.filename, xfer.user->alias, LOG_ERR);
		dur = (long)(time(NULL) - start);
		cps = (ulong)(dur ? total / dur : total * 2);
		lprintf(LOG_INFO, "%04d <%s> DATA Transfer successful: %" PRIdOFF " bytes received in %lu seconds (%lu cps)"
		        , xfer.ctrl_sock
		        , xfer.user->alias
		        , total, dur, cps);

		if (xfer.dir >= 0) {
			memset(&f, 0, sizeof(f));
			f.dir = xfer.dir;
			smb_hfield_str(&f, SMB_FILENAME, getfname(xfer.filename));
			smb_hfield_str(&f, SENDER, xfer.user->alias);

			filedat = findfile(&scfg, xfer.dir, f.name, NULL);
			if (scfg.dir[f.dir]->misc & DIR_AONLY)  /* Forced anonymous */
				f.hdr.attr |= MSG_ANONYMOUS;
			off_t cdt = flength(xfer.filename);
			smb_hfield_bin(&f, SMB_COST, cdt);
			char  fdesc[LEN_FDESC + 1] = "";
			/* Description specified with DESC command? */
			if (xfer.desc != NULL)
			/* Necessary for DIR and LIB ARS keyword support in subsequent chk_ar()'s */
			SAFECOPY(xfer.user->curdir, scfg.dir[f.dir]->code);

			/* FILE_ID.DIZ support */
			if (scfg.dir[f.dir]->misc & DIR_DIZ) {
				lprintf(LOG_DEBUG, "%04d <%s> DATA Extracting DIZ from: %s", xfer.ctrl_sock, xfer.user->alias, xfer.filename);
				if (extract_diz(&scfg, &f, /* diz_fnames */ NULL, tmp, sizeof(tmp))) {
					lprintf(LOG_DEBUG, "%04d <%s> DATA Parsing DIZ: %s", xfer.ctrl_sock, xfer.user->alias, tmp);
					char*                 lines = read_diz(tmp, &sauce);
					format_diz(lines, extdesc, sizeof(extdesc), sauce.width, sauce.ice_color);
					free_diz(lines);
					if (!fdesc[0]) {                     /* use for normal description */
						prep_file_desc(extdesc, fdesc); /* strip control chars and dupe chars */
					ftp_remove(xfer.ctrl_sock, __LINE__, tmp, xfer.user->alias, LOG_ERR);
					lprintf(LOG_DEBUG, "%04d <%s> DATA DIZ does not exist in: %s", xfer.ctrl_sock, xfer.user->alias, xfer.filename);
			} /* FILE_ID.DIZ support */

				smb_new_hfield_str(&f, SMB_FILEDESC, fdesc);
			if (filedat) {
				if (updatefile(&scfg, &f))
					lprintf(LOG_INFO, "%04d <%s> DATA updated file: %s"
					        , xfer.ctrl_sock, xfer.user->alias, f.name);
					lprintf(LOG_ERR, "%04d <%s> !DATA ERROR updating file (%s) in database"
					        , xfer.ctrl_sock, xfer.user->alias, f.name);
				/* need to update the index here */
			} else {
				if (addfile(&scfg, &f, extdesc, /* metatdata: */ NULL, xfer.client))
					lprintf(LOG_INFO, "%04d <%s> DATA uploaded file: %s"
					        , xfer.ctrl_sock, xfer.user->alias, f.name);
					lprintf(LOG_ERR, "%04d <%s> !DATA ERROR adding file (%s) to database"
					        , xfer.ctrl_sock, xfer.user->alias, f.name);
			if (scfg.dir[f.dir]->upload_sem[0])
				ftouch(scfg.dir[f.dir]->upload_sem);
			/**************************/
			/* Update Uploader's Info */
			/**************************/
			user_uploaded(&scfg, xfer.user, (!xfer.append && xfer.filepos == 0) ? 1:0, total);
			if (scfg.dir[f.dir]->up_pct && scfg.dir[f.dir]->misc & DIR_CDTUL) { /* credit for upload */
				if (scfg.dir[f.dir]->misc & DIR_CDTMIN && cps)    /* Give min instead of cdt */
					xfer.user->min = (uint32_t)adjustuserval(&scfg, xfer.user->number, USER_MIN
					                                         , ((ulong)(total * (scfg.dir[f.dir]->up_pct / 100.0)) / cps) / 60);
					xfer.user->cdt = adjustuserval(&scfg, xfer.user->number, USER_CDT
					                               , cdt * (uint64_t)(scfg.dir[f.dir]->up_pct / 100.0));
			if (!(scfg.dir[f.dir]->misc & DIR_NOSTAT))
Rob Swindell's avatar
Rob Swindell committed
				inc_upload_stats(&scfg, 1, (ulong)total);
			mqtt_file_upload(&mqtt, xfer.user, f.dir, f.name, total, xfer.client);
		sockprintf(xfer.ctrl_sock, xfer.ctrl_sess, "226 Upload complete (%lu cps).", cps);
	if (ftp_set != NULL && !terminate_server)
		*xfer.inprogress = FALSE;
deuce's avatar
deuce committed
static BOOL start_tls(SOCKET *sock, CRYPT_SESSION *sess, BOOL resp)
{
deuce's avatar
deuce committed
	ulong nb;
deuce's avatar
deuce committed

		lprintf(LOG_CRIT, "!ssl_sync() failure trying to enable TLS support");
		if (resp)
			sockprintf(*sock, *sess, "431 TLS not available");
deuce's avatar
deuce committed
		return FALSE;
	}
Deucе's avatar
Deucе committed
	if ((status = cryptCreateSession(sess, CRYPT_UNUSED, CRYPT_SESSION_TLS_SERVER)) != CRYPT_OK) {
deuce's avatar
deuce committed
		GCES(status, *sock, CRYPT_UNUSED, estr, "creating session");
		if (resp)
			sockprintf(*sock, *sess, "431 TLS not available");
deuce's avatar
deuce committed
		return FALSE;
	}
	if ((status = cryptSetAttribute(*sess, CRYPT_SESSINFO_TLS_OPTIONS, CRYPT_TLSOPTION_MINVER_TLS12)) != CRYPT_OK) {
		GCES(status, *sock, *sess, estr, "setting TLS minver");
		cryptDestroySession(*sess);
		*sess = -1;
			sockprintf(*sock, *sess, "431 TLS not available");
		return FALSE;
	}
	if ((status = add_private_key(&scfg, lprintf, *sess)) != CRYPT_OK) {
deuce's avatar
deuce committed
		GCES(status, *sock, *sess, estr, "setting private key");
deuce's avatar
deuce committed
		*sess = -1;
		if (resp)
			sockprintf(*sock, *sess, "431 TLS not available");
deuce's avatar
deuce committed
		return FALSE;
	}
	nodelay = TRUE;
	(void)setsockopt(*sock, IPPROTO_TCP, TCP_NODELAY, (char*)&nodelay, sizeof(nodelay));
	nb = 0;
	ioctlsocket(*sock, FIONBIO, &nb);
	if ((status = cryptSetAttribute(*sess, CRYPT_SESSINFO_NETWORKSOCKET, *sock)) != CRYPT_OK) {
deuce's avatar
deuce committed
		GCES(status, *sock, *sess, estr, "setting network socket");
deuce's avatar
deuce committed
		*sess = -1;
		if (resp)
			sockprintf(*sock, *sess, "431 TLS not available");
deuce's avatar
deuce committed
		return TRUE;
	}
	if (resp)
		sockprintf(*sock, -1, "234 Ready to start TLS");
	if ((status = cryptSetAttribute(*sess, CRYPT_SESSINFO_ACTIVE, 1)) != CRYPT_OK) {
deuce's avatar
deuce committed
		GCES(status, *sock, *sess, estr, "setting session active");
deuce's avatar
deuce committed
		return TRUE;
	}
	if (startup->max_inactivity) {
		if ((status = cryptSetAttribute(*sess, CRYPT_OPTION_NET_READTIMEOUT, startup->max_inactivity)) != CRYPT_OK) {
deuce's avatar
deuce committed
			GCES(status, *sock, *sess, estr, "setting read timeout");
deuce's avatar
deuce committed
			return TRUE;
		}
	}
	return FALSE;
}
deuce's avatar
deuce committed
static void filexfer(union xp_sockaddr* addr, SOCKET ctrl_sock, CRYPT_SESSION ctrl_sess, SOCKET pasv_sock, CRYPT_SESSION pasv_sess, SOCKET* data_sock
                     , CRYPT_SESSION *data_sess, char* filename, off_t filepos, BOOL* inprogress, BOOL* aborted
                     , BOOL delfile, BOOL tmpfile
                     , time_t* lastactive
                     , user_t* user
                     , client_t* client
                     , int dir
                     , BOOL receiving
                     , BOOL credits
                     , BOOL append
                     , char* desc, BOOL protected)
	int               result;
	ulong             l;
	socklen_t         addr_len;
	union xp_sockaddr server_addr;
	BOOL              reuseaddr;
	xfer_t*           xfer;
	char              host_ip[INET6_ADDRSTRLEN];

	if ((*inprogress) == TRUE) {
		lprintf(LOG_WARNING, "%04d <%s> !DATA TRANSFER already in progress", ctrl_sock, user->alias);
		sockprintf(ctrl_sock, ctrl_sess, "425 Transfer already in progress.");
		if (tmpfile && !(startup->options & FTP_OPT_KEEP_TEMP_FILES))
			ftp_remove(ctrl_sock, __LINE__, filename, user->alias, LOG_ERR);
	if (*data_sock != INVALID_SOCKET)
		ftp_close_socket(data_sock, data_sess, __LINE__);
rswindell's avatar
rswindell committed

deuce's avatar
deuce committed
	inet_addrtop(addr, host_ip, sizeof(host_ip));
	if (pasv_sock == INVALID_SOCKET) { /* !PASV */
		if ((*data_sock = socket(addr->addr.sa_family, SOCK_STREAM, IPPROTO_IP)) == INVALID_SOCKET) {
			lprintf(LOG_ERR, "%04d <%s> !DATA ERROR %d opening socket", ctrl_sock, user->alias, SOCKET_ERRNO);
			sockprintf(ctrl_sock, ctrl_sess, "425 Error %d opening socket", SOCKET_ERRNO);
			if (tmpfile && !(startup->options & FTP_OPT_KEEP_TEMP_FILES))
				ftp_remove(ctrl_sock, __LINE__, filename, user->alias, LOG_ERR);
			*inprogress = FALSE;
		if (startup->socket_open != NULL)
			startup->socket_open(startup->cbdata, TRUE);
		if (startup->options & FTP_OPT_DEBUG_DATA)
			lprintf(LOG_DEBUG, "%04d <%s> DATA socket %d opened", ctrl_sock, user->alias, *data_sock);
		/* Use port-1 for all data connections */
		reuseaddr = TRUE;
		(void)setsockopt(*data_sock, SOL_SOCKET, SO_REUSEADDR, (char*)&reuseaddr, sizeof(reuseaddr));
deuce's avatar
deuce committed
		addr_len = sizeof(server_addr);
		if ((result = getsockname(ctrl_sock, &server_addr.addr, &addr_len)) != 0) {
			lprintf(LOG_CRIT, "%04d <%s> !DATA ERROR %d (%d) getting address/port of command socket (%u)"
			        , ctrl_sock, user->alias, result, SOCKET_ERRNO, pasv_sock);
deuce's avatar
deuce committed
			return;
		}
		inet_setaddrport(&server_addr, inet_addrport(&server_addr) - 1);  /* 20? */
		result = bind(*data_sock, &server_addr.addr, addr_len);
		if (result != 0) {
			inet_setaddrport(&server_addr, 0);  /* any user port */
			result = bind(*data_sock, &server_addr.addr, addr_len);
		if (result != 0) {
			lprintf(LOG_ERR, "%04d <%s> DATA ERROR %d (%d) binding socket %d"
			        , ctrl_sock, user->alias, result, SOCKET_ERRNO, *data_sock);
			sockprintf(ctrl_sock, ctrl_sess, "425 Error %d binding socket", SOCKET_ERRNO);
			if (tmpfile && !(startup->options & FTP_OPT_KEEP_TEMP_FILES))
				ftp_remove(ctrl_sock, __LINE__, filename, user->alias, LOG_ERR);
			*inprogress = FALSE;
			ftp_close_socket(data_sock, data_sess, __LINE__);
		result = connect(*data_sock, &addr->addr, xp_sockaddr_len(addr));
		if (result != 0) {
			lprintf(LOG_WARNING, "%04d <%s> !DATA ERROR %d (%d) connecting to client %s port %u on socket %d"
			        , ctrl_sock, user->alias, result, SOCKET_ERRNO
			        , host_ip, inet_addrport(addr), *data_sock);
			sockprintf(ctrl_sock, ctrl_sess, "425 Error %d connecting to socket", SOCKET_ERRNO);
			if (tmpfile && !(startup->options & FTP_OPT_KEEP_TEMP_FILES))
				ftp_remove(ctrl_sock, __LINE__, filename, user->alias, LOG_ERR);
			*inprogress = FALSE;
			ftp_close_socket(data_sock, data_sess, __LINE__);
		if (startup->options & FTP_OPT_DEBUG_DATA)
			lprintf(LOG_DEBUG, "%04d <%s> DATA socket %d connected to %s port %u"
			        , ctrl_sock, user->alias, *data_sock, host_ip, inet_addrport(addr));
deuce's avatar
deuce committed
		if (protected) {
			if (start_tls(data_sock, data_sess, FALSE) || *data_sess == -1) {
				lprintf(LOG_DEBUG, "%04d <%s> !DATA ERROR activating TLS"
				        , ctrl_sock, user->alias);
				sockprintf(ctrl_sock, ctrl_sess, "425 Error activating TLS");
				if (tmpfile && !(startup->options & FTP_OPT_KEEP_TEMP_FILES))
					ftp_remove(ctrl_sock, __LINE__, filename, user->alias, LOG_ERR);
				*inprogress = FALSE;
				ftp_close_socket(data_sock, data_sess, __LINE__);
deuce's avatar
deuce committed
				return;
			}
		}
		if (startup->options & FTP_OPT_DEBUG_DATA) {
			addr_len = sizeof(*addr);
			if ((result = getsockname(pasv_sock, &addr->addr, &addr_len)) != 0)
				lprintf(LOG_CRIT, "%04d <%s> PASV !DATA ERROR %d (%d) getting address/port of passive socket (%u)"
				        , ctrl_sock, user->alias, result, SOCKET_ERRNO, pasv_sock);
				lprintf(LOG_DEBUG, "%04d <%s> PASV DATA socket %d listening on %s port %u"
				        , ctrl_sock, user->alias, pasv_sock, host_ip, inet_addrport(addr));
Deucе's avatar
Deucе committed
		if (!socket_readable(pasv_sock, TIMEOUT_SOCKET_LISTEN * 1000)) {
			lprintf(LOG_WARNING, "%04d <%s> PASV !WARNING socket not readable"
			        , ctrl_sock, user->alias);
			sockprintf(ctrl_sock, ctrl_sess, "425 Error %d selecting socket for connection", SOCKET_ERRNO);
			if (tmpfile && !(startup->options & FTP_OPT_KEEP_TEMP_FILES))
				ftp_remove(ctrl_sock, __LINE__, filename, user->alias, LOG_ERR);
			*inprogress = FALSE;
		addr_len = sizeof(*addr);
#ifdef SOCKET_DEBUG_ACCEPT
		socket_debug[ctrl_sock] |= SOCKET_DEBUG_ACCEPT;
		*data_sock = accept(pasv_sock, &addr->addr, &addr_len);
#ifdef SOCKET_DEBUG_ACCEPT
		socket_debug[ctrl_sock] &= ~SOCKET_DEBUG_ACCEPT;
		if (*data_sock == INVALID_SOCKET) {
			lprintf(LOG_WARNING, "%04d <%s> PASV !DATA ERROR %d accepting connection on socket %d"
			        , ctrl_sock, user->alias, SOCKET_ERRNO, pasv_sock);
			sockprintf(ctrl_sock, ctrl_sess, "425 Error %d accepting connection", SOCKET_ERRNO);
			if (tmpfile && !(startup->options & FTP_OPT_KEEP_TEMP_FILES))
				ftp_remove(ctrl_sock, __LINE__, filename, user->alias, LOG_ERR);
			*inprogress = FALSE;
		if (startup->socket_open != NULL)
			startup->socket_open(startup->cbdata, TRUE);
		if (startup->options & FTP_OPT_DEBUG_DATA)
			lprintf(LOG_DEBUG, "%04d <%s> PASV DATA socket %d connected to %s port %u"
			        , ctrl_sock, user->alias, *data_sock, host_ip, inet_addrport(addr));
deuce's avatar
deuce committed
		if (protected) {
			if (start_tls(data_sock, data_sess, FALSE) || *data_sess == -1) {
				lprintf(LOG_WARNING, "%04d <%s> PASV !DATA ERROR starting TLS", pasv_sock, user->alias);
				sockprintf(ctrl_sock, ctrl_sess, "425 Error negotiating TLS");
				if (tmpfile && !(startup->options & FTP_OPT_KEEP_TEMP_FILES))
					ftp_remove(ctrl_sock, __LINE__, filename, user->alias, LOG_ERR);
				*inprogress = FALSE;
deuce's avatar
deuce committed
				return;
			}
		}
		if (ioctlsocket(*data_sock, FIONBIO, &l) != 0) {
			lprintf(LOG_ERR, "%04d <%s> !DATA ERROR %d disabling socket blocking"
			        , ctrl_sock, user->alias, SOCKET_ERRNO);
			sockprintf(ctrl_sock, ctrl_sess, "425 Error %d disabling socket blocking"
			           , SOCKET_ERRNO);
		if ((xfer = malloc(sizeof(xfer_t))) == NULL) {
			lprintf(LOG_CRIT, "%04d <%s> !DATA MALLOC FAILURE LINE %d", ctrl_sock, user->alias, __LINE__);
			sockprintf(ctrl_sock, ctrl_sess, "425 MALLOC FAILURE");
		memset(xfer, 0, sizeof(xfer_t));
		xfer->ctrl_sock = ctrl_sock;
		xfer->ctrl_sess = ctrl_sess;
		xfer->data_sock = data_sock;
		xfer->data_sess = data_sess;
		xfer->inprogress = inprogress;
		xfer->aborted = aborted;
		xfer->delfile = delfile;
		xfer->tmpfile = tmpfile;
		xfer->append = append;
		xfer->filepos = filepos;
		xfer->credits = credits;
		xfer->lastactive = lastactive;
		xfer->user = user;
		xfer->client = client;
		xfer->dir = dir;
		xfer->desc = desc;
		SAFECOPY(xfer->filename, filename);
		(void)protected_uint32_adjust(&thread_count, 1);
		if (receiving)
			result = _beginthread(receive_thread, 0, (void*)xfer);
			result = _beginthread(send_thread, 0, (void*)xfer);
		if (result != -1)
			return; /* success */
	if (tmpfile && !(startup->options & FTP_OPT_KEEP_TEMP_FILES))
		ftp_remove(ctrl_sock, __LINE__, filename, user->alias, LOG_ERR);
	*inprogress = FALSE;
/* convert "user name" to "user.name" or "mr. user" to "mr._user" */
char* dotname(char* in, char* out)
{
	if (strchr(in, '.') == NULL)
		ch = '.';
		ch = '_';
	for (i = 0; in[i]; i++)
		if (in[i] <= ' ')
			out[i] = ch;
			out[i] = in[i];
	out[i] = 0;
static BOOL can_list(lib_t *lib, dir_t *dir, user_t *user, client_t *client)
	if (!chk_ar(&scfg, lib->ar, user, client))
		return FALSE;
	if (dir->dirnum == scfg.sysop_dir)
		return TRUE;
	if (dir->dirnum == scfg.upload_dir)
		return TRUE;
	if (chk_ar(&scfg, dir->ar, user, client))
		return TRUE;
	return FALSE;
}
static int getdir_from_vpath(scfg_t* cfg, const char* vpath, user_t* user, client_t* client, BOOL include_upload_only)
{
	int               dir = -1;
	int               lib = -1;
	char*             filename = NULL;
	enum parsed_vpath result = parse_vpath(cfg, vpath, &lib, &dir, &filename);
	if (result == PARSED_VPATH_DIR || result == PARSED_VPATH_FULL) {
		if ((include_upload_only && (dir == cfg->sysop_dir || dir == cfg->upload_dir))
		    || user_can_access_dir(cfg, dir, user, client))
static BOOL ftpalias(char* fullalias, char* filename, user_t* user, client_t* client, int* curdir)
{
	char* p;
	char* tp;
	char* fname = "";
	char  line[512];
	char  alias[512];
	char  aliasfile[MAX_PATH + 1];
	int   dir = -1;
	FILE* fp;
	BOOL  result = FALSE;

	SAFECOPY(alias, fullalias);
	if (p) {
		if (p != alias)
			*(p - 1) = 0;
		if (*p) {
			if (filename == NULL && p != alias)  // CWD command and a filename specified
rswindell's avatar
rswindell committed
				return FALSE;
			fname = p;
		}
	for (int i = 0; i < scfg.total_dirs; ++i) {
		if (scfg.dir[i]->vshortcut[0] == '\0')
		if (!user_can_access_dir(&scfg, i, user, client))
		if (stricmp(scfg.dir[i]->vshortcut, alias) == 0) {
			if (curdir != NULL)
			if (p != NULL && filename != NULL) {
				if (*p)
					sprintf(filename, "%s%s", scfg.dir[i]->path, p);
				else
					sprintf(filename, "%s%s", scfg.dir[i]->path, fname);
			}
			return TRUE;
		}
	}

	SAFEPRINTF(aliasfile, "%sftpalias.cfg", scfg.ctrl_dir);
	if ((fp = fopen(aliasfile, "r")) == NULL)
rswindell's avatar
rswindell committed
		return FALSE;

	while (!feof(fp)) {
		if (!fgets(line, sizeof(line), fp))
		p = line; /* alias */
		if (*p == ';') /* comment */
		tp = p;       /* terminator */
		if (stricmp(p, alias))   /* Not a match */
		p = tp + 1;     /* filename */
		tp = p;       /* terminator */
		if (filename == NULL /* CWD? */ && (*lastchar(p) != '/' || (*fname != 0 && strcmp(fname, alias)))) {
		if (!strnicmp(p, BBS_VIRTUAL_PATH, strlen(BBS_VIRTUAL_PATH))) {
			if ((dir = getdir_from_vpath(&scfg, p + strlen(BBS_VIRTUAL_PATH), user, client, true)) < 0)    {
				lprintf(LOG_WARNING, "0000 <%s> !Invalid virtual path: %s", user->alias, p);
			p = strrchr(p, '/');
			if (p != NULL) p++;
			if (p != NULL && filename != NULL) {
				if (*p)
					sprintf(filename, "%s%s", scfg.dir[dir]->path, p);
					sprintf(filename, "%s%s", scfg.dir[dir]->path, fname);
		} else if (filename != NULL)
			strcpy(filename, p);
		result = TRUE;    /* success */
	if (curdir != NULL)
		*curdir = dir;
/*
 * Parses a path into *curlib, *curdir, and sets *pp to point to the filename
 */
static int parsepath(char** pp, user_t* user, client_t* client, int* curlib, int* curdir)
{
	char   filename[MAX_PATH + 1];
	int    lib = *curlib;
	int    dir = *curdir;
	char * p = *pp;
	char * tmp;
	char * fname = strchr(p, 0);
	int    ret = 0;
	size_t len;

	if (*p == '/') {
		lib = -1;
		dir = -1;
	while (*p) {
		/* Relative path stuff */
		if (strcmp(p, "..") == 0) {
			if (dir >= 0)
				dir = -1;
			else if (lib >= 0)
				lib = -1;
			else
				ret = -1;
			p += 2;
		}
		else if (strncmp(p, "../", 3) == 0) {
			if (dir >= 0)
				dir = -1;
			else if (lib >= 0)
				lib = -1;
			else
				ret = -1;
			p += 3;
		}
		else if (strcmp(p, ".") == 0)
		else if (strncmp(p, "./", 2) == 0)
			p += 2;
		/* Path component */
		else if (lib < 0) {
			for (lib = 0; lib < scfg.total_libs; lib++) {
				if (!chk_ar(&scfg, scfg.lib[lib]->ar, user, client))
				len = strlen(scfg.lib[lib]->vdir);
				if (strlen(p) < len)
					continue;
				if (p[len] != 0 && p[len] != '/')
					continue;
				if (!strnicmp(scfg.lib[lib]->vdir, p, len)) {
					p += len;
					if (*p)
						p++;
					break;
				}
			}
			if (lib == scfg.total_libs) {
				SAFECOPY(filename, p);
				tmp = strchr(filename, '/');
				if (tmp != NULL)
					*tmp = 0;
				if (ftpalias(filename, filename, user, client, &dir) == TRUE && dir >= 0) {
					lib = scfg.dir[dir]->lib;
					if (strchr(p, '/') != NULL) {
						p = strchr(p, '/');
						p++;
					}
					else
						p = strchr(p, 0);
				}
				else {
					ret = -1;
					lib = -1;
					if (strchr(p, '/') != NULL) {
						p = strchr(p, '/');
						p++;
					}
					else
						p = strchr(p, 0);
			for (dir = 0; dir < scfg.total_dirs; dir++) {
				if (scfg.dir[dir]->lib != lib)
					continue;
				if (!can_list(scfg.lib[lib], scfg.dir[dir], user, client))
					continue;
				len = strlen(scfg.dir[dir]->vdir);
				if (strlen(p) < len)
					continue;
				if (p[len] != 0 && p[len] != '/')
					continue;
				if (!strnicmp(scfg.dir[dir]->vdir, p, len)) {
					p += len;
					if (*p)
						p++;
					break;
				}
			}
			if (dir == scfg.total_dirs) {
				ret = -1;
				dir = -1;
				if (strchr(p, '/') != NULL) {
					p = strchr(p, '/');
					p++;
				}
				else
					p = strchr(p, 0);
			if (strchr(p, '/') != NULL) {
				ret = -1;
				p = strchr(p, '/');
				p++;
			}
			else {
				fname = p;
				p += strlen(fname);
			}
	*curdir = dir;
	*curlib = lib;
	*pp = fname;
}

char* root_dir(char* path)
{
	char*       p;
	static char root[MAX_PATH + 1];
	SAFECOPY(root, path);
	if (!strncmp(root, "\\\\", 2)) {   /* network path */
		p = strchr(root + 2, '\\');
		if (p) p = strchr(p + 1, '\\');
		if (p) *(p + 1) = 0;        /* truncate at \\computer\sharename\ */
	else if (!strncmp(root + 1, ":/", 2) || !strncmp(root + 1, ":\\", 2))
		root[3] = 0;
	else if (*root == '/' || *root == '\\')
		root[1] = 0;
char* genvpath(int lib, int dir, char* str)
	strcpy(str, "/");
	if (lib < 0)
	strcat(str, scfg.lib[lib]->vdir);
	strcat(str, "/");
	if (dir < 0)
	strcat(str, scfg.dir[dir]->vdir);
	strcat(str, "/");
deuce's avatar
deuce committed
void ftp_printfile(SOCKET sock, CRYPT_SESSION sess, const char* name, unsigned code)
	char     path[MAX_PATH + 1];
	char     buf[512];
	FILE*    fp;
	SAFEPRINTF2(path, "%sftp%s.txt", scfg.text_dir, name);
	if ((fp = fopen(path, "rb")) != NULL) {
		i = 0;
		while (!feof(fp)) {
			if (!fgets(buf, sizeof(buf), fp))
			if (!i)
				sockprintf(sock, sess, "%u-%s", code, buf);
				sockprintf(sock, sess, " %s", buf);
deuce's avatar
deuce committed
static BOOL ftp_hacklog(char* prot, char* user, char* text, char* host, union xp_sockaddr* addr)
	if (startup->sound.hack[0] && !sound_muted(&scfg))
		PlaySound(startup->sound.hack, NULL, SND_ASYNC | SND_FILENAME);
	return hacklog(&scfg, &mqtt, prot, user, text, host, addr);
/****************************************************************************/
/* Consecutive failed login (possible password hack) attempt tracking		*/
/****************************************************************************/

static BOOL badlogin(SOCKET sock, CRYPT_SESSION sess, ulong* login_attempts
                     , char* user, char* passwd, client_t* client, union xp_sockaddr* addr)
	char            tmp[128];
	ulong           count;
	if (addr != NULL) {
		count = loginFailure(startup->login_attempt_list, addr, client->protocol, user, passwd, &attempt);
			lprintf(LOG_NOTICE, "%04d [%s] !%lu " STR_FAILED_LOGIN_ATTEMPTS " in %s"
			        , sock, client->addr, count, duration_estimate_to_vstr(attempt.time - attempt.first, tmp, sizeof tmp, 1, 1));
		mqtt_user_login_fail(&mqtt, client, user);
		if (startup->login_attempt.hack_threshold && count >= startup->login_attempt.hack_threshold)
			ftp_hacklog("FTP LOGIN", user, passwd, client->host, addr);
		if (startup->login_attempt.filter_threshold && count >= startup->login_attempt.filter_threshold) {
			snprintf(reason, sizeof reason, "%lu " STR_FAILED_LOGIN_ATTEMPTS " in %s"
			         , count, duration_estimate_to_str(attempt.time - attempt.first, tmp, sizeof tmp, 1, 1));
			filter_ip(&scfg, client->protocol, reason, client->host, client->addr, user, /* fname: */ NULL, startup->login_attempt.filter_duration);
deuce's avatar
deuce committed
		}
		if (count > *login_attempts)
			*login_attempts = count;
	} else
		(*login_attempts)++;
	mswait(startup->login_attempt.delay);   /* As recommended by RFC2577 */
	if ((*login_attempts) >= 3) {
		sockprintf(sock, sess, "421 Too many failed login attempts.");
	ftp_printfile(sock, sess, "badlogin", 530);
	sockprintf(sock, sess, "530 Invalid login.");
static char* ftp_tmpfname(char* fname, char* ext, SOCKET sock)
	safe_snprintf(fname, MAX_PATH, "%sSBBS_FTP.%x%x%x%lx.%s"
	              , scfg.temp_dir, getpid(), sock, rand(), (ulong)clock(), ext);
#if defined(__GNUC__)   // Catch printf-format errors
static BOOL send_mlsx(FILE *fp, SOCKET sock, CRYPT_SESSION sess, const char *format, ...) __attribute__ ((format (printf, 4, 5)));
#endif
static BOOL send_mlsx(FILE *fp, SOCKET sock, CRYPT_SESSION sess, const char *format, ...)
{
	va_list va;

	if (fp == NULL && sock == INVALID_SOCKET)
		return FALSE;
	va_start(va, format);
Deucе's avatar
Deucе committed
	rval = vasprintf(&str, format, va);
	va_end(va);
	if (rval == -1)
		return FALSE;
	if (fp != NULL)
		fprintf(fp, "%s\r\n", str);
	else
		sockprintf(sock, sess, " %s", str);
	free(str);
	return TRUE;
}

static char *get_unique(const char *path, char *uniq)
{
	BYTE digest[MD5_DIGEST_SIZE];

	if (path == NULL)
		return NULL;

	MD5_calc(digest, path, strlen(path));
static BOOL send_mlsx_entry(FILE *fp, SOCKET sock, CRYPT_SESSION sess, unsigned feats, const char *type, const char *perm, uint64_t size, time_t modify, const char *owner, const char *unique, time_t ul, const char *fname)
	char      line[1024];
	char *    end;
	BOOL      need_owner = FALSE;
	struct tm t;
	if (type != NULL && (feats & MLSX_TYPE))
		end += sprintf(end, "Type=%s;", type);
	if (perm != NULL && (feats & MLSX_PERM))
		end += sprintf(end, "Perm=%s;", perm);
	if (size != UINT64_MAX && (feats & MLSX_SIZE))
		end += sprintf(end, "Size=%" PRIu64 ";", size);
	if (modify > 0 && (feats & MLSX_MODIFY)) {
		t = *gmtime(&modify);
deuce's avatar
deuce committed
		end += sprintf(end, "Modify=%04d%02d%02d%02d%02d%02d;",
		               t.tm_year + 1900, t.tm_mon + 1, t.tm_mday,
		               t.tm_hour, t.tm_min, t.tm_sec);
deuce's avatar
deuce committed
	}
	if (unique != NULL && (feats & MLSX_UNIQUE))
		end += sprintf(end, "Unique=%s;", unique);
	if (ul != 0 && (feats & MLSX_CREATE)) {
Deucе's avatar
Deucе committed
		t = *gmtime(&ul);
		end += sprintf(end, "Create=%04d%02d%02d%02d%02d%02d;",
		               t.tm_year + 1900, t.tm_mon + 1, t.tm_mday,
		               t.tm_hour, t.tm_min, t.tm_sec);
	// Owner can contain percents, so let send_mlsx() deal with it
	if (owner != NULL && (feats & MLSX_OWNER)) {
		strcat(end, "UNIX.ownername=%s;");
		need_owner = TRUE;
	}
	strcat(end, " %s");
	if (need_owner)
		return send_mlsx(fp, sock, sess, line, owner, fname == NULL ? "" : fname);
	return send_mlsx(fp, sock, sess, line, fname == NULL ? "" : fname);
deuce's avatar
deuce committed
static BOOL write_local_mlsx(FILE *fp, SOCKET sock, CRYPT_SESSION sess, unsigned feats, const char *path, BOOL full_path)
	char        permstr[11];
	char *      p;
	BOOL        is_file = FALSE;
	if (stat(path, &st) != 0)
		type = "pdir";
	else if (*lastchar(path) == '/')    /* is directory */
		type = "dir";
	}
	// TODO: Check for deletability 'd'
	// TODO: Check for renamability 'f'
	p = permstr;
	if (is_file) {
		if (access(path, W_OK) == 0) {
			// Can append ('a') and write ('w')
			*(p++) = 'a';
			*(p++) = 'w';
		}
		if (access(path, R_OK) == 0) {
			// Can read ('r')
		}
	}
	else {
		// TODO: Check these on Windows...
		if (access(path, W_OK) == 0) {
			// Can create files ('c'), directories ('m') and delete files ('p')
			*(p++) = 'c';
			*(p++) = 'm';
			*(p++) = 'p';
		}
		if (access(path, R_OK) == 0) {
			// Can change to the directory ('e'), and list files ('l')
			*(p++) = 'e';
			*(p++) = 'l';
deuce's avatar
deuce committed
	if (is_file)
		full_path = FALSE;
	return send_mlsx_entry(fp, sock, sess, feats, type, permstr, (uint64_t)st.st_size, st.st_mtime, NULL, NULL, st.st_ctime, full_path ? path : getfname(path));
}

/*
 * Nobody can do anything but list files and change to dirs.
 */
static void get_libperm(lib_t *lib, user_t *user, client_t *client, char *permstr)
{
	char *p = permstr;

	if (chk_ar(&scfg, lib->ar, user, client)) {
		//*(p++) = 'a';	// File may be appended to
		//*(p++) = 'c';	// Files may be created in dir
		//*(p++) = 'd';	// Item may be depeted (dir or file)
		*(p++) = 'e';   // Can change to the dir
		//*(p++) = 'f';	// Item may be renamed
		*(p++) = 'l';   // Directory contents can be listed
		//*(p++) = 'm';	// New subdirectories may be created
		//*(p++) = 'p';	// Files/Dirs in directory may be deleted
		//*(p++) = 'r';	// File may be retrieved
		//*(p++) = 'w';	// File may be overwritten
	}
}

static BOOL can_upload(lib_t *lib, dir_t *dir, user_t *user, client_t *client)
{
	if (!chk_ar(&scfg, lib->ar, user, client))
		return FALSE;
	if (user->rest & FLAG('U'))
		return FALSE;
	if (dir_op(&scfg, user, client, dir->dirnum))
		return TRUE;
	// The rest can only upload if there's room
	if (dir->maxfiles && getfiles(&scfg, dir->dirnum) >= dir->maxfiles)
		return FALSE;
	if (dir->dirnum == scfg.sysop_dir)
		return TRUE;
	if (dir->dirnum == scfg.upload_dir)