1 /////////////////////////////////////////////////////////////////////////////
3 // Purpose: FTP protocol
4 // Author: Guilhem Lavaux
5 // Modified by: Mark Johnson, wxWindows@mj10777.de
6 // 20000917 : RmDir, GetLastResult, GetList
7 // Vadim Zeitlin (numerous fixes and rewrites to all part of the
8 // code, support ASCII/Binary modes, better error reporting, more
9 // robust Abort(), support for arbitrary FTP commands, ...)
10 // Created: 07/07/1997
12 // Copyright: (c) 1997, 1998 Guilhem Lavaux
13 // Licence: wxWindows license
14 /////////////////////////////////////////////////////////////////////////////
16 // ============================================================================
18 // ============================================================================
21 #pragma implementation "ftp.h"
24 // ----------------------------------------------------------------------------
26 // ----------------------------------------------------------------------------
28 // For compilers that support precompilation, includes "wx.h".
29 #include "wx/wxprec.h"
35 #if wxUSE_SOCKETS && wxUSE_STREAMS
39 #include "wx/string.h"
45 #include "wx/sckaddr.h"
46 #include "wx/socket.h"
48 #include "wx/sckstrm.h"
49 #include "wx/protocol/protocol.h"
50 #include "wx/protocol/ftp.h"
52 #if defined(__WXMAC__)
53 #include "wx/mac/macsock.h"
60 // ----------------------------------------------------------------------------
62 // ----------------------------------------------------------------------------
64 // the length of FTP status code (3 digits)
65 static const size_t LEN_CODE
= 3;
67 // ----------------------------------------------------------------------------
69 // ----------------------------------------------------------------------------
71 IMPLEMENT_DYNAMIC_CLASS(wxFTP
, wxProtocol
)
72 IMPLEMENT_PROTOCOL(wxFTP
, wxT("ftp"), wxT("ftp"), TRUE
)
74 // ============================================================================
76 // ============================================================================
78 // ----------------------------------------------------------------------------
79 // wxFTP constructor and destructor
80 // ----------------------------------------------------------------------------
84 m_lastError
= wxPROTO_NOERR
;
86 m_currentTransfermode
= NONE
;
88 m_user
= wxT("anonymous");
89 m_passwd
<< wxGetUserId() << wxT('@') << wxGetFullHostName();
92 SetFlags(wxSOCKET_NONE
);
105 // ----------------------------------------------------------------------------
106 // wxFTP connect and login methods
107 // ----------------------------------------------------------------------------
109 bool wxFTP::Connect(wxSockAddress
& addr
, bool WXUNUSED(wait
))
111 if ( !wxProtocol::Connect(addr
) )
113 m_lastError
= wxPROTO_NETERR
;
119 m_lastError
= wxPROTO_CONNERR
;
123 // we should have 220 welcome message
124 if ( !CheckResult('2') )
131 command
.Printf(wxT("USER %s"), m_user
.c_str());
132 char rc
= SendCommand(command
);
135 // 230 return: user accepted without password
145 command
.Printf(wxT("PASS %s"), m_passwd
.c_str());
146 if ( !CheckCommand(command
, '2') )
155 bool wxFTP::Connect(const wxString
& host
)
159 addr
.Service(wxT("ftp"));
161 return Connect(addr
);
168 m_lastError
= wxPROTO_STREAMING
;
174 if ( !CheckCommand(wxT("QUIT"), '2') )
176 wxLogDebug(_T("Failed to close connection gracefully."));
180 return wxSocketClient::Close();
183 // ============================================================================
185 // ============================================================================
187 // ----------------------------------------------------------------------------
188 // Send command to FTP server
189 // ----------------------------------------------------------------------------
191 char wxFTP::SendCommand(const wxString
& command
)
195 m_lastError
= wxPROTO_STREAMING
;
199 wxString tmp_str
= command
+ wxT("\r\n");
200 const wxWX2MBbuf tmp_buf
= tmp_str
.mb_str();
201 if ( Write(wxMBSTRINGCAST tmp_buf
, strlen(tmp_buf
)).Error())
203 m_lastError
= wxPROTO_NETERR
;
208 // don't show the passwords in the logs (even in debug ones)
209 wxString cmd
, password
;
210 if ( command
.Upper().StartsWith(_T("PASS "), &password
) )
212 cmd
<< _T("PASS ") << wxString(_T('*'), password
.length());
219 wxLogTrace(FTP_TRACE_MASK
, _T("==> %s"), cmd
.c_str());
220 #endif // __WXDEBUG__
225 // ----------------------------------------------------------------------------
226 // Recieve servers reply
227 // ----------------------------------------------------------------------------
229 char wxFTP::GetResult()
233 // m_lastResult will contain the entire server response, possibly on
235 m_lastResult
.clear();
237 // we handle multiline replies here according to RFC 959: it says that a
238 // reply may either be on 1 line of the form "xyz ..." or on several lines
239 // in whuch case it looks like
243 // and the intermeidate lines may start with xyz or not
244 bool badReply
= FALSE
;
245 bool firstLine
= TRUE
;
246 bool endOfReply
= FALSE
;
247 while ( !endOfReply
&& !badReply
)
250 m_lastError
= ReadLine(line
);
254 if ( !m_lastResult
.empty() )
256 // separate from last line
257 m_lastResult
+= _T('\n');
260 m_lastResult
+= line
;
262 // unless this is an intermediate line of a multiline reply, it must
263 // contain the code in the beginning and '-' or ' ' following it
264 if ( line
.Len() < LEN_CODE
+ 1 )
272 wxLogTrace(FTP_TRACE_MASK
, _T("<== %s %s"),
273 code
.c_str(), line
.c_str());
276 else // line has at least 4 chars
278 // this is the char which tells us what we're dealing with
279 wxChar chMarker
= line
.GetChar(LEN_CODE
);
283 code
= wxString(line
, LEN_CODE
);
284 wxLogTrace(FTP_TRACE_MASK
, _T("<== %s %s"),
285 code
.c_str(), line
.c_str() + LEN_CODE
+ 1);
302 else // subsequent line of multiline reply
304 if ( wxStrncmp(line
, code
, LEN_CODE
) == 0 )
306 if ( chMarker
== _T(' ') )
311 wxLogTrace(FTP_TRACE_MASK
, _T("<== %s %s"),
312 code
.c_str(), line
.c_str() + LEN_CODE
+ 1);
316 // just part of reply
317 wxLogTrace(FTP_TRACE_MASK
, _T("<== %s %s"),
318 code
.c_str(), line
.c_str());
326 wxLogDebug(_T("Broken FTP server: '%s' is not a valid reply."),
327 m_lastResult
.c_str());
329 m_lastError
= wxPROTO_PROTERR
;
334 // if we got here we must have a non empty code string
338 // ----------------------------------------------------------------------------
339 // wxFTP simple commands
340 // ----------------------------------------------------------------------------
342 bool wxFTP::SetTransferMode(TransferMode transferMode
)
344 if ( transferMode
== m_currentTransfermode
)
351 switch ( transferMode
)
354 wxFAIL_MSG(_T("unknown FTP transfer mode"));
366 if ( !DoSimpleCommand(_T("TYPE"), mode
) )
368 wxLogError(_("Failed to set FTP transfer mode to %s."), (const wxChar
*)
369 (transferMode
== ASCII
? _("ASCII") : _("binary")));
374 // If we get here the operation has been succesfully completed
375 // Set the status-member
376 m_currentTransfermode
= transferMode
;
381 bool wxFTP::DoSimpleCommand(const wxChar
*command
, const wxString
& arg
)
383 wxString fullcmd
= command
;
386 fullcmd
<< _T(' ') << arg
;
389 if ( !CheckCommand(fullcmd
, '2') )
391 wxLogDebug(_T("FTP command '%s' failed."), fullcmd
.c_str());
399 bool wxFTP::ChDir(const wxString
& dir
)
401 // some servers might not understand ".." if they use different directory
402 // tree conventions, but they always understand CDUP - should we use it if
403 // dir == ".."? OTOH, do such servers (still) exist?
405 return DoSimpleCommand(_T("CWD"), dir
);
408 bool wxFTP::MkDir(const wxString
& dir
)
410 return DoSimpleCommand(_T("MKD"), dir
);
413 bool wxFTP::RmDir(const wxString
& dir
)
415 return DoSimpleCommand(_T("RMD"), dir
);
418 wxString
wxFTP::Pwd()
422 if ( CheckCommand(wxT("PWD"), '2') )
424 // the result is at least that long if CheckCommand() succeeded
425 const wxChar
*p
= m_lastResult
.c_str() + LEN_CODE
+ 1;
428 wxLogDebug(_T("Missing starting quote in reply for PWD: %s"), p
);
436 // check if the quote is doubled
438 if ( !*p
|| *p
!= _T('"') )
440 // no, this is the end
443 //else: yes, it is: this is an embedded quote in the
444 // filename, treat as normal char
452 wxLogDebug(_T("Missing ending quote in reply for PWD: %s"),
453 m_lastResult
.c_str() + LEN_CODE
+ 1);
459 wxLogDebug(_T("FTP PWD command failed."));
465 bool wxFTP::Rename(const wxString
& src
, const wxString
& dst
)
469 str
= wxT("RNFR ") + src
;
470 if ( !CheckCommand(str
, '3') )
473 str
= wxT("RNTO ") + dst
;
475 return CheckCommand(str
, '2');
478 bool wxFTP::RmFile(const wxString
& path
)
481 str
= wxT("DELE ") + path
;
483 return CheckCommand(str
, '2');
486 // ----------------------------------------------------------------------------
487 // wxFTP download and upload
488 // ----------------------------------------------------------------------------
490 class wxInputFTPStream
: public wxSocketInputStream
493 wxInputFTPStream(wxFTP
*ftp
, wxSocketBase
*sock
)
494 : wxSocketInputStream(*sock
)
498 // FIXME make the timeout configurable
500 // set a shorter than default timeout
501 m_i_socket
->SetTimeout(60); // 1 minute
504 size_t GetSize() const { return m_ftpsize
; }
506 virtual ~wxInputFTPStream()
510 if ( LastError() == wxStream_NOERROR
)
512 // wait for "226 transfer completed"
513 m_ftp
->CheckResult('2');
515 m_ftp
->m_streaming
= FALSE
;
522 // delete m_i_socket; // moved to top of destructor to accomodate wu-FTPd >= 2.6.0
529 class wxOutputFTPStream
: public wxSocketOutputStream
532 wxOutputFTPStream(wxFTP
*ftp_clt
, wxSocketBase
*sock
)
533 : wxSocketOutputStream(*sock
), m_ftp(ftp_clt
)
537 virtual ~wxOutputFTPStream(void)
541 // close data connection first, this will generate "transfer
546 m_ftp
->CheckResult('2');
548 m_ftp
->m_streaming
= FALSE
;
552 // abort data connection first
555 // and close it after
563 wxSocketClient
*wxFTP::GetPort()
567 if ( !DoSimpleCommand(_T("PASV")) )
569 wxLogError(_("The FTP server doesn't support passive mode."));
574 const wxChar
*addrStart
= wxStrchr(m_lastResult
, _T('('));
577 m_lastError
= wxPROTO_PROTERR
;
582 const wxChar
*addrEnd
= wxStrchr(addrStart
, _T(')'));
585 m_lastError
= wxPROTO_PROTERR
;
590 wxString
straddr(addrStart
+ 1, addrEnd
);
592 wxSscanf(straddr
, wxT("%d,%d,%d,%d,%d,%d"),
593 &a
[2],&a
[3],&a
[4],&a
[5],&a
[0],&a
[1]);
595 wxUint32 hostaddr
= (wxUint16
)a
[5] << 24 |
596 (wxUint16
)a
[4] << 16 |
597 (wxUint16
)a
[3] << 8 |
599 wxUint16 port
= (wxUint16
)a
[0] << 8 | a
[1];
602 addr
.Hostname(hostaddr
);
605 wxSocketClient
*client
= new wxSocketClient();
606 if ( !client
->Connect(addr
) )
612 client
->Notify(FALSE
);
623 if ( !CheckCommand(wxT("ABOR"), '4') )
626 return CheckResult('2');
629 wxInputStream
*wxFTP::GetInputStream(const wxString
& path
)
632 wxInputFTPStream
*in_stream
;
634 if ( ( m_currentTransfermode
== NONE
) && !SetTransferMode(BINARY
) )
637 wxSocketClient
*sock
= GetPort();
641 m_lastError
= wxPROTO_NETERR
;
645 wxString tmp_str
= wxT("RETR ") + wxURL::ConvertFromURI(path
);
646 if ( !CheckCommand(tmp_str
, '1') )
651 in_stream
= new wxInputFTPStream(this, sock
);
653 pos_size
= m_lastResult
.Index(wxT('('));
654 if ( pos_size
!= wxNOT_FOUND
)
656 wxString str_size
= m_lastResult(pos_size
+1, m_lastResult
.Index(wxT(')'))-1);
658 in_stream
->m_ftpsize
= wxAtoi(WXSTRINGCAST str_size
);
661 sock
->SetFlags(wxSOCKET_WAITALL
);
666 wxOutputStream
*wxFTP::GetOutputStream(const wxString
& path
)
668 if ( ( m_currentTransfermode
== NONE
) && !SetTransferMode(BINARY
) )
671 wxSocketClient
*sock
= GetPort();
673 wxString tmp_str
= wxT("STOR ") + path
;
674 if ( !CheckCommand(tmp_str
, '1') )
679 return new wxOutputFTPStream(this, sock
);
682 // ----------------------------------------------------------------------------
683 // FTP directory listing
684 // ----------------------------------------------------------------------------
686 bool wxFTP::GetList(wxArrayString
& files
,
687 const wxString
& wildcard
,
690 wxSocketBase
*sock
= GetPort();
694 // NLST : List of Filenames (including Directory's !)
695 // LIST : depending on BS of FTP-Server
696 // - Unix : result like "ls" command
697 // - Windows : like "dir" command
699 wxString
line(details
? _T("LIST") : _T("NLST"));
702 line
<< _T(' ') << wildcard
;
705 if (!CheckCommand(line
, '1'))
710 while ( ReadLine(sock
, line
) == wxPROTO_NOERR
)
716 // the file list should be terminated by "226 Transfer complete""
717 if ( !CheckResult('2') )
723 bool wxFTP::FileExists(const wxString
& fileName
)
725 // This function checks if the file specified in fileName exists in the
726 // current dir. It does so by simply doing an NLST (via GetList).
727 // If this succeeds (and the list is not empty) the file exists.
730 wxArrayString fileList
;
732 if ( GetList(fileList
, fileName
, FALSE
) )
734 // Some ftp-servers (Ipswitch WS_FTP Server 1.0.5 does this)
735 // displays this behaviour when queried on a non-existing file:
736 // NLST this_file_does_not_exist
737 // 150 Opening ASCII data connection for directory listing
738 // (no data transferred)
739 // 226 Transfer complete
740 // Here wxFTP::GetList(...) will succeed but it will return an empty
742 retval
= !fileList
.IsEmpty();
748 // ----------------------------------------------------------------------------
750 // ----------------------------------------------------------------------------
752 int wxFTP::GetFileSize(const wxString
& fileName
)
754 // return the filesize of the given file if possible
755 // return -1 otherwise (predominantly if file doesn't exist
760 // Check for existance of file via wxFTP::FileExists(...)
761 if ( FileExists(fileName
) )
765 // First try "SIZE" command using BINARY(IMAGE) transfermode
766 // Especially UNIX ftp-servers distinguish between the different
767 // transfermodes and reports different filesizes accordingly.
768 // The BINARY size is the interesting one: How much memory
769 // will we need to hold this file?
770 TransferMode oldTransfermode
= m_currentTransfermode
;
771 SetTransferMode(BINARY
);
772 command
<< _T("SIZE ") << fileName
;
774 bool ok
= CheckCommand(command
, '2');
778 // The answer should be one line: "213 <filesize>\n"
779 // 213 is File Status (STD9)
780 // "SIZE" is not described anywhere..? It works on most servers
782 if ( wxSscanf(GetLastResult().c_str(), _T("%i %i"),
783 &statuscode
, &filesize
) == 2 )
785 // We've gotten a good reply.
790 // Something bad happened.. A "2yz" reply with no size
796 // Set transfermode back to the original. Only the "SIZE"-command
797 // is dependant on transfermode
798 if ( oldTransfermode
!= NONE
)
800 SetTransferMode(oldTransfermode
);
803 if ( !ok
) // this is not a direct else clause.. The size command might return an invalid "2yz" reply
805 // The server didn't understand the "SIZE"-command or it
806 // returned an invalid reply.
807 // We now try to get details for the file with a "LIST"-command
808 // and then parse the output from there..
809 wxArrayString fileList
;
810 if ( GetList(fileList
, fileName
, TRUE
) )
812 if ( !fileList
.IsEmpty() )
814 // We _should_ only get one line in return, but just to be
815 // safe we run through the line(s) returned and look for a
816 // substring containing the name we are looking for. We
817 // stop the iteration at the first occurrence of the
818 // filename. The search is not case-sensitive.
819 bool foundIt
= FALSE
;
822 for ( i
= 0; !foundIt
&& i
< fileList
.Count(); i
++ )
824 foundIt
= fileList
[i
].Upper().Contains(fileName
.Upper());
829 // The index i points to the first occurrence of
830 // fileName in the array Now we have to find out what
831 // format the LIST has returned. There are two
832 // "schools": Unix-like
834 // '-rw-rw-rw- owner group size month day time filename'
838 // 'date size filename'
840 // check if the first character is '-'. This would
841 // indicate Unix-style (this also limits this function
842 // to searching for files, not directories)
843 if ( fileList
[i
].Mid(0, 1) == _T("-") )
846 if ( wxSscanf(fileList
[i
].c_str(),
847 _T("%*s %*s %*s %*s %i %*s %*s %*s %*s"),
850 // We've gotten a good response
855 // Hmm... Invalid response
856 wxLogTrace(FTP_TRACE_MASK
,
857 _T("Invalid LIST response"));
860 else // Windows-style response (?)
862 if ( wxSscanf(fileList
[i
].c_str(),
863 _T("%*s %*s %i %*s"),
871 // something bad happened..?
872 wxLogTrace(FTP_TRACE_MASK
,
873 _T("Invalid or unknown LIST response"));
882 // filesize might still be -1 when exiting
887 #if WXWIN_COMPATIBILITY_2
889 wxList
*wxFTP::GetList(const wxString
& wildcard
, bool details
)
891 wxSocketBase
*sock
= GetPort();
894 wxList
*file_list
= new wxList
;
896 // NLST : List of Filenames (including Directory's !)
897 // LIST : depending on BS of FTP-Server
898 // - Unix : result like "ls" command
899 // - Windows : like "dir" command
902 line
= _T("NLST"); // Default
905 if (!wildcard
.IsNull())
907 if (!CheckCommand(line
, '1'))
913 while (GetLine(sock
, line
) == wxPROTO_NOERR
)
915 file_list
->Append((wxObject
*)(new wxString(line
)));
917 if (!CheckResult('2'))
920 file_list
->DeleteContents(TRUE
);
926 #endif // WXWIN_COMPATIBILITY_2