1 /*
2  * DSFML - The Simple and Fast Multimedia Library for D
3  *
4  * Copyright (c) 2013 - 2018 Jeremy DeHaan (dehaan.jeremiah@gmail.com)
5  *
6  * This software is provided 'as-is', without any express or implied warranty.
7  * In no event will the authors be held liable for any damages arising from the
8  * use of this software.
9  *
10  * Permission is granted to anyone to use this software for any purpose,
11  * including commercial applications, and to alter it and redistribute it
12  * freely, subject to the following restrictions:
13  *
14  * 1. The origin of this software must not be misrepresented; you must not claim
15  * that you wrote the original software. If you use this software in a product,
16  * an acknowledgment in the product documentation would be appreciated but is
17  * not required.
18  *
19  * 2. Altered source versions must be plainly marked as such, and must not be
20  * misrepresented as being the original software.
21  *
22  * 3. This notice may not be removed or altered from any source distribution
23  *
24  *
25  * DSFML is based on SFML (Copyright Laurent Gomila)
26  */
27 
28 /**
29  * The $(U Ftp) class is a very simple FTP client that allows you to communicate with
30  * an FTP server. The FTP protocol allows you to manipulate a remote file system
31  * (list files, upload, download, create, remove, ...).
32  *
33  * Using the FTP client consists of 4 parts:
34  * $(UL
35  * $(LI Connecting to the FTP server)
36  * $(LI Logging in (either as a registered user or anonymously))
37  * $(LI Sending commands to the server)
38  * $(LI Disconnecting (this part can be done implicitly by the destructor)))
39  *
40  * $(PARA
41  * Every command returns a FTP response, which contains the status code as well
42  * as a message from the server. Some commands such as `getWorkingDirectory()`
43  * and `getDirectoryListing()` return additional data, and use a class derived
44  * from `Ftp.Response` to provide this data. The most often used commands are
45  * directly provided as member functions, but it is also possible to use
46  * specific commands with the `sendCommand()` function.
47  *
48  * Note that response statuses >= 1000 are not part of the FTP standard,
49  * they are generated by SFML when an internal error occurs.
50  *
51  * All commands, especially upload and download, may take some time to complete.
52  * This is important to know if you don't want to block your application while
53  * the server is completing the task.)
54  *
55  * Example:
56  * ---
57  * // Create a new FTP client
58  * auto ftp = new Ftp();
59  *
60  * // Connect to the server
61  * auto response = ftp.connect("ftp://ftp.myserver.com");
62  * if (response.isOk())
63  *     writeln("Connected");
64  *
65  * // Log in
66  * response = ftp.login("laurent", "dF6Zm89D");
67  * if (response.isOk())
68  *     writeln("Logged in");
69  *
70  * // Print the working directory
71  * auto directory = ftp.getWorkingDirectory();
72  * if (directory.isOk())
73  *     writeln("Working directory: ", directory.getDirectory());
74  *
75  * // Create a new directory
76  * response = ftp.createDirectory("files");
77  * if (response.isOk())
78  *     writeln("Created new directory");
79  *
80  * // Upload a file to this new directory
81  * response = ftp.upload("local-path/file.txt", "files", sf::Ftp::Ascii);
82  * if (response.isOk())
83  *     writeln("File uploaded");
84  *
85  * // Send specific commands (here: FEAT to list supported FTP features)
86  * response = ftp.sendCommand("FEAT");
87  * if (response.isOk())
88  *     writeln("Feature list:\n", response.getMessage());
89  *
90  * // Disconnect from the server (optional)
91  * ftp.disconnect();
92  * ---
93  */
94 module nudsfml.network.ftp;
95 
96 public import nudsfml.system.time;
97 
98 import nudsfml.network.ipaddress;
99 
100 /**
101  * An FTP client.
102  */
103 class Ftp
104 {
105     /// Enumeration of transfer modes.
106     enum TransferMode
107     {
108         /// Binary mode (file is transfered as a sequence of bytes)
109         Binary,
110         /// Text mode using ASCII encoding.
111         Ascii,
112         /// Text mode using EBCDIC encoding.
113         Ebcdic,
114     }
115 
116     package sfFtp* sfPtr;
117 
118     /// Default Constructor.
119     this()
120     {
121         sfPtr = sfFtp_create();
122     }
123 
124     /// Destructor.
125     ~this()
126     {
127         import nudsfml.system.config;
128         mixin(destructorOutput);
129         sfFtp_destroy(sfPtr);
130     }
131 
132 
133     /**
134      * Get the current working directory.
135      *
136      * The working directory is the root path for subsequent operations
137      * involving directories and/or filenames.
138      *
139      * Returns: Server response to the request.
140      */
141     DirectoryResponse getWorkingDirectory()
142     {
143         return new DirectoryResponse(sfFtp_getWorkingDirectory(sfPtr));
144     }
145 
146     /**
147      * Get the contents of the given directory.
148      *
149      * This function retrieves the sub-directories and files contained in the
150      * given directory. It is not recursive. The directory parameter is relative
151      * to the current working directory.
152      *
153      * Params:
154      * directory = Directory to list
155      *
156      * Returns: Server response to the request.
157      */
158     ListingResponse getDirectoryListing(const(char)[] directory = "")
159     {
160         import nudsfml.system.string;
161         return new ListingResponse(sfFtp_getDirectoryListing(sfPtr, directory.ptr, directory.length));
162     }
163 
164     /**
165      * Change the current working directory.
166      *
167      * The new directory must be relative to the current one.
168      *
169      * Params:
170      * directory = New working directory
171      *
172      * Returns: Server response to the request.
173      */
174     Response changeDirectory(const(char)[] directory)
175     {
176         import nudsfml.system.string;
177         return new Response(sfFtp_changeDirectory(sfPtr, directory.ptr,
178                                                   directory.length));
179     }
180 
181     /**
182      * Connect to the specified FTP server.
183      *
184      * The port has a default value of 21, which is the standard port used by
185      * the FTP protocol. You shouldn't use a different value, unless you really
186      * know what you do.
187      *
188      * This function tries to connect to the server so it may take a while to
189      * complete, especially if the server is not reachable. To avoid blocking
190      * your application for too long, you can use a timeout. The default value,
191      * Time.Zero, means that the system timeout will be used (which is
192      * usually pretty long).
193      *
194      * Params:
195      * 		address = Address of the FTP server to connect to
196      * 		port    = Port used for the connection
197      * 		timeout = Maximum time to wait
198      *
199      * Returns: Server response to the request.
200      */
201     Response connect(IpAddress address, ushort port = 21, Time timeout = Time.Zero)
202     {
203         return new Response(sfFtp_connect(sfPtr, &address, port, timeout.asMicroseconds()));
204     }
205 
206     /**
207      * Connect to the specified FTP server.
208      *
209      * The port has a default value of 21, which is the standard port used by
210      * the FTP protocol. You shouldn't use a different value, unless you really
211      * know what you do.
212      *
213      * This function tries to connect to the server so it may take a while to
214      * complete, especially if the server is not reachable. To avoid blocking
215      * your application for too long, you can use a timeout. The default value,
216      * Time.Zero, means that the system timeout will be used (which is
217      * usually pretty long).
218      *
219      * Params:
220      * 		address = Name or ddress of the FTP server to connect to
221      * 		port    = Port used for the connection
222      * 		timeout = Maximum time to wait
223      *
224      * Returns: Server response to the request.
225      */
226     Response connect(const(char)[] address, ushort port = 21, Time timeout = Time.Zero)
227     {
228         auto iaddress = IpAddress(address);
229         return new Response(sfFtp_connect(sfPtr, &iaddress, port, timeout.asMicroseconds()));
230     }
231 
232     /**
233      * Remove an existing directory.
234      *
235      * The directory to remove must be relative to the current working
236      * directory. Use this function with caution, the directory will be removed
237      * permanently!
238      *
239      * Params:
240      * 		name = Name of the directory to remove
241      *
242      * Returns: Server response to the request.
243      */
244     Response deleteDirectory(const(char)[] name)
245     {
246         import nudsfml.system.string;
247         return new Response(sfFtp_deleteDirectory(sfPtr, name.ptr, name.length));
248     }
249 
250     /**
251      * Remove an existing file.
252      *
253      * The file name must be relative to the current working directory. Use this
254      * function with caution, the file will be removed permanently!
255      *
256      * Params:
257      *      name = Name of the file to remove
258      *
259      * Returns: Server response to the request.
260      */
261     Response deleteFile(const(char)[] name)
262     {
263         import nudsfml.system.string;
264         return new Response(sfFtp_deleteFile(sfPtr, name.ptr, name.length));
265     }
266 
267     /**
268      * Close the connection with the server.
269      *
270      * Returns: Server response to the request.
271      */
272     Response disconnect()
273     {
274         import nudsfml.system.string;
275         return new Response(sfFtp_disconnect(sfPtr));
276     }
277 
278     /**
279      * Download a file from the server.
280      *
281      * The filename of the distant file is relative to the current working
282      * directory of the server, and the local destination path is relative to
283      * the current directory of your application.
284      *
285      * Params:
286      * 		remoteFile = Filename of the distant file to download
287      * 		localPath  = Where to put to file on the local computer
288      * 		mode = Transfer mode
289      *
290      * Returns: Server response to the request.
291      */
292     Response download(const(char)[] remoteFile, const(char)[] localPath, TransferMode mode = TransferMode.Binary)
293     {
294         import nudsfml.system.string;
295         return new Response(sfFtp_download(sfPtr, remoteFile.ptr, remoteFile.length, localPath.ptr, localPath.length ,mode));
296     }
297 
298     /**
299      * Send a null command to keep the connection alive.
300      *
301      * This command is useful because the server may close the connection
302      * automatically if no command is sent.
303      *
304      * Returns: Server response to the request.
305      */
306     Response keepAlive()
307     {
308         return new Response(sfFtp_keepAlive(sfPtr));
309     }
310 
311     /**
312      * Log in using an anonymous account.
313      *
314      * Logging in is mandatory after connecting to the server. Users that are
315      * not logged in cannot perform any operation.
316      *
317      * Returns: Server response to the request.
318      */
319     Response login()
320     {
321         return new Response(sfFtp_loginAnonymous(sfPtr));
322     }
323 
324     /**
325      * Log in using a username and a password.
326      *
327      * Logging in is mandatory after connecting to the server. Users that are
328      * not logged in cannot perform any operation.
329      *
330      * Params:
331      * 		name = User name
332      * 		password = The password
333      *
334      * Returns: Server response to the request.
335      */
336     Response login(const(char)[] name, const(char)[] password)
337     {
338         import nudsfml.system.string;
339         return new Response(sfFtp_login(sfPtr, name.ptr, name.length, password.ptr, password.length));
340     }
341 
342     /**
343      * Go to the parent directory of the current one.
344      *
345      * Returns: Server response to the request.
346      */
347     Response parentDirectory()
348     {
349         import nudsfml.system.string;
350         return new Response(sfFtp_parentDirectory(sfPtr));
351     }
352 
353     /**
354      * Create a new directory.
355      *
356      * The new directory is created as a child of the current working directory.
357      *
358      * Params:
359      * 		name = Name of the directory to create
360      *
361      * Returns: Server response to the request.
362      */
363     Response createDirectory(const(char)[] name)
364     {
365         import nudsfml.system.string;
366         return new Response(sfFtp_createDirectory(sfPtr, name.ptr, name.length));
367     }
368 
369     /**
370      * Rename an existing file.
371      *
372      * The filenames must be relative to the current working directory.
373      *
374      * Params:
375      * 		file = File to rename
376      * 		newName = New name of the file
377      *
378      * Returns: Server response to the request.
379      */
380     Response renameFile(const(char)[] file, const(char)[] newName)
381     {
382         import nudsfml.system.string;
383         return new Response(sfFtp_renameFile(sfPtr, file.ptr, file.length, newName.ptr, newName.length));
384     }
385 
386     /**
387      * Upload a file to the server.
388      *
389      * The name of the local file is relative to the current working directory
390      * of your application, and the remote path is relative to the current
391      * directory of the FTP server.
392      *
393      * Params:
394      * 		localFile = Path of the local file to upload
395      * 		remotePath = Where to put the file on the server
396      * 		mode = Transfer mode
397      *
398      * Returns: Server response to the request.
399      */
400     Response upload(const(char)[] localFile, const(char)[] remotePath, TransferMode mode = TransferMode.Binary)
401     {
402         import nudsfml.system.string;
403         return new Response(sfFtp_upload(sfPtr, localFile.ptr, localFile.length, remotePath.ptr, remotePath.length, mode));
404     }
405 
406     /**
407      * Send a command to the FTP server.
408      *
409      * While the most often used commands are provided as member functions in
410      * the Ftp class, this method can be used to send any FTP command to the
411      * server. If the command requires one or more parameters, they can be
412      * specified in parameter. If the server returns information, you can
413      * extract it from the response using getMessage().
414      *
415      * Params:
416      * 		command = Command to send
417      * 		parameter = Command parameter
418      *
419      * Returns: Server response to the request.
420      */
421     Response sendCommand(const(char)[] command, const(char)[] parameter) {
422         import nudsfml.system.string;
423         return new Response(sfFtp_sendCommand(sfPtr, command.ptr, command.length, parameter.ptr, parameter.length));
424     }
425 
426     /// Specialization of FTP response returning a directory.
427     static class DirectoryResponse:Response
428     {
429         private string Directory;
430 
431         //Internally used constructor
432         package this(sfFtpDirectoryResponse* FtpDirectoryResponce)
433         {
434             import nudsfml.system.string;
435 
436             Directory =nudsfml.system..string.toString(sfFtpDirectoryResponse_getDirectory(FtpDirectoryResponce));
437 
438             super(sfFtpDirectoryResponse_getStatus(FtpDirectoryResponce), sfFtpDirectoryResponse_getMessage(FtpDirectoryResponce));
439 
440             sfFtpDirectoryResponse_destroy(FtpDirectoryResponce);
441         }
442 
443         /**
444          * Get the directory returned in the response.
445          *
446          * Returns: Directory name.
447          */
448         string getDirectory() const
449         {
450             return Directory;
451         }
452     }
453 
454     /// Specialization of FTP response returning a filename lisiting.
455     static class ListingResponse:Response
456     {
457         private string[] Filenames;
458 
459         //Internally used constructor
460         package this(sfFtpListingResponse* FtpListingResponce)
461         {
462             import nudsfml.system.string;
463 
464             Filenames.length = sfFtpListingResponse_getCount(FtpListingResponce);
465             for(int i = 0; i < Filenames.length; i++)
466             {
467                 Filenames[i] =nudsfml.system..string.toString(sfFtpListingResponse_getName(FtpListingResponce,i));
468             }
469 
470             super(sfFtpListingResponse_getStatus(FtpListingResponce), sfFtpListingResponse_getMessage(FtpListingResponce));
471 
472             sfFtpListingResponse_destroy(FtpListingResponce);
473         }
474 
475         /**
476          * Return the array of directory/file names.
477          *
478          * Returns: Array containing the requested listing.
479          */
480         const(string[]) getFilenames() const
481         {
482             return Filenames;
483         }
484     }
485 
486     ///Define a FTP response.
487     static class Response
488     {
489         /// Status codes possibly returned by a FTP response.
490         enum Status
491         {
492             RestartMarkerReply = 110,
493             ServiceReadySoon = 120,
494             DataConnectionAlreadyOpened = 125,
495             OpeningDataConnection = 150,
496 
497             Ok = 200,
498             PointlessCommand = 202,
499             SystemStatus = 211,
500             DirectoryStatus = 212,
501             FileStatus = 213,
502             HelpMessage = 214,
503             SystemType = 215,
504             ServiceReady = 220,
505             ClosingConnection = 221,
506             DataConnectionOpened = 225,
507             ClosingDataConnection = 226,
508             EnteringPassiveMode = 227,
509             LoggedIn = 230,
510             FileActionOk = 250,
511             DirectoryOk = 257,
512 
513             NeedPassword = 331,
514             NeedAccountToLogIn = 332,
515             NeedInformation = 350,
516             ServiceUnavailable = 421,
517             DataConnectionUnavailable = 425,
518             TransferAborted = 426,
519             FileActionAborted = 450,
520             LocalError = 451,
521             InsufficientStorageSpace = 452,
522 
523             CommandUnknown = 500,
524             ParametersUnknown = 501,
525             CommandNotImplemented = 502,
526             BadCommandSequence = 503,
527             ParameterNotImplemented = 504,
528             NotLoggedIn = 530,
529             NeedAccountToStore = 532,
530             FileUnavailable = 550,
531             PageTypeUnknown = 551,
532             NotEnoughMemory = 552,
533             FilenameNotAllowed = 553,
534 
535             InvalidResponse = 1000,
536             ConnectionFailed = 1001,
537             ConnectionClosed = 1002,
538             InvalidFile = 1003,
539         }
540 
541         private Status FtpStatus;
542         private string Message;
543 
544         //Internally used constructor.
545         package this(sfFtpResponse* FtpResponce)
546         {
547             this(sfFtpResponse_getStatus(FtpResponce),sfFtpResponse_getMessage(FtpResponce));
548             sfFtpResponse_destroy(FtpResponce);
549         }
550 
551         //Internally used constructor.
552         package this(Ftp.Response.Status status = Ftp.Response.Status.InvalidResponse, const(char)* message = "")
553         {
554             import nudsfml.system.string;
555             FtpStatus = status;
556             Message =nudsfml.system..string.toString(message);
557         }
558 
559         /**
560          * Get the full message contained in the response.
561          *
562          * Returns: The message.
563          */
564         string getMessage() const
565         {
566             return Message;
567         }
568 
569         /**
570          * Get the status code of the response.
571          *
572          * Returns: Status code.
573          */
574         Status getStatus() const
575         {
576             return FtpStatus;
577         }
578 
579         /**
580          * Check if the status code means a success.
581          *
582          * This function is defined for convenience, it is equivalent to testing
583          * if the status code is < 400.
584          *
585          * Returns: true if the status is a success, false if it is a failure.
586          */
587         bool isOk() const
588         {
589             return FtpStatus< 400;
590         }
591     }
592 }
593 
594 unittest
595 {
596     version(DSFML_Unittest_Network)
597     {
598         import std.stdio;
599         import nudsfml.system.err;
600 
601         writeln("Unittest for Ftp");
602 
603         auto ftp = new Ftp();
604 
605         auto responce = ftp.connect("ftp.hq.nasa.gov");//Thanks, NASA!
606 
607         if(responce.isOk())
608         {
609             writeln("Connected! Huzzah!");
610         }
611         else
612         {
613             writeln("Uh-oh");
614             writeln(responce.getStatus());
615             assert(0);
616         }
617 
618         //annonymous log in
619         responce = ftp.login();
620         if(responce.isOk())
621         {
622             writeln("Logged in! Huzzah!");
623         }
624         else
625         {
626             writeln("Uh-oh");
627             writeln(responce.getStatus());
628             assert(0);
629         }
630 
631         auto directory = ftp.getWorkingDirectory();
632         if (directory.isOk())
633         {
634             writeln("Working directory: ", directory.getDirectory());
635         }
636 
637         auto listing = ftp.getDirectoryListing();
638 
639         if(listing.isOk())
640         {
641             const(string[]) list = listing.getFilenames();
642 
643             size_t length;
644 
645             if(list.length > 10)
646             {
647                 length = 10;
648             }
649             else
650             {
651                 length = list.length;
652             }
653 
654             for(int i= 0; i < length; ++i)
655             {
656                 writeln(list[i]);
657             }
658         }
659 
660         writeln();
661     }
662 }
663 
664 private extern(C):
665 
666 struct sfFtpDirectoryResponse;
667 struct sfFtpListingResponse;
668 struct sfFtpResponse;
669 struct sfFtp;
670 
671 //FTP Listing Response Functions
672 
673 ///Destroy a FTP listing response
674 void sfFtpListingResponse_destroy(sfFtpListingResponse* ftpListingResponse);
675 
676 
677 ///Get the status code of a FTP listing response
678 Ftp.Response.Status sfFtpListingResponse_getStatus(const sfFtpListingResponse* ftpListingResponse);
679 
680 
681 ///Get the full message contained in a FTP listing response
682  const(char)* sfFtpListingResponse_getMessage(const(sfFtpListingResponse)* ftpListingResponse);
683 
684 ///Return the number of directory/file names contained in a FTP listing response
685  size_t sfFtpListingResponse_getCount(const(sfFtpListingResponse)* ftpListingResponse);
686 
687 ///Return a directory/file name contained in a FTP listing response
688  const(char)* sfFtpListingResponse_getName(const(sfFtpListingResponse)* ftpListingResponse, size_t index);
689 
690 //FTP Directory Responce Functions
691 
692 ///Destroy a FTP directory response
693  void sfFtpDirectoryResponse_destroy(sfFtpDirectoryResponse* ftpDirectoryResponse);
694 
695 ///Get the status code of a FTP directory response
696 Ftp.Response.Status sfFtpDirectoryResponse_getStatus(const(sfFtpDirectoryResponse)* ftpDirectoryResponse);
697 
698 ///Get the full message contained in a FTP directory response
699  const(char)* sfFtpDirectoryResponse_getMessage(const(sfFtpDirectoryResponse)* ftpDirectoryResponse);
700 
701 ///Get the directory returned in a FTP directory response
702  const(char)* sfFtpDirectoryResponse_getDirectory(const(sfFtpDirectoryResponse)* ftpDirectoryResponse);
703 
704 //FTP Responce functions
705 
706 ///Destroy a FTP response
707  void sfFtpResponse_destroy(sfFtpResponse* ftpResponse);
708 
709 ///Get the status code of a FTP response
710 Ftp.Response.Status sfFtpResponse_getStatus(const(sfFtpResponse)* ftpResponse);
711 
712 ///Get the full message contained in a FTP response
713 const (char)* sfFtpResponse_getMessage(const sfFtpResponse* ftpResponse);
714 
715 ////FTP functions
716 
717 ///Create a new Ftp object
718 sfFtp* sfFtp_create();
719 
720 ///Destroy a Ftp object
721 void sfFtp_destroy(sfFtp* ftp);
722 
723 ///Connect to the specified FTP server
724 sfFtpResponse* sfFtp_connect(sfFtp* ftp, IpAddress* serverIP, ushort port, long timeout);
725 
726 ///Log in using an anonymous account
727 sfFtpResponse* sfFtp_loginAnonymous(sfFtp* ftp);
728 
729 ///Log in using a username and a password
730 sfFtpResponse* sfFtp_login(sfFtp* ftp, const(char)* userName, size_t userNameLength, const(char)* password, size_t passwordLength);
731 
732 ///Close the connection with the server
733 sfFtpResponse* sfFtp_disconnect(sfFtp* ftp);
734 
735 ///Send a null command to keep the connection alive
736 sfFtpResponse* sfFtp_keepAlive(sfFtp* ftp);
737 
738 ///Get the current working directory
739 sfFtpDirectoryResponse* sfFtp_getWorkingDirectory(sfFtp* ftp);
740 
741 ///Get the contents of the given directory
742 sfFtpListingResponse* sfFtp_getDirectoryListing(sfFtp* ftp, const(char)* directory, size_t length);
743 
744 ///Change the current working directory
745 sfFtpResponse* sfFtp_changeDirectory(sfFtp* ftp, const(char)* directory, size_t length);
746 
747 ///Go to the parent directory of the current one
748 sfFtpResponse* sfFtp_parentDirectory(sfFtp* ftp);
749 
750 ///Create a new directory
751 sfFtpResponse* sfFtp_createDirectory(sfFtp* ftp, const(char)* name, size_t length);
752 
753 ///Remove an existing directory
754 sfFtpResponse* sfFtp_deleteDirectory(sfFtp* ftp, const(char)* name, size_t length);
755 
756 ///Rename an existing file
757 sfFtpResponse* sfFtp_renameFile(sfFtp* ftp, const(char)* file, size_t fileLength, const(char)* newName, size_t newNameLength);
758 
759 ///Remove an existing file
760 sfFtpResponse* sfFtp_deleteFile(sfFtp* ftp, const(char)* name, size_t length);
761 
762 ///Download a file from a FTP server
763 sfFtpResponse* sfFtp_download(sfFtp* ftp, const(char)* distantFile, size_t distantFileLength, const(char)* destPath, size_t destPathLength, int mode);
764 
765 ///Upload a file to a FTP server
766 sfFtpResponse* sfFtp_upload(sfFtp* ftp, const(char)* localFile, size_t localFileLength, const(char)* destPath, size_t destPathLength, int mode);
767 
768 ///Send a command to a FTP server
769 sfFtpResponse* sfFtp_sendCommand(sfFtp* ftp, const(char)* command, size_t commandLength, const(char)* parameter, size_t parameterLength);