5,276,406 members and growing! (16,056 online)
Email Password   helpLost your password?
Web Development » Internet / Network » FTP     Intermediate License: The MIT License

CFtpServer: A complete, fast, and reliable FTP server class

By Poumailloux Julien

An article on using the CFtpServer C++ class.
VC6, C++, Windows, MFC, VS, VS6, Dev

Posted: 22 Dec 2005
Updated: 5 May 2006
Views: 69,588
Announcements
Want a new Job?



Search    
Advanced Search
Sitemap
36 votes for this Article.
Popularity: 7.37 Rating: 4.74 out of 5
0 votes, 0.0%
1
0 votes, 0.0%
2
2 votes, 5.6%
3
5 votes, 13.9%
4
29 votes, 80.6%
5

Introduction

FTP servers are great; they allow you to store data you want to save, remotely. However, most of the time, you have to install a lot of things on the remote computer: the FTP server itself, including, most of the time, dozens of files, their dependencies, etc.

Here is where the CFtpServer class is useful:

  • It includes a complete FTP server, which doesn't require dozens of files, and has no WinInet dependency. It's small, fast, and compiles on Win32 (VC++6/GCC) and Linux (GCC). Furthermore, it is a class, so it can be used in plenty of other sources.
  • Also, CFtpServer supports 38 commands: USER, PASS, SITE, SITE EXEC, HELP, REIN, SYST, STRU, MODE, TYPE, PORT, PASV, LIST, NLST, CWD, XCWD, FEAT, MDTM, PWD, XPWD, CDUP, XCUP, STAT, ABOR, REST, RETR, STOR, APPE, STOU, SIZE, DELE, RNFR, RNTO, MKD, XMKD, RMD, XRMD.
  • CFtpServer is multithreaded, and runs in parallel with your code, preventing blocking operations from impeding your application's performance.
  • You can create as many different users as you want, and set individual privileges for each of them.

Using the code

First of all, on Win32, we need to initialize Winsock:

#ifdef WIN32
    WSADATA WSAData;
    WORD wVersionRequested = MAKEWORD( 1, 1 );
    if( WSAStartup(wVersionRequested, &WSAData) != 0 ) {
        return 0;
    }
#endif

The class name is CFtpServer. First, we need to declare the class:

CFtpServer FtpServer;

Then, we configure it:

FtpServer.SetDataPortRange( 100,900 );
FtpServer.AllowAnonymous( true, "C:/anonymous" );

We tell the class which TCP-port can be used for transfers (here, [100-999]), and we tell the class to allow anonymous users.

Then, we create a user:

CFtpServer::UserNode *FtpUser =
      FtpServer.AddUser( "test", "pass", "C:\\");

Here, the user's login is test, his password is pass, and his start directory is: C:/. Besides this, we can check if the user was successfully created, by checking the pointer returned by FtpServer.AddUser. If it is equal to NULL, the user hasn't been created, and the start path may not exist.

After that, we set the maximum number of clients who can log in as this user, and we give him some privileges:

FtpServer.SetUserMaxClient( FtpUser, CFtpServer::Unlimited );
FtpServer.SetUsetPriv( FtpUser, CFtpServer::READFILE |
                 CFtpServer::WRITEFILE | CFtpServer::LIST |
                 CFtpServer::DELETEFILE | CFtpServer::CREATEDIR |
                 CFtpServer::DELETEDIR );

FtpUser will be given the right to list files and directories, to read, write (including creating files), and delete (including renaming files) files, and to create and delete directories.

At last, we ask the class to start listening - here, it will listen on the TCP-port 21 and on all network interfaces - and to accept incoming clients:

    // If you only want to listen on the TCP Loopback interface,

    // replace 'INNADDR_ANY' by 'inet_addr("127.0.0.1")'.

    FtpServer.StartListening( INADDR_ANY, 21 );
    FtpServer.StartAccepting();

If your program has nothing else to do, we ask the main thread to wait, forever:

    while( 1 )
    #ifdef WIN32
        Sleep( 1000 );
    #else
        usleep( 1000 );
    #endif

Here we go, the FTP server is running!

Now, the public part of the CFtpServer class:

public:

    /* Constructor */
    CFtpServer(void);
    /* Destructor */
    ~CFtpServer(void);

    /****************************************
     * USER
     ****************************************/

    /* The Structure which will be allocated for each User. */
    struct UserNode
    {
        int iMaxClient;
        unsigned char ucPriv;
        #ifdef CFTPSERVER_USE_EXTRACMD
            unsigned char ucExtraCmd;
        #endif
        char *szName;
        char *szPasswd;
        bool bIsEnabled;
        char *szStartDir;
        int iNbClient;
        struct UserNode *PrevUser;
        struct UserNode *NextUser;
    };

    /* Enumerate the different Privilegies a User can get. */
    enum
    {
        READFILE    = 0x1,
        WRITEFILE    = 0x2,
        DELETEFILE    = 0x4,
        LIST        = 0x8,
        CREATEDIR    = 0x10,
        DELETEDIR    = 0x20,
    };

    /* Create a new User. Don't create a user named "anonymous", 
       call AllowAnonymous();.
        Arguments:
            -the User Name.
            -the User Password.
            -the User Start directory.
        Returns:
            -on success: the adress of the New-User's 
                         CFtpServer::UserNode structure.
            -on error: NULL.
    */
    struct CFtpServer::UserNode *AddUser( const char *szName, 
                  const char *szPasswd, const char* szStartDir );

    /* Set the Privilegies of a User.
        Arguments:
            -a pointer to the CFtpServer::UserNode 
             structure of the User.
            -the user's privilegies of the CFtpServer 
             Enum of privilegies, separated by the | operator.
        Returns:
            -on success: true.
            -on error: false.
    */
    bool SetUserPriv( struct CFtpServer::UserNode *User, 
                                       unsigned char ucPriv );

    /* Delete a User, and by the way all the Clients 
       connected to this User.
        Arguments:
            -a pointer to the CFtpServer::UserNode 
             structure of the User.
        Returns:
            -on success: true.
            -on error: false.
    */
    bool DelUser( struct CFtpServer::UserNode * User );

    /* Get the Head of the User List.
        Arguments:
        Returns:
            -the Head of the User List.
    */
    struct CFtpServer::UserNode *GetUserListHead( void ) {
        return this->UserListHead;
    }

    /* Get the Last User of the List.
        Arguments:
        Returns:
            -the Last User of the List.
    */
    struct CFtpServer::UserNode *GetUserListLast( void ) {
        return this->UserListLast;
    }

    /* Get the Next User of the List.
        Arguments:
            -a pointer to a CFtpServer::UserNode structure.
        Returns:
            -the Next User of the list.
    */
    struct CFtpServer::UserNode *GetNextUser( 
               const struct CFtpServer::UserNode *User ) {
        if( User )
            return User->NextUser;
        return NULL;
    }

    /* Get the Previous User of the List.
        Arguments:
            -a pointer to a CFtpServer::UserNode structure.
        Returns:
            -the Previous User of the List.
    */
    struct CFtpServer::UserNode *GetPreviousUser( 
            const struct CFtpServer::UserNode *User ) {
        if( User )
            return User->PrevUser;
        return NULL;
    }

    enum {
        Error = -2,
        None = -1,
        Unlimited = 0
    };
    /* Set the maximum number of Clients which can be 
       connected to the User at a time.
        Arguments:
            -a pointer to the CFtpServer::UserNode 
             structure of the User.
            -the number of clients who will be able to be 
             connected to the user at a time.
             CFtpServer::UserMaxClient.Unlimited: Unlimited.
             CFtpServer::UserMaxClient.None: None.
        Returns:
            -on success: true.
            -on error: false.
    */
    bool SetUserMaxClient( struct CFtpServer::UserNode *User, 
                                              int iMaxClient );

    /* Get the maximum number of Clients which can be connected 
       to the User at a time.
        Arguments:
            -a pointer to the CFtpServer::UserNode 
             structure of the User.
        Returns:
            -on success: the number of clients which can be 
             connected to the User at a time.
            -on error: -2.
    */
    unsigned int GetUserMaxClient( 
            const struct CFtpServer::UserNode *User ) {
        if( User )
            return User->iMaxClient;
        return 0;
    }

    /* Delete all the Users, and by the way all the Server's 
       Clients connected to a User. */
    void DelAllUser( void );

    /* Get a User's privilegies 
        Arguments:
            -a pointer to the CFtpServer::UserNode 
             structure of the User.
        Returns:
            -on success: the user's privilegies concatenated 
             with the bitwise inclusive binary operator "|".
            -on error: 0.
    */
    unsigned char GetUserPriv( 
            const struct CFtpServer::UserNode *User ) {
        if( User ) {
            return User->ucPriv;
        } else return 0;
    }

    /* Get a pointer to a User's Name.
        Arguments:
            -a pointer to the CFtpServer::UserNode 
             structure of the User.
        Returns:
            -on success: a pointer to the User's Name.
            -on error: NULL.
    */
    const char* GetUserLogin( 
            const struct CFtpServer::UserNode *User ) {
        if( User ) {
            return User->szName;
        } else return NULL;
    }

    /* Get a pointer to a User's Password.
        Arguments:
            -a pointer to the CFtpServer::UserNode 
             structure of the User.
        Returns:
            -on success: a pointer to the User's Password.
            -on error: NULL.
    */
    const char* GetUserPasswd( 
            const struct CFtpServer::UserNode *User ) {
        if( User ) {
            return User->szPasswd;
        } else return NULL;
    }

    /* Get a pointer to a User's Start Directory.
        Arguments:
            -a pointer to the CFtpServer::UserNode 
             structure of the User.
        Returns:
            -on success: a pointer to the User's Start Directory.
            -on error: NULL.
    */
    const char* GetUserStartDir( 
            const struct CFtpServer::UserNode *User ) {
        if( User ) {
            return User->szStartDir;
        } else return NULL;
    }
    
    #ifdef CFTPSERVER_USE_EXTRACMD

        /* Enum the Extra Commands a User can got. */
        enum eFtpExtraCmd {
            ExtraCmd_EXEC    = 0x1,
        };

        /* Set the supported Extra-Commands of a User.
            Arguments:
                -a pointer to the CFtpServer::UserNode 
                 structure of the User.
                -the user's Extra-Commands concatenated 
                 with the bitwise inclusive binary operator "|".
            Returns:
                -on success: true.
                -on error: false.
        */
        bool SetUserExtraCmd( struct CFtpServer::UserNode *User, 
                                          unsigned char dExtraCmd );

        /* Get the supported Extra-Commands of a User.
            Arguments:
                -a pointer to the CFtpServer::UserNode 
                 structure of the User.
            Returns:
                -on succes: the user's Extra-Commands concatenated 
                 with the bitwise inclusive binary operator "|".
                -on error: 0.
        */
        unsigned char GetUserExtraCmd( 
                const struct CFtpServer::UserNode *User ) {
            if( User ) {
                return User->ucExtraCmd;
            } else return 0;
        }

    #endif

    /***************************************
     * START / STOP
     ***************************************/

    /* Ask the Server to Start Listening on the TCP-Port 
       supplied by SetPort().
        Arguments:
            -the Network Adress CFtpServer will listen on.
             Example: INADDR_ANY for all local interfaces.
                      inet_addr( "127.0.0.1" ) for the TCP 
                      Loopback interface.
            -the TCP-Port on which CFtpServer will listen.
        Returns:
            -on success: true.
            -on error: false, the supplied Adress or 
             TCP-Port may not be valid.
    */
    bool StartListening( unsigned long ulAddr, 
                            unsigned short int usPort );

    /* Ask the Server to Stop Listening.
        Returns:
            -on success: true.
            -on error: false.
    */
    bool StopListening( void );

    /* Check if the Server is currently Listening.
        Returns:
            -true: if the Server is currently listening.
            -false: if the Server isn't currently listening.
    */
    bool IsListening( void ) {
        return this->bIsListening;
    }

    /* Ask the Server to Start Accepting Clients.
        Returns:
            -on success: true.
            -on error: false.
    */
    bool StartAccepting( void );

    /* Check if the Server is currently Accpeting Clients.
        Returns:
            -true: if the Server is currently accepting clients.
            -false: if the Server isn't currently accepting clients.
    */
    bool IsAccepting( void ) {
        return this->bIsAccepting;
    }

    /****************************************
     * CONFIG
     ****************************************/

    /* Get the TCP Port on which CFtpServer will listen 
       for incoming clients.
        Arguments:
        Returns:
            -on success: the TCP-Port.
            -on error: 0.
    */
    unsigned short GetListeningPort( void ) {
        return this->usPort;
    }

    /* Set the TCP Port Range CFtpServer can use to Send and 
       Receive Files or Data.
        Arguments:
            -the First Port of the Range.
            -the Number of Ports, including the First 
             previously given.
        Returns:
            -on success: true.
            -on error: false.
    */
    bool SetDataPortRange( unsigned short int usDataPortStart, 
                                        unsigned int iNumber );

    /* Get the TCP Port Range CFtpServer can use to Send 
       and Receive Files or Data.
        Arguments:
            -a Pointer to the First Port.
            -a Pointer to the Number of Ports, including 
             the First.
        Returns:
            -on success: true.
            -on error: false.
    */
    bool GetDataPortRange( unsigned short int *usDataPortStart, 
                                                int *iNumber ) {
        if( usDataPortStart && iNumber ) {
            *usDataPortStart = this->DataPortRange.usStart;
            *iNumber = this->DataPortRange.iNumber;
            return true;
        }
        return false;
    }

    /* Allow or disallow Anonymous users. Its privilegies 
       will be set to
        CFtpServer::READFILE | CFtpServer::LIST.
        Arguments:
            -true if you want CFtpServer to accept anonymous 
             clients, otherwise false.
            -the Anonymous User Start Directory.
        Returns:
            -on success: true.
            -on error: false.
    */
    bool AllowAnonymous( bool bDoAllow, const char *szStartPath );

    /* Check if Anonymous Users are allowed.
        Returns:
            -true: if Anonymous Users are allowed.
            -false: if Anonymous Users aren't allowed.
    */
    bool IsAnonymousAllowed( void ) {
        return this->bAllowAnonymous;
    }

    #ifdef CFTPSERVER_ANTIBRUTEFORCING

        /* Set the delay the Server will wait when 
           checking for the Client's pass. */
        void SetCheckPassDelay( int iCheckPassDelay ) {
            this->iCheckPassDelay = iCheckPassDelay;
        }

        /* Get the delay the Server will wait when 
           checking for the Client's pass.
            Returns:
                -the delay the Server will wait when 
                 checking for the Client's pass.
        */
        int GetSetCheckPassDelay( void ) {
            return this->iCheckPassDelay;
        }

    #endif

    /****************************************
     * STATISTICS
     ****************************************/

    /* Get in real-time the number of Clients 
       connected to the Server.
        Returns:
            -the current number of Clients connected 
             to the Server.
    */
    int GetNbClient( void ) {
        return this->iNbClient;
    }

    /* Get in real-time the number of existing Users.
        Returns:
            -the current number of existing Users.
    */
    int GetNbUser( void ) {
        return this->iNbUser;
    }

    /* Get the number of Clients currently connected 
       using the specified User.
        Arguments:
            -a pointer to the CFtpServer::UserNode 
             structure of the User.
        Returns:
            -on success: the count of Clients using 
             currently the User.
            -on error: -1.
    */
    int GetNbClientUsingUser( 
            struct CFtpServer::UserNode *User ) {
        if( User ) {
            return User->iNbClient;
        } else return -1;
    }

Enjoy!

Quick test!

If you want to check if the server is running, or if a user exists, start the example application and type the following in a command prompt:

telnet localhost 21
USER test
PASS pass
STAT /
QUIT

Of course, assuming that you made the application listen on port 21 (default), and that the user's name you are trying to use is 'test' and its password is 'pass'. If it doesn't write "530 Please login with valid USER and PASS" and you see the user's main directory listing, then it works :).

For example:

> telnet localhost 21
< 220 Browser Ftp Server.


> USER test

< 331 Password required for this user.
> PASS pass
< 230 User Logged In.
> STAT /
< 213-Status follows:
< -rwx------ 1 user group           0 Feb  4 16:26 AUTOEXEC.BAT
< drwx------ 1 user group           0 Feb  5 15:16 BJPrinter
< -rwx------ 1 user group         212 Feb  4 16:20 boot.ini


< -rwx------ 1 user group        4952 Aug 30  2002 Bootfont.bin

< -rwx------ 1 user group           0 Feb  4 16:26 CONFIG.SYS
< drwx------ 1 user group           0 Feb  4 16:31 Documents and
                                                         Settings
< -rwx------ 1 user group  1073254400 Feb  5 13:51 hiberfil.sys
< -rwx------ 1 user group           0 Feb  4 16:26 IO.SYS
< -rwx------ 1 user group           0 Feb  4 16:26 MSDOS.SYS
< -rwx------ 1 user group       47564 Aug  3  2004 NTDETECT.COM
< -rwx------ 1 user group      251712 Aug  3  2004 ntldr
< -rwx------ 1 user group  1610612736 Feb  5 13:51 pagefile.sys


< drwx------ 1 user group           0 Feb  5 15:12 Program Files

< drwx------ 1 user group           0 Feb  5 14:09 RECYCLER
< drwx------ 1 user group           0 Feb  4 16:30 System Volume
                                                         Information
< drwx------ 1 user group           0 Feb  5 14:04 torrent
< drwx------ 1 user group           0 Feb  4 22:10 UT2004Demo
< drwx------ 1 user group           0 Feb  5 16:19 WINDOWS
< 213 End of status
> QUIT
< 221 Goodbye.

Here, I have added '< ' or '> ' at the beginning of each line in order to make it more understandable: '> ' means 'you send', and '< ' means 'you receive'.

Points of interest

Writing the CFtpServer class was pretty fun. I'm now glad to release it to the community.

History

  • v3.1 - 22-04-2006
    • 16-04-2006: New: Added a define not to check if the Client IP and the IP supplied with the PORT command are the same.
    • 18-04-2006: Fix: A logged in user could login as another user without sending the password.
    • 18-04-2006: Fix: CWD could give access to the first parent directory of a user's start directory.
    • 18-04-2006: Fix: BuildPath() and SimplifyPath() have been improved.
    • 18-04-2006: New: Add thread synchronisation for the User and Client shared data list.
  • v3.0 - 03-04-2006
    • 12-03-2006: Fix: Minor fixes.
    • 12-03-2006: Fix: File uploaded and the newly created ones were created as read-only.
    • 12-03-2006: Fix: Doesn't crash anymore when listing files containing non-ASCII char in their names.
    • 13-03-2006: Fix: In BuildPath(), the memory allocated was only 1 byte.
    • 03-04-2006: Fix: Some code dealing with the client list and user list has been rewritten.
  • v2.9 - 08-03-2006
    • 02-03-2006: Fix: CDUP was broken, always went to root.
    • 03-03-2006: Fix: Minor fixes, compilation warnings have been solved.
    • 03-03-2006: Fix: SetPort() has been removed, it caused errors on Win32 due to some SDK. Now, the listening TCP-port need to be set in StartListening().
    • 05-03-2006: Fix: BuildPath() and SimplifyPath() have been improved, in rapidity and reliability.
    • 04-03-2006: New: Using GCC, add -D_FILE_OFFSET_BITS=64 to the compilation line for large file support.
    • 07-03-2006: Fix: The first letter of the first element of a list wasn't sent.
    • 07-03-2006: New: Now, the users can be enumerated: New functions: GetUserListHead(), GetUserListLast(), GetNextUser(), GetPreviousUser().
  • v2.8 - 25-02-2006
    • 23-02-2006: Fix: CWD didn't check if the supplied path was a directory.
    • 23-02-2006: Fix: AddUser() didn't check if the supplied start path was a directory.
    • 23-02-2006: Fix: Improved random port generation.
    • 24-02-2006: New: Now, you can select the network interface on which you want to listen.
  • v2.7 - 22-02-2006
    • 19-02-2006: New: Under Linux based OSs, it now uses SO_REUSEADDR to improve performance.
    • 22-02-2006: Fix: Improved large file support.
    • 22-02-2006: Fix: Now, CFtpServer compiles without any error under SunOS 5.9 (Thanks to Daniel Fruzynski).
  • V2.6 - 16-02-2006
    • 14-02-2006: Fix: Extra commands weren't correctly handled.
    • 16-02-2006: Fix: CFtpServer compiles and runs under Linux.
    • 16-02-2006: New: SetDataPortRange(): Set the TCP port range the CFtpServer can use to send and receive data.
    • 16-02-2006: New: GetDataPortRange(): Get the TCP port range the CFtpServer can use to send and receive data.
    • 16-02-2006: Fix: Minor fixes.
  • V2.5 - 11-02-2006
    • 07-02-2006: New: Example project has been improved.
    • 07-02-2006: New: Now checks if the IP supplied in the PORT command is that of the client.
    • 07-02-2006: New: Now on PASV command, the IP sent by the server depends on the network interface.
    • 08-02-2006: Fix: AddUser() didn't check if the user's name already existed.
  • V2.4 - 06-02-2006
    • Fix: Access to NULL pointer when logged in as anonymous.
    • Fix: Passive mode was broken on some computers.
  • V2.3 - 05-02-2006
    • 29-12-2005: Fix: Path simplification was broken.
    • 05-02-2006: Fix: Command reception wasn't reliable when the command was sent byte per byte.
  • V2.2 - 28-12-2005
    • 21-12-2005: Fix: Anonymous user had no privilege when initialized.
    • 21-12-2005: Fix: Now, checks if the supplied TCP port is valid.
    • 22-12-2005: New: SetUserMaxClient().
    • 28-12-2005: Fix: User's privileges weren't correctly parsed.
    • 28-12-2005: Fix: Used C header in C++ code.
    • 28-12-2005: Fix: Memory allocated by 'new' wasn't fully deallocated.
  • V2.1 - Initial release on The Code Project.

License

This article, along with any associated source code and files, is licensed under The MIT License

About the Author

Poumailloux Julien



Location: France France

Other popular Internet / Network articles:

Article Top
Sign Up to vote for this article
You must Sign In to use this message board.
FAQ FAQ Noise ToleranceSearch Search Messages 
 Layout  Per page   
 Msgs 1 to 25 of 125 (Total in Forum: 125) (Refresh)FirstPrevNext
Subject  Author Date 
Generalerror C2872 with boost librarymemberwhria7813:55 23 May '08  
Generalcould sure use IPv6 support!memberSpike0xFF11:43 26 Mar '08  
GeneralUsing Programmaticallymembertriplebit4:25 9 Nov '07  
GeneralBuildPathmember__TSX__21:21 30 Apr '07  
GeneralGREAT WORKmemberMark Echeagaray9:21 23 Mar '07  
GeneralGREAT WORKmember9:03 23 Mar '07  
NewsCFtpServer 4.0 is out !memberPoumailloux Julien7:36 3 Sep '06  
GeneralBug if a non-alphabet filenamememberWHRIA15:51 12 Aug '06  
GeneralRe: Bug if a non-alphabet filenamememberPoumailloux Julien8:31 13 Aug '06  
GeneralRe: Bug if a non-alphabet filename [modified]memberWHRIA3:37 18 Aug '06  
GeneralRe: Bug if a non-alphabet filenamememberPoumailloux Julien7:38 3 Sep '06  
GeneralLIST_Command Function does not work on a single file.memberWHRIA2:22 26 Jul '06  
GeneralRe: LIST_Command Function does not work on a single file.memberPoumailloux Julien5:05 28 Jul '06  
AnswerRe: LIST_Command Function does not work on a single file.memberPoumailloux Julien2:27 6 Aug '06  
GeneralThanks you very much !memberWHRIA14:36 8 Aug '06  
GeneralRename commandmemberfiregrom23:40 28 Jun '06  
AnswerRe: Rename command [modified]memberPoumailloux Julien1:06 29 Jun '06  
QuestionRETR Commandmemberfiregrom3:31 18 Aug '06  
AnswerRe: RETR CommandmemberPoumailloux Julien7:41 3 Sep '06  
GeneralPassive Connection failed !!!memberbp12347510:27 16 May '06  
GeneralCould you add this function?memberyearn_remy2:32 9 May '06