fteqw/engine/http/ftpserver.c
Spoike 9ae7e2621d SOFTWARE RENDERING IS BROKEN: DO NOT USE ASM VERSION.
Lots of changes.
CSQC should be functional, but is still tied to debug builds. It WILL have some bugs still, hopefully I'll be able to clean them up better if people test it a bit.
Precompiled headers are working properly now. Compile times are now much quicker in msvc. This takes most of the files this commit.
Restructured how client commands work. They're buffered outside the network message, some multithreaded code is in. It needs a bit of testing before it's active.


git-svn-id: https://svn.code.sf.net/p/fteqw/code/trunk@885 fc73d0e0-1445-4013-8a0c-d673dee63da5
2005-02-28 07:16:19 +00:00

879 lines
20 KiB
C

#include "quakedef.h"
#ifdef WEBSERVER
#include "iweb.h"
//hows this as a bug.
//TCP data can travel at different speeds.
//If the later bits of a data channel arrive after the message saying that a transfer was compleate,
//the later bits of the file may not arrive before the client closes the conenction.
//this is a major bug and can prevent the server from giving files at a high pl/ping
#ifdef _WIN32
#define EWOULDBLOCK WSAEWOULDBLOCK
#define EMSGSIZE WSAEMSGSIZE
#define ECONNRESET WSAECONNRESET
#define ECONNABORTED WSAECONNABORTED
#define ECONNREFUSED WSAECONNREFUSED
#define EADDRNOTAVAIL WSAEADDRNOTAVAIL
#define qerrno WSAGetLastError()
#else
#define qerrno errno
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/uio.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#ifdef sun
#include <sys/filio.h>
#endif
#ifdef NeXT
#include <libc.h>
#endif
#define closesocket close
#define ioctlsocket ioctl
#endif
char *COM_ParseOut (char *data, char *out, int outlen);
static iwboolean ftpserverinitied = false;
static int ftpserversocket;
typedef struct FTPclient_s{
char name[64];
char pwd[64];
int auth; //has it got auth?
char path[256];
char commandbuffer[256];
char messagebuffer[256];
int cmdbuflen;
int msgbuflen;
int controlsock;
int datasock; //FTP only allows one transfer per connection.
int dataislisten;
int datadir; //0 no data, 1 reading, 2 writing
IWEBFILE *file;
unsigned long blocking;
struct FTPclient_s *next;
} FTPclient_t;
FTPclient_t *FTPclient;
void FTP_ServerInit(void)
{
struct sockaddr_in address;
unsigned long _true = true;
int i;
int port = 21;
if ((ftpserversocket = socket (PF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1)
{
Sys_Error ("FTP_TCP_OpenSocket: socket:", strerror(qerrno));
}
if (ioctlsocket (ftpserversocket, FIONBIO, &_true) == -1)
{
Sys_Error ("FTP_TCP_OpenSocket: ioctl FIONBIO:", strerror(qerrno));
}
address.sin_family = AF_INET;
//ZOID -- check for interface binding option
if ((i = COM_CheckParm("-ip")) != 0 && i < com_argc) {
address.sin_addr.s_addr = inet_addr(com_argv[i+1]);
Con_TPrintf(TL_NETBINDINTERFACE,
inet_ntoa(address.sin_addr));
} else
address.sin_addr.s_addr = INADDR_ANY;
if (port == PORT_ANY)
address.sin_port = 0;
else
address.sin_port = htons((short)port);
if( bind (ftpserversocket, (void *)&address, sizeof(address)) == -1)
{
closesocket(ftpserversocket);
return;
}
listen(ftpserversocket, 3);
ftpserverinitied = true;
IWebPrintf("FTP server is running\n");
return;
}
void FTP_ServerShutdown(void)
{
closesocket(ftpserversocket);
ftpserverinitied = false;
IWebPrintf("FTP server is deactivated\n");
}
static int SendFileNameTo(char *fname, int size, void *param)
{
int socket = (int)param; //64->32... this is safe due to where it's called from. It's just not so portable.
// int i;
char buffer[256+1];
char *slash;
int isdir = fname[strlen(fname)-1] == '/';
#ifndef WEBSVONLY //copy protection of the like that QWSV normally has.
if (!isdir)
if (!SV_AllowDownload(fname)) //don't advertise if we're going to disallow it
return true;
#endif
if (isdir)
fname[strlen(fname)-1] = '\0';
while((slash = strchr(fname, '/')))
fname = slash+1;
if (isdir)
sprintf(buffer, "drw-r--r--\t1\troot\troot\t%8i Jan 1 12:00 %s\r\n", size, fname);
else
sprintf(buffer, "-rw-r--r--\t1\troot\troot\t%8i Jan 1 12:00 %s\r\n", size, fname);
// strcpy(buffer, fname);
// for (i = strlen(buffer); i < 40; i+=8)
// strcat(buffer, "\t");
send(socket, buffer, strlen(buffer), 0);
return true;
}
int FTP_SV_makelistensocket(unsigned long blocking)
{
char name[256];
int sock;
struct hostent *hent;
struct sockaddr_in address;
// int fromlen;
address.sin_family = AF_INET;
if (gethostname(name, sizeof(name)) == -1)
return INVALID_SOCKET;
hent = gethostbyname(name);
if (!hent)
return INVALID_SOCKET;
address.sin_addr.s_addr = *(int *)(hent->h_addr_list[0]);
address.sin_port = 0;
if ((sock = socket (PF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1)
{
Sys_Error ("FTP_TCP_OpenSocket: socket:", strerror(qerrno));
}
if (ioctlsocket (sock, FIONBIO, &blocking) == -1)
{
Sys_Error ("FTP_TCP_OpenSocket: ioctl FIONBIO:", strerror(qerrno));
}
if( bind (sock, (void *)&address, sizeof(address)) == -1)
{
closesocket(sock);
return INVALID_SOCKET;
}
listen(sock, 2);
return sock;
}
iwboolean FTP_SVSocketToString (int socket, char *s)
{
struct sockaddr_in addr;
int adrlen = sizeof(addr);
if (getsockname(socket, (struct sockaddr*)&addr, &adrlen) == -1)
return false;
sprintf(s, "%i,%i,%i,%i,%i,%i", ((qbyte *)&addr.sin_addr)[0], ((qbyte *)&addr.sin_addr)[1], ((qbyte *)&addr.sin_addr)[2], ((qbyte *)&addr.sin_addr)[3], ((qbyte *)&addr.sin_port)[0], ((qbyte *)&addr.sin_port)[1]);
return true;
}
iwboolean FTP_SVRemoteSocketToString (int socket, char *s)
{
struct sockaddr_in addr;
int adrlen = sizeof(addr);
addr.sin_family = AF_INET;
if (getpeername(socket, (struct sockaddr*)&addr, &adrlen) == -1)
return false;
sprintf(s, "%i,%i,%i,%i,%i,%i", ((qbyte *)&addr.sin_addr)[0], ((qbyte *)&addr.sin_addr)[1], ((qbyte *)&addr.sin_addr)[2], ((qbyte *)&addr.sin_addr)[3], ((qbyte *)&addr.sin_port)[0], ((qbyte *)&addr.sin_port)[1]);
return true;
}
/*
* Responsable for sending all control server -> client messages.
* Queues the message if it cannot send now.
* Kicks if too big a queue.
*/
void QueueMessage(FTPclient_t *cl, char *msg)
{
if (send (cl->controlsock, msg, strlen(msg), 0) == -1)
{ //wasn't sent
if (strlen(msg) + strlen(cl->messagebuffer) >= sizeof(cl->messagebuffer)-1)
closesocket(cl->controlsock); //but don't mark it as closed, so we get errors later (for this is how we shall tell).
strcat(cl->messagebuffer, msg);
}
}
void VARGS QueueMessageva(FTPclient_t *cl, char *fmt, ...)
{
va_list argptr;
char msg[1024];
va_start (argptr, fmt);
_vsnprintf (msg,sizeof(msg)-1, fmt,argptr);
va_end (argptr);
if (send (cl->controlsock, msg, strlen(msg), 0) == -1)
{ //wasn't sent
if (strlen(msg) + strlen(cl->messagebuffer) >= sizeof(cl->messagebuffer)-1)
closesocket(cl->controlsock); //but don't mark it as closed, so we get errors later (for this is how we shall tell).
strcat(cl->messagebuffer, msg);
}
}
iwboolean FTP_ServerThinkForConnection(FTPclient_t *cl)
{
int ret;
struct sockaddr_in from;
int fromlen;
char *msg, *line;
char mode[64];
static char resource[8192];
if (cl->datadir == 1)
{
int pos, sent;
int ammount, wanted = sizeof(resource);
pos = IWebFTell(cl->file);
ammount = IWebFRead(resource, 1, wanted, cl->file);
sent = send(cl->datasock, resource, ammount, 0);
if (sent == -1)
{
IWebFSeek(cl->file, pos, SEEK_SET);
if (qerrno != EWOULDBLOCK)
{
closesocket(cl->datasock);
cl->datasock = INVALID_SOCKET;
IWebFClose(cl->file);
cl->file = NULL;
QueueMessage (cl, "226 Transfer complete .\r\n");
cl->datadir = 0;
}
}
else
{
if (sent != ammount)
IWebFSeek(cl->file, pos + sent, SEEK_SET);
if (ammount != wanted && sent == ammount) //file is over
{
send(cl->datasock, resource, 0, 0);
send(cl->datasock, resource, 0, 0);
send(cl->datasock, resource, 0, 0);
closesocket(cl->datasock);
cl->datasock = INVALID_SOCKET;
IWebFClose(cl->file);
cl->file = NULL;
QueueMessage (cl, "226 Transfer complete .\r\n");
cl->datadir = 0;
}
}
}
else if (cl->datadir == 2)
{
int len;
while((len = recv(cl->datasock, resource, sizeof(resource), 0)) >0 )
{
IWebFWrite(resource, len, 1, cl->file);
}
if (len == -1)
{
if (qerrno != EWOULDBLOCK)
{
closesocket(cl->datasock);
cl->datasock = INVALID_SOCKET;
if (cl->file)
IWebFClose(cl->file);
cl->file = NULL;
QueueMessage (cl, "226 Transfer complete .\r\n");
cl->datadir = 0;
}
}
if (len == 0)
{
QueueMessage (cl, "226 Transfer complete .\r\n");
IWebFClose(cl->file);
cl->file = NULL;
cl->datadir = 0;
}
}
ret = recv(cl->controlsock, cl->commandbuffer+cl->cmdbuflen, sizeof(cl->commandbuffer)-1 - cl->cmdbuflen, 0);
if (ret == -1)
{
if (qerrno == EWOULDBLOCK)
return false; //remove
if (qerrno == ECONNABORTED || qerrno == ECONNRESET)
return true;
Con_TPrintf (TL_NETGETPACKETERROR, strerror(qerrno));
return true;
}
if (*cl->messagebuffer)
{
if (send (cl->controlsock, cl->messagebuffer, strlen(cl->messagebuffer), 0) != -1)
*cl->messagebuffer = '\0'; //YAY! It went!
}
if (ret == 0)
return false;
cl->cmdbuflen += ret;
cl->commandbuffer[cl->cmdbuflen] = 0;
line = cl->commandbuffer;
while (1)
{
msg = line;
while (*line)
{
if (*line == '\r')
*line = ' ';
if (*line == '\n')
break;
line++;
}
if (!*line) //broken client
{
memmove(cl->commandbuffer, line, strlen(line)+1);
cl->cmdbuflen = strlen(line);
break;
}
*line = '\0';
line++;
IWebPrintf("FTP: %s\n", msg);
msg = COM_ParseOut(msg, mode, sizeof(mode));
if (!stricmp(mode, "SYST"))
{
QueueMessage (cl, "215 UNIX Type: L8.\r\n"); //some browsers can be wierd about things.
}
else if (!stricmp(mode, "user"))
{
msg = COM_ParseOut(msg, cl->name, sizeof(cl->name));
QueueMessage (cl, "331 User name received, will be checked with password.\r\n");
}
else if (!stricmp(mode, "pass"))
{
msg = COM_ParseOut(msg, cl->pwd, sizeof(cl->pwd));
cl->auth = IWebAuthorize(cl->name, cl->pwd);
if (cl->auth)
QueueMessage (cl, "230 User logged in.\r\n");
else
QueueMessage (cl, "530 Username or Password was incorrect or otherwise invalid.\r\n");
}
else if (!stricmp(mode, "TYPE"))
{
if (!cl->auth)
{
QueueMessage (cl, "530 Not logged in.\r\n");
continue;
}
msg = COM_ParseOut(msg, resource, sizeof(resource));
if (!stricmp(resource, "A")) //ascii
{
QueueMessage (cl, "200 asci selected.\r\n");
}
else if (!stricmp(resource, "I")) //binary
{
QueueMessage (cl, "200 binary selected.\r\n");
}
else
{
QueueMessage (cl, "200 asci selected.\r\n");
}
}
else if (!stricmp(mode, "PWD"))
{
if (!cl->auth)
{
QueueMessage (cl, "530 Not logged in.\r\n");
continue;
}
QueueMessageva (cl, "257 \"%s\"\r\n", cl->path);
}
else if (!stricmp(mode, "CWD"))
{
char *p;
if (!cl->auth)
{
QueueMessage (cl, "530 Not logged in.\r\n");
continue;
}
Q_strcpyline(cl->path, msg+1, sizeof(cl->path));//path starts after cmd and single space
for (p = cl->path+strlen(cl->path)-1; *p == ' ' && p >= cl->path; p--)
*p = '\0';
QueueMessage (cl, "200 directory changed.\r\n");
}
else if (!stricmp(mode, "PASV"))
{
if (!cl->auth)
{
QueueMessage (cl, "530 Not logged in.\r\n");
continue;
}
if (cl->datasock != INVALID_SOCKET)
{
closesocket(cl->datasock);
cl->datasock = INVALID_SOCKET;
}
cl->datasock = FTP_SV_makelistensocket(cl->blocking);
if (cl->datasock == INVALID_SOCKET)
QueueMessage (cl, "425 server was unable to make a listen socket\r\n");
else
{
FTP_SVSocketToString(cl->datasock, resource);
QueueMessageva (cl, "227 Entering Passive Mode (%s).\r\n", resource);
}
cl->dataislisten = true;
}
else if (!stricmp(mode, "PORT"))
{
if (!cl->auth)
{
QueueMessage (cl, "530 Not logged in.\r\n");
continue;
}
if (cl->datasock != INVALID_SOCKET)
{
closesocket(cl->datasock);
cl->datasock = INVALID_SOCKET;
}
msg = COM_ParseOut(msg, resource, sizeof(resource));
cl->dataislisten = false;
if ((cl->datasock = socket (PF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1)
{
Sys_Error ("FTP_UDP_OpenSocket: socket:", strerror(qerrno));
}
if (ioctlsocket (cl->datasock, FIONBIO, &cl->blocking) == -1)
{
Sys_Error ("FTTP_UDP_OpenSocket: ioctl FIONBIO:", strerror(qerrno));
}
from.sin_family = AF_INET;
from.sin_addr.s_addr = INADDR_ANY;
from.sin_port = 0;
if( bind (cl->datasock, (void *)&from, sizeof(from)) == -1)
{
closesocket(cl->datasock);
cl->datasock=INVALID_SOCKET;
QueueMessage (cl, "425 server bind error.\r\n");
continue;
}
fromlen = sizeof(from);
FTP_StringToAdr(resource, (qbyte *)&from.sin_addr, (qbyte *)&from.sin_port);
connect(cl->datasock, (struct sockaddr *)&from, fromlen);
QueueMessage (cl, "200 Opened data channel.\r\n");
}
else if (!stricmp(mode, "LIST"))
{
char buffer[256];
if (!cl->auth)
{
QueueMessage (cl, "530 Not logged in.\r\n");
continue;
}
if (cl->dataislisten) //accept a connect.
{
int temp;
struct sockaddr_in adr;
int adrlen = sizeof(adr);
temp = accept(cl->datasock, (struct sockaddr *)&adr, &adrlen);
closesocket(cl->datasock);
cl->datasock = temp;
cl->dataislisten = false;
if (cl->datasock == INVALID_SOCKET)
{
QueueMessageva (cl, "425 Your client connected too slowly - %i.\r\n", qerrno);
continue;
}
}
if (cl->datasock == INVALID_SOCKET)
{
QueueMessage (cl, "503 Bad sequence of commands.\r\n");
continue;
}
if (*cl->path == '/')
strcpy(buffer, cl->path+1);
else
strcpy(buffer, cl->path);
if (*buffer) //last characture should be a /
if (buffer[strlen(buffer)-1] != '/')
strcat(buffer, "/");
strcat(buffer, "*");
QueueMessage (cl, "125 Opening FAKE ASCII mode data connection for file.\r\n");
COM_EnumerateFiles(buffer, SendFileNameTo, (void*)cl->datasock); //32->64 this is safe
QueueMessage (cl, "226 Transfer complete.\r\n");
closesocket(cl->datasock);
cl->datasock = INVALID_SOCKET;
}
// else if (!stricmp(mode, "SIZE")) //why IE can't use the list command to find file length, I've no idea.
// {
// msg = COM_ParseOut(msg, resource, sizeof(resource));
// }
else if (!stricmp(mode, "RETR"))
{
if (!cl->auth)
{
QueueMessage (cl, "530 Not logged in.\r\n");
continue;
}
if (cl->dataislisten) //accept a connect.
{
int temp;
struct sockaddr_in adr;
int adrlen = sizeof(adr);
temp = accept(cl->datasock, (struct sockaddr *)&adr, &adrlen);
closesocket(cl->datasock);
cl->datasock = temp;
cl->dataislisten = false;
if (cl->datasock == INVALID_SOCKET)
{
QueueMessageva (cl, "425 Your client connected too slowly - %i.\r\n", qerrno);
continue;
}
}
if (cl->datasock == INVALID_SOCKET)
{
QueueMessage (cl, "503 Bad sequence of commands.\r\n");
continue;
}
msg = COM_ParseOut(msg, resource, sizeof(resource));
if (!cl->auth & IWEBACC_READ)
{
QueueMessage (cl, "550 No read access.\r\n");
continue;
}
if (!*resource == '/')
{
memmove(resource+strlen(cl->path), resource, strlen(resource)+1);
memcpy(resource, cl->path, strlen(cl->path));
}
if (*resource == '/')
{
if (SV_AllowDownload(resource+1))
cl->file = IWebFOpenRead(resource+1);
else
cl->file = IWebGenerateFile(resource+1, NULL, 0);
}
else
{
if (SV_AllowDownload(resource))
cl->file = IWebFOpenRead(resource);
else
cl->file = IWebGenerateFile(resource, NULL, 0);
}
if (!cl->file)
{
QueueMessage (cl, "550 File not found.\r\n");
}
else
{ //send data
QueueMessage (cl, "125 Opening BINARY mode data connection for file.\r\n");
cl->datadir = 1;
}
}
else if (!stricmp(mode, "STOR"))
{
if (!cl->auth)
{
QueueMessage (cl, "530 Not logged in.\r\n");
continue;
}
Q_strcpyline(mode, msg+1, sizeof(mode));
if (!(cl->auth & IWEBACC_FULL) && (((cl->auth & IWEBACC_WRITE && !IWebAllowUpLoad(cl->path+1, cl->name)) || !(cl->auth & IWEBACC_WRITE))))
{
QueueMessage (cl, "550 Permission denied.\r\n");
}
else
{
if (cl->dataislisten) //accept a connect.
{
int temp;
struct sockaddr_in adr;
int adrlen = sizeof(adr);
temp = accept(cl->datasock, (struct sockaddr *)&adr, &adrlen);
closesocket(cl->datasock);
cl->datasock = temp;
cl->dataislisten = false;
if (cl->datasock == INVALID_SOCKET)
{
QueueMessageva (cl, "425 Your client connected too slowly - %i.\r\n", qerrno);
continue;
}
}
if (cl->datasock == INVALID_SOCKET)
{
QueueMessage (cl, "502 Bad sequence of commands.\r\n");
continue;
}
// msg = COM_ParseOut(msg, mode, sizeof(mode));
if (*mode == '/')
sprintf(resource, "%s%s", cl->path, mode);
else
sprintf(resource, "%s%s", cl->path, mode);
cl->file = IWebFOpenRead(resource);
if (cl->file)
{
IWebFClose(cl->file);
QueueMessage (cl, "550 File already exists.\r\n");
continue;
}
cl->file = IWebFOpenWrite(resource, false);
if (!cl->file)
{
QueueMessage (cl, "550 Couldn't open output.\r\n");
}
else
{ //send data
QueueMessage (cl, "125 Opening BINARY mode data connection for input.\r\n");
cl->datadir = 2;
}
}
}
else if (!stricmp(mode, "STRU"))
{
if (!cl->auth)
{
QueueMessage (cl, "530 Not logged in.\r\n");
continue;
}
msg = COM_ParseOut(msg, resource, sizeof(resource));
if (!strcmp(resource, "F"))
{
QueueMessage (cl, "200 recordless structure selected.\r\n");
}
else
{
QueueMessage (cl, "504 not implemented (it's a simple server).\r\n");
}
}
else if (!stricmp(mode, "NOOP"))
{
QueueMessage (cl, "200 Do something then!\r\n");
}
else if (!stricmp(mode, "QUIT"))
{
QueueMessage (cl, "200 About to quit.\r\n");
return true;
}
else
{
QueueMessage (cl, "502 Command not implemented.\r\n");
}
}
return false;
}
#if defined(WEBSVONLY) && defined(_WIN32)
unsigned int WINAPI BlockingClient(FTPclient_t *cl)
{
unsigned long _false = false;
if (ioctlsocket (cl->controlsock, FIONBIO, &_false) == -1)
{
IWebPrintf ("FTP_ServerRun: blocking error: %s\n", strerror(qerrno));
return 0;
}
cl->blocking = false;
while (!FTP_ServerThinkForConnection(cl))
{
Sleep(10);
}
if (cl->file)
IWebFClose(cl->file);
closesocket(cl->controlsock);
if (cl->datasock)
closesocket(cl->datasock);
IWebFree(cl);
return 0;
}
#endif
iwboolean FTP_ServerRun(iwboolean ftpserverwanted)
{
FTPclient_t *cl, *prevcl;
struct sockaddr_in from;
int fromlen;
int clientsock;
unsigned long _true = true;
if (!ftpserverinitied)
{
if (ftpserverwanted)
FTP_ServerInit();
return false;
}
else if (!ftpserverwanted)
{
FTP_ServerShutdown();
return false;
}
prevcl = NULL;
for (cl = FTPclient; cl; cl = cl->next)
{
if (FTP_ServerThinkForConnection(cl))
{
if (cl->file)
IWebFClose(cl->file);
closesocket(cl->controlsock);
if (cl->datasock)
closesocket(cl->datasock);
if (prevcl)
{
prevcl->next = cl->next;
IWebFree(cl);
cl = prevcl;
if (!cl) //kills loop
break;
}
else
{
FTPclient = cl->next;
IWebFree(cl);
cl = FTPclient;
if (!cl) //kills loop
break;
}
}
prevcl = cl;
}
fromlen = sizeof(from);
clientsock = accept(ftpserversocket, (struct sockaddr *)&from, &fromlen);
if (clientsock == -1)
{
if (qerrno == EWOULDBLOCK)
return false;
if (qerrno == ECONNABORTED || qerrno == ECONNRESET)
{
Con_TPrintf (TL_CONNECTIONLOSTORABORTED);
return false;
}
Con_TPrintf (TL_NETGETPACKETERROR, strerror(qerrno));
return false;
}
//is this needed?
if (ioctlsocket (clientsock, FIONBIO, &_true) == -1)
{
IWebPrintf ("FTP_ServerRun: blocking error: %s\n", strerror(qerrno));
return false;
}
cl = IWebMalloc(sizeof(FTPclient_t));
if (!cl) //iwebmalloc is allowed to fail.
{
char *msg = "421 Not enough memory is allocated.\r\n"; //don't be totally anti social
send(clientsock, msg, strlen(msg), 0);
closesocket(clientsock); //try to forget this ever happend
return true;
}
{
char resource[256];
FTP_SVRemoteSocketToString(clientsock, resource);
IWebPrintf("FTP connect from %s\n", resource);
}
cl->controlsock = clientsock;
cl->datasock = INVALID_SOCKET;
cl->next = FTPclient;
cl->blocking = false;
strcpy(cl->path, "/");
QueueMessage(cl, "220-QuakeWorld FTP Server.\r\n220 Welcomes all new users.\r\n");
#if defined(WEBSVONLY) && defined(_WIN32)
if (!CreateThread(NULL, 128, BlockingClient, cl, 0, NULL))
#endif
FTPclient = cl;
return true;
}
#endif