1 /////////////////////////////////////////////////////////////////////////////
2 // Name: common/ftp.cpp
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 // Randall Fox (support for active mode)
11 // Created: 07/07/1997
13 // Copyright: (c) 1997, 1998 Guilhem Lavaux
14 // (c) 1998-2004 wxWidgets team
15 // Licence: wxWindows licence
16 /////////////////////////////////////////////////////////////////////////////
18 // ============================================================================
20 // ============================================================================
22 // ----------------------------------------------------------------------------
24 // ----------------------------------------------------------------------------
26 // For compilers that support precompilation, includes "wx.h".
27 #include "wx/wxprec.h"
33 #if wxUSE_PROTOCOL_FTP
37 #include "wx/string.h"
43 #include "wx/sckaddr.h"
44 #include "wx/socket.h"
46 #include "wx/sckstrm.h"
47 #include "wx/protocol/protocol.h"
48 #include "wx/protocol/ftp.h"
50 #if defined(__WXMAC__)
51 #include "wx/mac/macsock.h"
58 // ----------------------------------------------------------------------------
60 // ----------------------------------------------------------------------------
62 // the length of FTP status code (3 digits)
63 static const size_t LEN_CODE
= 3;
65 // ----------------------------------------------------------------------------
67 // ----------------------------------------------------------------------------
69 IMPLEMENT_DYNAMIC_CLASS(wxFTP
, wxProtocol
)
70 IMPLEMENT_PROTOCOL(wxFTP
, wxT("ftp"), wxT("ftp"), true)
72 // ============================================================================
74 // ============================================================================
76 // ----------------------------------------------------------------------------
77 // wxFTP constructor and destructor
78 // ----------------------------------------------------------------------------
82 m_lastError
= wxPROTO_NOERR
;
84 m_currentTransfermode
= NONE
;
86 m_user
= wxT("anonymous");
87 m_passwd
<< wxGetUserId() << wxT('@') << wxGetFullHostName();
90 SetFlags(wxSOCKET_NOWAIT
);
92 SetDefaultTimeout(60); // Default is Sixty Seconds
93 m_bEncounteredError
= false;
100 // if we are streaming, this will issue
101 // an FTP ABORT command, to tell the server we are aborting
105 // now this issues a "QUIT" command to tell the server we are
109 // ----------------------------------------------------------------------------
110 // wxFTP connect and login methods
111 // ----------------------------------------------------------------------------
113 bool wxFTP::Connect(wxSockAddress
& addr
, bool WXUNUSED(wait
))
115 if ( !wxProtocol::Connect(addr
) )
117 m_lastError
= wxPROTO_NETERR
;
123 m_lastError
= wxPROTO_CONNERR
;
127 // we should have 220 welcome message
128 if ( !CheckResult('2') )
135 command
.Printf(wxT("USER %s"), m_user
.c_str());
136 char rc
= SendCommand(command
);
139 // 230 return: user accepted without password
149 command
.Printf(wxT("PASS %s"), m_passwd
.c_str());
150 if ( !CheckCommand(command
, '2') )
159 bool wxFTP::Connect(const wxString
& host
)
163 addr
.Service(wxT("ftp"));
165 return Connect(addr
);
172 m_lastError
= wxPROTO_STREAMING
;
178 if ( !CheckCommand(wxT("QUIT"), '2') )
180 wxLogDebug(_T("Failed to close connection gracefully."));
184 return wxSocketClient::Close();
187 // ============================================================================
189 // ============================================================================
191 // ----------------------------------------------------------------------------
192 // Send command to FTP server
193 // ----------------------------------------------------------------------------
195 char wxFTP::SendCommand(const wxString
& command
)
199 m_lastError
= wxPROTO_STREAMING
;
203 wxString tmp_str
= command
+ wxT("\r\n");
204 const wxWX2MBbuf tmp_buf
= tmp_str
.mb_str();
205 if ( Write(wxMBSTRINGCAST tmp_buf
, strlen(tmp_buf
)).Error())
207 m_lastError
= wxPROTO_NETERR
;
212 // don't show the passwords in the logs (even in debug ones)
213 wxString cmd
, password
;
214 if ( command
.Upper().StartsWith(_T("PASS "), &password
) )
216 cmd
<< _T("PASS ") << wxString(_T('*'), password
.length());
223 wxLogTrace(FTP_TRACE_MASK
, _T("==> %s"), cmd
.c_str());
224 #endif // __WXDEBUG__
229 // ----------------------------------------------------------------------------
230 // Recieve servers reply
231 // ----------------------------------------------------------------------------
233 char wxFTP::GetResult()
235 // if we've already had a read or write timeout error, the connection is
236 // probably toast, so don't bother, it just wastes the users time
237 if ( m_bEncounteredError
)
242 // m_lastResult will contain the entire server response, possibly on
244 m_lastResult
.clear();
246 // we handle multiline replies here according to RFC 959: it says that a
247 // reply may either be on 1 line of the form "xyz ..." or on several lines
248 // in whuch case it looks like
252 // and the intermeidate lines may start with xyz or not
253 bool badReply
= false;
254 bool firstLine
= true;
255 bool endOfReply
= false;
256 while ( !endOfReply
&& !badReply
)
259 m_lastError
= ReadLine(this,line
);
262 m_bEncounteredError
= true;
266 if ( !m_lastResult
.empty() )
268 // separate from last line
269 m_lastResult
+= _T('\n');
272 m_lastResult
+= line
;
274 // unless this is an intermediate line of a multiline reply, it must
275 // contain the code in the beginning and '-' or ' ' following it
276 if ( line
.Len() < LEN_CODE
+ 1 )
284 wxLogTrace(FTP_TRACE_MASK
, _T("<== %s %s"),
285 code
.c_str(), line
.c_str());
288 else // line has at least 4 chars
290 // this is the char which tells us what we're dealing with
291 wxChar chMarker
= line
.GetChar(LEN_CODE
);
295 code
= wxString(line
, LEN_CODE
);
296 wxLogTrace(FTP_TRACE_MASK
, _T("<== %s %s"),
297 code
.c_str(), line
.c_str() + LEN_CODE
+ 1);
314 else // subsequent line of multiline reply
316 if ( wxStrncmp(line
, code
, LEN_CODE
) == 0 )
318 if ( chMarker
== _T(' ') )
323 wxLogTrace(FTP_TRACE_MASK
, _T("<== %s %s"),
324 code
.c_str(), line
.c_str() + LEN_CODE
+ 1);
328 // just part of reply
329 wxLogTrace(FTP_TRACE_MASK
, _T("<== %s %s"),
330 code
.c_str(), line
.c_str());
338 wxLogDebug(_T("Broken FTP server: '%s' is not a valid reply."),
339 m_lastResult
.c_str());
341 m_lastError
= wxPROTO_PROTERR
;
346 // if we got here we must have a non empty code string
347 return (char)code
[0u];
350 // ----------------------------------------------------------------------------
351 // wxFTP simple commands
352 // ----------------------------------------------------------------------------
354 bool wxFTP::SetTransferMode(TransferMode transferMode
)
356 if ( transferMode
== m_currentTransfermode
)
363 switch ( transferMode
)
366 wxFAIL_MSG(_T("unknown FTP transfer mode"));
378 if ( !DoSimpleCommand(_T("TYPE"), mode
) )
380 wxLogError(_("Failed to set FTP transfer mode to %s."), (const wxChar
*)
381 (transferMode
== ASCII
? _("ASCII") : _("binary")));
386 // If we get here the operation has been successfully completed
387 // Set the status-member
388 m_currentTransfermode
= transferMode
;
393 bool wxFTP::DoSimpleCommand(const wxChar
*command
, const wxString
& arg
)
395 wxString fullcmd
= command
;
398 fullcmd
<< _T(' ') << arg
;
401 if ( !CheckCommand(fullcmd
, '2') )
403 wxLogDebug(_T("FTP command '%s' failed."), fullcmd
.c_str());
411 bool wxFTP::ChDir(const wxString
& dir
)
413 // some servers might not understand ".." if they use different directory
414 // tree conventions, but they always understand CDUP - should we use it if
415 // dir == ".."? OTOH, do such servers (still) exist?
417 return DoSimpleCommand(_T("CWD"), dir
);
420 bool wxFTP::MkDir(const wxString
& dir
)
422 return DoSimpleCommand(_T("MKD"), dir
);
425 bool wxFTP::RmDir(const wxString
& dir
)
427 return DoSimpleCommand(_T("RMD"), dir
);
430 wxString
wxFTP::Pwd()
434 if ( CheckCommand(wxT("PWD"), '2') )
436 // the result is at least that long if CheckCommand() succeeded
437 wxString::const_iterator p
= m_lastResult
.begin() + LEN_CODE
+ 1;
440 wxLogDebug(_T("Missing starting quote in reply for PWD: %s"),
441 wxString(p
, m_lastResult
.end()));
445 for ( ++p
; (bool)*p
; ++p
) // FIXME-DMARS
449 // check if the quote is doubled
451 if ( !*p
|| *p
!= _T('"') )
453 // no, this is the end
456 //else: yes, it is: this is an embedded quote in the
457 // filename, treat as normal char
465 wxLogDebug(_T("Missing ending quote in reply for PWD: %s"),
466 m_lastResult
.c_str() + LEN_CODE
+ 1);
472 wxLogDebug(_T("FTP PWD command failed."));
478 bool wxFTP::Rename(const wxString
& src
, const wxString
& dst
)
482 str
= wxT("RNFR ") + src
;
483 if ( !CheckCommand(str
, '3') )
486 str
= wxT("RNTO ") + dst
;
488 return CheckCommand(str
, '2');
491 bool wxFTP::RmFile(const wxString
& path
)
494 str
= wxT("DELE ") + path
;
496 return CheckCommand(str
, '2');
499 // ----------------------------------------------------------------------------
500 // wxFTP download and upload
501 // ----------------------------------------------------------------------------
503 class wxInputFTPStream
: public wxSocketInputStream
506 wxInputFTPStream(wxFTP
*ftp
, wxSocketBase
*sock
)
507 : wxSocketInputStream(*sock
)
510 // socket timeout automatically set in GetPort function
513 virtual ~wxInputFTPStream()
515 delete m_i_socket
; // keep at top
517 // when checking the result, the stream will
518 // almost always show an error, even if the file was
519 // properly transfered, thus, lets just grab the result
521 // we are looking for "226 transfer completed"
522 char code
= m_ftp
->GetResult();
525 // it was a good transfer.
527 m_ftp
->m_streaming
= false;
533 // the connection is probably toast. issue an abort, and
534 // then a close. there won't be any more waiting
535 // for this connection
540 // There was a problem with the transfer and the server
541 // has acknowledged it. If we issue an "ABORT" now, the user
542 // would get the "226" for the abort and think the xfer was
543 // complete, thus, don't do anything here, just return
548 DECLARE_NO_COPY_CLASS(wxInputFTPStream
)
551 class wxOutputFTPStream
: public wxSocketOutputStream
554 wxOutputFTPStream(wxFTP
*ftp_clt
, wxSocketBase
*sock
)
555 : wxSocketOutputStream(*sock
), m_ftp(ftp_clt
)
559 virtual ~wxOutputFTPStream(void)
563 // close data connection first, this will generate "transfer
568 m_ftp
->GetResult(); // save result so user can get to it
570 m_ftp
->m_streaming
= false;
574 // abort data connection first
577 // and close it after
584 DECLARE_NO_COPY_CLASS(wxOutputFTPStream
)
587 void wxFTP::SetDefaultTimeout(wxUint32 Value
)
589 m_uiDefaultTimeout
= Value
;
590 SetTimeout(Value
); // sets it for this socket
594 wxSocketBase
*wxFTP::GetPort()
597 PASSIVE: Client sends a "PASV" to the server. The server responds with
598 an address and port number which it will be listening on. Then
599 the client connects to the server at the specified address and
602 ACTIVE: Client sends the server a PORT command which includes an
603 address and port number which the client will be listening on.
604 The server then connects to the client at that address and
608 wxSocketBase
*socket
= m_bPassive
? GetPassivePort() : GetActivePort();
611 m_bEncounteredError
= true;
615 // Now set the time for the new socket to the default or user selected
617 socket
->SetTimeout(m_uiDefaultTimeout
);
622 wxSocketBase
*wxFTP::AcceptIfActive(wxSocketBase
*sock
)
627 // now wait for a connection from server
628 wxSocketServer
*sockSrv
= (wxSocketServer
*)sock
;
629 if ( !sockSrv
->WaitForAccept() )
631 m_lastError
= wxPROTO_CONNERR
;
632 wxLogError(_("Timeout while waiting for FTP server to connect, try passive mode."));
638 sock
= sockSrv
->Accept(true);
645 wxString
wxFTP::GetPortCmdArgument(const wxIPV4address
& addrLocal
,
646 const wxIPV4address
& addrNew
)
648 // Just fills in the return value with the local IP
649 // address of the current socket. Also it fill in the
650 // PORT which the client will be listening on
652 wxString addrIP
= addrLocal
.IPAddress();
653 int portNew
= addrNew
.Service();
655 // We need to break the PORT number in bytes
656 addrIP
.Replace(_T("."), _T(","));
658 << wxString::Format(_T("%d"), portNew
>> 8) << _T(',')
659 << wxString::Format(_T("%d"), portNew
& 0xff);
661 // Now we have a value like "10,0,0,1,5,23"
665 wxSocketBase
*wxFTP::GetActivePort()
667 // we need an address to listen on
668 wxIPV4address addrNew
, addrLocal
;
670 addrNew
.AnyAddress();
671 addrNew
.Service(0); // pick an open port number.
673 wxSocketServer
*sockSrv
= new wxSocketServer(addrNew
);
676 // We use Ok() here to see if everything is ok
677 m_lastError
= wxPROTO_PROTERR
;
682 //gets the new address, actually it is just the port number
683 sockSrv
->GetLocal(addrNew
);
685 // Now we create the argument of the PORT command, we send in both
686 // addresses because the addrNew has an IP of "0.0.0.0", so we need the
687 // value in addrLocal
688 wxString port
= GetPortCmdArgument(addrLocal
, addrNew
);
689 if ( !DoSimpleCommand(_T("PORT "), port
) )
691 m_lastError
= wxPROTO_PROTERR
;
693 wxLogError(_("The FTP server doesn't support the PORT command."));
697 sockSrv
->Notify(false); // Don't send any events
701 wxSocketBase
*wxFTP::GetPassivePort()
703 if ( !DoSimpleCommand(_T("PASV")) )
705 wxLogError(_("The FTP server doesn't support passive mode."));
709 const wxChar
*addrStart
= wxStrchr(m_lastResult
, _T('('));
710 const wxChar
*addrEnd
= addrStart
? wxStrchr(addrStart
, _T(')')) : NULL
;
713 m_lastError
= wxPROTO_PROTERR
;
718 // get the port number and address
720 wxString
straddr(addrStart
+ 1, addrEnd
);
721 wxSscanf(straddr
, wxT("%d,%d,%d,%d,%d,%d"),
722 &a
[2],&a
[3],&a
[4],&a
[5],&a
[0],&a
[1]);
724 wxUint32 hostaddr
= (wxUint16
)a
[2] << 24 |
725 (wxUint16
)a
[3] << 16 |
726 (wxUint16
)a
[4] << 8 |
728 wxUint16 port
= (wxUint16
)(a
[0] << 8 | a
[1]);
731 addr
.Hostname(hostaddr
);
734 wxSocketClient
*client
= new wxSocketClient();
735 if ( !client
->Connect(addr
) )
741 client
->Notify(false);
752 if ( !CheckCommand(wxT("ABOR"), '4') )
755 return CheckResult('2');
758 wxInputStream
*wxFTP::GetInputStream(const wxString
& path
)
760 if ( ( m_currentTransfermode
== NONE
) && !SetTransferMode(BINARY
) )
763 wxSocketBase
*sock
= GetPort();
767 m_lastError
= wxPROTO_NETERR
;
771 wxString tmp_str
= wxT("RETR ") + wxURI::Unescape(path
);
772 if ( !CheckCommand(tmp_str
, '1') )
775 sock
= AcceptIfActive(sock
);
779 sock
->SetFlags(wxSOCKET_WAITALL
);
783 wxInputFTPStream
*in_stream
= new wxInputFTPStream(this, sock
);
788 wxOutputStream
*wxFTP::GetOutputStream(const wxString
& path
)
790 if ( ( m_currentTransfermode
== NONE
) && !SetTransferMode(BINARY
) )
793 wxSocketBase
*sock
= GetPort();
795 wxString tmp_str
= wxT("STOR ") + path
;
796 if ( !CheckCommand(tmp_str
, '1') )
799 sock
= AcceptIfActive(sock
);
803 return new wxOutputFTPStream(this, sock
);
806 // ----------------------------------------------------------------------------
807 // FTP directory listing
808 // ----------------------------------------------------------------------------
810 bool wxFTP::GetList(wxArrayString
& files
,
811 const wxString
& wildcard
,
814 wxSocketBase
*sock
= GetPort();
818 // NLST : List of Filenames (including Directory's !)
819 // LIST : depending on BS of FTP-Server
820 // - Unix : result like "ls" command
821 // - Windows : like "dir" command
823 wxString
line(details
? _T("LIST") : _T("NLST"));
824 if ( !wildcard
.empty() )
826 line
<< _T(' ') << wildcard
;
829 if ( !CheckCommand(line
, '1') )
831 m_lastError
= wxPROTO_PROTERR
;
832 wxLogDebug(_T("FTP 'LIST' command returned unexpected result from server"));
837 sock
= AcceptIfActive(sock
);
842 while (ReadLine(sock
, line
) == wxPROTO_NOERR
)
849 // the file list should be terminated by "226 Transfer complete""
850 return CheckResult('2');
853 bool wxFTP::FileExists(const wxString
& fileName
)
855 // This function checks if the file specified in fileName exists in the
856 // current dir. It does so by simply doing an NLST (via GetList).
857 // If this succeeds (and the list is not empty) the file exists.
860 wxArrayString fileList
;
862 if ( GetList(fileList
, fileName
, false) )
864 // Some ftp-servers (Ipswitch WS_FTP Server 1.0.5 does this)
865 // displays this behaviour when queried on a nonexistent file:
866 // NLST this_file_does_not_exist
867 // 150 Opening ASCII data connection for directory listing
868 // (no data transferred)
869 // 226 Transfer complete
870 // Here wxFTP::GetList(...) will succeed but it will return an empty
872 retval
= !fileList
.IsEmpty();
878 // ----------------------------------------------------------------------------
880 // ----------------------------------------------------------------------------
882 int wxFTP::GetFileSize(const wxString
& fileName
)
884 // return the filesize of the given file if possible
885 // return -1 otherwise (predominantly if file doesn't exist
890 // Check for existance of file via wxFTP::FileExists(...)
891 if ( FileExists(fileName
) )
895 // First try "SIZE" command using BINARY(IMAGE) transfermode
896 // Especially UNIX ftp-servers distinguish between the different
897 // transfermodes and reports different filesizes accordingly.
898 // The BINARY size is the interesting one: How much memory
899 // will we need to hold this file?
900 TransferMode oldTransfermode
= m_currentTransfermode
;
901 SetTransferMode(BINARY
);
902 command
<< _T("SIZE ") << fileName
;
904 bool ok
= CheckCommand(command
, '2');
908 // The answer should be one line: "213 <filesize>\n"
909 // 213 is File Status (STD9)
910 // "SIZE" is not described anywhere..? It works on most servers
912 if ( wxSscanf(GetLastResult().c_str(), _T("%i %i"),
913 &statuscode
, &filesize
) == 2 )
915 // We've gotten a good reply.
920 // Something bad happened.. A "2yz" reply with no size
926 // Set transfermode back to the original. Only the "SIZE"-command
927 // is dependant on transfermode
928 if ( oldTransfermode
!= NONE
)
930 SetTransferMode(oldTransfermode
);
933 // this is not a direct else clause.. The size command might return an
934 // invalid "2yz" reply
937 // The server didn't understand the "SIZE"-command or it
938 // returned an invalid reply.
939 // We now try to get details for the file with a "LIST"-command
940 // and then parse the output from there..
941 wxArrayString fileList
;
942 if ( GetList(fileList
, fileName
, true) )
944 if ( !fileList
.IsEmpty() )
946 // We _should_ only get one line in return, but just to be
947 // safe we run through the line(s) returned and look for a
948 // substring containing the name we are looking for. We
949 // stop the iteration at the first occurrence of the
950 // filename. The search is not case-sensitive.
951 bool foundIt
= false;
954 for ( i
= 0; !foundIt
&& i
< fileList
.GetCount(); i
++ )
956 foundIt
= fileList
[i
].Upper().Contains(fileName
.Upper());
961 // The index i points to the first occurrence of
962 // fileName in the array Now we have to find out what
963 // format the LIST has returned. There are two
964 // "schools": Unix-like
966 // '-rw-rw-rw- owner group size month day time filename'
970 // 'date size filename'
972 // check if the first character is '-'. This would
973 // indicate Unix-style (this also limits this function
974 // to searching for files, not directories)
975 if ( fileList
[i
].Mid(0, 1) == _T("-") )
978 if ( wxSscanf(fileList
[i
].c_str(),
979 _T("%*s %*s %*s %*s %i %*s %*s %*s %*s"),
982 // Hmm... Invalid response
983 wxLogTrace(FTP_TRACE_MASK
,
984 _T("Invalid LIST response"));
987 else // Windows-style response (?)
989 if ( wxSscanf(fileList
[i
].c_str(),
990 _T("%*s %*s %i %*s"),
993 // something bad happened..?
994 wxLogTrace(FTP_TRACE_MASK
,
995 _T("Invalid or unknown LIST response"));
1004 // filesize might still be -1 when exiting
1008 #endif // wxUSE_PROTOCOL_FTP