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 licence
14 /////////////////////////////////////////////////////////////////////////////
16 // ============================================================================
18 // ============================================================================
20 #if defined(__GNUG__) && !defined(NO_GCC_PRAGMA)
21 #pragma implementation "ftp.h"
24 // ----------------------------------------------------------------------------
26 // ----------------------------------------------------------------------------
28 // For compilers that support precompilation, includes "wx.h".
29 #include "wx/wxprec.h"
35 #if wxUSE_PROTOCOL_FTP
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()
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
528 DECLARE_NO_COPY_CLASS(wxInputFTPStream
)
531 class wxOutputFTPStream
: public wxSocketOutputStream
534 wxOutputFTPStream(wxFTP
*ftp_clt
, wxSocketBase
*sock
)
535 : wxSocketOutputStream(*sock
), m_ftp(ftp_clt
)
539 virtual ~wxOutputFTPStream(void)
543 // close data connection first, this will generate "transfer
548 m_ftp
->CheckResult('2');
550 m_ftp
->m_streaming
= FALSE
;
554 // abort data connection first
557 // and close it after
564 DECLARE_NO_COPY_CLASS(wxOutputFTPStream
)
567 wxSocketClient
*wxFTP::GetPort()
571 if ( !DoSimpleCommand(_T("PASV")) )
573 wxLogError(_("The FTP server doesn't support passive mode."));
578 const wxChar
*addrStart
= wxStrchr(m_lastResult
, _T('('));
581 m_lastError
= wxPROTO_PROTERR
;
586 const wxChar
*addrEnd
= wxStrchr(addrStart
, _T(')'));
589 m_lastError
= wxPROTO_PROTERR
;
594 wxString
straddr(addrStart
+ 1, addrEnd
);
596 wxSscanf(straddr
, wxT("%d,%d,%d,%d,%d,%d"),
597 &a
[2],&a
[3],&a
[4],&a
[5],&a
[0],&a
[1]);
599 wxUint32 hostaddr
= (wxUint16
)a
[2] << 24 |
600 (wxUint16
)a
[3] << 16 |
601 (wxUint16
)a
[4] << 8 |
603 wxUint16 port
= (wxUint16
)a
[0] << 8 | a
[1];
606 addr
.Hostname(hostaddr
);
609 wxSocketClient
*client
= new wxSocketClient();
610 if ( !client
->Connect(addr
) )
616 client
->Notify(FALSE
);
627 if ( !CheckCommand(wxT("ABOR"), '4') )
630 return CheckResult('2');
633 wxInputStream
*wxFTP::GetInputStream(const wxString
& path
)
636 wxInputFTPStream
*in_stream
;
638 if ( ( m_currentTransfermode
== NONE
) && !SetTransferMode(BINARY
) )
641 wxSocketClient
*sock
= GetPort();
645 m_lastError
= wxPROTO_NETERR
;
649 wxString tmp_str
= wxT("RETR ") + wxURL::ConvertFromURI(path
);
650 if ( !CheckCommand(tmp_str
, '1') )
655 in_stream
= new wxInputFTPStream(this, sock
);
657 pos_size
= m_lastResult
.Index(wxT('('));
658 if ( pos_size
!= wxNOT_FOUND
)
660 wxString str_size
= m_lastResult(pos_size
+1, m_lastResult
.Index(wxT(')'))-1);
662 in_stream
->m_ftpsize
= wxAtoi(WXSTRINGCAST str_size
);
665 sock
->SetFlags(wxSOCKET_WAITALL
);
670 wxOutputStream
*wxFTP::GetOutputStream(const wxString
& path
)
672 if ( ( m_currentTransfermode
== NONE
) && !SetTransferMode(BINARY
) )
675 wxSocketClient
*sock
= GetPort();
677 wxString tmp_str
= wxT("STOR ") + path
;
678 if ( !CheckCommand(tmp_str
, '1') )
683 return new wxOutputFTPStream(this, sock
);
686 // ----------------------------------------------------------------------------
687 // FTP directory listing
688 // ----------------------------------------------------------------------------
690 bool wxFTP::GetList(wxArrayString
& files
,
691 const wxString
& wildcard
,
694 wxSocketBase
*sock
= GetPort();
698 // NLST : List of Filenames (including Directory's !)
699 // LIST : depending on BS of FTP-Server
700 // - Unix : result like "ls" command
701 // - Windows : like "dir" command
703 wxString
line(details
? _T("LIST") : _T("NLST"));
706 line
<< _T(' ') << wildcard
;
709 if (!CheckCommand(line
, '1'))
714 while ( ReadLine(sock
, line
) == wxPROTO_NOERR
)
720 // the file list should be terminated by "226 Transfer complete""
721 if ( !CheckResult('2') )
727 bool wxFTP::FileExists(const wxString
& fileName
)
729 // This function checks if the file specified in fileName exists in the
730 // current dir. It does so by simply doing an NLST (via GetList).
731 // If this succeeds (and the list is not empty) the file exists.
734 wxArrayString fileList
;
736 if ( GetList(fileList
, fileName
, FALSE
) )
738 // Some ftp-servers (Ipswitch WS_FTP Server 1.0.5 does this)
739 // displays this behaviour when queried on a non-existing file:
740 // NLST this_file_does_not_exist
741 // 150 Opening ASCII data connection for directory listing
742 // (no data transferred)
743 // 226 Transfer complete
744 // Here wxFTP::GetList(...) will succeed but it will return an empty
746 retval
= !fileList
.IsEmpty();
752 // ----------------------------------------------------------------------------
754 // ----------------------------------------------------------------------------
756 int wxFTP::GetFileSize(const wxString
& fileName
)
758 // return the filesize of the given file if possible
759 // return -1 otherwise (predominantly if file doesn't exist
764 // Check for existance of file via wxFTP::FileExists(...)
765 if ( FileExists(fileName
) )
769 // First try "SIZE" command using BINARY(IMAGE) transfermode
770 // Especially UNIX ftp-servers distinguish between the different
771 // transfermodes and reports different filesizes accordingly.
772 // The BINARY size is the interesting one: How much memory
773 // will we need to hold this file?
774 TransferMode oldTransfermode
= m_currentTransfermode
;
775 SetTransferMode(BINARY
);
776 command
<< _T("SIZE ") << fileName
;
778 bool ok
= CheckCommand(command
, '2');
782 // The answer should be one line: "213 <filesize>\n"
783 // 213 is File Status (STD9)
784 // "SIZE" is not described anywhere..? It works on most servers
786 if ( wxSscanf(GetLastResult().c_str(), _T("%i %i"),
787 &statuscode
, &filesize
) == 2 )
789 // We've gotten a good reply.
794 // Something bad happened.. A "2yz" reply with no size
800 // Set transfermode back to the original. Only the "SIZE"-command
801 // is dependant on transfermode
802 if ( oldTransfermode
!= NONE
)
804 SetTransferMode(oldTransfermode
);
807 // this is not a direct else clause.. The size command might return an
808 // invalid "2yz" reply
811 // The server didn't understand the "SIZE"-command or it
812 // returned an invalid reply.
813 // We now try to get details for the file with a "LIST"-command
814 // and then parse the output from there..
815 wxArrayString fileList
;
816 if ( GetList(fileList
, fileName
, TRUE
) )
818 if ( !fileList
.IsEmpty() )
820 // We _should_ only get one line in return, but just to be
821 // safe we run through the line(s) returned and look for a
822 // substring containing the name we are looking for. We
823 // stop the iteration at the first occurrence of the
824 // filename. The search is not case-sensitive.
825 bool foundIt
= FALSE
;
828 for ( i
= 0; !foundIt
&& i
< fileList
.Count(); i
++ )
830 foundIt
= fileList
[i
].Upper().Contains(fileName
.Upper());
835 // The index i points to the first occurrence of
836 // fileName in the array Now we have to find out what
837 // format the LIST has returned. There are two
838 // "schools": Unix-like
840 // '-rw-rw-rw- owner group size month day time filename'
844 // 'date size filename'
846 // check if the first character is '-'. This would
847 // indicate Unix-style (this also limits this function
848 // to searching for files, not directories)
849 if ( fileList
[i
].Mid(0, 1) == _T("-") )
852 if ( wxSscanf(fileList
[i
].c_str(),
853 _T("%*s %*s %*s %*s %i %*s %*s %*s %*s"),
856 // Hmm... Invalid response
857 wxLogTrace(FTP_TRACE_MASK
,
858 _T("Invalid LIST response"));
861 else // Windows-style response (?)
863 if ( wxSscanf(fileList
[i
].c_str(),
864 _T("%*s %*s %i %*s"),
867 // something bad happened..?
868 wxLogTrace(FTP_TRACE_MASK
,
869 _T("Invalid or unknown LIST response"));
878 // filesize might still be -1 when exiting
882 #endif // wxUSE_PROTOCOL_FTP