/* Synchronet FTP server */

/* $Id$ */
// vi: tabstop=4

/****************************************************************************
 * @format.tab-size 4		(Plain Text/Source Code File Header)			*
 * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
 *																			*
 * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
 *																			*
 * This program is free software; you can redistribute it and/or			*
 * modify it under the terms of the GNU General Public License				*
 * as published by the Free Software Foundation; either version 2			*
 * of the License, or (at your option) any later version.					*
 * See the GNU General Public License for more details: gpl.txt or			*
 * http://www.fsf.org/copyleft/gpl.html										*
 *																			*
 * Anonymous FTP access to the most recent released source is available at	*
 * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
 *																			*
 * Anonymous CVS access to the development source and modification history	*
 * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
 * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
 *     (just hit return, no password is necessary)							*
 * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
 *																			*
 * For Synchronet coding style and modification guidelines, see				*
 * http://www.synchro.net/source.html										*
 *																			*
 * You are encouraged to submit any modifications (preferably in Unix diff	*
 * format) via e-mail to mods@synchro.net									*
 *																			*
 * Note: If this box doesn't appear square, then you need to fix your tabs.	*
 ****************************************************************************/

/* ANSI C Library headers */
#include <stdio.h>
#include <stdlib.h>			/* ltoa in GNU C lib */
#include <stdarg.h>			/* va_list, varargs */
#include <string.h>			/* strrchr */
#include <fcntl.h>			/* O_WRONLY, O_RDONLY, etc. */
#include <errno.h>			/* EACCES */
#include <ctype.h>			/* toupper */
#include <sys/stat.h>		/* S_IWRITE */

/* Synchronet-specific headers */
#undef SBBS	/* this shouldn't be defined unless building sbbs.dll/libsbbs.so */
#include "sbbs.h"
#include "text.h"			/* TOTAL_TEXT */
#include "ftpsrvr.h"
#include "telnet.h"
#include "js_rtpool.h"
#include "js_request.h"
#include "multisock.h"
#include "ssl.h"
#include "cryptlib.h"
#include "xpprintf.h"		// vasprintf

/* Constants */

#define FTP_SERVER				"Synchronet FTP Server"

#define STATUS_WFC				"Listening"
#define ANONYMOUS				"anonymous"

#define BBS_VIRTUAL_PATH		"bbs:/""/"	/* this is actually bbs:<slash><slash> */
#define LOCAL_FSYS_DIR			"local:"
#define BBS_FSYS_DIR			"bbs:"
#define BBS_HIDDEN_ALIAS		"hidden"

#define TIMEOUT_THREAD_WAIT		60		/* Seconds */

#define TIMEOUT_SOCKET_LISTEN	30		/* Seconds */

#define XFER_REPORT_INTERVAL	60		/* Seconds */

#define INDEX_FNAME_LEN			15

#define	NAME_LEN				15		/* User name length for listings */

#define MLSX_TYPE	(1<<0)
#define MLSX_PERM	(1<<1)
#define MLSX_SIZE	(1<<2)
#define MLSX_MODIFY	(1<<3)
#define MLSX_OWNER	(1<<4)

static ftp_startup_t*	startup=NULL;
static scfg_t	scfg;
static struct xpms_set *ftp_set = NULL;
static protected_uint32_t active_clients;
static protected_uint32_t thread_count;
static volatile time_t	uptime=0;
static volatile ulong	served=0;
static volatile BOOL	terminate_server=FALSE;
static char		revision[16];
static char 	*text[TOTAL_TEXT];
static str_list_t recycle_semfiles;
static str_list_t shutdown_semfiles;

#ifdef SOCKET_DEBUG
	static BYTE 	socket_debug[0x10000]={0};

	#define	SOCKET_DEBUG_CTRL		(1<<0)	/* 0x01 */
	#define SOCKET_DEBUG_SEND		(1<<1)	/* 0x02 */
	#define SOCKET_DEBUG_READLINE	(1<<2)	/* 0x04 */
	#define SOCKET_DEBUG_ACCEPT		(1<<3)	/* 0x08 */
	#define SOCKET_DEBUG_SENDTHREAD	(1<<4)	/* 0x10 */
	#define SOCKET_DEBUG_TERMINATE	(1<<5)	/* 0x20 */
	#define SOCKET_DEBUG_RECV_CHAR	(1<<6)	/* 0x40 */
	#define SOCKET_DEBUG_FILEXFER	(1<<7)	/* 0x80 */
#endif

char* genvpath(int lib, int dir, char* str);

typedef struct {
	SOCKET			socket;
	union xp_sockaddr	client_addr;
	socklen_t		client_addr_len;
} ftp_t;


static const char *ftp_mon[]={"Jan","Feb","Mar","Apr","May","Jun"
            ,"Jul","Aug","Sep","Oct","Nov","Dec"};

BOOL direxist(char *dir)
{
	if(access(dir,0)==0)
		return(TRUE);
	else
		return(FALSE);
}

BOOL dir_op(scfg_t* cfg, user_t* user, client_t* client, uint dirnum)
{
	return(user->level>=SYSOP_LEVEL
		|| (cfg->dir[dirnum]->op_ar!=NULL && cfg->dir[dirnum]->op_ar[0] && chk_ar(cfg,cfg->dir[dirnum]->op_ar,user,client)));
}

static 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);

	if(level <= LOG_ERR) {
		char errmsg[sizeof(sbuf)+16];
		SAFEPRINTF(errmsg, "ftp %s", sbuf);
		errorlog(&scfg, 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 || level > startup->log_level)
		return(0);

#if defined(_WIN32)
	if(IsBadCodePtr((FARPROC)startup->lputs))
		return(0);
#endif

    return startup->lputs(startup->cbdata,level,sbuf);
}

#ifdef _WINSOCKAPI_

static 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

static void status(char* str)
{
	if(startup!=NULL && startup->status!=NULL)
	    startup->status(startup->cbdata,str);
}

static void update_clients(void)
{
	if(startup!=NULL && startup->clients!=NULL)
		startup->clients(startup->cbdata,protected_uint32_value(active_clients));
}

static void client_on(SOCKET sock, client_t* client, BOOL update)
{
	if(startup!=NULL && startup->client_on!=NULL)
		startup->client_on(startup->cbdata,TRUE,sock,client,update);
}

static void client_off(SOCKET sock)
{
	if(startup!=NULL && startup->client_on!=NULL)
		startup->client_on(startup->cbdata,FALSE,sock,NULL,FALSE);
}

static void thread_up(BOOL setuid)
{
	if(startup!=NULL && startup->thread_up!=NULL)
		startup->thread_up(startup->cbdata,TRUE, setuid);
}

static int32_t thread_down(void)
{
	int32_t count = protected_uint32_adjust(&thread_count,-1);
	if(startup!=NULL && startup->thread_up!=NULL)
		startup->thread_up(startup->cbdata,FALSE, FALSE);
	return count;
}

static void ftp_open_socket_cb(SOCKET sock, void *cbdata)
{
	char	error[256];

	if(startup!=NULL && startup->socket_open!=NULL)
		startup->socket_open(startup->cbdata,TRUE);
	if(set_socket_options(&scfg, sock, "FTP", error, sizeof(error)))
		lprintf(LOG_ERR,"%04d !ERROR %s",sock, error);
}

static void ftp_close_socket_cb(SOCKET sock, void *cbdata)
{
	if(startup!=NULL && startup->socket_open!=NULL)
		startup->socket_open(startup->cbdata,FALSE);
}

static SOCKET ftp_open_socket(int domain, int type)
{
	SOCKET	sock;

	sock=socket(domain, type, IPPROTO_IP);
	if(sock != INVALID_SOCKET)
		ftp_open_socket_cb(sock, NULL);
	return(sock);
}

#ifdef __BORLANDC__
#pragma argsused
#endif
static int ftp_close_socket(SOCKET* sock, CRYPT_SESSION *sess, int line)
{
	int		result;

	if (*sess != -1) {
		cryptDestroySession(*sess);
		*sess = -1;
	}

	if((*sock)==INVALID_SOCKET) {
		lprintf(LOG_WARNING,"0000 !INVALID_SOCKET in close_socket from line %u",line);
		return(-1);
	}

	shutdown(*sock,SHUT_RDWR);	/* required on Unix */

	result=closesocket(*sock);
	if(startup!=NULL && startup->socket_open!=NULL) 
		startup->socket_open(startup->cbdata,FALSE);

	if(result!=0) {
		if(ERROR_VALUE!=ENOTSOCK)
			lprintf(LOG_WARNING,"%04d !ERROR %d closing socket from line %u",*sock,ERROR_VALUE,line);
	}
	*sock=INVALID_SOCKET;

	return(result);
}

static int sockprintf(SOCKET sock, CRYPT_SESSION sess, char *fmt, ...)
{
	int		len;
	int		maxlen;
	int		result;
	va_list argptr;
	char	sbuf[1024];
	fd_set	socket_set;
	struct timeval tv;

    va_start(argptr,fmt);
    len=vsnprintf(sbuf,maxlen=sizeof(sbuf)-2,fmt,argptr);
    va_end(argptr);

	if(len<0 || len>maxlen) /* format error or output truncated */
		len=maxlen;
	if(startup!=NULL && startup->options&FTP_OPT_DEBUG_TX)
		lprintf(LOG_DEBUG,"%04d TX%s: %.*s", sock, sess != -1 ? "S" : "", len, sbuf);
	memcpy(sbuf+len,"\r\n",2);
	len+=2;

	if(sock==INVALID_SOCKET) {
		lprintf(LOG_WARNING,"!INVALID SOCKET in call to sockprintf");
		return(0);
	}

	/* Check socket for writability (using select) */
	tv.tv_sec=300;
	tv.tv_usec=0;

	FD_ZERO(&socket_set);
	FD_SET(sock,&socket_set);

	if((result=select(sock+1,NULL,&socket_set,NULL,&tv))<1) {
		if(result==0)
			lprintf(LOG_WARNING,"%04d !TIMEOUT selecting socket for send"
				,sock);
		else
			lprintf(LOG_WARNING,"%04d !ERROR %d selecting socket for send"
				,sock, ERROR_VALUE);
		return(0);
	}

	if (sess != -1) {
		int tls_sent;
		int sent = 0;
		while (sent < len) {
			result = cryptPushData(sess, sbuf+sent, len-sent, &tls_sent);
			if (result == CRYPT_OK)
				sent += tls_sent;
			else {
				lprintf(LOG_DEBUG, "pushData returned %d\n", result);
				if (result != -25)
				return 0;
			}
			result = cryptFlushData(sess);
			if (result != CRYPT_OK) {
				lprintf(LOG_DEBUG, "cryptFlushData() returned %d\n", result);
				return 0;
			}
		}
	}
	else {
		while((result=sendsocket(sock,sbuf,len))!=len) {
			if(result==SOCKET_ERROR) {
				if(ERROR_VALUE==EWOULDBLOCK) {
					YIELD();
					continue;
				}
				if(ERROR_VALUE==ECONNRESET) 
					lprintf(LOG_WARNING,"%04d Connection reset by peer on send",sock);
				else if(ERROR_VALUE==ECONNABORTED)
					lprintf(LOG_WARNING,"%04d Connection aborted by peer on send",sock);
				else
					lprintf(LOG_WARNING,"%04d !ERROR %d sending",sock,ERROR_VALUE);
				return(0);
			}
			lprintf(LOG_WARNING,"%04d !ERROR: short send: %u instead of %u",sock,result,len);
		}
	}
	return(len);
}


/* Returns the directory index of a virtual lib/dir path (e.g. main/games/filename) */
int getdir(char* p, user_t* user, client_t* client)
{
	char*	tp;
	char	path[MAX_PATH+1];
	uint	dir;
	uint	lib;

	SAFECOPY(path,p);
	p=path;

	if(*p=='/') 
		p++;
	else if(!strncmp(p,"./",2))
		p+=2;

	tp=strchr(p,'/');
	if(tp) *tp=0;
	for(lib=0;lib<scfg.total_libs;lib++) {
		if(!chk_ar(&scfg,scfg.lib[lib]->ar,user,client))
			continue;
		if(!stricmp(scfg.lib[lib]->sname,p))
			break;
	}
	if(lib>=scfg.total_libs) 
		return(-1);

	if(tp!=NULL)
		p=tp+1;

	tp=strchr(p,'/');
	if(tp) *tp=0;
	for(dir=0;dir<scfg.total_dirs;dir++) {
		if(scfg.dir[dir]->lib!=lib)
			continue;
		if(dir!=scfg.sysop_dir && dir!=scfg.upload_dir 
			&& !chk_ar(&scfg,scfg.dir[dir]->ar,user,client))
			continue;
		if(!stricmp(scfg.dir[dir]->code_suffix,p))
			break;
	}
	if(dir>=scfg.total_dirs) 
		return(-1);

	return(dir);
}

/*********************************/
/* JavaScript Data and Functions */
/*********************************/
#ifdef JAVASCRIPT

js_server_props_t js_server_props;

static JSBool
js_write(JSContext *cx, uintN argc, jsval *arglist)
{
	jsval *argv=JS_ARGV(cx, arglist);
    uintN		i;
    JSString*	str=NULL;
	FILE*	fp;
	jsrefcount	rc;
	char		*p = NULL;
	size_t		len;

	JS_SET_RVAL(cx, arglist, JSVAL_VOID);

	if((fp=(FILE*)JS_GetContextPrivate(cx))==NULL)
		return(JS_FALSE);

    for (i = 0; i < argc; i++) {
		str = JS_ValueToString(cx, argv[i]);
		if (!str)
		    return JS_FALSE;
		JSSTRING_TO_MSTRING(cx, str, p, &len);
		HANDLE_PENDING(cx, p);
		rc=JS_SUSPENDREQUEST(cx);
		if(p) {
			fwrite(p, len, 1, fp);
			free(p);
		}
		JS_RESUMEREQUEST(cx, rc);
	}

	if(str==NULL)
		JS_SET_RVAL(cx, arglist, JSVAL_VOID);
	else
		JS_SET_RVAL(cx, arglist, STRING_TO_JSVAL(str));
	return(JS_TRUE);
}

static JSBool
js_writeln(JSContext *cx, uintN argc, jsval *arglist)
{
	FILE*	fp;
	jsrefcount	rc;

	JS_SET_RVAL(cx, arglist, JSVAL_VOID);

	if((fp=(FILE*)JS_GetContextPrivate(cx))==NULL)
		return(JS_FALSE);

	js_write(cx,argc,arglist);
	rc=JS_SUSPENDREQUEST(cx);
	fprintf(fp,"\r\n");
	JS_RESUMEREQUEST(cx, rc);
	return(JS_TRUE);
}

static JSFunctionSpec js_global_functions[] = {
	{"write",           js_write,           1},		/* write to HTML file */
	{"writeln",         js_writeln,         1},		/* write to HTML file */
	{"print",			js_writeln,         1},		/* alias for writeln */
	{0}
};

static void
js_ErrorReporter(JSContext *cx, const char *message, JSErrorReport *report)
{
	char	line[64];
	char	file[MAX_PATH+1];
	char*	warning;
	FILE*	fp;
	int		log_level;

	fp=(FILE*)JS_GetContextPrivate(cx);
	
	if(report==NULL) {
		lprintf(LOG_ERR,"!JavaScript: %s", message);
		if(fp!=NULL)
			fprintf(fp,"!JavaScript: %s", message);
		return;
    }

	if(report->filename)
		sprintf(file," %s",report->filename);
	else
		file[0]=0;

	if(report->lineno)
		sprintf(line," line %u",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 {
		log_level=LOG_ERR;
		warning="";
	}

	lprintf(log_level,"!JavaScript %s%s%s: %s",warning,file,line,message);
	if(fp!=NULL)
		fprintf(fp,"!JavaScript %s%s%s: %s",warning,file,line,message);
}

static JSContext* 
js_initcx(JSRuntime* runtime, SOCKET sock, JSObject** glob, JSObject** ftp, js_callback_t* cb)
{
	JSContext*	js_cx;
	BOOL		success=FALSE;
	BOOL		rooted=FALSE;

	lprintf(LOG_DEBUG,"%04d JavaScript: Initializing context (stack: %lu bytes)"
		,sock,startup->js.cx_stack);

    if((js_cx = JS_NewContext(runtime, startup->js.cx_stack))==NULL)
		return(NULL);
	JS_BEGINREQUEST(js_cx);

	lprintf(LOG_DEBUG,"%04d JavaScript: Context created",sock);

    JS_SetErrorReporter(js_cx, js_ErrorReporter);

	memset(cb, 0, sizeof(js_callback_t));

	/* ToDo: call js_CreateCommonObjects() instead */

	do {

		lprintf(LOG_DEBUG,"%04d JavaScript: Initializing Global object",sock);
		if(!js_CreateGlobalObject(js_cx, &scfg, NULL, &startup->js, glob))
			break;
		rooted=TRUE;

		if(!JS_DefineFunctions(js_cx, *glob, js_global_functions)) 
			break;

		/* Internal JS Object */
		if(js_CreateInternalJsObject(js_cx, *glob, cb, &startup->js)==NULL)
			break;

		lprintf(LOG_DEBUG,"%04d JavaScript: Initializing System object",sock);
		if(js_CreateSystemObject(js_cx, *glob, &scfg, uptime, startup->host_name, SOCKLIB_DESC)==NULL) 
			break;

		if((*ftp=JS_DefineObject(js_cx, *glob, "ftp", NULL
			,NULL,JSPROP_ENUMERATE|JSPROP_READONLY))==NULL)
			break;

		if(js_CreateServerObject(js_cx,*glob,&js_server_props)==NULL)
			break;

		success=TRUE;

	} while(0);

	if(!success) {
		if(rooted)
			JS_RemoveObjectRoot(js_cx, glob);
		JS_ENDREQUEST(js_cx);
		JS_DestroyContext(js_cx);
		return(NULL);
	}

	return(js_cx);
}

BOOL js_add_file(JSContext* js_cx, JSObject* array, 
				 char* name, char* desc, char* ext_desc,
				 ulong size, ulong credits, 
				 time_t time, time_t uploaded, time_t last_downloaded, 
				 ulong times_downloaded, ulong misc, 
				 char* uploader, char* link)
{
	JSObject*	file;
	JSString*	js_str;
	jsval		val;
	jsuint		index;

	if((file=JS_NewObject(js_cx, NULL, NULL, NULL))==NULL)
		return(FALSE);

	if((js_str=JS_NewStringCopyZ(js_cx, name))==NULL)
		return(FALSE);
	val=STRING_TO_JSVAL(js_str);
	if(!JS_SetProperty(js_cx, file, "name", &val))
		return(FALSE);

	if((js_str=JS_NewStringCopyZ(js_cx, desc))==NULL)
		return(FALSE);
	val=STRING_TO_JSVAL(js_str);
	if(!JS_SetProperty(js_cx, file, "description", &val))
		return(FALSE);

	if((js_str=JS_NewStringCopyZ(js_cx, ext_desc))==NULL)
		return(FALSE);
	val=STRING_TO_JSVAL(js_str);
	if(!JS_SetProperty(js_cx, file, "extended_description", &val))
		return(FALSE);

	val=INT_TO_JSVAL(size);
	if(!JS_SetProperty(js_cx, file, "size", &val))
		return(FALSE);

	val=INT_TO_JSVAL(credits);
	if(!JS_SetProperty(js_cx, file, "credits", &val))
		return(FALSE);

	val=DOUBLE_TO_JSVAL((double)time);
	if(!JS_SetProperty(js_cx, file, "time", &val))
		return(FALSE);

	val=INT_TO_JSVAL((int32)uploaded);
	if(!JS_SetProperty(js_cx, file, "uploaded", &val))
		return(FALSE);

	val=INT_TO_JSVAL((int32)last_downloaded);
	if(!JS_SetProperty(js_cx, file, "last_downloaded", &val))
		return(FALSE);

	val=INT_TO_JSVAL(times_downloaded);
	if(!JS_SetProperty(js_cx, file, "times_downloaded", &val))
		return(FALSE);

	if((js_str=JS_NewStringCopyZ(js_cx, uploader))==NULL)
		return(FALSE);
	val=STRING_TO_JSVAL(js_str);
	if(!JS_SetProperty(js_cx, file, "uploader", &val))
		return(FALSE);

	val=INT_TO_JSVAL(misc);
	if(!JS_SetProperty(js_cx, file, "settings", &val))
		return(FALSE);

	if((js_str=JS_NewStringCopyZ(js_cx, link))==NULL)
		return(FALSE);
	val=STRING_TO_JSVAL(js_str);
	if(!JS_SetProperty(js_cx, file, "link", &val))
		return(FALSE);

	if(!JS_GetArrayLength(js_cx, array, &index))
		return(FALSE);

	val=OBJECT_TO_JSVAL(file);
	return(JS_SetElement(js_cx, array, index, &val));
}

BOOL js_generate_index(JSContext* js_cx, JSObject* parent, 
					   SOCKET sock, FILE* fp, int lib, int dir, user_t* user, client_t* client)
{
	char		str[256];
	char		path[MAX_PATH+1];
	char		spath[MAX_PATH+1];	/* script path */
	char		vpath[MAX_PATH+1];	/* virtual path */
	char		aliasfile[MAX_PATH+1];
	char		extdesc[513];
	char*		p;
	char*		tp;
	char*		np;
	char*		dp;
	char		aliasline[512];
	BOOL		alias_dir;
	BOOL		success=FALSE;
	FILE*		alias_fp;
	uint		i;
	file_t		f;
	glob_t		g;
	jsval		val;
	jsval		rval;
	JSObject*	lib_obj=NULL;
	JSObject*	dir_obj=NULL;
	JSObject*	file_array=NULL;
	JSObject*	dir_array=NULL;
	JSObject*	js_script=NULL;
	JSString*	js_str;
	long double		start=xp_timer();
	jsrefcount	rc;

	lprintf(LOG_DEBUG,"%04d JavaScript: Generating HTML Index for %s"
		,sock, genvpath(lib,dir,str));

	JS_SetContextPrivate(js_cx, fp);

	do {	/* pseudo try/catch */

		if((file_array=JS_NewArrayObject(js_cx, 0, NULL))==NULL) {
			lprintf(LOG_ERR,"%04d !JavaScript FAILED to create file_array",sock);
			break;
		}

		/* file[] */
		val=OBJECT_TO_JSVAL(file_array);
		if(!JS_SetProperty(js_cx, parent, "file_list", &val)) {
			lprintf(LOG_ERR,"%04d !JavaScript FAILED to set file property",sock);
			break;
		}

		if((dir_array=JS_NewArrayObject(js_cx, 0, NULL))==NULL) {
			lprintf(LOG_ERR,"%04d !JavaScript FAILED to create dir_array",sock);
			break;
		}

		/* dir[] */
		val=OBJECT_TO_JSVAL(dir_array);
		if(!JS_SetProperty(js_cx, parent, "dir_list", &val)) {
			lprintf(LOG_ERR,"%04d !JavaScript FAILED to set dir property",sock);
			break;
		}

		rc=JS_SUSPENDREQUEST(js_cx);
		if(strcspn(startup->html_index_script,"/\\")==strlen(startup->html_index_script)) {
			sprintf(spath,"%s%s",scfg.mods_dir,startup->html_index_script);
			if(scfg.mods_dir[0]==0 || !fexist(spath))
				sprintf(spath,"%s%s",scfg.exec_dir,startup->html_index_script);
		} else
			sprintf(spath,"%.*s",(int)sizeof(spath)-4,startup->html_index_script);
		/* Add extension if not specified */
		if(!strchr(spath,'.'))
			strcat(spath,".js");

		if(!fexist(spath)) {
			JS_RESUMEREQUEST(js_cx, rc);
			lprintf(LOG_ERR,"%04d !HTML JavaScript (%s) doesn't exist",sock,spath);
			break;
		}
		JS_RESUMEREQUEST(js_cx, rc);

		if((js_str=JS_NewStringCopyZ(js_cx, startup->html_index_file))==NULL)
			break;
		val=STRING_TO_JSVAL(js_str);
		if(!JS_SetProperty(js_cx, parent, "html_index_file", &val)) {
			lprintf(LOG_ERR,"%04d !JavaScript FAILED to set html_index_file property",sock);
			break;
		}

		/* curlib */
		if((lib_obj=JS_NewObject(js_cx, NULL, 0, NULL))==NULL) {
			lprintf(LOG_ERR,"%04d !JavaScript FAILED to create lib_obj",sock);
			break;
		}

		val=OBJECT_TO_JSVAL(lib_obj);
		if(!JS_SetProperty(js_cx, parent, "curlib", &val)) {
			lprintf(LOG_ERR,"%04d !JavaScript FAILED to set curlib property",sock);
			break;
		}

		/* curdir */
		if((dir_obj=JS_NewObject(js_cx, NULL, 0, NULL))==NULL) {
			lprintf(LOG_ERR,"%04d !JavaScript FAILED to create dir_obj",sock);
			break;
		}

		val=OBJECT_TO_JSVAL(dir_obj);
		if(!JS_SetProperty(js_cx, parent, "curdir", &val)) {
			lprintf(LOG_ERR,"%04d !JavaScript FAILED to set curdir property",sock);
			break;
		}

		SAFECOPY(vpath,"/");

		if(lib>=0) { /* root */

			strcat(vpath,scfg.lib[lib]->sname);
			strcat(vpath,"/");

			if((js_str=JS_NewStringCopyZ(js_cx, scfg.lib[lib]->sname))==NULL)
				break;
			val=STRING_TO_JSVAL(js_str);
			if(!JS_SetProperty(js_cx, lib_obj, "name", &val)) {
				lprintf(LOG_ERR,"%04d !JavaScript FAILED to set curlib.name property",sock);
				break;
			}

			if((js_str=JS_NewStringCopyZ(js_cx, scfg.lib[lib]->lname))==NULL)
				break;
			val=STRING_TO_JSVAL(js_str);
			if(!JS_SetProperty(js_cx, lib_obj, "description", &val)) {
				lprintf(LOG_ERR,"%04d !JavaScript FAILED to set curlib.desc property",sock);
				break;
			}
		}

		if(dir>=0) { /* 1st level */
			strcat(vpath,scfg.dir[dir]->code_suffix);
			strcat(vpath,"/");

			if((js_str=JS_NewStringCopyZ(js_cx, scfg.dir[dir]->code))==NULL)
				break;
			val=STRING_TO_JSVAL(js_str);
			if(!JS_SetProperty(js_cx, dir_obj, "code", &val)) {
				lprintf(LOG_ERR,"%04d !JavaScript FAILED to set curdir.code property",sock);
				break;
			}

			if((js_str=JS_NewStringCopyZ(js_cx, scfg.dir[dir]->sname))==NULL)
				break;
			val=STRING_TO_JSVAL(js_str);
			if(!JS_SetProperty(js_cx, dir_obj, "name", &val)) {
				lprintf(LOG_ERR,"%04d !JavaScript FAILED to set curdir.name property",sock);
				break;
			}

			if((js_str=JS_NewStringCopyZ(js_cx, scfg.dir[dir]->lname))==NULL)
				break;
			val=STRING_TO_JSVAL(js_str);
			if(!JS_SetProperty(js_cx, dir_obj, "description", &val)) {
				lprintf(LOG_ERR,"%04d !JavaScript FAILED to set curdir.desc property",sock);
				break;
			}

			val=INT_TO_JSVAL(scfg.dir[dir]->misc);
			if(!JS_SetProperty(js_cx, dir_obj, "settings", &val)) {
				lprintf(LOG_ERR,"%04d !JavaScript FAILED to set curdir.misc property",sock);
				break;
			}
		}

		if((js_str=JS_NewStringCopyZ(js_cx, vpath))==NULL)
			break;
		val=STRING_TO_JSVAL(js_str);
		if(!JS_SetProperty(js_cx, parent, "path", &val)) {
			lprintf(LOG_ERR,"%04d !JavaScript FAILED to set curdir property",sock);
			break;
		}

		if(lib<0) {	/* root dir */

			rc=JS_SUSPENDREQUEST(js_cx);
			/* File Aliases */
			sprintf(path,"%sftpalias.cfg",scfg.ctrl_dir);
			if((alias_fp=fopen(path,"r"))!=NULL) {

				while(!feof(alias_fp)) {
					if(!fgets(aliasline,sizeof(aliasline),alias_fp))
						break;

					p=aliasline;	/* alias pointer */
					SKIP_WHITESPACE(p);

					if(*p==';')	/* comment */
						continue;

					tp=p;		/* terminator pointer */
					FIND_WHITESPACE(tp);
					if(*tp) *tp=0;

					np=tp+1;	/* filename pointer */
					SKIP_WHITESPACE(np);

					tp=np;		/* terminator pointer */
					FIND_WHITESPACE(tp);
					if(*tp) *tp=0;

					dp=tp+1;	/* description pointer */
					SKIP_WHITESPACE(dp);
					truncsp(dp);

					if(stricmp(dp,BBS_HIDDEN_ALIAS)==0)
						continue;

					alias_dir=FALSE;

					/* Virtual Path? */
					if(!strnicmp(np,BBS_VIRTUAL_PATH,strlen(BBS_VIRTUAL_PATH))) {
						if((dir=getdir(np+strlen(BBS_VIRTUAL_PATH),user,client))<0)
							continue; /* No access or invalid virtual path */
						tp=strrchr(np,'/');
						if(tp==NULL) 
							continue;
						tp++;
						if(*tp) {
							SAFEPRINTF2(aliasfile,"%s%s",scfg.dir[dir]->path,tp);
							np=aliasfile;
						}
						else 
							alias_dir=TRUE;
					}

					if(!alias_dir && !fexist(np))
						continue;

					if(alias_dir) {
						if(!chk_ar(&scfg,scfg.dir[dir]->ar,user,client))
							continue;
						SAFEPRINTF2(vpath,"/%s/%s",p,startup->html_index_file);
					} else
						SAFECOPY(vpath,p);
					JS_RESUMEREQUEST(js_cx, rc);
					js_add_file(js_cx
						,alias_dir ? dir_array : file_array
						,p				/* filename */
						,dp				/* description */
						,NULL			/* extdesc */
						,flength(np)	/* size */
						,0				/* credits */
						,fdate(np)		/* time */
						,fdate(np)		/* uploaded */
						,0				/* last downloaded */
						,0				/* times downloaded */
						,0				/* misc */
						,scfg.sys_id	/* uploader */
						,vpath			/* link */
						);
					rc=JS_SUSPENDREQUEST(js_cx);
				}

				fclose(alias_fp);
			}
			JS_RESUMEREQUEST(js_cx, rc);

			/* QWK Packet */
			if(startup->options&FTP_OPT_ALLOW_QWK /* && fexist(qwkfile) */) {
				sprintf(str,"%s.qwk",scfg.sys_id);
				SAFEPRINTF(vpath,"/%s",str);
				js_add_file(js_cx
					,file_array 
					,str						/* filename */
					,"QWK Message Packet"		/* description */
					,NULL						/* extdesc */
					,10240						/* size */
					,0							/* credits */
					,time(NULL)					/* time */
					,0							/* uploaded */
					,0							/* last downloaded */
					,0							/* times downloaded */
					,0							/* misc */
					,scfg.sys_id				/* uploader */
					,str						/* link */
					);
			}

			/* Library Folders */
			for(i=0;i<scfg.total_libs;i++) {
				if(!chk_ar(&scfg,scfg.lib[i]->ar,user,client))
					continue;
				SAFEPRINTF2(vpath,"/%s/%s",scfg.lib[i]->sname,startup->html_index_file);
				js_add_file(js_cx
					,dir_array 
					,scfg.lib[i]->sname			/* filename */
					,scfg.lib[i]->lname			/* description */
					,NULL						/* extdesc */
					,0,0,0,0,0,0,0				/* unused */
					,scfg.sys_id				/* uploader */
					,vpath						/* link */
					);
			}
		} else if(dir<0) {
			/* Directories */
			for(i=0;i<scfg.total_dirs;i++) {
				if(scfg.dir[i]->lib!=lib)
					continue;
				if(/* i!=scfg.sysop_dir && i!=scfg.upload_dir && */
					!chk_ar(&scfg,scfg.dir[i]->ar,user,client))
					continue;
				SAFEPRINTF3(vpath,"/%s/%s/%s"
					,scfg.lib[scfg.dir[i]->lib]->sname
					,scfg.dir[i]->code_suffix
					,startup->html_index_file);
				js_add_file(js_cx
					,dir_array 
					,scfg.dir[i]->sname			/* filename */
					,scfg.dir[i]->lname			/* description */
					,NULL						/* extdesc */
					,getfiles(&scfg,i)			/* size */
					,0,0,0,0,0					/* unused */
					,scfg.dir[i]->misc			/* misc */
					,scfg.sys_id				/* uploader */
					,vpath						/* link */
					);

			}
		} else if(chk_ar(&scfg,scfg.dir[dir]->ar,user,client)){
			SAFEPRINTF(path,"%s*",scfg.dir[dir]->path);
			rc=JS_SUSPENDREQUEST(js_cx);
			glob(path,0,NULL,&g);
			for(i=0;i<(int)g.gl_pathc;i++) {
				if(isdir(g.gl_pathv[i]))
					continue;
	#ifdef _WIN32
				GetShortPathName(g.gl_pathv[i], str, sizeof(str));
	#else
				SAFECOPY(str,g.gl_pathv[i]);
	#endif
				padfname(getfname(str),f.name);
				f.dir=dir;
				if(getfileixb(&scfg,&f)) {
					f.size=0; /* flength(g.gl_pathv[i]); */
					getfiledat(&scfg,&f);
					if(f.misc&FM_EXTDESC) {
						extdesc[0]=0;
						getextdesc(&scfg, dir, f.datoffset, extdesc);
						/* Remove Ctrl-A Codes and Ex-ASCII code */
						remove_ctrl_a(extdesc,extdesc);
					}
					SAFEPRINTF3(vpath,"/%s/%s/%s"
						,scfg.lib[scfg.dir[dir]->lib]->sname
						,scfg.dir[dir]->code_suffix
						,getfname(g.gl_pathv[i]));
					JS_RESUMEREQUEST(js_cx, rc);
					js_add_file(js_cx
						,file_array 
						,getfname(g.gl_pathv[i])	/* filename */
						,f.desc						/* description */
						,f.misc&FM_EXTDESC ? extdesc : NULL
						,f.size						/* size */
						,f.cdt						/* credits */
						,f.date						/* time */
						,f.dateuled					/* uploaded */
						,f.datedled					/* last downloaded */
						,f.timesdled				/* times downloaded */
						,f.misc						/* misc */
						,f.uler						/* uploader */
						,getfname(g.gl_pathv[i])	/* link */
						);
					rc=JS_SUSPENDREQUEST(js_cx);
				}
			}
			globfree(&g);
			JS_RESUMEREQUEST(js_cx, rc);
		}


		/* RUN SCRIPT */
		JS_ClearPendingException(js_cx);

		if((js_script=JS_CompileFile(js_cx, parent, spath))==NULL) {
			lprintf(LOG_ERR,"%04d !JavaScript FAILED to compile script (%s)",sock,spath);
			break;
		}

		js_PrepareToExecute(js_cx, parent, spath, /* startup_dir: */NULL, parent);
		if((success=JS_ExecuteScript(js_cx, parent, js_script, &rval))!=TRUE) {
			lprintf(LOG_ERR,"%04d !JavaScript FAILED to execute script (%s)",sock,spath);
			break;
		}

		lprintf(LOG_DEBUG,"%04d JavaScript: Done executing script: %s (%.2Lf seconds)"
			,sock,spath,xp_timer()-start);

	} while(0);


	JS_DeleteProperty(js_cx, parent, "path");
	JS_DeleteProperty(js_cx, parent, "sort");
	JS_DeleteProperty(js_cx, parent, "reverse");
	JS_DeleteProperty(js_cx, parent, "file_list");
	JS_DeleteProperty(js_cx, parent, "dir_list");
	JS_DeleteProperty(js_cx, parent, "curlib");
	JS_DeleteProperty(js_cx, parent, "curdir");
	JS_DeleteProperty(js_cx, parent, "html_index_file");

	return(success);
}


#endif	/* ifdef JAVASCRIPT */

BOOL upload_stats(ulong bytes)
{
	char	str[MAX_PATH+1];
	int		file;
	uint32_t	val;

	sprintf(str,"%sdsts.dab",scfg.ctrl_dir);
	if((file=nopen(str,O_RDWR))==-1) 
		return(FALSE);

	lseek(file,20L,SEEK_SET);   /* Skip timestamp, logons and logons today */
	read(file,&val,4);        /* Uploads today         */
	val++;
	lseek(file,-4L,SEEK_CUR);
	write(file,&val,4);
	read(file,&val,4);        /* Upload bytes today    */
	val+=bytes;
	lseek(file,-4L,SEEK_CUR);
	write(file,&val,4);
	close(file);
	return(TRUE);
}

BOOL download_stats(ulong bytes)
{
	char	str[MAX_PATH+1];
	int		file;
	uint32_t	val;

	sprintf(str,"%sdsts.dab",scfg.ctrl_dir);
	if((file=nopen(str,O_RDWR))==-1) 
		return(FALSE);

	lseek(file,28L,SEEK_SET);   /* Skip timestamp, logons and logons today */
	read(file,&val,4);        /* Downloads today         */
	val++;
	lseek(file,-4L,SEEK_CUR);
	write(file,&val,4);
	read(file,&val,4);        /* Download bytes today    */
	val+=bytes;
	lseek(file,-4L,SEEK_CUR);
	write(file,&val,4);
	close(file);
	return(TRUE);
}

void recverror(SOCKET socket, int rd, int line)
{
	if(rd==0) 
		lprintf(LOG_NOTICE,"%04d Socket closed by peer on receive (line %u)"
			,socket, line);
	else if(rd==SOCKET_ERROR) {
		if(ERROR_VALUE==ECONNRESET) 
			lprintf(LOG_NOTICE,"%04d Connection reset by peer on receive (line %u)"
				,socket, line);
		else if(ERROR_VALUE==ECONNABORTED) 
			lprintf(LOG_NOTICE,"%04d Connection aborted by peer on receive (line %u)"
				,socket, line);
		else
			lprintf(LOG_NOTICE,"%04d !ERROR %d receiving on socket (line %u)"
				,socket, ERROR_VALUE, line);
	} else
		lprintf(LOG_WARNING,"%04d !ERROR: recv on socket returned unexpected value: %d (line %u)"
			,socket, rd, line);
}

static int sock_recvbyte(SOCKET sock, CRYPT_SESSION sess, char *buf, time_t *lastactive)
{
	int len=0;
	fd_set	socket_set;
	struct	timeval	tv;
	int ret;
	int i;

	if(ftp_set==NULL || terminate_server) {
		sockprintf(sock,sess,"421 Server downed, aborting.");
		lprintf(LOG_WARNING,"%04d Server downed, aborting",sock);
		return(0);
	}
	if (sess > -1) {
		/* Try a read with no timeout first. */
		cryptSetAttribute(sess, CRYPT_OPTION_NET_READTIMEOUT, 0);
		while (1) {
			ret = cryptPopData(sess, buf, 1, &len);
			/* Successive reads will be with the full timeout after a select() */
			cryptSetAttribute(sess, CRYPT_OPTION_NET_READTIMEOUT, startup->max_inactivity);
			switch(ret) {
				case CRYPT_OK:
					break;
				case CRYPT_ERROR_TIMEOUT:
					lprintf(LOG_WARNING,"%04d !TIMEOUT in sock_recvbyte (%u seconds):  INACTIVE SOCKET",sock,startup->max_inactivity);
					return -1;
				case CRYPT_ERROR_COMPLETE:
					return 0;
				default:
					lprintf(LOG_WARNING,"%04d !Cryptlib error in sock_recvbyte:  %d",sock,ret);
					if (ret < -1)
						return ret;
					return -2;
			}
			if (len)
				return len;
			
			if((time(NULL)-(*lastactive))>startup->max_inactivity) {
				lprintf(LOG_WARNING,"%04d Disconnecting due to to inactivity",sock);
				sockprintf(sock,sess,"421 Disconnecting due to inactivity (%u seconds)."
					,startup->max_inactivity);
				return(0);
			}

			tv.tv_sec=startup->max_inactivity;
			tv.tv_usec=0;

			FD_ZERO(&socket_set);
			FD_SET(sock,&socket_set);

			i=select(sock+1,&socket_set,NULL,NULL,&tv);

			if(i<1) {
				if(i==0) {
					if((time(NULL)-(*lastactive))>startup->max_inactivity) {
						lprintf(LOG_WARNING,"%04d Disconnecting due to to inactivity",sock);
						sockprintf(sock,sess,"421 Disconnecting due to inactivity (%u seconds)."
							,startup->max_inactivity);
						return(0);
					}
					continue;
				}
				recverror(sock,i,__LINE__);
				return(i);
			}
		}
	}
	else {
		while (1) {
			tv.tv_sec=startup->max_inactivity;
			tv.tv_usec=0;

			FD_ZERO(&socket_set);
			FD_SET(sock,&socket_set);

			i=select(sock+1,&socket_set,NULL,NULL,&tv);

			if(i<1) {
				if(i==0) {
					if((time(NULL)-(*lastactive))>startup->max_inactivity) {
						lprintf(LOG_WARNING,"%04d Disconnecting due to to inactivity",sock);
						sockprintf(sock,sess,"421 Disconnecting due to inactivity (%u seconds)."
							,startup->max_inactivity);
						return(0);
					}
					continue;
				}
				recverror(sock,i,__LINE__);
				return(i);
			}
	#ifdef SOCKET_DEBUG_RECV_CHAR
			socket_debug[sock]|=SOCKET_DEBUG_RECV_CHAR;
	#endif
			i=recv(sock, buf, 1, 0);
	#ifdef SOCKET_DEBUG_RECV_CHAR
			socket_debug[sock]&=~SOCKET_DEBUG_RECV_CHAR;
	#endif
			return i;
		}
	}
}

int sockreadline(SOCKET socket, CRYPT_SESSION sess, char* buf, int len, time_t* lastactive)
{
	char	ch;
	int		i,rd=0;

	buf[0]=0;

	if(socket==INVALID_SOCKET) {
		lprintf(LOG_WARNING,"INVALID SOCKET in call to sockreadline");
		return(0);
	}

	while(rd<len-1) {
		i = sock_recvbyte(socket, sess, &ch, lastactive);

		if(i<1) {
			recverror(socket,i,__LINE__);
			return(i);
		}
		if(ch=='\n' /* && rd>=1 */) { /* Mar-9-2003: terminate on sole LF */
			break;
		}	
		buf[rd++]=ch;
	}
	if(rd>0 && buf[rd-1]=='\r')
		buf[rd-1]=0;
	else
		buf[rd]=0;

	return(rd);
}

void DLLCALL ftp_terminate(void)
{
   	lprintf(LOG_INFO,"FTP Server terminate");
	terminate_server=TRUE;
}

int ftp_remove(SOCKET sock, int line, const char* fname)
{
	int ret=0;

	if(fexist(fname) && (ret=remove(fname))!=0) {
		if(fexist(fname))	// In case there was a race condition (other host deleted file first)
			lprintf(LOG_ERR,"%04d !ERROR %d (%s) (line %d) removing file: %s", sock, errno, STRERROR(errno), line, fname);
	}
	return ret;
}

typedef struct {
	SOCKET		ctrl_sock;
	CRYPT_SESSION	ctrl_sess;
	SOCKET*		data_sock;
	CRYPT_SESSION*	data_sess;
	BOOL*		inprogress;
	BOOL*		aborted;
	BOOL		delfile;
	BOOL		tmpfile;
	BOOL		credits;
	BOOL		append;
	long		filepos;
	char		filename[MAX_PATH+1];
	time_t*		lastactive;
	user_t*		user;
	client_t*	client;
	int			dir;
	char*		desc;
} xfer_t;

static void send_thread(void* arg)
{
	char		buf[8192];
	char		fname[MAX_PATH+1];
	char		str[128];
	char		tmp[128];
	char		username[128];
	char		host_ip[INET6_ADDRSTRLEN];
	int			i;
	int			rd;
	int			wr;
	long		mod;
	ulong		l;
	ulong		total=0;
	ulong		last_total=0;
	ulong		dur;
	ulong		cps;
	ulong		length;
	BOOL		error=FALSE;
	FILE*		fp;
	file_t		f;
	xfer_t		xfer;
	time_t		now;
	time_t		start;
	time_t		last_report;
	user_t		uploader;
	union xp_sockaddr	addr;
	socklen_t	addr_len;
	fd_set		socket_set;
	struct timeval tv;

	xfer=*(xfer_t*)arg;
	free(arg);

	SetThreadName("sbbs/ftpSend");
	thread_up(TRUE /* setuid */);

	length=flength(xfer.filename);

	if((fp=fnopen(NULL,xfer.filename,O_RDONLY|O_BINARY))==NULL	/* non-shareable open failed */
		&& (fp=fopen(xfer.filename,"rb"))==NULL) {				/* shareable open failed */
		lprintf(LOG_ERR,"%04d !DATA ERROR %d opening %s",xfer.ctrl_sock,errno,xfer.filename);
		sockprintf(xfer.ctrl_sock,xfer.ctrl_sess,"450 ERROR %d opening %s.",errno,xfer.filename);
		if(xfer.tmpfile && !(startup->options&FTP_OPT_KEEP_TEMP_FILES))
			ftp_remove(xfer.ctrl_sock, __LINE__, xfer.filename);
		ftp_close_socket(xfer.data_sock,xfer.data_sess,__LINE__);
		*xfer.inprogress=FALSE;
		thread_down();
		return;
	}

#ifdef SOCKET_DEBUG_SENDTHREAD
			socket_debug[xfer.ctrl_sock]|=SOCKET_DEBUG_SENDTHREAD;
#endif

	*xfer.aborted=FALSE;
	if(startup->options&FTP_OPT_DEBUG_DATA || xfer.filepos)
		lprintf(LOG_DEBUG,"%04d DATA socket %d sending %s from offset %lu"
			,xfer.ctrl_sock,*xfer.data_sock,xfer.filename,xfer.filepos);

	fseek(fp,xfer.filepos,SEEK_SET);
	last_report=start=time(NULL);
	while((xfer.filepos+total)<length) {

		now=time(NULL);

		/* Periodic progress report */
		if(total && now>=last_report+XFER_REPORT_INTERVAL) {
			if(xfer.filepos)
				sprintf(str," from offset %lu",xfer.filepos);
			else
				str[0]=0;
			lprintf(LOG_INFO,"%04d Sent %lu bytes (%lu total) of %s (%lu cps)%s"
				,xfer.ctrl_sock,total,length,xfer.filename
				,(ulong)((total-last_total)/(now-last_report))
				,str);
			last_total=total;
			last_report=now;
		}

		if(*xfer.aborted==TRUE) {
			lprintf(LOG_WARNING,"%04d !DATA Transfer aborted",xfer.ctrl_sock);
			sockprintf(xfer.ctrl_sock,xfer.ctrl_sess,"426 Transfer aborted.");
			error=TRUE;
			break;
		}
		if(ftp_set==NULL || terminate_server) {
			lprintf(LOG_WARNING,"%04d !DATA Transfer locally aborted",xfer.ctrl_sock);
			sockprintf(xfer.ctrl_sock,xfer.ctrl_sess,"426 Transfer locally aborted.");
			error=TRUE;
			break;
		}

		/* Check socket for writability (using select) */
		tv.tv_sec=1;
		tv.tv_usec=0;

		FD_ZERO(&socket_set);
		FD_SET(*xfer.data_sock,&socket_set);

		i=select((*xfer.data_sock)+1,NULL,&socket_set,NULL,&tv);
		if(i==SOCKET_ERROR) {
			lprintf(LOG_WARNING,"%04d !DATA ERROR %d selecting socket %d for send"
				,xfer.ctrl_sock, ERROR_VALUE, *xfer.data_sock);
			sockprintf(xfer.ctrl_sock,xfer.ctrl_sess,"426 Transfer error.");
			error=TRUE;
			break;
		}
		if(i<1)
			continue;

		fseek(fp,xfer.filepos+total,SEEK_SET);
		rd=fread(buf,sizeof(char),sizeof(buf),fp);
		if(rd<1) /* EOF or READ error */
			break;

#ifdef SOCKET_DEBUG_SEND
		socket_debug[xfer.ctrl_sock]|=SOCKET_DEBUG_SEND;
#endif
		if (*xfer.data_sess != -1) {
			int status = cryptPushData(*xfer.data_sess, buf, rd, &wr);
			if (status != CRYPT_OK) {
				lprintf(LOG_DEBUG, "PushData() returned %d\n", status);
				wr = -1;
			}
			else {
				status = cryptFlushData(*xfer.data_sess);
				if (status != CRYPT_OK) {
					lprintf(LOG_DEBUG, "cryptFlushData() returned %d\n", status);
					wr = -1;
				}
			}
		}
		else
			wr=sendsocket(*xfer.data_sock,buf,rd);
#ifdef SOCKET_DEBUG_SEND
		socket_debug[xfer.ctrl_sock]&=~SOCKET_DEBUG_SEND;
#endif
		if(wr<1) {
			if(wr==SOCKET_ERROR) {
				if(ERROR_VALUE==EWOULDBLOCK) {
					/*lprintf(LOG_WARNING,"%04d DATA send would block, retrying",xfer.ctrl_sock);*/
					YIELD();
					continue;
				}
				else if(ERROR_VALUE==ECONNRESET) 
					lprintf(LOG_WARNING,"%04d DATA Connection reset by peer, sending on socket %d"
						,xfer.ctrl_sock,*xfer.data_sock);
				else if(ERROR_VALUE==ECONNABORTED) 
					lprintf(LOG_WARNING,"%04d DATA Connection aborted by peer, sending on socket %d"
						,xfer.ctrl_sock,*xfer.data_sock);
				else
					lprintf(LOG_WARNING,"%04d !DATA ERROR %d sending on data socket %d"
						,xfer.ctrl_sock,ERROR_VALUE,*xfer.data_sock);
				/* Send NAK */
				sockprintf(xfer.ctrl_sock,xfer.ctrl_sess,"426 Error %d sending on DATA channel"
					,ERROR_VALUE);
				error=TRUE;
				break;
			}
			if(wr==0) {
				lprintf(LOG_WARNING,"%04d !DATA socket %d disconnected",xfer.ctrl_sock, *xfer.data_sock);
				sockprintf(xfer.ctrl_sock,xfer.ctrl_sess,"426 DATA channel disconnected");
				error=TRUE;
				break;
			}
			lprintf(LOG_ERR,"%04d !DATA SEND ERROR %d (%d) on socket %d"
				,xfer.ctrl_sock, wr, ERROR_VALUE, *xfer.data_sock);
			sockprintf(xfer.ctrl_sock,xfer.ctrl_sess,"451 DATA send error");
			error=TRUE;
			break;
		}
		total+=wr;
		*xfer.lastactive=time(NULL);
		//YIELD();
	}

	if((i=ferror(fp))!=0) 
		lprintf(LOG_ERR,"%04d !FILE ERROR %d (%d)",xfer.ctrl_sock,i,errno);

	ftp_close_socket(xfer.data_sock,xfer.data_sess,__LINE__);	/* Signal end of file */
	if(startup->options&FTP_OPT_DEBUG_DATA)
		lprintf(LOG_DEBUG,"%04d DATA socket closed",xfer.ctrl_sock);
	
	if(!error) {
		dur=(long)(time(NULL)-start);
		cps=dur ? total/dur : total*2;
		lprintf(LOG_INFO,"%04d Transfer successful: %lu bytes sent in %lu seconds (%lu cps)"
			,xfer.ctrl_sock
			,total,dur,cps);
		sockprintf(xfer.ctrl_sock,xfer.ctrl_sess,"226 Download complete (%lu cps).",cps);

		if(xfer.dir>=0) {
			memset(&f,0,sizeof(f));
#ifdef _WIN32
			GetShortPathName(xfer.filename,fname,sizeof(fname));
#else
			SAFECOPY(fname,xfer.filename);
#endif
			padfname(getfname(fname),f.name);
			f.dir=xfer.dir;
			f.size=total;
			if(getfileixb(&scfg,&f)==TRUE && getfiledat(&scfg,&f)==TRUE) {
				f.timesdled++;
				putfiledat(&scfg,&f);
				f.datedled=time32(NULL);
				putfileixb(&scfg,&f);

				lprintf(LOG_INFO,"%04d %s downloaded: %s (%lu times total)"
					,xfer.ctrl_sock
					,xfer.user->alias
					,xfer.filename
					,f.timesdled);
				/**************************/
				/* Update Uploader's Info */
				/**************************/
				uploader.number=matchuser(&scfg,f.uler,TRUE /*sysop_alias*/);
				if(uploader.number
					&& uploader.number!=xfer.user->number 
					&& getuserdat(&scfg,&uploader)==0
					&& uploader.firston<f.dateuled) {
					l=f.cdt;
					if(!(scfg.dir[f.dir]->misc&DIR_CDTDL))	/* Don't give credits on d/l */
						l=0;
					if(scfg.dir[f.dir]->misc&DIR_CDTMIN && cps) { /* Give min instead of cdt */
						mod=((ulong)(l*(scfg.dir[f.dir]->dn_pct/100.0))/cps)/60;
						adjustuserrec(&scfg,uploader.number,U_MIN,10,mod);
						sprintf(tmp,"%lu minute",mod);
					} else {
						mod=(ulong)(l*(scfg.dir[f.dir]->dn_pct/100.0));
						adjustuserrec(&scfg,uploader.number,U_CDT,10,mod);
						ultoac(mod,tmp);
					}
					if(!(scfg.dir[f.dir]->misc&DIR_QUIET)) {
						addr_len = sizeof(addr);
						if(uploader.level>=SYSOP_LEVEL
							&& getpeername(xfer.ctrl_sock,&addr.addr,&addr_len)==0
							&& inet_addrtop(&addr, host_ip, sizeof(host_ip))!=NULL)
							SAFEPRINTF2(username,"%s [%s]",xfer.user->alias,host_ip);
						else
							SAFECOPY(username,xfer.user->alias);
						/* Inform uploader of downloaded file */
						safe_snprintf(str,sizeof(str),text[DownloadUserMsg]
							,getfname(xfer.filename)
							,xfer.filepos ? "partially FTP-" : "FTP-"
							,username,tmp); 
						putsmsg(&scfg,uploader.number,str); 
					}
				}
			}
			if(!xfer.tmpfile && !xfer.delfile && !(scfg.dir[f.dir]->misc&DIR_NOSTAT))
				download_stats(total);
		}	

		if(xfer.credits) {
			user_downloaded(&scfg, xfer.user, 1, total);
			if(xfer.dir>=0 && !is_download_free(&scfg,xfer.dir,xfer.user,xfer.client))
				subtract_cdt(&scfg, xfer.user, xfer.credits);
		}
	}

	fclose(fp);
	if(ftp_set!=NULL && !terminate_server)
		*xfer.inprogress=FALSE;
	if(xfer.tmpfile) {
		if(!(startup->options&FTP_OPT_KEEP_TEMP_FILES))
			ftp_remove(xfer.ctrl_sock, __LINE__, xfer.filename);
	} 
	else if(xfer.delfile && !error)
		ftp_remove(xfer.ctrl_sock, __LINE__, xfer.filename);

#if defined(SOCKET_DEBUG_SENDTHREAD)
			socket_debug[xfer.ctrl_sock]&=~SOCKET_DEBUG_SENDTHREAD;
#endif

	thread_down();
}

static void receive_thread(void* arg)
{
	char*		p;
	char		str[128];
	char		buf[8192];
	char		ext[F_EXBSIZE+1];
	char		desc[F_EXBSIZE+1];
	char		cmd[MAX_PATH*2];
	char		tmp[MAX_PATH+1];
	char		fname[MAX_PATH+1];
	int			i;
	int			rd;
	int			file;
	ulong		total=0;
	ulong		last_total=0;
	ulong		dur;
	ulong		cps;
	BOOL		error=FALSE;
	BOOL		filedat;
	FILE*		fp;
	file_t		f;
	xfer_t		xfer;
	time_t		now;
	time_t		start;
	time_t		last_report;
	fd_set		socket_set;
	struct timeval tv;
	CRYPT_SESSION	sess = -1;

	xfer=*(xfer_t*)arg;
	free(arg);

	SetThreadName("sbbs/ftpReceive");
	thread_up(TRUE /* setuid */);

	if((fp=fopen(xfer.filename,xfer.append ? "ab" : "wb"))==NULL) {
		lprintf(LOG_ERR,"%04d !DATA ERROR %d opening %s",xfer.ctrl_sock,errno,xfer.filename);
		sockprintf(xfer.ctrl_sock,sess,"450 ERROR %d opening %s.",errno,xfer.filename);
		ftp_close_socket(xfer.data_sock,xfer.data_sess,__LINE__);
		*xfer.inprogress=FALSE;
		thread_down();
		return;
	}

	if(xfer.append)
		xfer.filepos=filelength(fileno(fp));

	*xfer.aborted=FALSE;
	if(xfer.filepos || startup->options&FTP_OPT_DEBUG_DATA)
		lprintf(LOG_DEBUG,"%04d DATA socket %d receiving %s from offset %lu"
			,xfer.ctrl_sock,*xfer.data_sock,xfer.filename,xfer.filepos);

	fseek(fp,xfer.filepos,SEEK_SET);
	last_report=start=time(NULL);
	while(1) {

		now=time(NULL);

		/* Periodic progress report */
		if(total && now>=last_report+XFER_REPORT_INTERVAL) {
			if(xfer.filepos)
				sprintf(str," from offset %lu",xfer.filepos);
			else
				str[0]=0;
			lprintf(LOG_INFO,"%04d Received %lu bytes of %s (%lu cps)%s"
				,xfer.ctrl_sock,total,xfer.filename
				,(ulong)((total-last_total)/(now-last_report))
				,str);
			last_total=total;
			last_report=now;
		}
		if(startup->max_fsize && (xfer.filepos+total) > startup->max_fsize) {
			lprintf(LOG_WARNING,"%04d !DATA received %lu bytes of %s exceeds maximum allowed (%lu bytes)"
				,xfer.ctrl_sock, xfer.filepos+total, xfer.filename, startup->max_fsize);
			sockprintf(xfer.ctrl_sock,sess,"552 File size exceeds maximum allowed (%lu bytes)", startup->max_fsize);
			error=TRUE;
			break;
		}
		if(*xfer.aborted==TRUE) {
			lprintf(LOG_WARNING,"%04d !DATA Transfer aborted",xfer.ctrl_sock);
			/* Send NAK */
			sockprintf(xfer.ctrl_sock,sess,"426 Transfer aborted.");
			error=TRUE;
			break;
		}
		if(ftp_set==NULL || terminate_server) {
			lprintf(LOG_WARNING,"%04d !DATA Transfer locally aborted",xfer.ctrl_sock);
			/* Send NAK */
			sockprintf(xfer.ctrl_sock,sess,"426 Transfer locally aborted.");
			error=TRUE;
			break;
		}

		/* Check socket for readability (using select) */
		tv.tv_sec=1;
		tv.tv_usec=0;

		FD_ZERO(&socket_set);
		FD_SET(*xfer.data_sock,&socket_set);

		i=select((*xfer.data_sock)+1,&socket_set,NULL,NULL,&tv);
		if(i==SOCKET_ERROR) {
			lprintf(LOG_WARNING,"%04d !DATA ERROR %d selecting socket %d for receive"
				,xfer.ctrl_sock, ERROR_VALUE, *xfer.data_sock);
			sockprintf(xfer.ctrl_sock,sess,"426 Transfer error.");
			error=TRUE;
			break;
		}
		if(i<1)
			continue;

#if defined(SOCKET_DEBUG_RECV_BUF)
		socket_debug[xfer.ctrl_sock]|=SOCKET_DEBUG_RECV_BUF;
#endif
		if (*xfer.data_sess != -1) {
			int status = cryptPopData(*xfer.data_sess, buf, sizeof(buf), &rd);
			if (status != CRYPT_OK)
				rd = -1;
		}
		else {
			rd=recv(*xfer.data_sock,buf,sizeof(buf),0);
		}
#if defined(SOCKET_DEBUG_RECV_BUF)
		socket_debug[xfer.ctrl_sock]&=~SOCKET_DEBUG_RECV_BUF;
#endif
		if(rd<1) {
			if(rd==0) { /* Socket closed */
				if(startup->options&FTP_OPT_DEBUG_DATA)
					lprintf(LOG_DEBUG,"%04d DATA socket %d closed by client"
						,xfer.ctrl_sock,*xfer.data_sock);
				break;
			}
			if(rd==SOCKET_ERROR) {
				if(ERROR_VALUE==EWOULDBLOCK) {
					/*lprintf(LOG_WARNING,"%04d DATA recv would block, retrying",xfer.ctrl_sock);*/
					YIELD();
					continue;
				}
				else if(ERROR_VALUE==ECONNRESET) 
					lprintf(LOG_WARNING,"%04d DATA Connection reset by peer, receiving on socket %d"
						,xfer.ctrl_sock,*xfer.data_sock);
				else if(ERROR_VALUE==ECONNABORTED) 
					lprintf(LOG_WARNING,"%04d DATA Connection aborted by peer, receiving on socket %d"
						,xfer.ctrl_sock,*xfer.data_sock);
				else
					lprintf(LOG_WARNING,"%04d !DATA ERROR %d receiving on data socket %d"
						,xfer.ctrl_sock,ERROR_VALUE,*xfer.data_sock);
				/* Send NAK */
				sockprintf(xfer.ctrl_sock,sess,"426 Error %d receiving on DATA channel"
					,ERROR_VALUE);
				error=TRUE;
				break;
			}
			lprintf(LOG_ERR,"%04d !DATA ERROR recv returned %d on socket %d"
				,xfer.ctrl_sock,rd,*xfer.data_sock);
			/* Send NAK */
			sockprintf(xfer.ctrl_sock,sess,"451 Unexpected socket error: %d",rd);
			error=TRUE;
			break;
		}
		fwrite(buf,1,rd,fp);
		total+=rd;
		*xfer.lastactive=time(NULL);
		YIELD();
	}

	fclose(fp);

	ftp_close_socket(xfer.data_sock,xfer.data_sess,__LINE__);
	if(error && startup->options&FTP_OPT_DEBUG_DATA)
		lprintf(LOG_DEBUG,"%04d DATA socket %d closed",xfer.ctrl_sock,*xfer.data_sock);
	
	if(xfer.filepos+total < startup->min_fsize) {
		lprintf(LOG_WARNING,"%04d DATA received %lu bytes for %s, less than minimum required (%lu bytes)"
			,xfer.ctrl_sock, xfer.filepos+total, xfer.filename, startup->min_fsize);
		sockprintf(xfer.ctrl_sock,sess,"550 File size less than minimum required (%lu bytes)"
			,startup->min_fsize);
		error=TRUE;
	}
	if(error) {
		if(!xfer.append)
			ftp_remove(xfer.ctrl_sock, __LINE__, xfer.filename);
	} else {
		dur=(long)(time(NULL)-start);
		cps=dur ? total/dur : total*2;
		lprintf(LOG_INFO,"%04d Transfer successful: %lu bytes received in %lu seconds (%lu cps)"
			,xfer.ctrl_sock
			,total,dur,cps);

		if(xfer.dir>=0) {
			memset(&f,0,sizeof(f));
#ifdef _WIN32
			GetShortPathName(xfer.filename,fname,sizeof(fname));
#else
			SAFECOPY(fname,xfer.filename);
#endif
			padfname(getfname(fname),f.name);
			f.dir=xfer.dir;
			filedat=getfileixb(&scfg,&f);
			if(scfg.dir[f.dir]->misc&DIR_AONLY)  /* Forced anonymous */
				f.misc|=FM_ANON;
			f.cdt=flength(xfer.filename);
			f.dateuled=time32(NULL);

			/* Description specified with DESC command? */
			if(xfer.desc!=NULL && *xfer.desc!=0)	
				SAFECOPY(f.desc,xfer.desc);

			/* 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 */
			p=strrchr(f.name,'.');
			if(p!=NULL && scfg.dir[f.dir]->misc&DIR_DIZ) {
				for(i=0;i<scfg.total_fextrs;i++)
					if(!stricmp(scfg.fextr[i]->ext,p+1) 
						&& chk_ar(&scfg,scfg.fextr[i]->ar,xfer.user,xfer.client))
						break;
				if(i<scfg.total_fextrs) {
					sprintf(tmp,"%sFILE_ID.DIZ",scfg.temp_dir);
					if(fexistcase(tmp))
						ftp_remove(xfer.ctrl_sock, __LINE__, tmp);
					cmdstr(&scfg,xfer.user,scfg.fextr[i]->cmd,fname,"FILE_ID.DIZ",cmd);
					lprintf(LOG_DEBUG,"%04d Extracting DIZ: %s",xfer.ctrl_sock,cmd);
					system(cmd);
					if(!fexistcase(tmp)) {
						sprintf(tmp,"%sDESC.SDI",scfg.temp_dir);
						if(fexistcase(tmp))
							ftp_remove(xfer.ctrl_sock, __LINE__, tmp);
						cmdstr(&scfg,xfer.user,scfg.fextr[i]->cmd,fname,"DESC.SDI",cmd);
						lprintf(LOG_DEBUG,"%04d Extracting DIZ: %s",xfer.ctrl_sock,cmd);
						system(cmd); 
						fexistcase(tmp);	/* fixes filename case */
					}
					if((file=nopen(tmp,O_RDONLY))!=-1) {
						lprintf(LOG_DEBUG,"%04d Parsing DIZ: %s",xfer.ctrl_sock,tmp);
						memset(ext,0,sizeof(ext));
						read(file,ext,sizeof(ext)-1);
						for(i=sizeof(ext)-1;i;i--)	/* trim trailing spaces */
							if(ext[i-1]>' ')
								break;
						ext[i]=0;
						if(!f.desc[0]) {			/* use for normal description */
							SAFECOPY(desc,ext);
							strip_exascii(desc, desc);	/* strip extended ASCII chars */
							prep_file_desc(desc, desc);	/* strip control chars and dupe chars */
							for(i=0;desc[i];i++)	/* find approprate first char */
								if(isalnum(desc[i]))
									break;
							SAFECOPY(f.desc,desc+i); 
						}
						close(file);
						ftp_remove(xfer.ctrl_sock, __LINE__, tmp);
						f.misc|=FM_EXTDESC; 
					} else
						lprintf(LOG_DEBUG,"%04d DIZ Does not exist: %s",xfer.ctrl_sock,tmp);
				} 
			} /* FILE_ID.DIZ support */

			if(f.desc[0]==0) 	/* no description given, use (long) filename */
				SAFECOPY(f.desc,getfname(xfer.filename));

			SAFECOPY(f.uler,xfer.user->alias);	/* exception here, Aug-27-2002 */
			if(filedat) {
				if(!putfiledat(&scfg,&f))
					lprintf(LOG_ERR,"%04d !ERROR updating file (%s) in database",xfer.ctrl_sock,f.name);
				/* need to update the index here */
			} else {
				if(!addfiledat(&scfg,&f))
					lprintf(LOG_ERR,"%04d !ERROR adding file (%s) to database",xfer.ctrl_sock,f.name);
			}

			if(f.misc&FM_EXTDESC)
				putextdesc(&scfg,f.dir,f.datoffset,ext);

			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=adjustuserrec(&scfg,xfer.user->number,U_MIN,10
						,((ulong)(total*(scfg.dir[f.dir]->up_pct/100.0))/cps)/60);
				else
					xfer.user->cdt=adjustuserrec(&scfg,xfer.user->number,U_CDT,10
						,(ulong)(f.cdt*(scfg.dir[f.dir]->up_pct/100.0))); 
			}
			if(!(scfg.dir[f.dir]->misc&DIR_NOSTAT))
				upload_stats(total);
		}
		/* Send ACK */
		sockprintf(xfer.ctrl_sock,sess,"226 Upload complete (%lu cps).",cps);
	}

	if(ftp_set!=NULL && !terminate_server)
		*xfer.inprogress=FALSE;

	thread_down();
}

static BOOL start_tls(SOCKET *sock, CRYPT_SESSION *sess, BOOL resp)
{
	BOOL nodelay;
	ulong nb;
	int status;
	char *estr;

	if (get_ssl_cert(&scfg, NULL) == -1) {
		lprintf(LOG_ERR, "Unable to get certificate");
		if (resp)
			sockprintf(*sock, *sess, "431 TLS not available");
		return FALSE;
	}
	if (cryptCreateSession(sess, CRYPT_UNUSED, CRYPT_SESSION_SSL_SERVER) != CRYPT_OK) {
		lprintf(LOG_ERR, "Unable to create TLS session");
		if (resp)
			sockprintf(*sock, *sess, "431 TLS not available");
		return FALSE;
	}
	if (cryptSetAttribute(*sess, CRYPT_SESSINFO_SSL_OPTIONS, CRYPT_SSLOPTION_DISABLE_CERTVERIFY) != CRYPT_OK) {
		lprintf(LOG_ERR, "Unable to disable certificate verification");
		cryptDestroySession(*sess);
		*sess = -1;
		if(resp)
			sockprintf(*sock, *sess, "431 TLS not available");
		return FALSE;
	}
	if (cryptSetAttribute(*sess, CRYPT_SESSINFO_PRIVATEKEY, scfg.tls_certificate) != CRYPT_OK) {
		lprintf(LOG_ERR, "Unable to set private key");
		cryptDestroySession(*sess);
		*sess = -1;
		if (resp)
			sockprintf(*sock, *sess, "431 TLS not available");
		return FALSE;
	}
	nodelay = TRUE;
	setsockopt(*sock,IPPROTO_TCP,TCP_NODELAY,(char*)&nodelay,sizeof(nodelay));
	nb=0;
	ioctlsocket(*sock,FIONBIO,&nb);
	if (cryptSetAttribute(*sess, CRYPT_SESSINFO_NETWORKSOCKET, *sock) != CRYPT_OK) {
		lprintf(LOG_ERR, "Unable to set network socket");
		cryptDestroySession(*sess);
		*sess = -1;
		if (resp)
			sockprintf(*sock, *sess, "431 TLS not available");
		return TRUE;
	}
	if (resp)
		sockprintf(*sock, -1, "234 Ready to start TLS");
	if ((status = cryptSetAttribute(*sess, CRYPT_SESSINFO_ACTIVE, 1)) != CRYPT_OK) {
		estr = get_crypt_error(*sess);
		lprintf(LOG_ERR, "Unable to set session active (%d:%s)", status, estr);
		free_crypt_attrstr(estr);
		return TRUE;
	}
	if (startup->max_inactivity) {
		if (cryptSetAttribute(*sess, CRYPT_OPTION_NET_READTIMEOUT, startup->max_inactivity) != CRYPT_OK) {
			lprintf(LOG_ERR, "Unable to set max inactivity");
			return TRUE;
		}
	}
	return FALSE;
}

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, long 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;
	struct timeval	tv;
	fd_set			socket_set;
	char		host_ip[INET6_ADDRSTRLEN];

	if((*inprogress)==TRUE) {
		lprintf(LOG_WARNING,"%04d !TRANSFER already in progress",ctrl_sock);
		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);
		return;
	}
	*inprogress=TRUE;

	if(*data_sock!=INVALID_SOCKET)
		ftp_close_socket(data_sock,data_sess,__LINE__);

	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 !DATA ERROR %d opening socket", ctrl_sock, ERROR_VALUE);
			sockprintf(ctrl_sock,ctrl_sess,"425 Error %d opening socket",ERROR_VALUE);
			if(tmpfile && !(startup->options&FTP_OPT_KEEP_TEMP_FILES))
				ftp_remove(ctrl_sock, __LINE__, filename);
			*inprogress=FALSE;
			return;
		}
		if(startup->socket_open!=NULL)
			startup->socket_open(startup->cbdata,TRUE);
		if(startup->options&FTP_OPT_DEBUG_DATA)
			lprintf(LOG_DEBUG,"%04d DATA socket %d opened",ctrl_sock,*data_sock);

		/* Use port-1 for all data connections */
		reuseaddr=TRUE;
		setsockopt(*data_sock,SOL_SOCKET,SO_REUSEADDR,(char*)&reuseaddr,sizeof(reuseaddr));

		addr_len = sizeof(server_addr);
		if((result=getsockname(ctrl_sock, &server_addr.addr,&addr_len))!=0) {
			lprintf(LOG_ERR,"%04d !ERROR %d (%d) getting address/port of command socket (%u)"
				,ctrl_sock,result,ERROR_VALUE,pasv_sock);
			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 !DATA ERROR %d (%d) binding socket %d"
				,ctrl_sock, result, ERROR_VALUE, *data_sock);
			sockprintf(ctrl_sock,ctrl_sess,"425 Error %d binding socket",ERROR_VALUE);
			if(tmpfile && !(startup->options&FTP_OPT_KEEP_TEMP_FILES))
				ftp_remove(ctrl_sock, __LINE__, filename);
			*inprogress=FALSE;
			ftp_close_socket(data_sock,data_sess,__LINE__);
			return;
		}

		result=connect(*data_sock, &addr->addr,xp_sockaddr_len(addr));
		if(result!=0) {
			lprintf(LOG_WARNING,"%04d !DATA ERROR %d (%d) connecting to client %s port %u on socket %d"
					,ctrl_sock,result,ERROR_VALUE
					,host_ip,inet_addrport(addr),*data_sock);
			sockprintf(ctrl_sock,ctrl_sess,"425 Error %d connecting to socket",ERROR_VALUE);
			if(tmpfile && !(startup->options&FTP_OPT_KEEP_TEMP_FILES))
				ftp_remove(ctrl_sock, __LINE__, filename);
			*inprogress=FALSE;
			ftp_close_socket(data_sock,data_sess,__LINE__);
			return;
		}
		if(startup->options&FTP_OPT_DEBUG_DATA)
			lprintf(LOG_DEBUG,"%04d DATA socket %d connected to %s port %u"
				,ctrl_sock,*data_sock,host_ip,inet_addrport(addr));

		if (protected) {
			if (start_tls(data_sock, data_sess, FALSE) || *data_sess == -1) {
				lprintf(LOG_DEBUG,"%04d !DATA ERROR activating TLS"
					,ctrl_sock,*data_sock,host_ip,inet_addrport(addr));
				sockprintf(ctrl_sock,ctrl_sess,"425 Error activating TLS");
				if(tmpfile && !(startup->options&FTP_OPT_KEEP_TEMP_FILES))
					ftp_remove(ctrl_sock, __LINE__, filename);
				*inprogress=FALSE;
				ftp_close_socket(data_sock,data_sess,__LINE__);
				return;
			}
		}
	} else {	/* PASV */

		if(startup->options&FTP_OPT_DEBUG_DATA) {
			addr_len=sizeof(*addr);
			if((result=getsockname(pasv_sock, &addr->addr,&addr_len))!=0)
				lprintf(LOG_ERR,"%04d !ERROR %d (%d) getting address/port of passive socket (%u)"
					,ctrl_sock,result,ERROR_VALUE,pasv_sock);
			else
				lprintf(LOG_DEBUG,"%04d PASV DATA socket %d listening on %s port %u"
					,ctrl_sock,pasv_sock,host_ip,inet_addrport(addr));
		}

		/* Setup for select() */
		tv.tv_sec=TIMEOUT_SOCKET_LISTEN;
		tv.tv_usec=0;

		FD_ZERO(&socket_set);
		FD_SET(pasv_sock,&socket_set);

#if defined(SOCKET_DEBUG_SELECT)
		socket_debug[ctrl_sock]|=SOCKET_DEBUG_SELECT;
#endif
		result=select(pasv_sock+1,&socket_set,NULL,NULL,&tv);
#if defined(SOCKET_DEBUG_SELECT)
		socket_debug[ctrl_sock]&=~SOCKET_DEBUG_SELECT;
#endif
		if(result<1) {
			lprintf(LOG_WARNING,"%04d !PASV select returned %d (error: %d)",ctrl_sock,result,ERROR_VALUE);
			sockprintf(ctrl_sock,ctrl_sess,"425 Error %d selecting socket for connection",ERROR_VALUE);
			if(tmpfile && !(startup->options&FTP_OPT_KEEP_TEMP_FILES))
				ftp_remove(ctrl_sock, __LINE__, filename);
			*inprogress=FALSE;
			return;
		}
			
		addr_len=sizeof(*addr);
#ifdef SOCKET_DEBUG_ACCEPT
		socket_debug[ctrl_sock]|=SOCKET_DEBUG_ACCEPT;
#endif
		*data_sock=accept(pasv_sock,&addr->addr,&addr_len);
#ifdef SOCKET_DEBUG_ACCEPT
		socket_debug[ctrl_sock]&=~SOCKET_DEBUG_ACCEPT;
#endif
		if(*data_sock==INVALID_SOCKET) {
			lprintf(LOG_WARNING,"%04d !PASV DATA ERROR %d accepting connection on socket %d"
				,ctrl_sock,ERROR_VALUE,pasv_sock);
			sockprintf(ctrl_sock,ctrl_sess,"425 Error %d accepting connection",ERROR_VALUE);
			if(tmpfile && !(startup->options&FTP_OPT_KEEP_TEMP_FILES))
				ftp_remove(ctrl_sock, __LINE__, filename);
			*inprogress=FALSE;
			return;
		}
		if(startup->socket_open!=NULL)
			startup->socket_open(startup->cbdata,TRUE);
		if(startup->options&FTP_OPT_DEBUG_DATA)
			lprintf(LOG_DEBUG,"%04d PASV DATA socket %d connected to %s port %u"
				,ctrl_sock,*data_sock,host_ip,inet_addrport(addr));
		if (protected) {
			if (start_tls(data_sock, data_sess, FALSE) || *data_sess == -1) {
				lprintf(LOG_WARNING,"%04d !PASV ERROR starting TLS", pasv_sock);
				sockprintf(ctrl_sock,ctrl_sess,"425 Error negotiating TLS", ERROR_VALUE);
				if(tmpfile && !(startup->options&FTP_OPT_KEEP_TEMP_FILES))
					ftp_remove(ctrl_sock, __LINE__, filename);
				*inprogress=FALSE;
				return;
			}
		}
	}

	do {

		l=1;

		if(ioctlsocket(*data_sock, FIONBIO, &l)!=0) {
			lprintf(LOG_ERR,"%04d !DATA ERROR %d disabling socket blocking"
				,ctrl_sock, ERROR_VALUE);
			sockprintf(ctrl_sock,ctrl_sess,"425 Error %d disabling socket blocking"
				,ERROR_VALUE);
			break;
		}

		if((xfer=malloc(sizeof(xfer_t)))==NULL) {
			lprintf(LOG_CRIT,"%04d !MALLOC FAILURE LINE %d",ctrl_sock,__LINE__);
			sockprintf(ctrl_sock,ctrl_sess,"425 MALLOC FAILURE");
			break;
		}
		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);
		protected_uint32_adjust(&thread_count,1);
		if(receiving)
			result=_beginthread(receive_thread,0,(void*)xfer);
		else
			result=_beginthread(send_thread,0,(void*)xfer);

		if(result!=-1)
			return;	/* success */

	} while(0);

	/* failure */
	if(tmpfile && !(startup->options&FTP_OPT_KEEP_TEMP_FILES))
		ftp_remove(ctrl_sock, __LINE__, filename);
	*inprogress=FALSE;
}

/* convert "user name" to "user.name" or "mr. user" to "mr._user" */
char* dotname(char* in, char* out)
{
	char	ch;
	int		i;

	if(strchr(in,'.')==NULL)
		ch='.';
	else
		ch='_';
	for(i=0;in[i];i++)
		if(in[i]<=' ')
			out[i]=ch;
		else
			out[i]=in[i];
	out[i]=0;
	return(out);
}

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;
}

/*
 * 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)
{
	int lib = *curlib;
	int dir = *curdir;
	char *p = *pp;
	char *fname = strchr(p, 0);
	int ret = 0;
	size_t len;

	if (*p == '/') {
		lib = -1;
		dir = -1;
		p++;
	}

	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)
			p++;
		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))
					continue;
				len = strlen(scfg.lib[lib]->sname);
				if (strlen(p) < len)
					continue;
				if (p[len] != 0 && p[len] != '/')
					continue;
				if(!strnicmp(scfg.lib[lib]->sname,p,len)) {
					p += len;
					if (*p)
						p++;
					break;
				}
			}
			if (lib == scfg.total_libs) {
				ret = -1;
				lib = -1;
				if (strchr(p, '/') != NULL) {
					p = strchr(p, '/');
					p++;
				}
				else
					p = strchr(p, 0);
			}
		}
		else if (dir < 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]->code_suffix);
				if (strlen(p) < len)
					continue;
				if (p[len] != 0 && p[len] != '/')
					continue;
				if(!strnicmp(scfg.dir[dir]->code_suffix,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);
			}
		}
		else {	// Filename
			if (strchr(p, '/') != NULL) {
				ret = -1;
				p = strchr(p, '/');
				p++;
			}
			else {
				fname = p;
				p += strlen(fname);
			}
		}
	}
	*curdir = dir;
	*curlib = lib;
	*pp = fname;
	return ret;
}

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;

	sprintf(aliasfile,"%sftpalias.cfg",scfg.ctrl_dir);
	if((fp=fopen(aliasfile,"r"))==NULL) 
		return(FALSE);

	SAFECOPY(alias,fullalias);
	p=strrchr(alias+1,'/');
	if(p) {
		*p=0;
		fname=p+1;
	}

	if(filename==NULL /* directory */ && *fname /* filename specified */) {
		fclose(fp);
		return(FALSE);
	}

	while(!feof(fp)) {
		if(!fgets(line,sizeof(line),fp))
			break;

		p=line;	/* alias */
		SKIP_WHITESPACE(p);
		if(*p==';')	/* comment */
			continue;

		tp=p;		/* terminator */
		FIND_WHITESPACE(tp);
		if(*tp) *tp=0;

		if(stricmp(p,alias))	/* Not a match */
			continue;

		p=tp+1;		/* filename */
		SKIP_WHITESPACE(p);

		tp=p;		/* terminator */
		FIND_WHITESPACE(tp);
		if(*tp) *tp=0;

		if(!strnicmp(p,BBS_VIRTUAL_PATH,strlen(BBS_VIRTUAL_PATH))) {
			if((dir=getdir(p+strlen(BBS_VIRTUAL_PATH),user,client))<0)	{
				lprintf(LOG_WARNING,"0000 !Invalid virtual path (%s) for %s",p,user->alias);
				/* invalid or no access */
				continue;
			}
			p=strrchr(p,'/');
			if(p!=NULL) p++;
			if(p!=NULL && filename!=NULL) {
				if(*p)
					sprintf(filename,"%s%s",scfg.dir[dir]->path,p);
				else
					sprintf(filename,"%s%s",scfg.dir[dir]->path,fname);
			}
		} else if(filename!=NULL)
			strcpy(filename,p);

		result=TRUE;	/* success */
		break;
	}
	fclose(fp);
	if(curdir!=NULL)
		*curdir=dir;
	return(result);
}

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;

	return(root);
}

char* genvpath(int lib, int dir, char* str)
{
	strcpy(str,"/");
	if(lib<0)
		return(str);
	strcat(str,scfg.lib[lib]->sname);
	strcat(str,"/");
	if(dir<0)
		return(str);
	strcat(str,scfg.dir[dir]->code_suffix);
	strcat(str,"/");
	return(str);
}

void ftp_printfile(SOCKET sock, CRYPT_SESSION sess, const char* name, unsigned code)
{
	char	path[MAX_PATH+1];
	char	buf[512];
	FILE*	fp;
	unsigned i;

	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))
				break;
			truncsp(buf);
			if(!i)
				sockprintf(sock,sess,"%u-%s",code,buf);
			else
				sockprintf(sock,sess," %s",buf);
			i++;
		}
		fclose(fp);
	}
}

static BOOL ftp_hacklog(char* prot, char* user, char* text, char* host, union xp_sockaddr* addr)
{
#ifdef _WIN32
	if(startup->hack_sound[0] && !(startup->options&FTP_OPT_MUTE)) 
		PlaySound(startup->hack_sound, NULL, SND_ASYNC|SND_FILENAME);
#endif

	return hacklog(&scfg, 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, char* host, union xp_sockaddr* addr)
{
	ulong count;
	char	host_ip[INET6_ADDRSTRLEN];

	if(addr!=NULL) {
		count=loginFailure(startup->login_attempt_list, addr, "FTP", user, passwd);
		if(startup->login_attempt.hack_threshold && count>=startup->login_attempt.hack_threshold)
			ftp_hacklog("FTP LOGIN", user, passwd, host, addr);
		if(startup->login_attempt.filter_threshold && count>=startup->login_attempt.filter_threshold) {
			inet_addrtop(addr, host_ip, sizeof(host_ip));
			filter_ip(&scfg, "FTP", "- TOO MANY CONSECUTIVE FAILED LOGIN ATTEMPTS"
				,host, host_ip, user, /* fname: */NULL);
		}
		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.");
		return(TRUE);
	}
	ftp_printfile(sock,sess,"badlogin",530);
	sockprintf(sock,sess,"530 Invalid login.");
	return(FALSE);
}

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(),clock(),ext);
	return(fname);
}

static BOOL send_mlsx(FILE *fp, SOCKET sock, CRYPT_SESSION sess, const char *format, ...)
{
	va_list va;
	char *str;

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

static BOOL send_mlsx_entry(FILE *fp, SOCKET sock, CRYPT_SESSION sess, unsigned feats, const char *type, const char *perm, uint64_t size, uint64_t modify, const char *owner, const char *fname)
{
	char line[1024];
	char *end;
	BOOL need_owner = FALSE;

	end=line;
	*end=0;
	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 != UINT64_MAX && (feats & MLSX_MODIFY))
		end += sprintf(end, "Modify=%" PRIu64 ";", modify);
	// 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);
}

static BOOL write_local_mlsx(FILE *fp, SOCKET sock, CRYPT_SESSION sess, unsigned feats, const char *path)
{
	const char *type;
	char permstr[11];
	char *p;
	BOOL is_file = FALSE;

	if (!strcmp(path, "."))
		type="cdir";
	else if (!strcmp(path, ".."))
		type="pdir";
	else if (isdir(path))
		type="dir";
	else {
		is_file = TRUE;
		type="file";
	}
	// 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')
			*(p++)='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';
		}
	}
	*p=0;
	return send_mlsx_entry(fp, sock, sess, feats, type, permstr, (uint64_t)flength(path), (uint64_t)fdate(path), NULL, 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
	}
	*p=0;
}

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)
		return TRUE;
	if (chk_ar(&scfg, dir->ul_ar,user,client))
		return TRUE;
	if ((user->exempt & FLAG('U')))
		return TRUE;
	return FALSE;
}

static BOOL can_delete_files(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('D'))
		return FALSE;
	if (!chk_ar(&scfg,dir->ar,user,client))
		return FALSE;
	if (dir_op(&scfg,user,client,dir->dirnum))
		return TRUE;
	if (user->exempt&FLAG('R'))
		return TRUE;
	return FALSE;
}

static void get_dirperm(lib_t *lib, dir_t *dir, user_t *user, client_t *client, char *permstr)
{
	char *p = permstr;

	//*(p++) = 'a';	// File may be appended to
	if (can_upload(lib, dir, user, client))
		*(p++) = 'c';	// Files may be created in dir
	//*(p++) = 'd';	// Item may be depeted (dir or file)
	if (can_list(lib, dir, user, client)) {
		*(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
	if (can_delete_files(lib, dir, user, client))
		*(p++) = 'p';	// Files/Dirs in directory may be deleted
	//*(p++) = 'r';	// File may be retrieved
	//*(p++) = 'w';	// File may be overwritten
	*p=0;
}

static BOOL can_append(lib_t *lib, dir_t *dir, user_t *user, client_t *client, file_t *file)
{
	if (!chk_ar(&scfg,lib->ar,user,client))
		return FALSE;
	if (user->rest&FLAG('U'))
		return FALSE;
	if (dir->dirnum != scfg.sysop_dir && dir->dirnum != scfg.upload_dir && !chk_ar(&scfg,dir->ar,user,client))
		return FALSE;
	if(!dir_op(&scfg,user,client,dir->dirnum) && !(user->exempt&FLAG('U'))) {
		if(!chk_ar(&scfg,dir->ul_ar,user,client))
			return FALSE;
	}
	if(!getfileixb(&scfg,file) || !getfiledat(&scfg,file))
		return FALSE;
	if (stricmp(file->uler,user->alias))
		return FALSE;
	// Check credits?
	return TRUE;
}

static BOOL can_delete(lib_t *lib, dir_t *dir, user_t *user, client_t *client, file_t *file)
{
	if (user->rest&FLAG('D'))
		return FALSE;
	if (!chk_ar(&scfg,lib->ar,user,client))
		return FALSE;
	if (!chk_ar(&scfg,dir->ar,user,client))
		return FALSE;
	if (!dir_op(&scfg, user, client, dir->dirnum))
		return FALSE;
	if (!(user->exempt&FLAG('R')))
		return FALSE;
	if(!getfileixb(&scfg,file) && !(startup->options&FTP_OPT_DIR_FILES) && !(dir->misc&DIR_FILES))
		return FALSE;
	return TRUE;
}

static BOOL can_download(lib_t *lib, dir_t *dir, user_t *user, client_t *client, file_t *file)
{
	if (user->rest&FLAG('D'))
		return FALSE;
	if (!chk_ar(&scfg,lib->ar,user,client))
		return FALSE;
	if (!chk_ar(&scfg,dir->ar,user,client))
		return FALSE;
	if (!chk_ar(&scfg,dir->dl_ar,user,client))
		return FALSE;
	if(!getfileixb(&scfg,file) && !(startup->options&FTP_OPT_DIR_FILES) && !(dir->misc&DIR_FILES))
		return FALSE;
	// TODO: Verify credits
	return TRUE;
}

static void get_fileperm(lib_t *lib, dir_t *dir, user_t *user, client_t *client, file_t *file, char *permstr)
{
	char *p = permstr;

	if (can_append(lib, dir, user, client, file))
		*(p++) = 'a';	// File may be appended to
	//*(p++) = 'c';	// Files may be created in dir
	if (can_delete(lib, dir, user, client, file))
		*(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
	if (can_download(lib, dir, user, client, file))
		*(p++) = 'r';	// File may be retrieved
	//*(p++) = 'w';	// File may be overwritten
	*p = 0;
}

static void get_owner_name(file_t *file, char *namestr)
{
	char *p;

	if (file) {
		if (file->misc & FM_ANON)
			strcpy(namestr, ANONYMOUS);
		else
			strcpy(namestr, file->uler);
	}
	else
		strcpy(namestr, scfg.sys_id);

	// Now ensure it's an RCHAR string.
	for (p=namestr; *p; p++) {
		if (*p >= '!' && *p <= ')')
			continue;
		else if (*p >= '+' && *p <= ':')
			continue;
		else if (*p >= '?' && *p <= 'Z')
			continue;
		else if (*p == '\\')
			continue;
		else if (*p == '^')
			continue;
		else if (*p == '_')
			continue;
		else if (*p >= 'a' && *p <= 'z')
			continue;
		else if (*p == ' ')
			*p = '.';
		else
			*p = '_';
	}
}

static void ctrl_thread(void* arg)
{
	unsigned	mlsx_feats = (MLSX_TYPE | MLSX_PERM | MLSX_SIZE | MLSX_MODIFY | MLSX_OWNER);
	char		buf[512];
	char		str[128];
	char*		cmd;
	char*		p;
	char*		np;
	char*		tp;
	char*		dp;
	char*		ap;
	char*		filespec;
	char*		mode="active";
	char		old_char;
	char		password[64];
	char		fname[MAX_PATH+1];
	char		qwkfile[MAX_PATH+1];
	char		aliasfile[MAX_PATH+1];
	char		mls_path[MAX_PATH+1];
	char		*mls_fname;
	char		permstr[11];
	char		aliasline[512];
	char		desc[501]="";
	char		sys_pass[128];
	char		host_name[256];
	char		host_ip[INET6_ADDRSTRLEN];
	char		data_ip[INET6_ADDRSTRLEN];
	uint16_t	data_port;
	char		path[MAX_PATH+1];
	char		local_dir[MAX_PATH+1];
	char		ren_from[MAX_PATH+1]="";
	char		html_index_ext[MAX_PATH+1];
	WORD		port;
	uint32_t	ip_addr;
	socklen_t	addr_len;
	unsigned	h1,h2,h3,h4;
	u_short		p1,p2;	/* For PORT command */
	int			i;
	int			rd;
	int			result;
	int			lib;
	int			dir;
	int			curlib=-1;
	int			curdir=-1;
	int			orglib;
	int			orgdir;
	long		filepos=0L;
	long		timeleft;
	ulong		l;
	ulong		login_attempts=0;
	ulong		avail;	/* disk space */
	ulong		count;
	BOOL		detail;
	BOOL		success;
	BOOL		getdate;
	BOOL		getsize;
	BOOL		delecmd;
	BOOL		delfile;
	BOOL		tmpfile;
	BOOL		credits;
	BOOL		filedat=FALSE;
	BOOL		transfer_inprogress;
	BOOL		transfer_aborted;
	BOOL		sysop=FALSE;
	BOOL		local_fsys=FALSE;
	BOOL		alias_dir;
	BOOL		append;
	BOOL		reuseaddr;
	FILE*		fp;
	FILE*		alias_fp;
	SOCKET		sock;
	SOCKET		tmp_sock;
	SOCKET		pasv_sock=INVALID_SOCKET;
	CRYPT_SESSION	pasv_sess=-1;
	SOCKET		data_sock=INVALID_SOCKET;
	CRYPT_SESSION	data_sess=-1;
	HOSTENT*	host;
	union xp_sockaddr	addr;
	union xp_sockaddr	data_addr;
	union xp_sockaddr	pasv_addr;
	ftp_t		ftp=*(ftp_t*)arg;
	user_t		user;
	time_t		t;
	time_t		now;
	time_t		logintime=0;
	time_t		lastactive;
	time_t		file_date;
	file_t		f;
	glob_t		g;
	node_t		node;
	client_t	client;
	struct tm	tm;
	struct tm 	cur_tm;
#ifdef JAVASCRIPT
	jsval		js_val;
	JSRuntime*	js_runtime=NULL;
	JSContext*	js_cx=NULL;
	JSObject*	js_glob;
	JSObject*	js_ftp;
	JSString*	js_str;
	js_callback_t	js_callback;
#endif
	login_attempt_t attempted;
	CRYPT_SESSION	sess = -1;
	BOOL		got_pbsz = FALSE;
	BOOL		protection = FALSE;

	SetThreadName("sbbs/ftpControl");
	thread_up(TRUE /* setuid */);

	lastactive=time(NULL);

	sock=ftp.socket;
	memcpy(&data_addr, &ftp.client_addr, ftp.client_addr_len);
	/* Default data port is ctrl port-1 */
	data_port = inet_addrport(&data_addr)-1;
	
	lprintf(LOG_DEBUG,"%04d CTRL thread started", sock);

	free(arg);

#ifdef _WIN32
	if(startup->answer_sound[0] && !(startup->options&FTP_OPT_MUTE)) 
		PlaySound(startup->answer_sound, NULL, SND_ASYNC|SND_FILENAME);
#endif

	transfer_inprogress = FALSE;
	transfer_aborted = FALSE;

	l=1;

	if((i=ioctlsocket(sock, FIONBIO, &l))!=0) {
		lprintf(LOG_ERR,"%04d !ERROR %d (%d) disabling socket blocking"
			,sock, i, ERROR_VALUE);
		sockprintf(sock,sess,"425 Error %d disabling socket blocking"
			,ERROR_VALUE);
		ftp_close_socket(&sock,&sess,__LINE__);
		thread_down();
		return;
	}

	memset(&user,0,sizeof(user));

	inet_addrtop(&ftp.client_addr, host_ip, sizeof(host_ip));

	lprintf(LOG_INFO,"%04d CTRL connection accepted from: %s port %u"
		,sock, host_ip, inet_addrport(&ftp.client_addr));

	if(startup->options&FTP_OPT_NO_HOST_LOOKUP)
		strcpy(host_name,"<no name>");
	else {
		if(getnameinfo(&ftp.client_addr.addr, sizeof(ftp.client_addr), host_name, sizeof(host_name), NULL, 0, NI_NAMEREQD)!=0)
			strcpy(host_name,"<no name>");
	}

	if(!(startup->options&FTP_OPT_NO_HOST_LOOKUP))
		lprintf(LOG_INFO,"%04d Hostname: %s", sock, host_name);

	ulong banned = loginBanned(&scfg, startup->login_attempt_list, sock, host_name, startup->login_attempt, &attempted);
	if(banned || trashcan(&scfg,host_ip,"ip")) {
		if(banned) {
			char ban_duration[128];
			lprintf(LOG_NOTICE, "%04d !TEMPORARY BAN of %s (%u login attempts, last: %s) - remaining: %s"
				,sock, host_ip, attempted.count-attempted.dupes, attempted.user, seconds_to_str(banned, ban_duration));
		} else
			lprintf(LOG_NOTICE,"%04d !CLIENT BLOCKED in ip.can: %s", sock, host_ip);
		sockprintf(sock,sess,"550 Access denied.");
		ftp_close_socket(&sock,&sess,__LINE__);
		thread_down();
		return;
	}

	if(trashcan(&scfg,host_name,"host")) {
		lprintf(LOG_NOTICE,"%04d !CLIENT BLOCKED in host.can: %s", sock, host_name);
		sockprintf(sock,sess,"550 Access denied.");
		ftp_close_socket(&sock,&sess,__LINE__);
		thread_down();
		return;
	}

	/* For PASV mode */
	addr_len=sizeof(pasv_addr);
	if((result=getsockname(sock, &pasv_addr.addr,&addr_len))!=0) {
		lprintf(LOG_ERR,"%04d !ERROR %d (%d) getting address/port", sock, result, ERROR_VALUE);
		sockprintf(sock,sess,"425 Error %d getting address/port",ERROR_VALUE);
		ftp_close_socket(&sock,&sess,__LINE__);
		thread_down();
		return;
	} 

	protected_uint32_adjust(&active_clients, 1), 
	update_clients();

	/* 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(&ftp.client_addr);
	client.protocol="FTP";
	client.user=STR_UNKNOWN_USER;
	client_on(sock,&client,FALSE /* update */);

	if(startup->login_attempt.throttle
		&& (login_attempts=loginAttempts(startup->login_attempt_list, &ftp.client_addr)) > 1) {
		lprintf(LOG_DEBUG,"%04d Throttling suspicious connection from: %s (%u login attempts)"
			,sock, host_ip, login_attempts);
		mswait(login_attempts*startup->login_attempt.throttle);
	}

	sockprintf(sock,sess,"220-%s (%s)",scfg.sys_name, startup->host_name);
	sockprintf(sock,sess," Synchronet FTP Server %s-%s Ready"
		,revision,PLATFORM_DESC);
	sprintf(str,"%sftplogin.txt",scfg.text_dir);
	if((fp=fopen(str,"rb"))!=NULL) {
		while(!feof(fp)) {
			if(!fgets(buf,sizeof(buf),fp))
				break;
			truncsp(buf);
			sockprintf(sock,sess," %s",buf);
		}
		fclose(fp);
	}
	sockprintf(sock,sess,"220 Please enter your user name.");

#ifdef SOCKET_DEBUG_CTRL
	socket_debug[sock]|=SOCKET_DEBUG_CTRL;
#endif
	while(1) {

#ifdef SOCKET_DEBUG_READLINE
		socket_debug[sock]|=SOCKET_DEBUG_READLINE;
#endif
		rd = sockreadline(sock, sess, buf, sizeof(buf), &lastactive);
#ifdef SOCKET_DEBUG_READLINE
		socket_debug[sock]&=~SOCKET_DEBUG_READLINE;
#endif
		if(rd<1) {
			if(transfer_inprogress==TRUE) {
				lprintf(LOG_WARNING,"%04d Aborting transfer due to receive error",sock);
				transfer_aborted=TRUE;
			}
			break;
		}
		truncsp(buf);
		lastactive=time(NULL);
		cmd=buf;
		while(((BYTE)*cmd)==TELNET_IAC) {
			cmd++;
			lprintf(LOG_DEBUG,"%04d RX%s: Telnet cmd: %s",sock,sess == -1 ? "" : "S", telnet_cmd_desc(*cmd));
			cmd++;
		}
		while(*cmd && *cmd<' ') {
			lprintf(LOG_DEBUG,"%04d RX%s: %d (0x%02X)",sock,sess == -1 ? "" : "S", (BYTE)*cmd,(BYTE)*cmd);
			cmd++;
		}
		if(!(*cmd))
			continue;
		if(startup->options&FTP_OPT_DEBUG_RX)
			lprintf(LOG_DEBUG,"%04d RX%s: %s", sock, sess == -1 ? "" : "S", cmd);
		if(!stricmp(cmd, "NOOP")) {
			sockprintf(sock,sess,"200 NOOP command successful.");
			continue;
		}
		if(!stricmp(cmd, "HELP SITE") || !stricmp(cmd, "SITE HELP")) {
			sockprintf(sock,sess,"214-The following SITE commands are recognized (* => unimplemented):");
			sockprintf(sock,sess," HELP    VER     WHO     UPTIME");
			if(user.level>=SYSOP_LEVEL)
				sockprintf(sock,sess,
							" RECYCLE [ALL]");
			if(sysop)
				sockprintf(sock,sess,
							" EXEC <cmd>");
			sockprintf(sock,sess,"214 Direct comments to sysop@%s.",scfg.sys_inetaddr);
			continue;
		}
		if(!strnicmp(cmd, "HELP",4)) {
			sockprintf(sock,sess,"214-The following commands are recognized (* => unimplemented, # => extension):");
			sockprintf(sock,sess," USER    PASS    CWD     XCWD    CDUP    XCUP    PWD     XPWD");
			sockprintf(sock,sess," QUIT    REIN    PORT    PASV    LIST    NLST    NOOP    HELP");
			sockprintf(sock,sess," SIZE    MDTM    RETR    STOR    REST    ALLO    ABOR    SYST");
			sockprintf(sock,sess," TYPE    STRU    MODE    SITE    RNFR*   RNTO*   DELE*   DESC#");
			sockprintf(sock,sess," FEAT#   OPTS#   EPRT    EPSV    AUTH#   PBSZ#   PROT#   CCC#");
			sockprintf(sock,sess," MLSD#");
			sockprintf(sock,sess,"214 Direct comments to sysop@%s.",scfg.sys_inetaddr);
			continue;
		}
		if(!stricmp(cmd, "FEAT")) {
			sockprintf(sock,sess,"211-The following additional (post-RFC949) features are supported:");
			sockprintf(sock,sess," DESC");
			sockprintf(sock,sess," MDTM");
			sockprintf(sock,sess," SIZE");
			sockprintf(sock,sess," REST STREAM");
			sockprintf(sock,sess," AUTH TLS");
			sockprintf(sock,sess," PBSZ");
			sockprintf(sock,sess," PROT");
			sockprintf(sock,sess," MLST Type%s;Perm%s;Size%s;Modify%s;UNIX.ownername%s;",
				(mlsx_feats & MLSX_TYPE) ? "*" : "",
				(mlsx_feats & MLSX_PERM) ? "*" : "",
				(mlsx_feats & MLSX_SIZE) ? "*" : "",
				(mlsx_feats & MLSX_MODIFY) ? "*" : "",
				(mlsx_feats & MLSX_OWNER) ? "*" : ""
			);
			sockprintf(sock,sess," TVFS");
			sockprintf(sock,sess,"211 End");
			continue;
		}
		if(!strnicmp(cmd, "OPTS MLST",9)) {
			if (cmd[9] == 0) {
				mlsx_feats = 0;
				continue;
			}
			if (cmd[9] != ' ') {
				sockprintf(sock,sess,"501 Option not supported.");
				continue;
			}
			mlsx_feats = 0;
			for (p = cmd; *p; p++)
				*p = toupper(*p);
			if (strstr(cmd, "TYPE;"))
				mlsx_feats |= MLSX_TYPE;
			if (strstr(cmd, "PERM;"))
				mlsx_feats |= MLSX_PERM;
			if (strstr(cmd, "SIZE;"))
				mlsx_feats |= MLSX_SIZE;
			if (strstr(cmd, "MODIFY;"))
				mlsx_feats |= MLSX_MODIFY;
			if (strstr(cmd, "UNIX.OWNERNAME;"))
				mlsx_feats |= MLSX_OWNER;
			sockprintf(sock,sess,"200 %s%s%s%s%s",
				(mlsx_feats & MLSX_TYPE) ? "Type;" : "",
				(mlsx_feats & MLSX_PERM) ? "Perm;" : "",
				(mlsx_feats & MLSX_SIZE) ? "Size;" : "",
				(mlsx_feats & MLSX_MODIFY) ? "Modify;" : "",
				(mlsx_feats & MLSX_OWNER) ? "UNIX.ownername;" : ""
			);
			continue;
		}
		if(!strnicmp(cmd, "OPTS",4)) {
			sockprintf(sock,sess,"501 Option not supported.");
			continue;
		}
		if(!stricmp(cmd, "QUIT")) {
			ftp_printfile(sock,sess,"bye",221);
			sockprintf(sock,sess,"221 Goodbye. Closing control connection.");
			break;
		}
		if(!strnicmp(cmd, "USER ",5)) {
			sysop=FALSE;
			user.number=0;
			p=cmd+5;
			SKIP_WHITESPACE(p);
			truncsp(p);
			SAFECOPY(user.alias,p);
			user.number=matchuser(&scfg,user.alias,FALSE /*sysop_alias*/);
			if(!user.number && (stricmp(user.alias,"anonymous") == 0 || stricmp(user.alias, "ftp") == 0))
				user.number=matchuser(&scfg,"guest",FALSE);
			if(user.number && getuserdat(&scfg, &user)==0 && user.pass[0]==0) 
				sockprintf(sock,sess,"331 User name okay, give your full e-mail address as password.");
			else
				sockprintf(sock,sess,"331 User name okay, need password.");
			user.number=0;
			continue;
		}
		if(!strnicmp(cmd, "PASS ",5) && user.alias[0]) {
			user.number=0;
			p=cmd+5;
			SKIP_WHITESPACE(p);

			SAFECOPY(password,p);
			user.number=matchuser(&scfg,user.alias,FALSE /*sysop_alias*/);
			if(!user.number) {
				if(scfg.sys_misc&SM_ECHO_PW)
					lprintf(LOG_WARNING,"%04d !UNKNOWN USER: '%s' (password: %s)",sock,user.alias,p);
				else
					lprintf(LOG_WARNING,"%04d !UNKNOWN USER: '%s'",sock,user.alias);
				if(badlogin(sock, sess, &login_attempts, user.alias, p, host_name, &ftp.client_addr))
					break;
				continue;
			}
			if((i=getuserdat(&scfg, &user))!=0) {
				lprintf(LOG_ERR,"%04d !ERROR %d getting data for user #%d (%s)"
					,sock,i,user.number,user.alias);
				sockprintf(sock,sess,"530 Database error %d",i);
				user.number=0;
				continue;
			}
			if(user.misc&(DELETED|INACTIVE)) {
				lprintf(LOG_WARNING,"%04d !DELETED or INACTIVE user #%d (%s)"
					,sock,user.number,user.alias);
				user.number=0;
				if(badlogin(sock, sess, &login_attempts, NULL, NULL, NULL, NULL))
					break;
				continue;
			}
			if(user.rest&FLAG('T')) {
				lprintf(LOG_WARNING,"%04d !T RESTRICTED user #%d (%s)"
					,sock,user.number,user.alias);
				user.number=0;
				if(badlogin(sock, sess, &login_attempts, NULL, NULL, NULL, NULL))
					break;
				continue;
			}
			if(user.ltoday>=scfg.level_callsperday[user.level]
				&& !(user.exempt&FLAG('L'))) {
				lprintf(LOG_WARNING,"%04d !MAXIMUM LOGONS (%d) reached for %s"
					,sock,scfg.level_callsperday[user.level],user.alias);
				sockprintf(sock,sess,"530 Maximum logons per day reached.");
				user.number=0;
				continue;
			}
			if(user.rest&FLAG('L') && user.ltoday>=1) {
				lprintf(LOG_WARNING,"%04d !L RESTRICTED user #%d (%s) already on today"
					,sock,user.number,user.alias);
				sockprintf(sock,sess,"530 Maximum logons per day reached.");
				user.number=0;
				continue;
			}

			SAFEPRINTF2(sys_pass,"%s:%s",user.pass,scfg.sys_pass);
			if(!user.pass[0]) {	/* Guest/Anonymous */
				if(trashcan(&scfg,password,"email")) {
					lprintf(LOG_NOTICE,"%04d !BLOCKED e-mail address: %s",sock,password);
					user.number=0;
					if(badlogin(sock, sess, &login_attempts, NULL, NULL, NULL, NULL))
						break;
					continue;
				}
				lprintf(LOG_INFO,"%04d %s: <%s>",sock,user.alias,password);
				putuserrec(&scfg,user.number,U_NETMAIL,LEN_NETMAIL,password);
			}
			else if(user.level>=SYSOP_LEVEL && !stricmp(password,sys_pass)) {
				lprintf(LOG_INFO,"%04d Sysop access granted to %s", sock, user.alias);
				sysop=TRUE;
			}
			else if(stricmp(password,user.pass)) {
				if(scfg.sys_misc&SM_ECHO_PW)
					lprintf(LOG_WARNING,"%04d !FAILED Password attempt for user %s: '%s' expected '%s'"
						,sock, user.alias, password, user.pass);
				else
					lprintf(LOG_WARNING,"%04d !FAILED Password attempt for user %s"
						,sock, user.alias);
				user.number=0;
				if(badlogin(sock, sess, &login_attempts, user.alias, password, host_name, &ftp.client_addr))
					break;
				continue;
			}

			/* Update client display */
			if(user.pass[0]) {
				client.user=user.alias;
				loginSuccess(startup->login_attempt_list, &ftp.client_addr);
			} else {	/* anonymous */
				sprintf(str,"%s <%.32s>",user.alias,password);
				client.user=str;
			}
			client_on(sock,&client,TRUE /* update */);

			lprintf(LOG_INFO,"%04d %s logged in (%u today, %u total)"
				,sock,user.alias,user.ltoday+1, user.logons+1);
			logintime=time(NULL);
			timeleft=(long)gettimeleft(&scfg,&user,logintime);
			ftp_printfile(sock,sess,"hello",230);

#ifdef JAVASCRIPT
#ifdef JS_CX_PER_SESSION
			if(js_cx!=NULL) {

				if(js_CreateUserClass(js_cx, js_glob, &scfg)==NULL) 
					lprintf(LOG_ERR,"%04d !JavaScript ERROR creating user class",sock);

				if(js_CreateUserObject(js_cx, js_glob, &scfg, "user", user.number, &client)==NULL) 
					lprintf(LOG_ERR,"%04d !JavaScript ERROR creating user object",sock);

				if(js_CreateClientObject(js_cx, js_glob, "client", &client, sock, -1)==NULL) 
					lprintf(LOG_ERR,"%04d !JavaScript ERROR creating client object",sock);

				if(js_CreateFileAreaObject(js_cx, js_glob, &scfg, &user
					,startup->html_index_file)==NULL) 
					lprintf(LOG_ERR,"%04d !JavaScript ERROR creating file area object",sock);

			}
#endif
#endif

			if(sysop)
				sockprintf(sock,sess,"230-Sysop access granted.");
			sockprintf(sock,sess,"230-%s logged in.",user.alias);
			if(!(user.exempt&FLAG('D')) && (user.cdt+user.freecdt)>0)
				sockprintf(sock,sess,"230-You have %lu download credits."
					,user.cdt+user.freecdt);
			sockprintf(sock,sess,"230 You are allowed %lu minutes of use for this session."
				,timeleft/60);
			sprintf(qwkfile,"%sfile/%04d.qwk",scfg.data_dir,user.number);

			/* Adjust User Total Logons/Logons Today */
			user.logons++;
			user.ltoday++;
			SAFECOPY(user.modem,"FTP");
			SAFECOPY(user.comp,host_name);
			SAFECOPY(user.ipaddr,host_ip);
			user.logontime=(time32_t)logintime;
			putuserdat(&scfg, &user);

			continue;
		}
		if (!strnicmp(cmd, "AUTH ", 5)) {
			if(!stricmp(cmd, "AUTH TLS")) {
				if (sess != -1) {
					sockprintf(sock,sess,"534 Already in TLS mode");
					continue;
				}
				if (start_tls(&sock, &sess, TRUE))
					break;
				user.number=0;
				sysop=FALSE;
				filepos=0;
				got_pbsz = FALSE;
				protection = FALSE;
				continue;
			}
			sockprintf(sock,sess,"504 TLS is the only AUTH supported");
			continue;
		}
		if (!strnicmp(cmd, "PBSZ ", 5)) {
			if(!stricmp(cmd, "PBSZ 0") && sess != -1) {
				got_pbsz = TRUE;
				sockprintf(sock,sess,"200 OK");
				continue;
			}
			if (sess == -1) {
				sockprintf(sock,sess,"503 Need AUTH TLS first");
				continue;
			}
			if (strspn(cmd+5, "0123456789") == strlen(cmd+5)) {
				sockprintf(sock,sess,"200 PBSZ=0");
				continue;
			}
			sockprintf(sock,sess,"501 Unable to parse buffer size");
			continue;
		}
		if (!strnicmp(cmd, "PROT ", 5)) {
			if (sess == -1) {
				sockprintf(sock,sess,"503 No AUTH yet");
				continue;
			}
			if(!strnicmp(cmd, "PROT P",6) && sess != -1 && got_pbsz) {
				protection = TRUE;
				sockprintf(sock,sess,"200 Accepted");
				continue;
			}
			if(!strnicmp(cmd, "PROT C",6) && sess != -1 && got_pbsz) {
				protection = FALSE;
				sockprintf(sock,sess,"200 Accepted");
				continue;
			}
			sockprintf(sock,sess,"536 Only C and P are supported in TLS mode");
			continue;
		}
		if(!stricmp(cmd, "CCC")) {
			if (sess == -1) {
				sockprintf(sock,sess,"533 Not in TLS mode");
				continue;
			}
			sockprintf(sock,sess,"200 Accepted");
			cryptDestroySession(sess);
			sess = -1;
			continue;
		}

		if(!user.number) {
			sockprintf(sock,sess,"530 Please login with USER and PASS.");
			continue;
		}

		if(!(user.rest&FLAG('G')))
			getuserdat(&scfg, &user);	/* get current user data */

		if((timeleft=(long)gettimeleft(&scfg,&user,logintime))<1L) {
			sockprintf(sock,sess,"421 Sorry, you've run out of time.");
			lprintf(LOG_WARNING,"%04d Out of time, disconnecting",sock);
			break;
		}

		/********************************/
		/* These commands require login */
		/********************************/

		if(!stricmp(cmd, "REIN")) {
			lprintf(LOG_INFO,"%04d %s reinitialized control session",sock,user.alias);
			user.number=0;
			sysop=FALSE;
			filepos=0;
			sockprintf(sock,sess,"220 Control session re-initialized. Ready for re-login.");
			if (sess != -1) {
				cryptDestroySession(sess);
				sess = -1;
			}
			got_pbsz = FALSE;
			protection = FALSE;
			continue;
		}

		if(!stricmp(cmd, "SITE WHO")) {
			sockprintf(sock,sess,"211-Active Telnet Nodes:");
			for(i=0;i<scfg.sys_nodes && i<scfg.sys_lastnode;i++) {
				if((result=getnodedat(&scfg, i+1, &node, 0))!=0) {
					sockprintf(sock,sess," Error %d getting data for Telnet Node %d",result,i+1);
					continue;
				}
				if(node.status==NODE_INUSE)
					sockprintf(sock,sess," Node %3d: %s",i+1, username(&scfg,node.useron,str));
			}
			sockprintf(sock,sess,"211 End (%d active FTP clients)", protected_uint32_value(active_clients));
			continue;
		}
		if(!stricmp(cmd, "SITE VER")) {
			sockprintf(sock,sess,"211 %s",ftp_ver());
			continue;
		}
		if(!stricmp(cmd, "SITE UPTIME")) {
			sockprintf(sock,sess,"211 %s (%lu served)",sectostr((uint)(time(NULL)-uptime),str),served);
			continue;
		}
		if(!stricmp(cmd, "SITE RECYCLE") && user.level>=SYSOP_LEVEL) {
			startup->recycle_now=TRUE;
			sockprintf(sock,sess,"211 server will recycle when not in-use");
			continue;
		}
		if(!stricmp(cmd, "SITE RECYCLE ALL") && user.level>=SYSOP_LEVEL) {
			refresh_cfg(&scfg);
			sockprintf(sock,sess,"211 ALL servers/nodes will recycle when not in-use");
			continue;
		}
		if(!strnicmp(cmd,"SITE EXEC ",10) && sysop) {
			p=cmd+10;
			SKIP_WHITESPACE(p);
#ifdef __unix__
			fp=popen(p,"r");
			if(fp==NULL)
				sockprintf(sock,sess,"500 Error %d opening pipe to: %s",errno,p);
			else {
				while(!feof(fp)) {
					if(fgets(str,sizeof(str),fp)==NULL)
						break;
					sockprintf(sock,sess,"200-%s",str);
				}
				sockprintf(sock,sess,"200 %s returned %d",p,pclose(fp));
			}
#else
			sockprintf(sock,sess,"200 system(%s) returned %d",p,system(p));
#endif
			continue;
		}


#ifdef SOCKET_DEBUG_CTRL
		if(!stricmp(cmd, "SITE DEBUG")) {
			sockprintf(sock,sess,"211-Debug");
			for(i=0;i<sizeof(socket_debug);i++) 
				if(socket_debug[i]!=0)
					sockprintf(sock,sess,"211-socket %d = 0x%X",i,socket_debug[i]);
			sockprintf(sock,sess,"211 End");
			continue;
		}
#endif

		if(strnicmp(cmd, "PORT ",5)==0 || strnicmp(cmd, "EPRT ",5)==0 || strnicmp(cmd, "LPRT ",5)==0) {

			if(pasv_sock!=INVALID_SOCKET)  {
				ftp_close_socket(&pasv_sock,&pasv_sess,__LINE__);
			}

			p=cmd+5;
			SKIP_WHITESPACE(p);
			if(strnicmp(cmd, "PORT ",5)==0) {
				sscanf(p,"%u,%u,%u,%u,%hd,%hd",&h1,&h2,&h3,&h4,&p1,&p2);
				data_addr.in.sin_family=AF_INET;
				data_addr.in.sin_addr.s_addr=htonl((h1<<24)|(h2<<16)|(h3<<8)|h4);
				data_port = (p1<<8)|p2;
			} else if(strnicmp(cmd, "EPRT ", 5)==0) { /* EPRT */
				char	delim = *p;
				int		prot;
				char	addr_str[INET6_ADDRSTRLEN];

				memset(&data_addr, 0, sizeof(data_addr));
				if(*p)
					p++;
				prot=strtol(p,NULL,/* base: */10);
				switch(prot) {
					case 1:
						FIND_CHAR(p,delim);
						if(*p)
							p++;
						ap = p;
						FIND_CHAR(p,delim);
						old_char = *p;
						*p = 0;
						data_addr.in.sin_addr.s_addr=inet_addr(ap);
						*p = old_char;
						if (*p)
							p++;
						data_port=atoi(p);
						data_addr.in.sin_family=AF_INET;
						break;
					case 2:
						FIND_CHAR(p,delim);
						if(*p)
							p++;
						strncpy(addr_str, p, sizeof(addr_str));
						addr_str[sizeof(addr_str)-1]=0;
						tp=addr_str;
						FIND_CHAR(tp, delim);
						*tp=0;
						if(inet_ptoaddr(addr_str, &data_addr, sizeof(data_addr))==NULL) {
							lprintf(LOG_WARNING,"%04d Unable to parse IPv6 address %s",sock,addr_str);
							sockprintf(sock,sess,"522 Unable to parse IPv6 address (1)");
							continue;
						}
						FIND_CHAR(p,delim);
						if(*p)
							p++;
						data_port=atoi(p);
						data_addr.in6.sin6_family=AF_INET6;
						break;
					default:
						lprintf(LOG_WARNING,"%04d UNSUPPORTED protocol: %d", sock, prot);
						sockprintf(sock,sess,"522 Network protocol not supported, use (1)");
						continue;
				}
			}
			else {	/* LPRT */
				if(sscanf(p,"%u,%u",&h1, &h2)!=2) {
					lprintf(LOG_ERR, "Unable to parse LPRT %s", p);
					sockprintf(sock,sess, "521 Address family not supported");
					continue;
				}
				FIND_CHAR(p,',');
				if(*p)
					p++;
				FIND_CHAR(p,',');
				if(*p)
					p++;
				switch(h1) {
					case 4:	/* IPv4 */
						if(h2 != 4) {
							lprintf(LOG_ERR, "Unable to parse LPRT %s", p);
							sockprintf(sock,sess, "501 IPv4 Address is the wrong length");
							continue;
						}
						for(h1 = 0; h1 < h2; h1++) {
							((unsigned char *)(&data_addr.in.sin_addr))[h1]=atoi(p);
							FIND_CHAR(p,',');
							if(*p)
								p++;
						}
						if(atoi(p)!=2) {
							lprintf(LOG_ERR, "Unable to parse LPRT %s", p);
							sockprintf(sock,sess, "501 IPv4 Port is the wrong length");
							continue;
						}
						FIND_CHAR(p,',');
						if(*p)
							p++;
						for(h1 = 0; h1 < 2; h1++) {
							((unsigned char *)(&data_port))[1-h1]=atoi(p);
							FIND_CHAR(p,',');
							if(*p)
								p++;
						}
						data_addr.in.sin_family=AF_INET;
						break;
					case 6:	/* IPv6 */
						if(h2 != 16) {
							lprintf(LOG_ERR, "Unable to parse LPRT %s", p);
							sockprintf(sock,sess, "501 IPv6 Address is the wrong length");
							continue;
						}
						for(h1 = 0; h1 < h2; h1++) {
							((unsigned char *)(&data_addr.in6.sin6_addr))[h1]=atoi(p);
							FIND_CHAR(p,',');
							if(*p)
								p++;
						}
						if(atoi(p)!=2) {
							lprintf(LOG_ERR, "Unable to parse LPRT %s", p);
							sockprintf(sock,sess, "501 IPv6 Port is the wrong length");
							continue;
						}
						FIND_CHAR(p,',');
						if(*p)
							p++;
						for(h1 = 0; h1 < 2; h1++) {
							((unsigned char *)(&data_port))[1-h1]=atoi(p);
							FIND_CHAR(p,',');
							if(*p)
								p++;
						}
						data_addr.in6.sin6_family=AF_INET6;
						break;
					default:
						lprintf(LOG_ERR, "Unable to parse LPRT %s", p);
						sockprintf(sock,sess, "521 Address family not supported");
						continue;
				}
			}

			inet_addrtop(&data_addr, data_ip, sizeof(data_ip));
			if(data_port< IPPORT_RESERVED) {
				lprintf(LOG_WARNING,"%04d !SUSPECTED BOUNCE ATTACK ATTEMPT by %s to %s port %u"
					,sock,user.alias
					,data_ip,data_port);
				ftp_hacklog("FTP BOUNCE", user.alias, cmd, host_name, &ftp.client_addr);
				sockprintf(sock,sess,"504 Bad port number.");	
				continue; /* As recommended by RFC2577 */
			}
			inet_setaddrport(&data_addr, data_port);
			sockprintf(sock,sess,"200 PORT Command successful.");
			mode="active";
			continue;
		}

		if(stricmp(cmd, "PASV")==0 || stricmp(cmd, "P@SW")==0	/* Kludge required for SMC Barricade V1.2 */
			|| stricmp(cmd, "EPSV")==0 || strnicmp(cmd, "EPSV ", 5)==0 || stricmp(cmd, "LPSV")==0) {

			if(pasv_sock!=INVALID_SOCKET)
				ftp_close_socket(&pasv_sock,&pasv_sess,__LINE__);

			if((pasv_sock=ftp_open_socket(pasv_addr.addr.sa_family, SOCK_STREAM))==INVALID_SOCKET) {
				lprintf(LOG_WARNING,"%04d !PASV ERROR %d opening socket", sock,ERROR_VALUE);
				sockprintf(sock,sess,"425 Error %d opening PASV data socket", ERROR_VALUE);
				continue;
			}

			reuseaddr=FALSE;
			if((result=setsockopt(pasv_sock,SOL_SOCKET,SO_REUSEADDR,(char*)&reuseaddr,sizeof(reuseaddr)))!=0) {
				lprintf(LOG_WARNING,"%04d !PASV ERROR %d disabling REUSEADDR socket option"
					,sock,ERROR_VALUE);
				sockprintf(sock,sess,"425 Error %d disabling REUSEADDR socket option", ERROR_VALUE);
				continue;
			}

			if(startup->options&FTP_OPT_DEBUG_DATA)
				lprintf(LOG_DEBUG,"%04d PASV DATA socket %d opened",sock,pasv_sock);

			for(port=startup->pasv_port_low; port<=startup->pasv_port_high; port++) {

				if(startup->options&FTP_OPT_DEBUG_DATA)
					lprintf(LOG_DEBUG,"%04d PASV DATA trying to bind socket to port %u"
						,sock,port);

				inet_setaddrport(&pasv_addr, port);

				if((result=bind(pasv_sock, &pasv_addr.addr,xp_sockaddr_len(&pasv_addr)))==0)
					break;
				if(port==startup->pasv_port_high)
					break;
			}
			if(result!= 0) {
				lprintf(LOG_ERR,"%04d !PASV ERROR %d (%d) binding socket to port %u"
					,sock, result, ERROR_VALUE, port);
				sockprintf(sock,sess,"425 Error %d binding data socket",ERROR_VALUE);
				ftp_close_socket(&pasv_sock,&pasv_sess,__LINE__);
				continue;
			}
			if(startup->options&FTP_OPT_DEBUG_DATA)
				lprintf(LOG_DEBUG,"%04d PASV DATA socket %d bound to port %u",sock,pasv_sock,port);

			addr_len=sizeof(addr);
			if((result=getsockname(pasv_sock, &addr.addr,&addr_len))!=0) {
				lprintf(LOG_ERR,"%04d !PASV ERROR %d (%d) getting address/port"
					,sock, result, ERROR_VALUE);
				sockprintf(sock,sess,"425 Error %d getting address/port",ERROR_VALUE);
				ftp_close_socket(&pasv_sock,&pasv_sess,__LINE__);
				continue;
			} 

			if((result=listen(pasv_sock, 1))!= 0) {
				lprintf(LOG_ERR,"%04d !PASV ERROR %d (%d) listening on port %u"
					,sock, result, ERROR_VALUE,port);
				sockprintf(sock,sess,"425 Error %d listening on data socket",ERROR_VALUE);
				ftp_close_socket(&pasv_sock,&pasv_sess,__LINE__);
				continue;
			}

			port=inet_addrport(&addr);
			if(strnicmp(cmd, "EPSV", 4)==0)
				sockprintf(sock,sess,"229 Entering Extended Passive Mode (|||%hu|)", port);
			else if (stricmp(cmd,"LPSV")==0) {
				switch(addr.addr.sa_family) {
					case AF_INET:
						sockprintf(sock,sess, "228 Entering Long Passive Mode (4, 4, %d, %d, %d, %d, 2, %d, %d)"
							,((unsigned char *)&(addr.in.sin_addr))[0]
							,((unsigned char *)&(addr.in.sin_addr))[1]
							,((unsigned char *)&(addr.in.sin_addr))[2]
							,((unsigned char *)&(addr.in.sin_addr))[3]
							,((unsigned char *)&(addr.in.sin_port))[0]
							,((unsigned char *)&(addr.in.sin_port))[1]);
						break;
					case AF_INET6:
						sockprintf(sock,sess, "228 Entering Long Passive Mode (6, 16, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, 2, %d, %d)"
							,((unsigned char *)&(addr.in6.sin6_addr))[0]
							,((unsigned char *)&(addr.in6.sin6_addr))[1]
							,((unsigned char *)&(addr.in6.sin6_addr))[2]
							,((unsigned char *)&(addr.in6.sin6_addr))[3]
							,((unsigned char *)&(addr.in6.sin6_addr))[4]
							,((unsigned char *)&(addr.in6.sin6_addr))[5]
							,((unsigned char *)&(addr.in6.sin6_addr))[6]
							,((unsigned char *)&(addr.in6.sin6_addr))[7]
							,((unsigned char *)&(addr.in6.sin6_addr))[8]
							,((unsigned char *)&(addr.in6.sin6_addr))[9]
							,((unsigned char *)&(addr.in6.sin6_addr))[10]
							,((unsigned char *)&(addr.in6.sin6_addr))[11]
							,((unsigned char *)&(addr.in6.sin6_addr))[12]
							,((unsigned char *)&(addr.in6.sin6_addr))[13]
							,((unsigned char *)&(addr.in6.sin6_addr))[14]
							,((unsigned char *)&(addr.in6.sin6_addr))[15]
							,((unsigned char *)&(addr.in6.sin6_port))[0]
							,((unsigned char *)&(addr.in6.sin6_port))[1]);
						break;
				}
			}
			else {
				/* Choose IP address to use in passive response */
				ip_addr=0;
				/* TODO: IPv6 this here lookup */
				if(startup->options&FTP_OPT_LOOKUP_PASV_IP
					&& (host=gethostbyname(startup->host_name))!=NULL) 
					ip_addr=ntohl(*((ulong*)host->h_addr_list[0]));
				if(ip_addr==0 && (ip_addr=startup->pasv_ip_addr.s_addr)==0)
					ip_addr=ntohl(pasv_addr.in.sin_addr.s_addr);

				if(startup->options&FTP_OPT_DEBUG_DATA)
					lprintf(LOG_INFO,"%04d PASV DATA IP address in response: %u.%u.%u.%u (subject to NAT)"
						,sock
						,(ip_addr>>24)&0xff
						,(ip_addr>>16)&0xff
						,(ip_addr>>8)&0xff
						,ip_addr&0xff
						);
				sockprintf(sock,sess,"227 Entering Passive Mode (%u,%u,%u,%u,%hu,%hu)"
					,(ip_addr>>24)&0xff
					,(ip_addr>>16)&0xff
					,(ip_addr>>8)&0xff
					,ip_addr&0xff
					,(port>>8)&0xff
					,port&0xff
					);
			}
			mode="passive";
			continue;
		}

		if(!strnicmp(cmd, "TYPE ",5)) {
			sockprintf(sock,sess,"200 All files sent in BINARY mode.");
			continue;
		}

		if(!strnicmp(cmd, "ALLO",4)) {
			p=cmd+5;
			SKIP_WHITESPACE(p);
			if(*p)
				l=atol(p);	
			else
				l=0;
			if(local_fsys)
				avail=getfreediskspace(local_dir,0);
			else
				avail=getfreediskspace(scfg.data_dir,0);	/* Change to temp_dir? */
			if(l && l>avail)
				sockprintf(sock,sess,"504 Only %lu bytes available.",avail);
			else
				sockprintf(sock,sess,"200 %lu bytes available.",avail);
			continue;
		}

		if(!strnicmp(cmd, "REST",4)) {
			p=cmd+4;
			SKIP_WHITESPACE(p);
			if(*p)
				filepos=atol(p);
			else
				filepos=0;
			sockprintf(sock,sess,"350 Restarting at %lu. Send STORE or RETRIEVE to initiate transfer."
				,filepos);
			continue;
		}

		if(!strnicmp(cmd, "MODE ",5)) {
			p=cmd+5;
			SKIP_WHITESPACE(p);
			if(toupper(*p)!='S')
				sockprintf(sock,sess,"504 Only STREAM mode supported.");
			else
				sockprintf(sock,sess,"200 STREAM mode.");
			continue;
		}

		if(!strnicmp(cmd, "STRU ",5)) {
			p=cmd+5;
			SKIP_WHITESPACE(p);
			if(toupper(*p)!='F')
				sockprintf(sock,sess,"504 Only FILE structure supported.");
			else
				sockprintf(sock,sess,"200 FILE structure.");
			continue;
		}

		if(!stricmp(cmd, "SYST")) {
			sockprintf(sock,sess,"215 UNIX Type: L8");
			continue;
		}

		if(!stricmp(cmd, "ABOR")) {
			if(!transfer_inprogress)
				sockprintf(sock,sess,"226 No tranfer in progress.");
			else {
				lprintf(LOG_WARNING,"%04d %s aborting transfer"
					,sock,user.alias);
				transfer_aborted=TRUE;
				YIELD(); /* give send thread time to abort */
				sockprintf(sock,sess,"226 Transfer aborted.");
			}
			continue;
		}

		if(!strnicmp(cmd,"SMNT ",5) && sysop && !(startup->options&FTP_OPT_NO_LOCAL_FSYS)) {
			p=cmd+5;
			SKIP_WHITESPACE(p);
			if(!stricmp(p,BBS_FSYS_DIR)) 
				local_fsys=FALSE;
			else {
				if(!direxist(p)) {
					sockprintf(sock,sess,"550 Directory does not exist.");
					lprintf(LOG_WARNING,"%04d !%s attempted to mount invalid directory: %s"
						,sock, user.alias, p);
					continue;
				}
				local_fsys=TRUE;
				SAFECOPY(local_dir,p);
			}
			sockprintf(sock,sess,"250 %s file system mounted."
				,local_fsys ? "Local" : "BBS");
			lprintf(LOG_INFO,"%04d %s mounted %s file system"
				,sock, user.alias, local_fsys ? "local" : "BBS");
			continue;
		}

		/****************************/
		/* Local File System Access */
		/****************************/
		if(sysop && local_fsys && !(startup->options&FTP_OPT_NO_LOCAL_FSYS)) {
			if(local_dir[0] 
				&& local_dir[strlen(local_dir)-1]!='\\'
				&& local_dir[strlen(local_dir)-1]!='/')
				strcat(local_dir,"/");

			if(!strnicmp(cmd, "MLS", 3)) {
				if (cmd[3] == 'T' || cmd[3] == 'D') {
					if (cmd[3] == 'D') {
						if((fp=fopen(ftp_tmpfname(fname,"lst",sock),"w+b"))==NULL) {
							lprintf(LOG_ERR,"%04d !ERROR %d opening %s",sock,errno,fname);
							sockprintf(sock,sess, "451 Insufficient system storage");
							continue;
						}
					}

					p=cmd+4;
					SKIP_WHITESPACE(p);

					filespec=p;
					if (!local_dir[0])
						strcpy(local_dir, "/");
					SAFEPRINTF2(path,"%s%s",local_dir, filespec);
					p=FULLPATH(NULL, path, 0);
					strcpy(path, p);
					free(p);
					if (cmd[3] == 'T') {
						if (access(path, 0) == -1) {
							sockprintf(sock,sess, "550 No such path %s", path);
							continue;
						}
						sockprintf(sock,sess, "250- Listing %s", path);
					}
					else {
						if (access(path, 0) == -1) {
							sockprintf(sock,sess, "550 No such path %s", path);
							continue;
						}
						if (!isdir(path)) {
							sockprintf(sock,sess, "501 Not a directory");
							continue;
						}
						sockprintf(sock,sess, "150 Directory of %s", path);
						backslash(path);
						strcat(path, "*");
					}

					lprintf(LOG_INFO,"%04d %s MLSx listing: %s in %s mode", sock, user.alias, path, mode);

					now=time(NULL);
					if(localtime_r(&now,&cur_tm)==NULL)
						memset(&cur_tm,0,sizeof(cur_tm));

					if (cmd[3] == 'T') {
						write_local_mlsx(NULL, sock, sess, mlsx_feats, path);
						sockprintf(sock, sess, "250 End");
					}
					else {
						glob(path,0,NULL,&g);
						for(i=0;i<(int)g.gl_pathc;i++)
							write_local_mlsx(fp, INVALID_SOCKET, -1, mlsx_feats, g.gl_pathv[i]);
						globfree(&g);
						fclose(fp);
						filexfer(&data_addr,sock,sess,pasv_sock,pasv_sess,&data_sock,&data_sess,fname,0L
							,&transfer_inprogress,&transfer_aborted
							,TRUE	/* delfile */
							,TRUE	/* tmpfile */
							,&lastactive,&user,&client,-1,FALSE,FALSE,FALSE,NULL,protection);
					}
					continue;
				}
			}

			if(!strnicmp(cmd, "LIST", 4) || !strnicmp(cmd, "NLST", 4)) {
				if(!strnicmp(cmd, "LIST", 4))
					detail=TRUE;
				else
					detail=FALSE;

				if((fp=fopen(ftp_tmpfname(fname,"lst",sock),"w+b"))==NULL) {
					lprintf(LOG_ERR,"%04d !ERROR %d opening %s",sock,errno,fname);
					sockprintf(sock,sess, "451 Insufficient system storage");
					continue;
				}

				p=cmd+4;
				SKIP_WHITESPACE(p);

				if(*p=='-') {	/* -Letc */
					FIND_WHITESPACE(p);
					SKIP_WHITESPACE(p);
				}

				filespec=p;
				if(*filespec==0)
					filespec="*";

				SAFEPRINTF2(path,"%s%s",local_dir, filespec);
				lprintf(LOG_INFO,"%04d %s listing: %s in %s mode", sock, user.alias, path, mode);
				sockprintf(sock,sess, "150 Directory of %s%s", local_dir, filespec);

				now=time(NULL);
				if(localtime_r(&now,&cur_tm)==NULL) 
					memset(&cur_tm,0,sizeof(cur_tm));
			
				glob(path,0,NULL,&g);
				for(i=0;i<(int)g.gl_pathc;i++) {
					if(detail) {
						f.size=flength(g.gl_pathv[i]);
						t=fdate(g.gl_pathv[i]);
						if(localtime_r(&t,&tm)==NULL)
							memset(&tm,0,sizeof(tm));
						fprintf(fp,"%crw-r--r--   1 %-8s local %9"PRId32" %s %2d "
							,isdir(g.gl_pathv[i]) ? 'd':'-'
							,scfg.sys_id
							,f.size
							,ftp_mon[tm.tm_mon],tm.tm_mday);
						if(tm.tm_year==cur_tm.tm_year)
							fprintf(fp,"%02d:%02d %s\r\n"
								,tm.tm_hour,tm.tm_min
								,getfname(g.gl_pathv[i]));
						else
							fprintf(fp,"%5d %s\r\n"
								,1900+tm.tm_year
								,getfname(g.gl_pathv[i]));
					} else
						fprintf(fp,"%s\r\n",getfname(g.gl_pathv[i]));
				}
				globfree(&g);
				fclose(fp);
				filexfer(&data_addr,sock,sess,pasv_sock,pasv_sess,&data_sock,&data_sess,fname,0L
					,&transfer_inprogress,&transfer_aborted
					,TRUE	/* delfile */
					,TRUE	/* tmpfile */
					,&lastactive,&user,&client,-1,FALSE,FALSE,FALSE,NULL,protection);
				continue;
			} /* Local LIST/NLST */

			if(!strnicmp(cmd, "CWD ", 4) || !strnicmp(cmd,"XCWD ",5)) {
			    if(!strnicmp(cmd,"CWD ",4))
					p=cmd+4;
				else
					p=cmd+5;
				SKIP_WHITESPACE(p);
				tp=p;
				if(*tp=='/' || *tp=='\\') /* /local: and /bbs: are valid */
					tp++;
				if(!strnicmp(tp,BBS_FSYS_DIR,strlen(BBS_FSYS_DIR))) {
					local_fsys=FALSE;
					sockprintf(sock,sess,"250 CWD command successful (BBS file system mounted).");
					lprintf(LOG_INFO,"%04d %s mounted BBS file system", sock, user.alias);
					continue;
				}
				if(!strnicmp(tp,LOCAL_FSYS_DIR,strlen(LOCAL_FSYS_DIR))) {
					tp+=strlen(LOCAL_FSYS_DIR);	/* already mounted */
					p=tp;
				}

				if(p[1]==':' || !strncmp(p,"\\\\",2))
					SAFECOPY(path,p);
				else if(*p=='/' || *p=='\\')
					SAFEPRINTF2(path,"%s%s",root_dir(local_dir),p);
				else {
					SAFEPRINTF2(fname,"%s%s",local_dir,p);
					FULLPATH(path,fname,sizeof(path));
				}

				if(!direxist(path)) {
					sockprintf(sock,sess,"550 Directory does not exist (%s).",path);
					lprintf(LOG_WARNING,"%04d !%s attempted to change to an invalid directory: %s"
						,sock, user.alias, path);
				} else {
					SAFECOPY(local_dir,path);
					sockprintf(sock,sess,"250 CWD command successful (%s).", local_dir);
				}
				continue;
			} /* Local CWD */

			if(!stricmp(cmd,"CDUP") || !stricmp(cmd,"XCUP")) {
				SAFEPRINTF(path,"%s..",local_dir);
				if(FULLPATH(local_dir,path,sizeof(local_dir))==NULL)
					sockprintf(sock,sess,"550 Directory does not exist.");
				else
					sockprintf(sock,sess,"200 CDUP command successful.");
				continue;
			}

			if(!stricmp(cmd, "PWD") || !stricmp(cmd,"XPWD")) {
				if(strlen(local_dir)>3)
					local_dir[strlen(local_dir)-1]=0;	/* truncate '/' */

				sockprintf(sock,sess,"257 \"%s\" is current directory."
					,local_dir);
				continue;
			} /* Local PWD */

			if(!strnicmp(cmd, "MKD ", 4) || !strnicmp(cmd,"XMKD",4)) {
				p=cmd+4;
				SKIP_WHITESPACE(p);
				if(*p=='/')	/* absolute */
					SAFEPRINTF2(fname,"%s%s",root_dir(local_dir),p+1);
				else		/* relative */
					SAFEPRINTF2(fname,"%s%s",local_dir,p);

				if((i=MKDIR(fname))==0) {
					sockprintf(sock,sess,"257 \"%s\" directory created",fname);
					lprintf(LOG_NOTICE,"%04d %s created directory: %s",sock,user.alias,fname);
				} else {
					sockprintf(sock,sess,"521 Error %d creating directory: %s",i,fname);
					lprintf(LOG_WARNING,"%04d !%s attempted to create directory: %s (Error %d)"
						,sock,user.alias,fname,i);
				}
				continue;
			}

			if(!strnicmp(cmd, "RMD ", 4) || !strnicmp(cmd,"XRMD",4)) {
				p=cmd+4;
				SKIP_WHITESPACE(p);
				if(*p=='/')	/* absolute */
					SAFEPRINTF2(fname,"%s%s",root_dir(local_dir),p+1);
				else		/* relative */
					SAFEPRINTF2(fname,"%s%s",local_dir,p);

				if((i=rmdir(fname))==0) {
					sockprintf(sock,sess,"250 \"%s\" directory removed",fname);
					lprintf(LOG_NOTICE,"%04d %s removed directory: %s",sock,user.alias,fname);
				} else {
					sockprintf(sock,sess,"450 Error %d removing directory: %s",i,fname);
					lprintf(LOG_WARNING,"%04d !%s attempted to remove directory: %s (Error %d)"
						,sock,user.alias,fname,i);
				}
				continue;
			}

			if(!strnicmp(cmd, "RNFR ",5)) {
				p=cmd+5;
				SKIP_WHITESPACE(p);
				if(*p=='/')	/* absolute */
					SAFEPRINTF2(ren_from,"%s%s",root_dir(local_dir),p+1);
				else		/* relative */
					SAFEPRINTF2(ren_from,"%s%s",local_dir,p);
				if(!fexist(ren_from)) {
					sockprintf(sock,sess,"550 File not found: %s",ren_from);
					lprintf(LOG_WARNING,"%04d !%s attempted to rename %s (not found)"
						,sock,user.alias,ren_from);
				} else
					sockprintf(sock,sess,"350 File exists, ready for destination name");
				continue;
			}

			if(!strnicmp(cmd, "RNTO ",5)) {
				p=cmd+5;
				SKIP_WHITESPACE(p);
				if(*p=='/')	/* absolute */
					SAFEPRINTF2(fname,"%s%s",root_dir(local_dir),p+1);
				else		/* relative */
					SAFEPRINTF2(fname,"%s%s",local_dir,p);

				if((i=rename(ren_from, fname))==0) {
					sockprintf(sock,sess,"250 \"%s\" renamed to \"%s\"",ren_from,fname);
					lprintf(LOG_NOTICE,"%04d %s renamed %s to %s",sock,user.alias,ren_from,fname);
				} else {
					sockprintf(sock,sess,"450 Error %d renaming file: %s",i,ren_from);
					lprintf(LOG_WARNING,"%04d !%s attempted to rename file: %s (Error %d)"
						,sock,user.alias,ren_from,i);
				}
				continue;
			}


			if(!strnicmp(cmd, "RETR ", 5) || !strnicmp(cmd,"SIZE ",5) 
				|| !strnicmp(cmd, "MDTM ",5) || !strnicmp(cmd, "DELE ",5)) {
				p=cmd+5;
				SKIP_WHITESPACE(p);

				if(!strnicmp(p,LOCAL_FSYS_DIR,strlen(LOCAL_FSYS_DIR))) 
					p+=strlen(LOCAL_FSYS_DIR);	/* already mounted */

				if(p[1]==':')		/* drive specified */
					SAFECOPY(fname,p);
				else if(*p=='/')	/* absolute, current drive */
					SAFEPRINTF2(fname,"%s%s",root_dir(local_dir),p+1);
				else		/* relative */
					SAFEPRINTF2(fname,"%s%s",local_dir,p);
				if(!fexist(fname)) {
					lprintf(LOG_WARNING,"%04d !%s file not found: %s",sock,user.alias,fname);
					sockprintf(sock,sess,"550 File not found: %s",fname);
					continue;
				}
				if(!strnicmp(cmd,"SIZE ",5)) {
					sockprintf(sock,sess,"213 %"PRIuOFF,flength(fname));
					continue;
				}
				if(!strnicmp(cmd,"MDTM ",5)) {
					t=fdate(fname);
					if(gmtime_r(&t,&tm)==NULL) /* specifically use GMT/UTC representation */
						memset(&tm,0,sizeof(tm));
					sockprintf(sock,sess,"213 %u%02u%02u%02u%02u%02u"
						,1900+tm.tm_year,tm.tm_mon+1,tm.tm_mday
						,tm.tm_hour,tm.tm_min,tm.tm_sec);					
					continue;
				}
				if(!strnicmp(cmd,"DELE ",5)) {
					if((i=ftp_remove(sock, __LINE__, fname))==0) {
						sockprintf(sock,sess,"250 \"%s\" removed successfully.",fname);
						lprintf(LOG_NOTICE,"%04d %s deleted file: %s",sock,user.alias,fname);
					} else {
						sockprintf(sock,sess,"450 Error %d removing file: %s",i,fname);
						lprintf(LOG_WARNING,"%04d !%s attempted to delete file: %s (Error %d)"
							,sock,user.alias,fname,i);
					}
					continue;
				}
				/* RETR */
				lprintf(LOG_INFO,"%04d %s downloading: %s (%"PRIuOFF" bytes) in %s mode"
					,sock,user.alias,fname,flength(fname)
					,mode);
				sockprintf(sock,sess,"150 Opening BINARY mode data connection for file transfer.");
				filexfer(&data_addr,sock,sess,pasv_sock,pasv_sess,&data_sock,&data_sess,fname,filepos
					,&transfer_inprogress,&transfer_aborted,FALSE,FALSE
					,&lastactive,&user,&client,-1,FALSE,FALSE,FALSE,NULL,protection);
				continue;
			} /* Local RETR/SIZE/MDTM */

			if(!strnicmp(cmd, "STOR ", 5) || !strnicmp(cmd, "APPE ", 5)) {
				p=cmd+5;
				SKIP_WHITESPACE(p);

				if(!strnicmp(p,LOCAL_FSYS_DIR,strlen(LOCAL_FSYS_DIR))) 
					p+=strlen(LOCAL_FSYS_DIR);	/* already mounted */

				if(p[1]==':')		/* drive specified */
					SAFECOPY(fname,p);
				else if(*p=='/')	/* absolute, current drive */
					SAFEPRINTF2(fname,"%s%s",root_dir(local_dir),p+1);
				else				/* relative */
					SAFEPRINTF2(fname,"%s%s",local_dir,p);

				lprintf(LOG_INFO,"%04d %s uploading: %s in %s mode", sock,user.alias,fname
					,mode);
				sockprintf(sock,sess,"150 Opening BINARY mode data connection for file transfer.");
				filexfer(&data_addr,sock,sess,pasv_sock,pasv_sess,&data_sock,&data_sess,fname,filepos
					,&transfer_inprogress,&transfer_aborted,FALSE,FALSE
					,&lastactive
					,&user
					,&client
					,-1		/* dir */
					,TRUE	/* uploading */
					,FALSE	/* credits */
					,!strnicmp(cmd,"APPE",4) ? TRUE : FALSE	/* append */
					,NULL	/* desc */
					,protection
					);
				filepos=0;
				continue;
			} /* Local STOR */
		}

		if (!strnicmp(cmd, "MLS", 3)) {
			if (cmd[3] == 'D' || cmd[3] == 'T') {
				dir=curdir;
				lib=curlib;
				l = 0;

				if(cmd[4]!=0)
					lprintf(LOG_DEBUG,"%04d MLSx: %s",sock,cmd);

				/* path specified? */
				p=cmd+4;
				if (*p == ' ')
					p++;

				if (parsepath(&p,&user,&client,&lib,&dir) == -1) {
					sockprintf(sock,sess, "550 No such path");
					continue;
				}
				
				if (strchr(p, '/')) {
					sockprintf(sock,sess, "550 No such path");
					continue;
				}
				if (cmd[3] == 'T') {
					if (cmd[4])
						strcpy(mls_path, cmd+5);
					else
						strcpy(mls_path, p);
				}
				else {
					if (*p) {
						sockprintf(sock,sess, "501 Not a directory");
						continue;
					}
					strcpy(mls_path, p);
				}
				mls_fname = p;

				fp = NULL;
				if (cmd[3] == 'D') {
					if((fp=fopen(ftp_tmpfname(fname,"lst",sock),"w+b"))==NULL) {
						lprintf(LOG_ERR,"%04d !ERROR %d opening %s",sock,errno,fname);
						sockprintf(sock,sess, "451 Insufficient system storage");
						continue;
					}
					sockprintf(sock,sess,"150 Opening ASCII mode data connection for MLSD.");
				}
				now=time(NULL);
				if(localtime_r(&now,&cur_tm)==NULL)
					memset(&cur_tm,0,sizeof(cur_tm));

				/* ASCII Index File */
				if(startup->options&FTP_OPT_INDEX_FILE && startup->index_file_name[0]
						&& (cmd[3] == 'D' || strcmp(startup->index_file_name, mls_fname) == 0)) {
					if (cmd[3] == 'T')
						sockprintf(sock,sess, "250- Listing %s", startup->index_file_name);
					get_owner_name(NULL, str);
					send_mlsx_entry(fp, sock, sess, mlsx_feats, "file", "r", UINT64_MAX, UINT64_MAX, str, cmd[3] == 'T' ? mls_path : startup->index_file_name);
					l++;
				}
				/* HTML Index File */
				if(startup->options&FTP_OPT_INDEX_FILE && startup->html_index_file[0]
						&& (cmd[3] == 'D' || strcmp(startup->html_index_file, mls_fname) == 0)) {
					if (cmd[3] == 'T')
						sockprintf(sock,sess, "250- Listing %s", startup->html_index_file);
					get_owner_name(NULL, str);
					send_mlsx_entry(fp, sock, sess, mlsx_feats, "file", "r", UINT64_MAX, UINT64_MAX, str, cmd[3] == 'T' ? mls_path : startup->html_index_file);
					l++;
				}

				if(lib<0) { /* Root dir */
					if (cmd[3] == 'T' && !*mls_fname) {
						sockprintf(sock,sess, "250- Listing root");
						get_owner_name(NULL, str);
						send_mlsx_entry(fp, sock, sess, mlsx_feats, "dir", (startup->options&FTP_OPT_ALLOW_QWK) ? "elc" : "el", UINT64_MAX, UINT64_MAX, str, mls_path);
						l++;
					}
					lprintf(LOG_INFO,"%04d %s listing: root in %s mode",sock,user.alias, mode);

					/* QWK Packet */
					if(startup->options&FTP_OPT_ALLOW_QWK) {
						SAFEPRINTF(str,"%s.qwk",scfg.sys_id);
						if (cmd[3] == 'D' || strcmp(str, mls_fname) == 0) {
							if (cmd[3] == 'T')
								sockprintf(sock,sess, "250- Listing %s", str);
							get_owner_name(NULL, str);
							send_mlsx_entry(fp, sock, sess, mlsx_feats, "file", "r", UINT64_MAX, UINT64_MAX, str, cmd[3] == 'T' ? mls_path : str);
							l++;
						}
					}

					/* File Aliases */
					sprintf(aliasfile,"%sftpalias.cfg",scfg.ctrl_dir);
					if((alias_fp=fopen(aliasfile,"r"))!=NULL) {

						while(!feof(alias_fp)) {
							if(!fgets(aliasline,sizeof(aliasline),alias_fp))
								break;

							alias_dir=FALSE;

							p=aliasline;		/* alias pointer */
							SKIP_WHITESPACE(p);

							if(*p==';')	/* comment */
								continue;

							tp=p;		/* terminator pointer */
							FIND_WHITESPACE(tp);
							if(*tp) *tp=0;

							np=tp+1;	/* filename pointer */
							SKIP_WHITESPACE(np);

							tp=np;		/* terminator pointer */
							FIND_WHITESPACE(tp);
							if(*tp) *tp=0;

							dp=tp+1;	/* description pointer */
							SKIP_WHITESPACE(dp);
							truncsp(dp);

							if(stricmp(dp,BBS_HIDDEN_ALIAS)==0)
								continue;

							/* Virtual Path? */
							if(!strnicmp(np,BBS_VIRTUAL_PATH,strlen(BBS_VIRTUAL_PATH))) {
								if((dir=getdir(np+strlen(BBS_VIRTUAL_PATH),&user,&client))<0) {
									lprintf(LOG_WARNING,"0000 !Invalid virtual path (%s) for %s",np,user.alias);
									continue; /* No access or invalid virtual path */
								}
								tp=strrchr(np,'/');
								if(tp==NULL) 
									continue;
								tp++;
								if(*tp) {
									SAFEPRINTF2(aliasfile,"%s%s",scfg.dir[dir]->path,tp);
									np=aliasfile;
								}
								else 
									alias_dir=TRUE;
							}

							if(!alias_dir && !fexist(np)) {
								lprintf(LOG_WARNING,"0000 !Missing aliased file (%s) for %s",np,user.alias);
								continue;
							}

							if(cmd[3] == 'D' || strcmp(startup->html_index_file, mls_fname) == 0) {
								if (cmd[3] == 'T')
									sockprintf(sock,sess, "250- Listing %s", p);
								if (alias_dir==TRUE)
									send_mlsx_entry(fp, sock, sess, mlsx_feats, "dir", "el", UINT64_MAX, UINT64_MAX, NULL, cmd[3] == 'T' ? mls_path : p);
								else
									send_mlsx_entry(fp, sock, sess, mlsx_feats, "file", "r", (uint64_t)flength(np), (uint64_t)fdate(np), NULL, cmd[3] == 'T' ? mls_path : p);
								l++;
							}
						}

						fclose(alias_fp);
					}

					/* Library folders */
					for(i=0;i<scfg.total_libs;i++) {
						if(!chk_ar(&scfg,scfg.lib[i]->ar,&user,&client))
							continue;
						if (cmd[3] != 'D' && strcmp(scfg.lib[i]->sname, mls_fname) != 0)
							continue;
						if (cmd[3] == 'T')
							sockprintf(sock,sess, "250- Listing %s", scfg.lib[i]->sname);
						get_libperm(scfg.lib[i], &user, &client, permstr);
						get_owner_name(NULL, str);
						send_mlsx_entry(fp, sock, sess, mlsx_feats, "dir", permstr, UINT64_MAX, UINT64_MAX, str, cmd[3] == 'T' ? mls_path : scfg.lib[i]->sname);
						l++;
					}
				} else if(dir<0) {
					if (cmd[3] == 'T' && !*mls_fname) {
						sockprintf(sock,sess, "250- Listing %s", scfg.lib[lib]->sname);
						get_owner_name(NULL, str);
						send_mlsx_entry(fp, sock, sess, mlsx_feats, "dir", "el", UINT64_MAX, UINT64_MAX, str, mls_path);
						l++;
					}
					if (cmd[3] == 'D') {
						get_owner_name(NULL, str);
						send_mlsx_entry(fp, sock, sess, mlsx_feats, "pdir", (startup->options&FTP_OPT_ALLOW_QWK) ? "elc" : "el", UINT64_MAX, UINT64_MAX, str, "..");
					}
					lprintf(LOG_INFO,"%04d %s listing: %s library in %s mode"
						,sock,user.alias,scfg.lib[lib]->sname,mode);
					for(i=0;i<scfg.total_dirs;i++) {
						if(scfg.dir[i]->lib!=lib)
							continue;
						if(i!=(int)scfg.sysop_dir && i!=(int)scfg.upload_dir 
							&& !chk_ar(&scfg,scfg.dir[i]->ar,&user,&client))
							continue;
						if (cmd[3] != 'D' && strcmp(scfg.dir[i]->code_suffix, mls_fname) != 0)
							continue;
						if (cmd[3] == 'T')
							sockprintf(sock,sess, "250- Listing %s", scfg.dir[i]->code_suffix);
						get_dirperm(scfg.lib[lib], scfg.dir[i], &user, &client, permstr);
						get_owner_name(NULL, str);
						send_mlsx_entry(fp, sock, sess, mlsx_feats, "dir", permstr, UINT64_MAX, UINT64_MAX, str, cmd[3] == 'T' ? mls_path : scfg.dir[i]->code_suffix);
						l++;
					}
				} else if(chk_ar(&scfg,scfg.dir[dir]->ar,&user,&client)) {
					lprintf(LOG_INFO,"%04d %s listing: %s/%s directory in %s mode"
						,sock,user.alias,scfg.lib[lib]->sname,scfg.dir[dir]->code_suffix,mode);

					if (cmd[3] == 'T' && !*mls_fname) {
						sockprintf(sock,sess, "250- Listing %s/%s",scfg.lib[lib]->sname,scfg.dir[dir]->code_suffix);
						get_owner_name(NULL, str);
						send_mlsx_entry(fp, sock, sess, mlsx_feats, "dir", (startup->options&FTP_OPT_ALLOW_QWK) ? "elc" : "el", UINT64_MAX, UINT64_MAX, str, mls_path);
						l++;
					}
					if (cmd[3] == 'D') {
						get_libperm(scfg.lib[lib], &user, &client, permstr);
						get_owner_name(NULL, str);
						send_mlsx_entry(fp, sock, sess, mlsx_feats, "pdir", permstr, UINT64_MAX, UINT64_MAX, str, "..");
					}

					SAFEPRINTF2(path,"%s%s",scfg.dir[dir]->path,"*");
					glob(path,0,NULL,&g);
					for(i=0;i<(int)g.gl_pathc;i++) {
						if(isdir(g.gl_pathv[i]))
							continue;
#ifdef _WIN32
						GetShortPathName(g.gl_pathv[i], str, sizeof(str));
#else
						SAFECOPY(str,g.gl_pathv[i]);
#endif
						padfname(getfname(str),f.name);
						f.dir=dir;
						if((filedat=getfileixb(&scfg,&f))==FALSE
							&& !(startup->options&FTP_OPT_DIR_FILES)
							&& !(scfg.dir[dir]->misc&DIR_FILES))
							continue;
						if (cmd[3] != 'D' && strcmp(getfname(g.gl_pathv[i]), mls_fname) != 0)
							continue;
						if (cmd[3] == 'T')
							sockprintf(sock,sess, "250- Listing %s", p);
						get_fileperm(scfg.lib[lib], scfg.dir[dir], &user, &client, &f, permstr);
						get_owner_name(&f, str);
						send_mlsx_entry(fp, sock, sess, mlsx_feats, "file", permstr, (uint64_t)flength(g.gl_pathv[i]), (uint64_t)fdate(g.gl_pathv[i]), str, cmd[3] == 'T' ? mls_path : getfname(g.gl_pathv[i]));
						l++;
					}
					globfree(&g);
				} else 
					lprintf(LOG_INFO,"%04d %s listing: %s/%s directory in %s mode (empty - no access)"
						,sock,user.alias,scfg.lib[lib]->sname,scfg.dir[dir]->code_suffix,mode);

				if (cmd[3] == 'D') {
					fclose(fp);
					filexfer(&data_addr,sock,sess,pasv_sock,pasv_sess,&data_sock,&data_sess,fname,0L
						,&transfer_inprogress,&transfer_aborted
						,TRUE /* delfile */
						,TRUE /* tmpfile */
						,&lastactive,&user,&client,dir,FALSE,FALSE,FALSE,NULL,protection);
				}
				else {
					if (l==0)
						sockprintf(sock,sess, "550 No such path");
					else
						sockprintf(sock, sess, "250 End");
				}
				continue;
			}
		}

		if(!strnicmp(cmd, "LIST", 4) || !strnicmp(cmd, "NLST", 4)) {	
			dir=curdir;
			lib=curlib;

			if(cmd[4]!=0) 
				lprintf(LOG_DEBUG,"%04d LIST/NLST: %s",sock,cmd);

			/* path specified? */
			p=cmd+4;
			SKIP_WHITESPACE(p);

			if(*p=='-') {	/* -Letc */
				FIND_WHITESPACE(p);
				SKIP_WHITESPACE(p);
			}

			if((fp=fopen(ftp_tmpfname(fname,"lst",sock),"w+b"))==NULL) {
				lprintf(LOG_ERR,"%04d !ERROR %d opening %s",sock,errno,fname);
				sockprintf(sock,sess, "451 Insufficient system storage");
				continue;
			}
			sockprintf(sock,sess,"150 Opening ASCII mode data connection for /bin/ls.");

			if (parsepath(&p,&user,&client,&lib,&dir) == -1) {
				/* Empty list */
				fclose(fp);
				filexfer(&data_addr,sock,sess,pasv_sock,pasv_sess,&data_sock,&data_sess,fname,0L
					,&transfer_inprogress,&transfer_aborted
					,TRUE /* delfile */
					,TRUE /* tmpfile */
					,&lastactive,&user,&client,dir,FALSE,FALSE,FALSE,NULL,protection);
				continue;
			}
			filespec=p;
			if(*filespec==0)
				filespec="*";

			if(!strnicmp(cmd, "LIST", 4))
				detail=TRUE;
			else
				detail=FALSE;
			now=time(NULL);
			if(localtime_r(&now,&cur_tm)==NULL) 
				memset(&cur_tm,0,sizeof(cur_tm));

			/* ASCII Index File */
			if(startup->options&FTP_OPT_INDEX_FILE && startup->index_file_name[0]
				&& wildmatchi(startup->index_file_name, filespec, FALSE)) {
				if(detail)
					fprintf(fp,"-r--r--r--   1 %-*s %-8s %9ld %s %2d %02d:%02d %s\r\n"
						,NAME_LEN
						,scfg.sys_id
						,lib<0 ? scfg.sys_id : dir<0 
							? scfg.lib[lib]->sname : scfg.dir[dir]->code_suffix
						,512L
						,ftp_mon[cur_tm.tm_mon],cur_tm.tm_mday,cur_tm.tm_hour,cur_tm.tm_min
						,startup->index_file_name);
				else
					fprintf(fp,"%s\r\n",startup->index_file_name);
			} 
			/* HTML Index File */
			if(startup->options&FTP_OPT_HTML_INDEX_FILE && startup->html_index_file[0]
				&& wildmatchi(startup->html_index_file, filespec, FALSE)) {
				if(detail)
					fprintf(fp,"-r--r--r--   1 %-*s %-8s %9ld %s %2d %02d:%02d %s\r\n"
						,NAME_LEN
						,scfg.sys_id
						,lib<0 ? scfg.sys_id : dir<0 
							? scfg.lib[lib]->sname : scfg.dir[dir]->code_suffix
						,512L
						,ftp_mon[cur_tm.tm_mon],cur_tm.tm_mday,cur_tm.tm_hour,cur_tm.tm_min
						,startup->html_index_file);
				else
					fprintf(fp,"%s\r\n",startup->html_index_file);
			} 

			if(lib<0) { /* Root dir */
				lprintf(LOG_INFO,"%04d %s listing: root in %s mode",sock,user.alias, mode);

				/* QWK Packet */
				if(startup->options&FTP_OPT_ALLOW_QWK) {
					SAFEPRINTF(str,"%s.qwk",scfg.sys_id);
					if(wildmatchi(str, filespec, FALSE)) {
						if(detail) {
							if(fexistcase(qwkfile)) {
								t=fdate(qwkfile);
								l=flength(qwkfile);
							} else {
								t=time(NULL);
								l=10240;
							};
							if(localtime_r(&t,&tm)==NULL) 
								memset(&tm,0,sizeof(tm));
							fprintf(fp,"-r--r--r--   1 %-*s %-8s %9ld %s %2d %02d:%02d %s\r\n"
								,NAME_LEN
								,scfg.sys_id
								,scfg.sys_id
								,l
								,ftp_mon[tm.tm_mon],tm.tm_mday,tm.tm_hour,tm.tm_min
								,str);
						} else
							fprintf(fp,"%s\r\n",str);
					}
				} 

				/* File Aliases */
				sprintf(aliasfile,"%sftpalias.cfg",scfg.ctrl_dir);
				if((alias_fp=fopen(aliasfile,"r"))!=NULL) {

					while(!feof(alias_fp)) {
						if(!fgets(aliasline,sizeof(aliasline),alias_fp))
							break;

						alias_dir=FALSE;

						p=aliasline;		/* alias pointer */
						SKIP_WHITESPACE(p);

						if(*p==';')	/* comment */
							continue;

						tp=p;		/* terminator pointer */
						FIND_WHITESPACE(tp);
						if(*tp) *tp=0;

						np=tp+1;	/* filename pointer */
						SKIP_WHITESPACE(np);

						tp=np;		/* terminator pointer */
						FIND_WHITESPACE(tp);
						if(*tp) *tp=0;

						dp=tp+1;	/* description pointer */
						SKIP_WHITESPACE(dp);
						truncsp(dp);

						if(stricmp(dp,BBS_HIDDEN_ALIAS)==0)
							continue;

						if(!wildmatchi(p, filespec, FALSE))
							continue;

						/* Virtual Path? */
						if(!strnicmp(np,BBS_VIRTUAL_PATH,strlen(BBS_VIRTUAL_PATH))) {
							if((dir=getdir(np+strlen(BBS_VIRTUAL_PATH),&user,&client))<0) {
								lprintf(LOG_WARNING,"0000 !Invalid virtual path (%s) for %s",np,user.alias);
								continue; /* No access or invalid virtual path */
							}
							tp=strrchr(np,'/');
							if(tp==NULL) 
								continue;
							tp++;
							if(*tp) {
								SAFEPRINTF2(aliasfile,"%s%s",scfg.dir[dir]->path,tp);
								np=aliasfile;
							}
							else 
								alias_dir=TRUE;
						}

						if(!alias_dir && !fexist(np)) {
							lprintf(LOG_WARNING,"0000 !Missing aliased file (%s) for %s",np,user.alias);
							continue;
						}

						if(detail) {

							if(alias_dir==TRUE) {
								fprintf(fp,"drwxrwxrwx   1 %-*s %-8s %9ld %s %2d %02d:%02d %s\r\n"
									,NAME_LEN
									,scfg.sys_id
									,scfg.lib[scfg.dir[dir]->lib]->sname
									,512L
									,ftp_mon[cur_tm.tm_mon],cur_tm.tm_mday,cur_tm.tm_hour,cur_tm.tm_min
									,p);
							}
							else {
								t=fdate(np);
								if(localtime_r(&t,&tm)==NULL)
									memset(&tm,0,sizeof(tm));
								fprintf(fp,"-r--r--r--   1 %-*s %-8s %9"PRIdOFF" %s %2d %02d:%02d %s\r\n"
									,NAME_LEN
									,scfg.sys_id
									,scfg.sys_id
									,flength(np)
									,ftp_mon[tm.tm_mon],tm.tm_mday,tm.tm_hour,tm.tm_min
									,p);
							}
						} else
							fprintf(fp,"%s\r\n",p);

					}

					fclose(alias_fp);
				}

				/* Library folders */
				for(i=0;i<scfg.total_libs;i++) {
					if(!chk_ar(&scfg,scfg.lib[i]->ar,&user,&client))
						continue;
					if(!wildmatchi(scfg.lib[i]->sname, filespec, FALSE))
						continue;
					if(detail)
						fprintf(fp,"dr-xr-xr-x   1 %-*s %-8s %9ld %s %2d %02d:%02d %s\r\n"
							,NAME_LEN
							,scfg.sys_id
							,scfg.sys_id
							,512L
							,ftp_mon[cur_tm.tm_mon],cur_tm.tm_mday,cur_tm.tm_hour,cur_tm.tm_min
							,scfg.lib[i]->sname);
					else
						fprintf(fp,"%s\r\n",scfg.lib[i]->sname);
				}
			} else if(dir<0) {
				lprintf(LOG_INFO,"%04d %s listing: %s library in %s mode"
					,sock,user.alias,scfg.lib[lib]->sname,mode);
				for(i=0;i<scfg.total_dirs;i++) {
					if(scfg.dir[i]->lib!=lib)
						continue;
					if(i!=(int)scfg.sysop_dir && i!=(int)scfg.upload_dir 
						&& !chk_ar(&scfg,scfg.dir[i]->ar,&user,&client))
						continue;
					if(!wildmatchi(scfg.dir[i]->code_suffix, filespec, FALSE))
						continue;
					if(detail)
						fprintf(fp,"drwxrwxrwx   1 %-*s %-8s %9ld %s %2d %02d:%02d %s\r\n"
							,NAME_LEN
							,scfg.sys_id
							,scfg.lib[lib]->sname
							,512L
							,ftp_mon[cur_tm.tm_mon],cur_tm.tm_mday,cur_tm.tm_hour,cur_tm.tm_min
							,scfg.dir[i]->code_suffix);
					else
						fprintf(fp,"%s\r\n",scfg.dir[i]->code_suffix);
				}
			} else if(chk_ar(&scfg,scfg.dir[dir]->ar,&user,&client)) {
				lprintf(LOG_INFO,"%04d %s listing: %s/%s directory in %s mode"
					,sock,user.alias,scfg.lib[lib]->sname,scfg.dir[dir]->code_suffix,mode);

				SAFEPRINTF2(path,"%s%s",scfg.dir[dir]->path,filespec);
				glob(path,0,NULL,&g);
				for(i=0;i<(int)g.gl_pathc;i++) {
					if(isdir(g.gl_pathv[i]))
						continue;
#ifdef _WIN32
					GetShortPathName(g.gl_pathv[i], str, sizeof(str));
#else
					SAFECOPY(str,g.gl_pathv[i]);
#endif
					padfname(getfname(str),f.name);
					f.dir=dir;
					if((filedat=getfileixb(&scfg,&f))==FALSE
						&& !(startup->options&FTP_OPT_DIR_FILES)
						&& !(scfg.dir[dir]->misc&DIR_FILES))
						continue;
					if(detail) {
						f.size=flength(g.gl_pathv[i]);
						getfiledat(&scfg,&f);
						t=fdate(g.gl_pathv[i]);
						if(localtime_r(&t,&tm)==NULL)
							memset(&tm,0,sizeof(tm));
						if(filedat) {
							if(f.misc&FM_ANON)
								SAFECOPY(str,ANONYMOUS);
							else
								dotname(f.uler,str);
						} else
							SAFECOPY(str,scfg.sys_id);
						fprintf(fp,"-r--r--r--   1 %-*s %-8s %9"PRId32" %s %2d "
							,NAME_LEN
							,str
							,scfg.dir[dir]->code_suffix
							,f.size
							,ftp_mon[tm.tm_mon],tm.tm_mday);
						if(tm.tm_year==cur_tm.tm_year)
							fprintf(fp,"%02d:%02d %s\r\n"
								,tm.tm_hour,tm.tm_min
								,getfname(g.gl_pathv[i]));
						else
							fprintf(fp,"%5d %s\r\n"
								,1900+tm.tm_year
								,getfname(g.gl_pathv[i]));
					} else
						fprintf(fp,"%s\r\n",getfname(g.gl_pathv[i]));
				}
				globfree(&g);
			} else 
				lprintf(LOG_INFO,"%04d %s listing: %s/%s directory in %s mode (empty - no access)"
					,sock,user.alias,scfg.lib[lib]->sname,scfg.dir[dir]->code_suffix,mode);

			fclose(fp);
			filexfer(&data_addr,sock,sess,pasv_sock,pasv_sess,&data_sock,&data_sess,fname,0L
				,&transfer_inprogress,&transfer_aborted
				,TRUE /* delfile */
				,TRUE /* tmpfile */
				,&lastactive,&user,&client,dir,FALSE,FALSE,FALSE,NULL,protection);
			continue;
		}

		if(!strnicmp(cmd, "RETR ", 5) 
			|| !strnicmp(cmd, "SIZE ",5) 
			|| !strnicmp(cmd, "MDTM ",5)
			|| !strnicmp(cmd, "DELE ",5)) {
			getdate=FALSE;
			getsize=FALSE;
			delecmd=FALSE;
			file_date=0;
			if(!strnicmp(cmd,"SIZE ",5))
				getsize=TRUE;
			else if(!strnicmp(cmd,"MDTM ",5))
				getdate=TRUE;
			else if(!strnicmp(cmd,"DELE ",5))
				delecmd=TRUE;

			if(!getsize && !getdate && user.rest&FLAG('D')) {
				sockprintf(sock,sess,"550 Insufficient access.");
				filepos=0;
				continue;
			}
			credits=TRUE;
			success=FALSE;
			delfile=FALSE;
			tmpfile=FALSE;
			lib=curlib;
			dir=curdir;

			p=cmd+5;
			SKIP_WHITESPACE(p);

			if(!strnicmp(p,BBS_FSYS_DIR,strlen(BBS_FSYS_DIR))) 
				p+=strlen(BBS_FSYS_DIR);	/* already mounted */

			if(*p=='/') {
				lib=-1;
				p++;
			}
			else if(!strncmp(p,"./",2))
				p+=2;

			if(lib<0 && ftpalias(p, fname, &user, &client, &dir)==TRUE) {
				success=TRUE;
				credits=TRUE;	/* include in d/l stats */
				tmpfile=FALSE;
				delfile=FALSE;
				lprintf(LOG_INFO,"%04d %s %.4s by alias: %s"
					,sock,user.alias,cmd,p);
				p=getfname(fname);
				if(dir>=0)
					lib=scfg.dir[dir]->lib;
			}
			if(!success && lib<0 && (tp=strchr(p,'/'))!=NULL) {
				dir=-1;
				*tp=0;
				for(i=0;i<scfg.total_libs;i++) {
					if(!chk_ar(&scfg,scfg.lib[i]->ar,&user,&client))
						continue;
					if(!stricmp(scfg.lib[i]->sname,p))
						break;
				}
				if(i<scfg.total_libs) 
					lib=i;
				p=tp+1;
			}
			if(!success && dir<0 && (tp=strchr(p,'/'))!=NULL) {
				*tp=0;
				for(i=0;i<scfg.total_dirs;i++) {
					if(scfg.dir[i]->lib!=lib)
						continue;
					if(!chk_ar(&scfg,scfg.dir[i]->ar,&user,&client))
						continue;
					if(!stricmp(scfg.dir[i]->code_suffix,p))
						break;
				}
				if(i<scfg.total_dirs) 
					dir=i;
				p=tp+1;
			}

			sprintf(html_index_ext,"%s?",startup->html_index_file);

			sprintf(str,"%s.qwk",scfg.sys_id);
			if(lib<0 && startup->options&FTP_OPT_ALLOW_QWK 
				&& !stricmp(p,str) && !delecmd) {
				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(!ftouch(str))
						lprintf(LOG_ERR,"%04d !ERROR creating semaphore file: %s"
							,sock, str);
					t=time(NULL);
					while(fexist(str)) {
						if(!socket_check(sock,NULL,NULL,0))
							break;
						if(time(NULL)-t>startup->qwk_timeout)
							break;
						mswait(1000);
					}
					if(!socket_check(sock,NULL,NULL,0)) {
						ftp_remove(sock, __LINE__, str);
						continue;
					}
					if(fexist(str)) {
						lprintf(LOG_WARNING,"%04d !TIMEOUT waiting for QWK packet creation",sock);
						sockprintf(sock,sess,"451 Time-out waiting for packet creation.");
						ftp_remove(sock, __LINE__, str);
						filepos=0;
						continue;
					}
					if(!fexistcase(qwkfile)) {
						lprintf(LOG_INFO,"%04d No QWK Packet created (no new messages)",sock);
						sockprintf(sock,sess,"550 No QWK packet created (no new messages)");
						filepos=0;
						continue;
					}
				}
				SAFECOPY(fname,qwkfile);
				success=TRUE;
				delfile=TRUE;
				credits=FALSE;
				if(!getsize && !getdate)
					lprintf(LOG_INFO,"%04d %s downloading QWK packet (%"PRIuOFF" bytes) in %s mode"
						,sock,user.alias,flength(fname)
						,mode);
			/* ASCII Index File */
			} else if(startup->options&FTP_OPT_INDEX_FILE 
				&& !stricmp(p,startup->index_file_name)
				&& !delecmd) {
				if(getsize) {
					sockprintf(sock,sess, "550 Size not available for dynamically generated files");
					continue;
				}
				if((fp=fopen(ftp_tmpfname(fname,"ndx",sock),"w+b"))==NULL) {
					lprintf(LOG_ERR,"%04d !ERROR %d opening %s",sock,errno,fname);
					sockprintf(sock,sess, "451 Insufficient system storage");
					filepos=0;
					continue;
				}
				success=TRUE;
				if(getdate)
					file_date=time(NULL);
				else {
					lprintf(LOG_INFO,"%04d %s downloading index for %s in %s mode"
						,sock,user.alias,genvpath(lib,dir,str)
						,mode);
					credits=FALSE;
					tmpfile=TRUE;
					delfile=TRUE;
					fprintf(fp,"%-*s File/Folder Descriptions\r\n"
						,INDEX_FNAME_LEN,startup->index_file_name);
					if(startup->options&FTP_OPT_HTML_INDEX_FILE)
						fprintf(fp,"%-*s File/Folder Descriptions (HTML)\r\n"
							,INDEX_FNAME_LEN,startup->html_index_file);
					if(lib<0) {

						/* File Aliases */
						sprintf(aliasfile,"%sftpalias.cfg",scfg.ctrl_dir);
						if((alias_fp=fopen(aliasfile,"r"))!=NULL) {

							while(!feof(alias_fp)) {
								if(!fgets(aliasline,sizeof(aliasline),alias_fp))
									break;

								p=aliasline;	/* alias pointer */
								SKIP_WHITESPACE(p);

								if(*p==';')	/* comment */
									continue;

								tp=p;		/* terminator pointer */
								FIND_WHITESPACE(tp);
								if(*tp) *tp=0;

								np=tp+1;	/* filename pointer */
								SKIP_WHITESPACE(np);

								np++;		/* description pointer */
								FIND_WHITESPACE(np);

								while(*np && *np<' ') np++;

								truncsp(np);

								fprintf(fp,"%-*s %s\r\n",INDEX_FNAME_LEN,p,np);
							}

							fclose(alias_fp);
						}

						/* QWK Packet */
						if(startup->options&FTP_OPT_ALLOW_QWK /* && fexist(qwkfile) */) {
							sprintf(str,"%s.qwk",scfg.sys_id);
							fprintf(fp,"%-*s QWK Message Packet\r\n"
								,INDEX_FNAME_LEN,str);
						}

						/* Library Folders */
						for(i=0;i<scfg.total_libs;i++) {
							if(!chk_ar(&scfg,scfg.lib[i]->ar,&user,&client))
								continue;
							fprintf(fp,"%-*s %s\r\n"
								,INDEX_FNAME_LEN,scfg.lib[i]->sname,scfg.lib[i]->lname);
						}
					} else if(dir<0) {
						for(i=0;i<scfg.total_dirs;i++) {
							if(scfg.dir[i]->lib!=lib)
								continue;
							if(i!=(int)scfg.sysop_dir && i!=(int)scfg.upload_dir
								&& !chk_ar(&scfg,scfg.dir[i]->ar,&user,&client))
								continue;
							fprintf(fp,"%-*s %s\r\n"
								,INDEX_FNAME_LEN,scfg.dir[i]->code_suffix,scfg.dir[i]->lname);
						}
					} else if(chk_ar(&scfg,scfg.dir[dir]->ar,&user,&client)){
						sprintf(cmd,"%s*",scfg.dir[dir]->path);
						glob(cmd,0,NULL,&g);
						for(i=0;i<(int)g.gl_pathc;i++) {
							if(isdir(g.gl_pathv[i]))
								continue;
	#ifdef _WIN32
							GetShortPathName(g.gl_pathv[i], str, sizeof(str));
	#else
							SAFECOPY(str,g.gl_pathv[i]);
	#endif
							padfname(getfname(str),f.name);
							f.dir=dir;
							if(getfileixb(&scfg,&f)) {
								f.size=flength(g.gl_pathv[i]);
								getfiledat(&scfg,&f);
								fprintf(fp,"%-*s %s\r\n",INDEX_FNAME_LEN
									,getfname(g.gl_pathv[i]),f.desc);
							}
						}
						globfree(&g);
					}
					fclose(fp);
				}
			/* HTML Index File */
			} else if(startup->options&FTP_OPT_HTML_INDEX_FILE 
				&& (!stricmp(p,startup->html_index_file) 
				|| !strnicmp(p,html_index_ext,strlen(html_index_ext)))
				&& !delecmd) {
				success=TRUE;
				if(getsize) {
					sockprintf(sock,sess, "550 Size not available for dynamically generated files");
					continue;
				}
				else if(getdate)
					file_date=time(NULL);
				else {
#ifdef JAVASCRIPT
					if(startup->options&FTP_OPT_NO_JAVASCRIPT) {
						lprintf(LOG_ERR,"%04d !JavaScript disabled, cannot generate %s",sock,fname);
						sockprintf(sock,sess, "451 JavaScript disabled");
						filepos=0;
						continue;
					}
					if(js_runtime == NULL) {
						lprintf(LOG_DEBUG,"%04d JavaScript: Creating runtime: %lu bytes"
							,sock,startup->js.max_bytes);

						if((js_runtime = jsrt_GetNew(startup->js.max_bytes, 1000, __FILE__, __LINE__))==NULL) {
							lprintf(LOG_ERR,"%04d !ERROR creating JavaScript runtime",sock);
							sockprintf(sock,sess,"451 Error creating JavaScript runtime");
							filepos=0;
							continue;
						}
					}

					if(js_cx==NULL) {	/* Context not yet created, create it now */
						/* js_initcx() starts a request */
						if(((js_cx=js_initcx(js_runtime, sock,&js_glob,&js_ftp,&js_callback))==NULL)) {
							lprintf(LOG_ERR,"%04d !ERROR initializing JavaScript context",sock);
							sockprintf(sock,sess,"451 Error initializing JavaScript context");
							filepos=0;
							continue;
						}
						if(js_CreateUserClass(js_cx, js_glob, &scfg)==NULL) 
							lprintf(LOG_ERR,"%04d !JavaScript ERROR creating user class",sock);

						if(js_CreateFileClass(js_cx, js_glob)==NULL) 
							lprintf(LOG_ERR,"%04d !JavaScript ERROR creating file class",sock);

						if(js_CreateUserObject(js_cx, js_glob, &scfg, "user", &user, &client, /* global_user: */TRUE)==NULL) 
							lprintf(LOG_ERR,"%04d !JavaScript ERROR creating user object",sock);

						if(js_CreateClientObject(js_cx, js_glob, "client", &client, sock, -1)==NULL) 
							lprintf(LOG_ERR,"%04d !JavaScript ERROR creating client object",sock);

						if(js_CreateFileAreaObject(js_cx, js_glob, &scfg, &user, &client
							,startup->html_index_file)==NULL) 
							lprintf(LOG_ERR,"%04d !JavaScript ERROR creating file area object",sock);
					}
					else
						JS_BEGINREQUEST(js_cx);

					if((js_str=JS_NewStringCopyZ(js_cx, "name"))!=NULL) {
						js_val=STRING_TO_JSVAL(js_str);
						JS_SetProperty(js_cx, js_ftp, "sort", &js_val);
					}
					js_val=BOOLEAN_TO_JSVAL(FALSE);
					JS_SetProperty(js_cx, js_ftp, "reverse", &js_val);

					if(!strnicmp(p,html_index_ext,strlen(html_index_ext))) {
						p+=strlen(html_index_ext);
						tp=strrchr(p,'$');
						if(tp!=NULL)
							*tp=0;
						if(!strnicmp(p,"ext=",4)) {
							p+=4;
							if(!strcmp(p,"on"))
								user.misc|=EXTDESC;
							else
								user.misc&=~EXTDESC;
							if(!(user.rest&FLAG('G')))
								putuserrec(&scfg,user.number,U_MISC,8,ultoa(user.misc,str,16));
						} 
						else if(!strnicmp(p,"sort=",5)) {
							p+=5;
							tp=strchr(p,'&');
							if(tp!=NULL) {
								*tp=0;
								tp++;
								if(!stricmp(tp,"reverse")) {
									js_val=BOOLEAN_TO_JSVAL(TRUE);
									JS_SetProperty(js_cx, js_ftp, "reverse", &js_val);
								}
							}
							if((js_str=JS_NewStringCopyZ(js_cx, p))!=NULL) {
								js_val=STRING_TO_JSVAL(js_str);
								JS_SetProperty(js_cx, js_ftp, "sort", &js_val);
							}
						}
					}

					js_val=BOOLEAN_TO_JSVAL(INT_TO_BOOL(user.misc&EXTDESC));
					JS_SetProperty(js_cx, js_ftp, "extended_descriptions", &js_val);

					JS_ENDREQUEST(js_cx);
#endif
					if((fp=fopen(ftp_tmpfname(fname,"html",sock),"w+b"))==NULL) {
						lprintf(LOG_ERR,"%04d !ERROR %d opening %s",sock,errno,fname);
						sockprintf(sock,sess, "451 Insufficient system storage");
						filepos=0;
						continue;
					}
					lprintf(LOG_INFO,"%04d %s downloading HTML index for %s in %s mode"
						,sock,user.alias,genvpath(lib,dir,str)
						,mode);
					credits=FALSE;
					tmpfile=TRUE;
					delfile=TRUE;
#ifdef JAVASCRIPT
					JS_BEGINREQUEST(js_cx);
					js_val=INT_TO_JSVAL(timeleft);
					if(!JS_SetProperty(js_cx, js_ftp, "time_left", &js_val))
						lprintf(LOG_ERR,"%04d !JavaScript ERROR setting user.time_left",sock);
					js_generate_index(js_cx, js_ftp, sock, fp, lib, dir, &user, &client);
					JS_ENDREQUEST(js_cx);
#endif
					fclose(fp);
				}
			} else if(dir>=0) {

				if(!chk_ar(&scfg,scfg.dir[dir]->ar,&user,&client)) {
					lprintf(LOG_WARNING,"%04d !%s has insufficient access to /%s/%s"
						,sock,user.alias
						,scfg.lib[scfg.dir[dir]->lib]->sname
						,scfg.dir[dir]->code_suffix);
					sockprintf(sock,sess,"550 Insufficient access.");
					filepos=0;
					continue;
				}

				if(!getsize && !getdate && !delecmd
					&& !chk_ar(&scfg,scfg.dir[dir]->dl_ar,&user,&client)) {
					lprintf(LOG_WARNING,"%04d !%s has insufficient access to download from /%s/%s"
						,sock,user.alias
						,scfg.lib[scfg.dir[dir]->lib]->sname
						,scfg.dir[dir]->code_suffix);
					sockprintf(sock,sess,"550 Insufficient access.");
					filepos=0;
					continue;
				}

				if(delecmd && !dir_op(&scfg,&user,&client,dir) && !(user.exempt&FLAG('R'))) {
					lprintf(LOG_WARNING,"%04d !%s has insufficient access to delete files in /%s/%s"
						,sock,user.alias
						,scfg.lib[scfg.dir[dir]->lib]->sname
						,scfg.dir[dir]->code_suffix);
					sockprintf(sock,sess,"550 Insufficient access.");
					filepos=0;
					continue;
				}
				SAFEPRINTF2(fname,"%s%s",scfg.dir[dir]->path,p);
#ifdef _WIN32
				GetShortPathName(fname, str, sizeof(str));
#else
				SAFECOPY(str,fname);
#endif
				padfname(getfname(str),f.name);
				f.dir=dir;
				f.cdt=0;
				f.size=-1;
				filedat=getfileixb(&scfg,&f);
				if(!filedat && !(startup->options&FTP_OPT_DIR_FILES) && !(scfg.dir[dir]->misc&DIR_FILES)) {
					sockprintf(sock,sess,"550 File not found: %s",p);
					lprintf(LOG_WARNING,"%04d !%s file (%s%s) not in database for %.4s command"
						,sock,user.alias,genvpath(lib,dir,str),p,cmd);
					filepos=0;
					continue;
				}

				/* Verify credits */
				if(!getsize && !getdate && !delecmd
					&& !is_download_free(&scfg,dir,&user,&client)) {
					if(filedat)
						getfiledat(&scfg,&f);
					else
						f.cdt=flength(fname);
					if(f.cdt>(user.cdt+user.freecdt)) {
						lprintf(LOG_WARNING,"%04d !%s has insufficient credit to download /%s/%s/%s (%lu credits)"
							,sock,user.alias,scfg.lib[scfg.dir[dir]->lib]->sname
							,scfg.dir[dir]->code_suffix
							,p
							,f.cdt);
						sockprintf(sock,sess,"550 Insufficient credit (%lu required).",f.cdt);
						filepos=0;
						continue;
					}
				}

				if(strcspn(p,ILLEGAL_FILENAME_CHARS)!=strlen(p)) {
					success=FALSE;
					lprintf(LOG_WARNING,"%04d !ILLEGAL FILENAME ATTEMPT by %s: %s"
						,sock,user.alias,p);
					ftp_hacklog("FTP FILENAME", user.alias, cmd, host_name, &ftp.client_addr);
				} else {
					if(fexistcase(fname)) {
						success=TRUE;
						if(!getsize && !getdate && !delecmd)
							lprintf(LOG_INFO,"%04d %s downloading: %s (%"PRIuOFF" bytes) in %s mode"
								,sock,user.alias,fname,flength(fname)
								,mode);
					} 
				}
			}
#if defined(SOCKET_DEBUG_DOWNLOAD)
			socket_debug[sock]|=SOCKET_DEBUG_DOWNLOAD;
#endif

			if(getsize && success)
				sockprintf(sock,sess,"213 %"PRIuOFF, flength(fname));
			else if(getdate && success) {
				if(file_date==0)
					file_date = fdate(fname);
				if(gmtime_r(&file_date,&tm)==NULL)	/* specifically use GMT/UTC representation */
					memset(&tm,0,sizeof(tm));
				sockprintf(sock,sess,"213 %u%02u%02u%02u%02u%02u"
					,1900+tm.tm_year,tm.tm_mon+1,tm.tm_mday
					,tm.tm_hour,tm.tm_min,tm.tm_sec);
			} else if(delecmd && success) {
				if(removecase(fname)!=0) {
					lprintf(LOG_ERR,"%04d !ERROR %d deleting %s",sock,errno,fname);
					sockprintf(sock,sess,"450 %s could not be deleted (error: %d)"
						,fname,errno);
				} else {
					lprintf(LOG_NOTICE,"%04d %s deleted %s",sock,user.alias,fname);
					if(filedat) 
						removefiledat(&scfg,&f);
					sockprintf(sock,sess,"250 %s deleted.",fname);
				}
			} else if(success) {
				sockprintf(sock,sess,"150 Opening BINARY mode data connection for file transfer.");
				filexfer(&data_addr,sock,sess,pasv_sock,pasv_sess,&data_sock,&data_sess,fname,filepos
					,&transfer_inprogress,&transfer_aborted,delfile,tmpfile
					,&lastactive,&user,&client,dir,FALSE,credits,FALSE,NULL,protection);
			}
			else {
				sockprintf(sock,sess,"550 File not found: %s",p);
				lprintf(LOG_WARNING,"%04d !%s file (%s%s) not found for %.4s command"
					,sock,user.alias,genvpath(lib,dir,str),p,cmd);
			}
			filepos=0;
#if defined(SOCKET_DEBUG_DOWNLOAD)
			socket_debug[sock]&=~SOCKET_DEBUG_DOWNLOAD;
#endif
			continue;
		}

		if(!strnicmp(cmd, "DESC", 4)) {

			if(user.rest&FLAG('U')) {
				sockprintf(sock,sess,"553 Insufficient access.");
				continue;
			}

			p=cmd+4;
			SKIP_WHITESPACE(p);

			if(*p==0) 
				sockprintf(sock,sess,"501 No file description given.");
			else {
				SAFECOPY(desc,p);
				sockprintf(sock,sess,"200 File description set. Ready to STOR file.");
			}
			continue;
		}

		if(!strnicmp(cmd, "STOR ", 5) || !strnicmp(cmd, "APPE ", 5)) {

			if(user.rest&FLAG('U')) {
				sockprintf(sock,sess,"553 Insufficient access.");
				continue;
			}

			if(transfer_inprogress==TRUE) {
				lprintf(LOG_WARNING,"%04d !TRANSFER already in progress (%s)",sock,cmd);
				sockprintf(sock,sess,"425 Transfer already in progress.");
				continue;
			}

			append=FALSE;
			lib=curlib;
			dir=curdir;
			p=cmd+5;

			SKIP_WHITESPACE(p);

			if(!strnicmp(p,BBS_FSYS_DIR,strlen(BBS_FSYS_DIR))) 
				p+=strlen(BBS_FSYS_DIR);	/* already mounted */

			if(*p=='/') {
				lib=-1;
				p++;
			}
			else if(!strncmp(p,"./",2))
				p+=2;
			/* Need to add support for uploading to aliased directories */
			if(lib<0 && (tp=strchr(p,'/'))!=NULL) {
				dir=-1;
				*tp=0;
				for(i=0;i<scfg.total_libs;i++) {
					if(!chk_ar(&scfg,scfg.lib[i]->ar,&user,&client))
						continue;
					if(!stricmp(scfg.lib[i]->sname,p))
						break;
				}
				if(i<scfg.total_libs) 
					lib=i;
				p=tp+1;
			}
			if(dir<0 && (tp=strchr(p,'/'))!=NULL) {
				*tp=0;
				for(i=0;i<scfg.total_dirs;i++) {
					if(scfg.dir[i]->lib!=lib)
						continue;
					if(i!=(int)scfg.sysop_dir && i!=(int)scfg.upload_dir 
						&& !chk_ar(&scfg,scfg.dir[i]->ar,&user,&client))
						continue;
					if(!stricmp(scfg.dir[i]->code_suffix,p))
						break;
				}
				if(i<scfg.total_dirs) 
					dir=i;
				p=tp+1;
			}
			if(dir<0) {
				sprintf(str,"%s.rep",scfg.sys_id);
				if(!(startup->options&FTP_OPT_ALLOW_QWK)
					|| stricmp(p,str)) {
					lprintf(LOG_WARNING,"%04d !%s attempted to upload to invalid directory"
						,sock,user.alias);
					sockprintf(sock,sess,"553 Invalid directory.");
					continue;
				}
				sprintf(fname,"%sfile/%04d.rep",scfg.data_dir,user.number);
				lprintf(LOG_INFO,"%04d %s uploading: %s in %s mode"
					,sock,user.alias,fname
					,mode);
			} else {

				append=(strnicmp(cmd,"APPE",4)==0);
			
				if(!dir_op(&scfg,&user,&client,dir) && !(user.exempt&FLAG('U'))) {
					if(!chk_ar(&scfg,scfg.dir[dir]->ul_ar,&user,&client)) {
						lprintf(LOG_WARNING,"%04d !%s cannot upload to /%s/%s (insufficient access)"
							,sock,user.alias
							,scfg.lib[scfg.dir[dir]->lib]->sname
							,scfg.dir[dir]->code_suffix);
						sockprintf(sock,sess,"553 Insufficient access.");
						continue;
					}

					if(!append && scfg.dir[dir]->maxfiles && getfiles(&scfg,dir)>=scfg.dir[dir]->maxfiles) {
						lprintf(LOG_WARNING,"%04d !%s cannot upload to /%s/%s (directory full: %u files)"
							,sock,user.alias
							,scfg.lib[scfg.dir[dir]->lib]->sname
							,scfg.dir[dir]->code_suffix
							,getfiles(&scfg,dir));
						sockprintf(sock,sess,"553 Directory full.");
						continue;
					}
				}
				if(*p=='-'
					|| strcspn(p,ILLEGAL_FILENAME_CHARS)!=strlen(p)
					|| trashcan(&scfg,p,"file")) {
					lprintf(LOG_WARNING,"%04d !ILLEGAL FILENAME ATTEMPT by %s: %s"
						,sock,user.alias,p);
					sockprintf(sock,sess,"553 Illegal filename attempt");
					ftp_hacklog("FTP FILENAME", user.alias, cmd, host_name, &ftp.client_addr);
					continue;
				}
				SAFEPRINTF2(fname,"%s%s",scfg.dir[dir]->path,p);
				if((!append && filepos==0 && fexist(fname))
					|| (startup->options&FTP_OPT_INDEX_FILE 
						&& !stricmp(p,startup->index_file_name))
					|| (startup->options&FTP_OPT_HTML_INDEX_FILE 
						&& !stricmp(p,startup->html_index_file))
					) {
					lprintf(LOG_WARNING,"%04d !%s attempted to overwrite existing file: %s"
						,sock,user.alias,fname);
					sockprintf(sock,sess,"553 File already exists.");
					continue;
				}
				if(append || filepos) {	/* RESUME */
#ifdef _WIN32
					GetShortPathName(fname, str, sizeof(str));
#else
					SAFECOPY(str,fname);
#endif
					padfname(getfname(str),f.name);
					f.dir=dir;
					f.cdt=0;
					f.size=-1;
					if(!getfileixb(&scfg,&f) || !getfiledat(&scfg,&f)) {
						if(filepos) {
							lprintf(LOG_WARNING,"%04d !%s file (%s) not in database for %.4s command"
								,sock,user.alias,fname,cmd);
							sockprintf(sock,sess,"550 File not found: %s",p);
							continue;
						}
						append=FALSE;
					}
					/* Verify user is original uploader */
					if((append || filepos) && stricmp(f.uler,user.alias)) {
						lprintf(LOG_WARNING,"%04d !%s cannot resume upload of %s, uploaded by %s"
							,sock,user.alias,fname,f.uler);
						sockprintf(sock,sess,"553 Insufficient access (can't resume upload from different user).");
						continue;
					}
				}
				lprintf(LOG_INFO,"%04d %s uploading: %s to %s (%s) in %s mode"
					,sock,user.alias
					,p						/* filename */
					,genvpath(lib,dir,str)	/* virtual path */
					,scfg.dir[dir]->path	/* actual path */
					,mode);
			}
			sockprintf(sock,sess,"150 Opening BINARY mode data connection for file transfer.");
			filexfer(&data_addr,sock,sess,pasv_sock,pasv_sess,&data_sock,&data_sess,fname,filepos
				,&transfer_inprogress,&transfer_aborted,FALSE,FALSE
				,&lastactive
				,&user
				,&client
				,dir
				,TRUE	/* uploading */
				,TRUE	/* credits */
				,append
				,desc
				,protection
				);
			filepos=0;
			continue;
		}

		if(!stricmp(cmd,"CDUP") || !stricmp(cmd,"XCUP")) {
			if(curdir<0)
				curlib=-1;
			else
				curdir=-1;
			sockprintf(sock,sess,"200 CDUP command successful.");
			continue;
		}

		if(!strnicmp(cmd, "CWD ", 4) || !strnicmp(cmd,"XCWD ",5)) {
			p=cmd+4;
			SKIP_WHITESPACE(p);

			if(!strnicmp(p,BBS_FSYS_DIR,strlen(BBS_FSYS_DIR))) 
				p+=strlen(BBS_FSYS_DIR);	/* already mounted */

			if(*p=='/') {
				curlib=-1;
				curdir=-1;
				p++;
			}
			/* Local File System? */
			if(sysop && !(startup->options&FTP_OPT_NO_LOCAL_FSYS) 
				&& !strnicmp(p,LOCAL_FSYS_DIR,strlen(LOCAL_FSYS_DIR))) {	
				p+=strlen(LOCAL_FSYS_DIR);
				if(!direxist(p)) {
					sockprintf(sock,sess,"550 Directory does not exist.");
					lprintf(LOG_WARNING,"%04d !%s attempted to mount invalid directory: %s"
						,sock, user.alias, p);
					continue;
				}
				SAFECOPY(local_dir,p);
				local_fsys=TRUE;
				sockprintf(sock,sess,"250 CWD command successful (local file system mounted).");
				lprintf(LOG_INFO,"%04d %s mounted local file system", sock, user.alias);
				continue;
			}
			success=FALSE;

			/* Directory Alias? */
			if(curlib<0 && ftpalias(p,NULL,&user,&client,&curdir)==TRUE) {
				if(curdir>=0)
					curlib=scfg.dir[curdir]->lib;
				success=TRUE;
			}

			orglib=curlib;
			orgdir=curdir;
			tp=0;
			if(!strncmp(p,"...",3)) {
				curlib=-1;
				curdir=-1;
				p+=3;
			}
			if(!strncmp(p,"./",2))
				p+=2;
			else if(!strncmp(p,"..",2)) {
				if(curdir<0)
					curlib=-1;
				else
					curdir=-1;
				p+=2;
			}
			if(*p==0)
				success=TRUE;
			else if(!strcmp(p,".")) 
				success=TRUE;
			if(!success  && (curlib<0 || *p=='/')) { /* Root dir */
				if(*p=='/') p++;
				tp=strchr(p,'/');
				if(tp) *tp=0;
				for(i=0;i<scfg.total_libs;i++) {
					if(!chk_ar(&scfg,scfg.lib[i]->ar,&user,&client))
						continue;
					if(!stricmp(scfg.lib[i]->sname,p))
						break;
				}
				if(i<scfg.total_libs) {
					curlib=i;
					success=TRUE;
				}
			}
			if((!success && curdir<0) || (success && tp && *(tp+1))) {
				if(tp)
					p=tp+1;
				tp=lastchar(p);
				if(tp && *tp=='/') *tp=0;
				for(i=0;i<scfg.total_dirs;i++) {
					if(scfg.dir[i]->lib!=curlib)
						continue;
					if(i!=(int)scfg.sysop_dir && i!=(int)scfg.upload_dir
						&& !chk_ar(&scfg,scfg.dir[i]->ar,&user,&client))
						continue;
					if(!stricmp(scfg.dir[i]->code_suffix,p))
						break;
				}
				if(i<scfg.total_dirs) {
					curdir=i;
					success=TRUE;
				} else
					success=FALSE;
			}

			if(success)
				sockprintf(sock,sess,"250 CWD command successful.");
			else {
				sockprintf(sock,sess,"550 %s: No such file or directory.",p);
				curlib=orglib;
				curdir=orgdir;
			}
			continue;
		}

		if(!stricmp(cmd, "PWD") || !stricmp(cmd,"XPWD")) {
			if(curlib<0)
				sockprintf(sock,sess,"257 \"/\" is current directory.");
			else if(curdir<0)
				sockprintf(sock,sess,"257 \"/%s\" is current directory."
					,scfg.lib[curlib]->sname);
			else
				sockprintf(sock,sess,"257 \"/%s/%s\" is current directory."
					,scfg.lib[curlib]->sname
					,scfg.dir[curdir]->code_suffix);
			continue;
		}

		if(!strnicmp(cmd, "MKD", 3) || 
			!strnicmp(cmd,"XMKD",4) || 
			!strnicmp(cmd,"SITE EXEC",9)) {
			lprintf(LOG_WARNING,"%04d !SUSPECTED HACK ATTEMPT by %s: '%s'"
				,sock,user.alias,cmd);
			ftp_hacklog("FTP", user.alias, cmd, host_name, &ftp.client_addr);
		}		
		// TODO: STAT is mandatory
		sockprintf(sock,sess,"500 Syntax error: '%s'",cmd);
		lprintf(LOG_WARNING,"%04d !UNSUPPORTED COMMAND from %s: '%s'"
			,sock,user.alias,cmd);
	} /* while(1) */

#if defined(SOCKET_DEBUG_TERMINATE)
	socket_debug[sock]|=SOCKET_DEBUG_TERMINATE;
#endif

	if(transfer_inprogress==TRUE) {
		lprintf(LOG_DEBUG,"%04d Waiting for transfer to complete...",sock);
		count=0;
		while(transfer_inprogress==TRUE) {
			if(ftp_set==NULL || terminate_server) {
				mswait(2000);	/* allow xfer threads to terminate */
				break;
			}
			if(!transfer_aborted) {
				if(gettimeleft(&scfg,&user,logintime)<1) {
					lprintf(LOG_WARNING,"%04d Out of time, disconnecting",sock);
					sockprintf(sock,sess,"421 Sorry, you've run out of time.");
					ftp_close_socket(&data_sock,&data_sess,__LINE__);
					transfer_aborted=TRUE;
				}
				if((time(NULL)-lastactive)>startup->max_inactivity) {
					lprintf(LOG_WARNING,"%04d Disconnecting due to to inactivity",sock);
					sockprintf(sock,sess,"421 Disconnecting due to inactivity (%u seconds)."
						,startup->max_inactivity);
					ftp_close_socket(&data_sock,&data_sess,__LINE__);
					transfer_aborted=TRUE;
				}
			}
			if(count && (count%60)==0)
				lprintf(LOG_WARNING,"%04d Still waiting for transfer to complete "
					"(count=%lu, aborted=%d, lastactive=%lX) ..."
					,sock,count,transfer_aborted,lastactive);
			count++;
			mswait(1000);
		}
		lprintf(LOG_DEBUG,"%04d Done waiting for transfer to complete",sock);
	}

	if(user.number) {
		/* Update User Statistics */
		if(!logoutuserdat(&scfg, &user, time(NULL), logintime))
			lprintf(LOG_ERR,"%04d !ERROR in logoutuserdat",sock);
		/* Remove QWK-pack semaphore file (if left behind) */
		sprintf(str,"%spack%04u.now",scfg.data_dir,user.number);
		ftp_remove(sock, __LINE__, str);
		lprintf(LOG_INFO,"%04d %s logged off",sock,user.alias);
	}

#ifdef _WIN32
	if(startup->hangup_sound[0] && !(startup->options&FTP_OPT_MUTE)) 
		PlaySound(startup->hangup_sound, NULL, SND_ASYNC|SND_FILENAME);
#endif

#ifdef JAVASCRIPT
	if(js_cx!=NULL) {
		lprintf(LOG_DEBUG,"%04d JavaScript: Destroying context",sock);
		JS_BEGINREQUEST(js_cx);
		JS_RemoveObjectRoot(js_cx, &js_glob);
		JS_ENDREQUEST(js_cx);
		JS_DestroyContext(js_cx);	/* Free Context */
	}

	if(js_runtime!=NULL) {
		lprintf(LOG_DEBUG,"%04d JavaScript: Destroying runtime",sock);
		jsrt_Release(js_runtime);
	}

#endif

/*	status(STATUS_WFC); server thread should control status display */

	if(pasv_sock!=INVALID_SOCKET)
		ftp_close_socket(&pasv_sock,&pasv_sess,__LINE__);
	if(data_sock!=INVALID_SOCKET)
		ftp_close_socket(&data_sock,&data_sess,__LINE__);

	client_off(sock);

#ifdef SOCKET_DEBUG_CTRL
	socket_debug[sock]&=~SOCKET_DEBUG_CTRL;
#endif

#if defined(SOCKET_DEBUG_TERMINATE)
	socket_debug[sock]&=~SOCKET_DEBUG_TERMINATE;
#endif

	tmp_sock=sock;
	ftp_close_socket(&tmp_sock,&sess,__LINE__);

	{
		int32_t	clients = protected_uint32_adjust(&active_clients, -1);
		int32_t	threads = thread_down();
		update_clients();

		lprintf(LOG_INFO,"%04d CTRL thread terminated (%ld clients and %ld threads remain, %lu served)"
			,sock, clients, threads, served);
	}
}

static void cleanup(int code, int line)
{
#ifdef _DEBUG
	lprintf(LOG_DEBUG,"0000 cleanup called from line %d",line);
#endif

	if(protected_uint32_value(thread_count) > 1) {
		lprintf(LOG_DEBUG,"#### FTP Server waiting for %d child threads to terminate", protected_uint32_value(thread_count)-1);
		while(protected_uint32_value(thread_count) > 1) {
			mswait(100);
		}
	}

	free_cfg(&scfg);
	free_text(text);

	semfile_list_free(&recycle_semfiles);
	semfile_list_free(&shutdown_semfiles);

	if(ftp_set != NULL) {
		xpms_destroy(ftp_set, ftp_close_socket_cb, NULL);
		ftp_set = NULL;
	}

	update_clients();	/* active_clients is destroyed below */

	if(protected_uint32_value(active_clients))
		lprintf(LOG_WARNING,"#### !FTP Server terminating with %ld active clients", protected_uint32_value(active_clients));
	else
		protected_uint32_destroy(active_clients);

#ifdef _WINSOCKAPI_
	if(WSAInitialized && WSACleanup()!=0) 
		lprintf(LOG_ERR,"0000 !WSACleanup ERROR %d",ERROR_VALUE);
#endif

	thread_down();
	status("Down");
	if(terminate_server || code)
		lprintf(LOG_INFO,"#### FTP Server thread terminated (%lu clients served)", served);
	if(startup!=NULL && startup->terminated!=NULL)
		startup->terminated(startup->cbdata,code);
}

const char* DLLCALL ftp_ver(void)
{
	static char ver[256];
	char compiler[32];

	DESCRIBE_COMPILER(compiler);

	sscanf("$Revision$", "%*s %s", revision);

	sprintf(ver,"%s %s%s  "
		"Compiled %s %s with %s"
		,FTP_SERVER
		,revision
#ifdef _DEBUG
		," Debug"
#else
		,""
#endif
		,__DATE__, __TIME__, compiler);

	return(ver);
}

void DLLCALL ftp_server(void* arg)
{
	char*			p;
	char			path[MAX_PATH+1];
	char			error[256];
	char			compiler[32];
	char			str[256];
	union xp_sockaddr client_addr;
	socklen_t		client_addr_len;
	SOCKET			client_socket;
	int				i;
	time_t			t;
	time_t			start;
	time_t			initialized=0;
	ftp_t*			ftp;
	char			client_ip[INET6_ADDRSTRLEN];
	CRYPT_SESSION		none = -1;

	ftp_ver();

	startup=(ftp_startup_t*)arg;
	SetThreadName("sbbs/ftpServer");

#ifdef _THREAD_SUID_BROKEN
	if(thread_suid_broken)
		startup->seteuid(TRUE);
#endif

    if(startup==NULL) {
    	sbbs_beep(100,500);
    	fprintf(stderr, "No startup structure passed!\n");
    	return;
    }

	if(startup->size!=sizeof(ftp_startup_t)) {	/* verify size */
		sbbs_beep(100,500);
		sbbs_beep(300,500);
		sbbs_beep(100,500);
		fprintf(stderr, "Invalid startup structure!\n");
		return;
	}

	ZERO_VAR(js_server_props);
	SAFEPRINTF2(js_server_props.version,"%s %s",FTP_SERVER,revision);
	js_server_props.version_detail=ftp_ver();
	js_server_props.clients=&active_clients.value;
	js_server_props.options=&startup->options;
	js_server_props.interfaces=&startup->interfaces;

	uptime=0;
	served=0;
	startup->recycle_now=FALSE;
	startup->shutdown_now=FALSE;
	terminate_server=FALSE;
	protected_uint32_init(&thread_count, 0);

	do {
		/* Setup intelligent defaults */
		if(startup->port==0)					startup->port=IPPORT_FTP;
		if(startup->qwk_timeout==0)				startup->qwk_timeout=FTP_DEFAULT_QWK_TIMEOUT;		/* seconds */
		if(startup->max_inactivity==0)			startup->max_inactivity=FTP_DEFAULT_MAX_INACTIVITY;	/* seconds */
		if(startup->sem_chk_freq==0)			startup->sem_chk_freq=DEFAULT_SEM_CHK_FREQ;		/* seconds */
		if(startup->index_file_name[0]==0)		SAFECOPY(startup->index_file_name,"00index");
		if(startup->html_index_file[0]==0)		SAFECOPY(startup->html_index_file,"00index.html");
		if(startup->html_index_script[0]==0)	SAFECOPY(startup->html_index_script,"ftp-html.js");
		if(startup->options&FTP_OPT_HTML_INDEX_FILE)
			startup->options&=~FTP_OPT_NO_JAVASCRIPT;
		else
			startup->options|=FTP_OPT_NO_JAVASCRIPT;
		if(startup->js.max_bytes==0)			startup->js.max_bytes=JAVASCRIPT_MAX_BYTES;
		if(startup->js.cx_stack==0)				startup->js.cx_stack=JAVASCRIPT_CONTEXT_STACK;

		protected_uint32_adjust(&thread_count,1);
		thread_up(FALSE /* setuid */);

		status("Initializing");

		memset(&scfg, 0, sizeof(scfg));

		lprintf(LOG_INFO,"Synchronet FTP Server Revision %s%s"
			,revision
#ifdef _DEBUG
			," Debug"
#else
			,""
#endif
			);

		DESCRIBE_COMPILER(compiler);

		lprintf(LOG_INFO,"Compiled %s %s with %s", __DATE__, __TIME__, compiler);

		sbbs_srand();	/* Seed random number generator */

		if(!winsock_startup()) {
			cleanup(1,__LINE__);
			break;
		}

		t=time(NULL);
		lprintf(LOG_INFO,"Initializing on %.24s with options: %lx"
			,ctime_r(&t,str),startup->options);

		if(chdir(startup->ctrl_dir)!=0)
			lprintf(LOG_ERR,"!ERROR %d changing directory to: %s", errno, startup->ctrl_dir);

		/* Initial configuration and load from CNF files */
		SAFECOPY(scfg.ctrl_dir, startup->ctrl_dir);
		lprintf(LOG_INFO,"Loading configuration files from %s", scfg.ctrl_dir);
		scfg.size=sizeof(scfg);
		SAFECOPY(error,UNKNOWN_LOAD_ERROR);
		if(!load_cfg(&scfg, text, TRUE, error)) {
			lprintf(LOG_CRIT,"!ERROR %s",error);
			lprintf(LOG_CRIT,"!Failed to load configuration files");
			cleanup(1,__LINE__);
			break;
		}

		if(startup->host_name[0]==0)
			SAFECOPY(startup->host_name,scfg.sys_inetaddr);

		if((t=checktime())!=0) {   /* Check binary time */
			lprintf(LOG_ERR,"!TIME PROBLEM (%ld)",t);
		}

		if(uptime==0)
			uptime=time(NULL);	/* this must be done *after* setting the timezone */

		if(startup->temp_dir[0])
			SAFECOPY(scfg.temp_dir,startup->temp_dir);
		else
			SAFECOPY(scfg.temp_dir,"../temp");
	   	prep_dir(scfg.ctrl_dir, scfg.temp_dir, sizeof(scfg.temp_dir));
		if(!isdir(scfg.temp_dir) && MKDIR(scfg.temp_dir) != 0) {
			lprintf(LOG_ERR, "Error %d creating temp directory: %s", errno, scfg.temp_dir);
			cleanup(1,__LINE__);
			break;
		}
		lprintf(LOG_DEBUG,"Temporary file directory: %s", scfg.temp_dir);
		if(!isdir(scfg.temp_dir)) {
			lprintf(LOG_CRIT,"!Invalid temp directory: %s", scfg.temp_dir);
			cleanup(1,__LINE__);
			break;
		}

		if(!startup->max_clients) {
			startup->max_clients=scfg.sys_nodes;
			if(startup->max_clients<10)
				startup->max_clients=10;
		}
		lprintf(LOG_DEBUG,"Maximum clients: %d",startup->max_clients);

		/* Sanity-check the passive port range */
		if(startup->pasv_port_low || startup->pasv_port_high) {
			if(startup->pasv_port_low > startup->pasv_port_high
				|| startup->pasv_port_high-startup->pasv_port_low < (startup->max_clients-1)) {
				lprintf(LOG_WARNING,"!Correcting Passive Port Range (Low: %u, High: %u)"
					,startup->pasv_port_low,startup->pasv_port_high);
				if(startup->pasv_port_low)
					startup->pasv_port_high = startup->pasv_port_low+(startup->max_clients-1);
				else
					startup->pasv_port_low = startup->pasv_port_high-(startup->max_clients-1);
			}
			lprintf(LOG_DEBUG,"Passive Port Low: %u",startup->pasv_port_low);
			lprintf(LOG_DEBUG,"Passive Port High: %u",startup->pasv_port_high);
		}

		lprintf(LOG_DEBUG,"Maximum inactivity: %d seconds",startup->max_inactivity);

		protected_uint32_init(&active_clients, 0);
		update_clients();

		strlwr(scfg.sys_id); /* Use lower-case unix-looking System ID for group name */

		for(i=0;i<scfg.total_libs;i++) {
			strlwr(scfg.lib[i]->sname);
			dotname(scfg.lib[i]->sname,scfg.lib[i]->sname);
		}
		/* open a socket and wait for a client */
		ftp_set = xpms_create(startup->bind_retry_count, startup->bind_retry_delay, lprintf);
		
		if(ftp_set == NULL) {
			lprintf(LOG_CRIT,"!ERROR %d creating FTP socket set", ERROR_VALUE);
			cleanup(1, __LINE__);
			return;
		}
		lprintf(LOG_DEBUG,"FTP Server socket set created");

		/*
		 * Add interfaces
		 */
		xpms_add_list(ftp_set, PF_UNSPEC, SOCK_STREAM, 0, startup->interfaces, startup->port, "FTP Server", ftp_open_socket_cb, startup->seteuid, NULL);

		lprintf(LOG_INFO,"FTP Server listening");
		status(STATUS_WFC);

		/* Setup recycle/shutdown semaphore file lists */
		shutdown_semfiles=semfile_list_init(scfg.ctrl_dir,"shutdown","ftp");
		recycle_semfiles=semfile_list_init(scfg.ctrl_dir,"recycle","ftp");
		semfile_list_add(&recycle_semfiles,startup->ini_fname);
		SAFEPRINTF(path,"%sftpsrvr.rec",scfg.ctrl_dir);	/* legacy */
		semfile_list_add(&recycle_semfiles,path);
		if(!initialized) {
			semfile_list_check(&initialized,recycle_semfiles);
			semfile_list_check(&initialized,shutdown_semfiles);
		}

		/* signal caller that we've started up successfully */
		if(startup->started!=NULL)
    		startup->started(startup->cbdata);

		lprintf(LOG_INFO,"FTP Server thread started");

		while(ftp_set!=NULL && !terminate_server) {
			YIELD();
			if(protected_uint32_value(thread_count) <= 1) {
				if(!(startup->options&FTP_OPT_NO_RECYCLE)) {
					if((p=semfile_list_check(&initialized,recycle_semfiles))!=NULL) {
						lprintf(LOG_INFO,"0000 Recycle semaphore file (%s) detected",p);
						break;
					}
					if(startup->recycle_now==TRUE) {
						lprintf(LOG_NOTICE,"0000 Recycle semaphore signaled");
						startup->recycle_now=FALSE;
						break;
					}
				}
				if(((p=semfile_list_check(&initialized,shutdown_semfiles))!=NULL
						&& lprintf(LOG_INFO,"0000 Shutdown semaphore file (%s) detected",p))
					|| (startup->shutdown_now==TRUE
						&& lprintf(LOG_INFO,"0000 Shutdown semaphore signaled"))) {
					startup->shutdown_now=FALSE;
					terminate_server=TRUE;
					break;
				}
			}

			if(ftp_set==NULL || terminate_server)	/* terminated */
				break;

			/* now wait for connection */
			client_addr_len = sizeof(client_addr);
			client_socket = xpms_accept(ftp_set, &client_addr, &client_addr_len, startup->sem_chk_freq*1000, NULL);

			if(client_socket == INVALID_SOCKET)
				continue;

			if(startup->socket_open!=NULL)
				startup->socket_open(startup->cbdata,TRUE);

			inet_addrtop(&client_addr, client_ip, sizeof(client_ip));
			if(trashcan(&scfg,client_ip,"ip-silent")) {
				ftp_close_socket(&client_socket,&none,__LINE__);
				continue;
			}
			
			if(protected_uint32_value(active_clients)>=startup->max_clients) {
				lprintf(LOG_WARNING,"%04d !MAXIMUM CLIENTS (%d) reached, access denied"
					,client_socket, startup->max_clients);
				sockprintf(client_socket,-1,"421 Maximum active clients reached, please try again later.");
				mswait(3000);
				ftp_close_socket(&client_socket,&none,__LINE__);
				continue;
			}

			if((ftp=malloc(sizeof(ftp_t)))==NULL) {
				lprintf(LOG_CRIT,"%04d !ERROR allocating %d bytes of memory for ftp_t"
					,client_socket,sizeof(ftp_t));
				sockprintf(client_socket,-1,"421 System error, please try again later.");
				mswait(3000);
				ftp_close_socket(&client_socket,&none,__LINE__);
				continue;
			}

			ftp->socket=client_socket;
			memcpy(&ftp->client_addr, &client_addr, client_addr_len);
			ftp->client_addr_len = client_addr_len;

			protected_uint32_adjust(&thread_count,1);
			_beginthread(ctrl_thread, 0, ftp);
			served++;
		}

#if 0 /* def _DEBUG */
		lprintf(LOG_DEBUG,"0000 terminate_server: %d",terminate_server);
#endif
		if(protected_uint32_value(active_clients)) {
			lprintf(LOG_DEBUG,"Waiting for %d active clients to disconnect..."
				, protected_uint32_value(active_clients));
			start=time(NULL);
			while(protected_uint32_value(active_clients)) {
				if(time(NULL)-start>startup->max_inactivity) {
					lprintf(LOG_WARNING,"!TIMEOUT waiting for %d active clients"
						, protected_uint32_value(active_clients));
					break;
				}
				mswait(100);
			}
		}

		cleanup(0,__LINE__);

		if(!terminate_server) {
			lprintf(LOG_INFO,"Recycling server...");
			mswait(2000);
			if(startup->recycle!=NULL)
				startup->recycle(startup->cbdata);
		}

	} while(!terminate_server);

	protected_uint32_destroy(thread_count);
}