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"
41 #include "wx/wxcrtvararg.h"
44 #include "wx/sckaddr.h"
45 #include "wx/socket.h"
47 #include "wx/sckstrm.h"
48 #include "wx/protocol/protocol.h"
49 #include "wx/protocol/ftp.h"
55 // ----------------------------------------------------------------------------
57 // ----------------------------------------------------------------------------
59 // the length of FTP status code (3 digits)
60 static const size_t LEN_CODE
= 3;
62 // ----------------------------------------------------------------------------
64 // ----------------------------------------------------------------------------
66 IMPLEMENT_DYNAMIC_CLASS(wxFTP
, wxProtocol
)
67 IMPLEMENT_PROTOCOL(wxFTP
, wxT("ftp"), wxT("ftp"), true)
69 // ============================================================================
71 // ============================================================================
73 // ----------------------------------------------------------------------------
74 // wxFTP constructor and destructor
75 // ----------------------------------------------------------------------------
79 m_lastError
= wxPROTO_NOERR
;
81 m_currentTransfermode
= NONE
;
83 m_user
= wxT("anonymous");
84 m_passwd
<< wxGetUserId() << wxT('@') << wxGetFullHostName();
87 SetFlags(wxSOCKET_NOWAIT
);
89 SetDefaultTimeout(60); // Default is Sixty Seconds
90 m_bEncounteredError
= false;
97 // if we are streaming, this will issue
98 // an FTP ABORT command, to tell the server we are aborting
102 // now this issues a "QUIT" command to tell the server we are
106 // ----------------------------------------------------------------------------
107 // wxFTP connect and login methods
108 // ----------------------------------------------------------------------------
110 bool wxFTP::Connect(const wxSockAddress
& addr
, bool WXUNUSED(wait
))
112 if ( !wxProtocol::Connect(addr
) )
114 m_lastError
= wxPROTO_NETERR
;
120 m_lastError
= wxPROTO_CONNERR
;
124 // we should have 220 welcome message
125 if ( !CheckResult('2') )
132 command
.Printf(wxT("USER %s"), m_user
.c_str());
133 char rc
= SendCommand(command
);
136 // 230 return: user accepted without password
146 command
.Printf(wxT("PASS %s"), m_passwd
.c_str());
147 if ( !CheckCommand(command
, '2') )
156 bool wxFTP::Connect(const wxString
& host
)
160 addr
.Service(wxT("ftp"));
162 return Connect(addr
);
169 m_lastError
= wxPROTO_STREAMING
;
175 if ( !CheckCommand(wxT("QUIT"), '2') )
177 wxLogDebug(_T("Failed to close connection gracefully."));
181 return wxSocketClient::Close();
184 // ============================================================================
186 // ============================================================================
188 wxSocketBase
*wxFTP::AcceptIfActive(wxSocketBase
*sock
)
193 // now wait for a connection from server
194 wxSocketServer
*sockSrv
= (wxSocketServer
*)sock
;
195 if ( !sockSrv
->WaitForAccept() )
197 m_lastError
= wxPROTO_CONNERR
;
198 wxLogError(_("Timeout while waiting for FTP server to connect, try passive mode."));
204 sock
= sockSrv
->Accept(true);
217 if ( !CheckCommand(wxT("ABOR"), '4') )
220 return CheckResult('2');
223 void wxFTP::SetDefaultTimeout(wxUint32 Value
)
225 m_uiDefaultTimeout
= Value
;
226 SetTimeout(Value
); // sets it for this socket
229 // ----------------------------------------------------------------------------
230 // Send command to FTP server
231 // ----------------------------------------------------------------------------
233 char wxFTP::SendCommand(const wxString
& command
)
237 m_lastError
= wxPROTO_STREAMING
;
241 wxString tmp_str
= command
+ wxT("\r\n");
242 const wxWX2MBbuf tmp_buf
= tmp_str
.mb_str();
243 if ( Write(wxMBSTRINGCAST tmp_buf
, strlen(tmp_buf
)).Error())
245 m_lastError
= wxPROTO_NETERR
;
250 // don't show the passwords in the logs (even in debug ones)
251 wxString cmd
, password
;
252 if ( command
.Upper().StartsWith(_T("PASS "), &password
) )
254 cmd
<< _T("PASS ") << wxString(_T('*'), password
.length());
261 wxLogTrace(FTP_TRACE_MASK
, _T("==> %s"), cmd
.c_str());
262 #endif // __WXDEBUG__
267 // ----------------------------------------------------------------------------
268 // Receive servers reply
269 // ----------------------------------------------------------------------------
271 char wxFTP::GetResult()
273 // if we've already had a read or write timeout error, the connection is
274 // probably toast, so don't bother, it just wastes the users time
275 if ( m_bEncounteredError
)
280 // m_lastResult will contain the entire server response, possibly on
282 m_lastResult
.clear();
284 // we handle multiline replies here according to RFC 959: it says that a
285 // reply may either be on 1 line of the form "xyz ..." or on several lines
286 // in whuch case it looks like
290 // and the intermeidate lines may start with xyz or not
291 bool badReply
= false;
292 bool firstLine
= true;
293 bool endOfReply
= false;
294 while ( !endOfReply
&& !badReply
)
297 m_lastError
= ReadLine(this,line
);
300 m_bEncounteredError
= true;
304 if ( !m_lastResult
.empty() )
306 // separate from last line
307 m_lastResult
+= _T('\n');
310 m_lastResult
+= line
;
312 // unless this is an intermediate line of a multiline reply, it must
313 // contain the code in the beginning and '-' or ' ' following it
314 if ( line
.Len() < LEN_CODE
+ 1 )
322 wxLogTrace(FTP_TRACE_MASK
, _T("<== %s %s"),
323 code
.c_str(), line
.c_str());
326 else // line has at least 4 chars
328 // this is the char which tells us what we're dealing with
329 wxChar chMarker
= line
.GetChar(LEN_CODE
);
333 code
= wxString(line
, LEN_CODE
);
334 wxLogTrace(FTP_TRACE_MASK
, _T("<== %s %s"),
335 code
.c_str(), line
.c_str() + LEN_CODE
+ 1);
352 else // subsequent line of multiline reply
354 if ( line
.compare(0, LEN_CODE
, code
) == 0 )
356 if ( chMarker
== _T(' ') )
361 wxLogTrace(FTP_TRACE_MASK
, _T("<== %s %s"),
362 code
.c_str(), line
.c_str() + LEN_CODE
+ 1);
366 // just part of reply
367 wxLogTrace(FTP_TRACE_MASK
, _T("<== %s %s"),
368 code
.c_str(), line
.c_str());
376 wxLogDebug(_T("Broken FTP server: '%s' is not a valid reply."),
377 m_lastResult
.c_str());
379 m_lastError
= wxPROTO_PROTERR
;
384 // if we got here we must have a non empty code string
385 return (char)code
[0u];
388 // ----------------------------------------------------------------------------
389 // wxFTP simple commands
390 // ----------------------------------------------------------------------------
392 bool wxFTP::SetTransferMode(TransferMode transferMode
)
394 if ( transferMode
== m_currentTransfermode
)
401 switch ( transferMode
)
404 wxFAIL_MSG(_T("unknown FTP transfer mode"));
416 if ( !DoSimpleCommand(_T("TYPE"), mode
) )
418 wxLogError(_("Failed to set FTP transfer mode to %s."),
419 (transferMode
== ASCII
? _("ASCII") : _("binary")));
424 // If we get here the operation has been successfully completed
425 // Set the status-member
426 m_currentTransfermode
= transferMode
;
431 bool wxFTP::DoSimpleCommand(const wxChar
*command
, const wxString
& arg
)
433 wxString fullcmd
= command
;
436 fullcmd
<< _T(' ') << arg
;
439 if ( !CheckCommand(fullcmd
, '2') )
441 wxLogDebug(_T("FTP command '%s' failed."), fullcmd
.c_str());
449 bool wxFTP::ChDir(const wxString
& dir
)
451 // some servers might not understand ".." if they use different directory
452 // tree conventions, but they always understand CDUP - should we use it if
453 // dir == ".."? OTOH, do such servers (still) exist?
455 return DoSimpleCommand(_T("CWD"), dir
);
458 bool wxFTP::MkDir(const wxString
& dir
)
460 return DoSimpleCommand(_T("MKD"), dir
);
463 bool wxFTP::RmDir(const wxString
& dir
)
465 return DoSimpleCommand(_T("RMD"), dir
);
468 wxString
wxFTP::Pwd()
472 if ( CheckCommand(wxT("PWD"), '2') )
474 // the result is at least that long if CheckCommand() succeeded
475 wxString::const_iterator p
= m_lastResult
.begin() + LEN_CODE
+ 1;
478 wxLogDebug(_T("Missing starting quote in reply for PWD: %s"),
479 wxString(p
, m_lastResult
.end()));
483 for ( ++p
; (bool)*p
; ++p
) // FIXME-DMARS
487 // check if the quote is doubled
489 if ( !*p
|| *p
!= _T('"') )
491 // no, this is the end
494 //else: yes, it is: this is an embedded quote in the
495 // filename, treat as normal char
503 wxLogDebug(_T("Missing ending quote in reply for PWD: %s"),
504 m_lastResult
.c_str() + LEN_CODE
+ 1);
510 wxLogDebug(_T("FTP PWD command failed."));
516 bool wxFTP::Rename(const wxString
& src
, const wxString
& dst
)
520 str
= wxT("RNFR ") + src
;
521 if ( !CheckCommand(str
, '3') )
524 str
= wxT("RNTO ") + dst
;
526 return CheckCommand(str
, '2');
529 bool wxFTP::RmFile(const wxString
& path
)
532 str
= wxT("DELE ") + path
;
534 return CheckCommand(str
, '2');
537 // ----------------------------------------------------------------------------
538 // wxFTP port methods
539 // ----------------------------------------------------------------------------
541 wxSocketBase
*wxFTP::GetPort()
544 PASSIVE: Client sends a "PASV" to the server. The server responds with
545 an address and port number which it will be listening on. Then
546 the client connects to the server at the specified address and
549 ACTIVE: Client sends the server a PORT command which includes an
550 address and port number which the client will be listening on.
551 The server then connects to the client at that address and
555 wxSocketBase
*socket
= m_bPassive
? GetPassivePort() : GetActivePort();
558 m_bEncounteredError
= true;
562 // Now set the time for the new socket to the default or user selected
564 socket
->SetTimeout(m_uiDefaultTimeout
);
569 wxString
wxFTP::GetPortCmdArgument(const wxIPV4address
& addrLocal
,
570 const wxIPV4address
& addrNew
)
572 // Just fills in the return value with the local IP
573 // address of the current socket. Also it fill in the
574 // PORT which the client will be listening on
576 wxString addrIP
= addrLocal
.IPAddress();
577 int portNew
= addrNew
.Service();
579 // We need to break the PORT number in bytes
580 addrIP
.Replace(_T("."), _T(","));
582 << wxString::Format(_T("%d"), portNew
>> 8) << _T(',')
583 << wxString::Format(_T("%d"), portNew
& 0xff);
585 // Now we have a value like "10,0,0,1,5,23"
589 wxSocketBase
*wxFTP::GetActivePort()
591 // we need an address to listen on
592 wxIPV4address addrNew
, addrLocal
;
594 addrNew
.AnyAddress();
595 addrNew
.Service(0); // pick an open port number.
597 wxSocketServer
*sockSrv
= new wxSocketServer(addrNew
);
600 // We use Ok() here to see if everything is ok
601 m_lastError
= wxPROTO_PROTERR
;
606 //gets the new address, actually it is just the port number
607 sockSrv
->GetLocal(addrNew
);
609 // Now we create the argument of the PORT command, we send in both
610 // addresses because the addrNew has an IP of "0.0.0.0", so we need the
611 // value in addrLocal
612 wxString port
= GetPortCmdArgument(addrLocal
, addrNew
);
613 if ( !DoSimpleCommand(_T("PORT"), port
) )
615 m_lastError
= wxPROTO_PROTERR
;
617 wxLogError(_("The FTP server doesn't support the PORT command."));
621 sockSrv
->Notify(false); // Don't send any events
625 wxSocketBase
*wxFTP::GetPassivePort()
627 if ( !DoSimpleCommand(_T("PASV")) )
629 wxLogError(_("The FTP server doesn't support passive mode."));
633 size_t addrStart
= m_lastResult
.find(_T('('));
634 size_t addrEnd
= (addrStart
== wxString::npos
)
636 : m_lastResult
.find(_T(')'), addrStart
);
638 if ( addrEnd
== wxString::npos
)
640 m_lastError
= wxPROTO_PROTERR
;
644 // get the port number and address
646 wxString
straddr(m_lastResult
, addrStart
+ 1, addrEnd
- (addrStart
+ 1));
647 wxSscanf(straddr
, wxT("%d,%d,%d,%d,%d,%d"),
648 &a
[2],&a
[3],&a
[4],&a
[5],&a
[0],&a
[1]);
650 wxUint32 hostaddr
= (wxUint16
)a
[2] << 24 |
651 (wxUint16
)a
[3] << 16 |
652 (wxUint16
)a
[4] << 8 |
654 wxUint16 port
= (wxUint16
)(a
[0] << 8 | a
[1]);
657 addr
.Hostname(hostaddr
);
660 wxSocketClient
*client
= new wxSocketClient();
661 if ( !client
->Connect(addr
) )
667 client
->Notify(false);
673 // ----------------------------------------------------------------------------
674 // wxFTP download and upload
675 // ----------------------------------------------------------------------------
677 class wxInputFTPStream
: public wxSocketInputStream
680 wxInputFTPStream(wxFTP
*ftp
, wxSocketBase
*sock
)
681 : wxSocketInputStream(*sock
)
684 // socket timeout automatically set in GetPort function
687 virtual ~wxInputFTPStream()
689 delete m_i_socket
; // keep at top
691 // when checking the result, the stream will
692 // almost always show an error, even if the file was
693 // properly transfered, thus, lets just grab the result
695 // we are looking for "226 transfer completed"
696 char code
= m_ftp
->GetResult();
699 // it was a good transfer.
701 m_ftp
->m_streaming
= false;
707 // the connection is probably toast. issue an abort, and
708 // then a close. there won't be any more waiting
709 // for this connection
714 // There was a problem with the transfer and the server
715 // has acknowledged it. If we issue an "ABORT" now, the user
716 // would get the "226" for the abort and think the xfer was
717 // complete, thus, don't do anything here, just return
722 DECLARE_NO_COPY_CLASS(wxInputFTPStream
)
725 class wxOutputFTPStream
: public wxSocketOutputStream
728 wxOutputFTPStream(wxFTP
*ftp_clt
, wxSocketBase
*sock
)
729 : wxSocketOutputStream(*sock
), m_ftp(ftp_clt
)
733 virtual ~wxOutputFTPStream(void)
737 // close data connection first, this will generate "transfer
742 m_ftp
->GetResult(); // save result so user can get to it
744 m_ftp
->m_streaming
= false;
748 // abort data connection first
751 // and close it after
758 DECLARE_NO_COPY_CLASS(wxOutputFTPStream
)
761 wxInputStream
*wxFTP::GetInputStream(const wxString
& path
)
763 if ( ( m_currentTransfermode
== NONE
) && !SetTransferMode(BINARY
) )
766 wxSocketBase
*sock
= GetPort();
770 m_lastError
= wxPROTO_NETERR
;
774 wxString tmp_str
= wxT("RETR ") + wxURI::Unescape(path
);
775 if ( !CheckCommand(tmp_str
, '1') )
778 sock
= AcceptIfActive(sock
);
782 sock
->SetFlags(wxSOCKET_WAITALL
);
786 wxInputFTPStream
*in_stream
= new wxInputFTPStream(this, sock
);
791 wxOutputStream
*wxFTP::GetOutputStream(const wxString
& path
)
793 if ( ( m_currentTransfermode
== NONE
) && !SetTransferMode(BINARY
) )
796 wxSocketBase
*sock
= GetPort();
798 wxString tmp_str
= wxT("STOR ") + path
;
799 if ( !CheckCommand(tmp_str
, '1') )
802 sock
= AcceptIfActive(sock
);
806 return new wxOutputFTPStream(this, sock
);
809 // ----------------------------------------------------------------------------
810 // FTP directory listing
811 // ----------------------------------------------------------------------------
813 bool wxFTP::GetList(wxArrayString
& files
,
814 const wxString
& wildcard
,
817 wxSocketBase
*sock
= GetPort();
821 // NLST : List of Filenames (including Directory's !)
822 // LIST : depending on BS of FTP-Server
823 // - Unix : result like "ls" command
824 // - Windows : like "dir" command
826 wxString
line(details
? _T("LIST") : _T("NLST"));
827 if ( !wildcard
.empty() )
829 line
<< _T(' ') << wildcard
;
832 if ( !CheckCommand(line
, '1') )
834 m_lastError
= wxPROTO_PROTERR
;
835 wxLogDebug(_T("FTP 'LIST' command returned unexpected result from server"));
840 sock
= AcceptIfActive(sock
);
845 while (ReadLine(sock
, line
) == wxPROTO_NOERR
)
852 // the file list should be terminated by "226 Transfer complete""
853 return CheckResult('2');
856 bool wxFTP::FileExists(const wxString
& fileName
)
858 // This function checks if the file specified in fileName exists in the
859 // current dir. It does so by simply doing an NLST (via GetList).
860 // If this succeeds (and the list is not empty) the file exists.
863 wxArrayString fileList
;
865 if ( GetList(fileList
, fileName
, false) )
867 // Some ftp-servers (Ipswitch WS_FTP Server 1.0.5 does this)
868 // displays this behaviour when queried on a nonexistent file:
869 // NLST this_file_does_not_exist
870 // 150 Opening ASCII data connection for directory listing
871 // (no data transferred)
872 // 226 Transfer complete
873 // Here wxFTP::GetList(...) will succeed but it will return an empty
875 retval
= !fileList
.IsEmpty();
881 // ----------------------------------------------------------------------------
883 // ----------------------------------------------------------------------------
885 int wxFTP::GetFileSize(const wxString
& fileName
)
887 // return the filesize of the given file if possible
888 // return -1 otherwise (predominantly if file doesn't exist
893 // Check for existance of file via wxFTP::FileExists(...)
894 if ( FileExists(fileName
) )
898 // First try "SIZE" command using BINARY(IMAGE) transfermode
899 // Especially UNIX ftp-servers distinguish between the different
900 // transfermodes and reports different filesizes accordingly.
901 // The BINARY size is the interesting one: How much memory
902 // will we need to hold this file?
903 TransferMode oldTransfermode
= m_currentTransfermode
;
904 SetTransferMode(BINARY
);
905 command
<< _T("SIZE ") << fileName
;
907 bool ok
= CheckCommand(command
, '2');
911 // The answer should be one line: "213 <filesize>\n"
912 // 213 is File Status (STD9)
913 // "SIZE" is not described anywhere..? It works on most servers
915 if ( wxSscanf(GetLastResult().c_str(), _T("%i %i"),
916 &statuscode
, &filesize
) == 2 )
918 // We've gotten a good reply.
923 // Something bad happened.. A "2yz" reply with no size
929 // Set transfermode back to the original. Only the "SIZE"-command
930 // is dependant on transfermode
931 if ( oldTransfermode
!= NONE
)
933 SetTransferMode(oldTransfermode
);
936 // this is not a direct else clause.. The size command might return an
937 // invalid "2yz" reply
940 // The server didn't understand the "SIZE"-command or it
941 // returned an invalid reply.
942 // We now try to get details for the file with a "LIST"-command
943 // and then parse the output from there..
944 wxArrayString fileList
;
945 if ( GetList(fileList
, fileName
, true) )
947 if ( !fileList
.IsEmpty() )
949 // We _should_ only get one line in return, but just to be
950 // safe we run through the line(s) returned and look for a
951 // substring containing the name we are looking for. We
952 // stop the iteration at the first occurrence of the
953 // filename. The search is not case-sensitive.
954 bool foundIt
= false;
957 for ( i
= 0; !foundIt
&& i
< fileList
.GetCount(); i
++ )
959 foundIt
= fileList
[i
].Upper().Contains(fileName
.Upper());
964 // The index i points to the first occurrence of
965 // fileName in the array Now we have to find out what
966 // format the LIST has returned. There are two
967 // "schools": Unix-like
969 // '-rw-rw-rw- owner group size month day time filename'
973 // 'date size filename'
975 // check if the first character is '-'. This would
976 // indicate Unix-style (this also limits this function
977 // to searching for files, not directories)
978 if ( fileList
[i
].Mid(0, 1) == _T("-") )
981 if ( wxSscanf(fileList
[i
].c_str(),
982 _T("%*s %*s %*s %*s %i %*s %*s %*s %*s"),
985 // Hmm... Invalid response
986 wxLogTrace(FTP_TRACE_MASK
,
987 _T("Invalid LIST response"));
990 else // Windows-style response (?)
992 if ( wxSscanf(fileList
[i
].c_str(),
993 _T("%*s %*s %i %*s"),
996 // something bad happened..?
997 wxLogTrace(FTP_TRACE_MASK
,
998 _T("Invalid or unknown LIST response"));
1007 // filesize might still be -1 when exiting
1011 #endif // wxUSE_PROTOCOL_FTP