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 // ----------------------------------------------------------------------------
80 m_currentTransfermode
= NONE
;
82 m_username
= wxT("anonymous");
83 m_password
<< wxGetUserId() << wxT('@') << wxGetFullHostName();
86 SetFlags(wxSOCKET_NOWAIT
);
88 m_bEncounteredError
= false;
95 // if we are streaming, this will issue
96 // an FTP ABORT command, to tell the server we are aborting
100 // now this issues a "QUIT" command to tell the server we are
104 // ----------------------------------------------------------------------------
105 // wxFTP connect and login methods
106 // ----------------------------------------------------------------------------
108 bool wxFTP::Connect(const wxSockAddress
& addr
, bool WXUNUSED(wait
))
110 if ( !wxProtocol::Connect(addr
) )
112 m_lastError
= wxPROTO_NETERR
;
118 m_lastError
= wxPROTO_CONNERR
;
122 // we should have 220 welcome message
123 if ( !CheckResult('2') )
130 command
.Printf(wxT("USER %s"), m_username
.c_str());
131 char rc
= SendCommand(command
);
134 // 230 return: user accepted without password
135 m_lastError
= wxPROTO_NOERR
;
141 m_lastError
= wxPROTO_CONNERR
;
146 command
.Printf(wxT("PASS %s"), m_password
.c_str());
147 if ( !CheckCommand(command
, '2') )
149 m_lastError
= wxPROTO_CONNERR
;
154 m_lastError
= wxPROTO_NOERR
;
158 bool wxFTP::Connect(const wxString
& host
)
162 addr
.Service(wxT("ftp"));
164 return Connect(addr
);
171 m_lastError
= wxPROTO_STREAMING
;
177 if ( !CheckCommand(wxT("QUIT"), '2') )
179 m_lastError
= wxPROTO_CONNERR
;
180 wxLogDebug(_T("Failed to close connection gracefully."));
184 return wxSocketClient::Close();
187 // ============================================================================
189 // ============================================================================
191 wxSocketBase
*wxFTP::AcceptIfActive(wxSocketBase
*sock
)
196 // now wait for a connection from server
197 wxSocketServer
*sockSrv
= (wxSocketServer
*)sock
;
198 if ( !sockSrv
->WaitForAccept() )
200 m_lastError
= wxPROTO_CONNERR
;
201 wxLogError(_("Timeout while waiting for FTP server to connect, try passive mode."));
207 m_lastError
= wxPROTO_NOERR
;
208 sock
= sockSrv
->Accept(true);
221 if ( !CheckCommand(wxT("ABOR"), '4') )
224 return CheckResult('2');
228 // ----------------------------------------------------------------------------
229 // Send command to FTP server
230 // ----------------------------------------------------------------------------
232 char wxFTP::SendCommand(const wxString
& command
)
236 m_lastError
= wxPROTO_STREAMING
;
240 wxString tmp_str
= command
+ wxT("\r\n");
241 const wxWX2MBbuf tmp_buf
= tmp_str
.mb_str();
242 if ( Write(wxMBSTRINGCAST tmp_buf
, strlen(tmp_buf
)).Error())
244 m_lastError
= wxPROTO_NETERR
;
249 // don't show the passwords in the logs (even in debug ones)
250 wxString cmd
, password
;
251 if ( command
.Upper().StartsWith(_T("PASS "), &password
) )
253 cmd
<< _T("PASS ") << wxString(_T('*'), password
.length());
260 wxLogTrace(FTP_TRACE_MASK
, _T("==> %s"), cmd
.c_str());
261 #endif // __WXDEBUG__
263 m_lastError
= wxPROTO_NOERR
;
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 m_lastError
= wxPROTO_NOERR
;
386 // if we got here we must have a non empty code string
387 return (char)code
[0u];
390 // ----------------------------------------------------------------------------
391 // wxFTP simple commands
392 // ----------------------------------------------------------------------------
394 bool wxFTP::SetTransferMode(TransferMode transferMode
)
396 if ( transferMode
== m_currentTransfermode
)
403 switch ( transferMode
)
406 wxFAIL_MSG(_T("unknown FTP transfer mode"));
418 if ( !DoSimpleCommand(_T("TYPE"), mode
) )
420 wxLogError(_("Failed to set FTP transfer mode to %s."),
421 (transferMode
== ASCII
? _("ASCII") : _("binary")));
426 // If we get here the operation has been successfully completed
427 // Set the status-member
428 m_currentTransfermode
= transferMode
;
433 bool wxFTP::DoSimpleCommand(const wxChar
*command
, const wxString
& arg
)
435 wxString fullcmd
= command
;
438 fullcmd
<< _T(' ') << arg
;
441 if ( !CheckCommand(fullcmd
, '2') )
443 wxLogDebug(_T("FTP command '%s' failed."), fullcmd
.c_str());
444 m_lastError
= wxPROTO_NETERR
;
449 m_lastError
= wxPROTO_NOERR
;
453 bool wxFTP::ChDir(const wxString
& dir
)
455 // some servers might not understand ".." if they use different directory
456 // tree conventions, but they always understand CDUP - should we use it if
457 // dir == ".."? OTOH, do such servers (still) exist?
459 return DoSimpleCommand(_T("CWD"), dir
);
462 bool wxFTP::MkDir(const wxString
& dir
)
464 return DoSimpleCommand(_T("MKD"), dir
);
467 bool wxFTP::RmDir(const wxString
& dir
)
469 return DoSimpleCommand(_T("RMD"), dir
);
472 wxString
wxFTP::Pwd()
476 if ( CheckCommand(wxT("PWD"), '2') )
478 // the result is at least that long if CheckCommand() succeeded
479 wxString::const_iterator p
= m_lastResult
.begin() + LEN_CODE
+ 1;
482 wxLogDebug(_T("Missing starting quote in reply for PWD: %s"),
483 wxString(p
, m_lastResult
.end()));
487 for ( ++p
; (bool)*p
; ++p
) // FIXME-DMARS
491 // check if the quote is doubled
493 if ( !*p
|| *p
!= _T('"') )
495 // no, this is the end
498 //else: yes, it is: this is an embedded quote in the
499 // filename, treat as normal char
507 wxLogDebug(_T("Missing ending quote in reply for PWD: %s"),
508 m_lastResult
.c_str() + LEN_CODE
+ 1);
514 m_lastError
= wxPROTO_PROTERR
;
515 wxLogDebug(_T("FTP PWD command failed."));
521 bool wxFTP::Rename(const wxString
& src
, const wxString
& dst
)
525 str
= wxT("RNFR ") + src
;
526 if ( !CheckCommand(str
, '3') )
529 str
= wxT("RNTO ") + dst
;
531 return CheckCommand(str
, '2');
534 bool wxFTP::RmFile(const wxString
& path
)
537 str
= wxT("DELE ") + path
;
539 return CheckCommand(str
, '2');
542 // ----------------------------------------------------------------------------
543 // wxFTP port methods
544 // ----------------------------------------------------------------------------
546 wxSocketBase
*wxFTP::GetPort()
549 PASSIVE: Client sends a "PASV" to the server. The server responds with
550 an address and port number which it will be listening on. Then
551 the client connects to the server at the specified address and
554 ACTIVE: Client sends the server a PORT command which includes an
555 address and port number which the client will be listening on.
556 The server then connects to the client at that address and
560 wxSocketBase
*socket
= m_bPassive
? GetPassivePort() : GetActivePort();
563 m_bEncounteredError
= true;
567 // Now set the time for the new socket to the default or user selected
569 socket
->SetTimeout(m_uiDefaultTimeout
);
574 wxString
wxFTP::GetPortCmdArgument(const wxIPV4address
& addrLocal
,
575 const wxIPV4address
& addrNew
)
577 // Just fills in the return value with the local IP
578 // address of the current socket. Also it fill in the
579 // PORT which the client will be listening on
581 wxString addrIP
= addrLocal
.IPAddress();
582 int portNew
= addrNew
.Service();
584 // We need to break the PORT number in bytes
585 addrIP
.Replace(_T("."), _T(","));
587 << wxString::Format(_T("%d"), portNew
>> 8) << _T(',')
588 << wxString::Format(_T("%d"), portNew
& 0xff);
590 // Now we have a value like "10,0,0,1,5,23"
594 wxSocketBase
*wxFTP::GetActivePort()
596 // we need an address to listen on
597 wxIPV4address addrNew
, addrLocal
;
599 addrNew
.AnyAddress();
600 addrNew
.Service(0); // pick an open port number.
602 wxSocketServer
*sockSrv
= new wxSocketServer(addrNew
);
605 // We use Ok() here to see if everything is ok
606 m_lastError
= wxPROTO_PROTERR
;
611 //gets the new address, actually it is just the port number
612 sockSrv
->GetLocal(addrNew
);
614 // Now we create the argument of the PORT command, we send in both
615 // addresses because the addrNew has an IP of "0.0.0.0", so we need the
616 // value in addrLocal
617 wxString port
= GetPortCmdArgument(addrLocal
, addrNew
);
618 if ( !DoSimpleCommand(_T("PORT"), port
) )
620 m_lastError
= wxPROTO_PROTERR
;
622 wxLogError(_("The FTP server doesn't support the PORT command."));
626 m_lastError
= wxPROTO_NOERR
;
627 sockSrv
->Notify(false); // Don't send any events
631 wxSocketBase
*wxFTP::GetPassivePort()
633 if ( !DoSimpleCommand(_T("PASV")) )
635 m_lastError
= wxPROTO_PROTERR
;
636 wxLogError(_("The FTP server doesn't support passive mode."));
640 size_t addrStart
= m_lastResult
.find(_T('('));
641 size_t addrEnd
= (addrStart
== wxString::npos
)
643 : m_lastResult
.find(_T(')'), addrStart
);
645 if ( addrEnd
== wxString::npos
)
647 m_lastError
= wxPROTO_PROTERR
;
651 // get the port number and address
653 wxString
straddr(m_lastResult
, addrStart
+ 1, addrEnd
- (addrStart
+ 1));
654 wxSscanf(straddr
, wxT("%d,%d,%d,%d,%d,%d"),
655 &a
[2],&a
[3],&a
[4],&a
[5],&a
[0],&a
[1]);
657 wxUint32 hostaddr
= (wxUint16
)a
[2] << 24 |
658 (wxUint16
)a
[3] << 16 |
659 (wxUint16
)a
[4] << 8 |
661 wxUint16 port
= (wxUint16
)(a
[0] << 8 | a
[1]);
664 addr
.Hostname(hostaddr
);
667 wxSocketClient
*client
= new wxSocketClient();
668 if ( !client
->Connect(addr
) )
670 m_lastError
= wxPROTO_CONNERR
;
675 client
->Notify(false);
677 m_lastError
= wxPROTO_NOERR
;
682 // ----------------------------------------------------------------------------
683 // wxFTP download and upload
684 // ----------------------------------------------------------------------------
686 class wxInputFTPStream
: public wxSocketInputStream
689 wxInputFTPStream(wxFTP
*ftp
, wxSocketBase
*sock
)
690 : wxSocketInputStream(*sock
)
693 // socket timeout automatically set in GetPort function
696 virtual ~wxInputFTPStream()
698 delete m_i_socket
; // keep at top
700 // when checking the result, the stream will
701 // almost always show an error, even if the file was
702 // properly transfered, thus, lets just grab the result
704 // we are looking for "226 transfer completed"
705 char code
= m_ftp
->GetResult();
708 // it was a good transfer.
710 m_ftp
->m_streaming
= false;
716 // the connection is probably toast. issue an abort, and
717 // then a close. there won't be any more waiting
718 // for this connection
723 // There was a problem with the transfer and the server
724 // has acknowledged it. If we issue an "ABORT" now, the user
725 // would get the "226" for the abort and think the xfer was
726 // complete, thus, don't do anything here, just return
731 wxDECLARE_NO_COPY_CLASS(wxInputFTPStream
);
734 class wxOutputFTPStream
: public wxSocketOutputStream
737 wxOutputFTPStream(wxFTP
*ftp_clt
, wxSocketBase
*sock
)
738 : wxSocketOutputStream(*sock
), m_ftp(ftp_clt
)
742 virtual ~wxOutputFTPStream(void)
746 // close data connection first, this will generate "transfer
751 m_ftp
->GetResult(); // save result so user can get to it
753 m_ftp
->m_streaming
= false;
757 // abort data connection first
760 // and close it after
767 wxDECLARE_NO_COPY_CLASS(wxOutputFTPStream
);
770 wxInputStream
*wxFTP::GetInputStream(const wxString
& path
)
772 if ( ( m_currentTransfermode
== NONE
) && !SetTransferMode(BINARY
) )
774 m_lastError
= wxPROTO_CONNERR
;
778 wxSocketBase
*sock
= GetPort();
782 m_lastError
= wxPROTO_NETERR
;
786 wxString tmp_str
= wxT("RETR ") + wxURI::Unescape(path
);
787 if ( !CheckCommand(tmp_str
, '1') )
790 sock
= AcceptIfActive(sock
);
793 m_lastError
= wxPROTO_CONNERR
;
797 sock
->SetFlags(wxSOCKET_WAITALL
);
801 wxInputFTPStream
*in_stream
= new wxInputFTPStream(this, sock
);
803 m_lastError
= wxPROTO_NOERR
;
807 wxOutputStream
*wxFTP::GetOutputStream(const wxString
& path
)
809 if ( ( m_currentTransfermode
== NONE
) && !SetTransferMode(BINARY
) )
811 m_lastError
= wxPROTO_CONNERR
;
815 wxSocketBase
*sock
= GetPort();
817 wxString tmp_str
= wxT("STOR ") + path
;
818 if ( !CheckCommand(tmp_str
, '1') )
821 sock
= AcceptIfActive(sock
);
825 m_lastError
= wxPROTO_NOERR
;
826 return new wxOutputFTPStream(this, sock
);
829 // ----------------------------------------------------------------------------
830 // FTP directory listing
831 // ----------------------------------------------------------------------------
833 bool wxFTP::GetList(wxArrayString
& files
,
834 const wxString
& wildcard
,
837 wxSocketBase
*sock
= GetPort();
839 m_lastError
= wxPROTO_NETERR
;
843 // NLST : List of Filenames (including Directory's !)
844 // LIST : depending on BS of FTP-Server
845 // - Unix : result like "ls" command
846 // - Windows : like "dir" command
848 wxString
line(details
? _T("LIST") : _T("NLST"));
849 if ( !wildcard
.empty() )
851 line
<< _T(' ') << wildcard
;
854 if ( !CheckCommand(line
, '1') )
856 m_lastError
= wxPROTO_PROTERR
;
857 wxLogDebug(_T("FTP 'LIST' command returned unexpected result from server"));
862 sock
= AcceptIfActive(sock
);
864 m_lastError
= wxPROTO_CONNERR
;
869 while (ReadLine(sock
, line
) == wxPROTO_NOERR
)
876 // the file list should be terminated by "226 Transfer complete""
877 m_lastError
= wxPROTO_NOERR
;
878 return CheckResult('2');
881 bool wxFTP::FileExists(const wxString
& fileName
)
883 // This function checks if the file specified in fileName exists in the
884 // current dir. It does so by simply doing an NLST (via GetList).
885 // If this succeeds (and the list is not empty) the file exists.
888 wxArrayString fileList
;
890 if ( GetList(fileList
, fileName
, false) )
892 // Some ftp-servers (Ipswitch WS_FTP Server 1.0.5 does this)
893 // displays this behaviour when queried on a nonexistent file:
894 // NLST this_file_does_not_exist
895 // 150 Opening ASCII data connection for directory listing
896 // (no data transferred)
897 // 226 Transfer complete
898 // Here wxFTP::GetList(...) will succeed but it will return an empty
900 retval
= !fileList
.IsEmpty();
906 // ----------------------------------------------------------------------------
908 // ----------------------------------------------------------------------------
910 int wxFTP::GetFileSize(const wxString
& fileName
)
912 // return the filesize of the given file if possible
913 // return -1 otherwise (predominantly if file doesn't exist
918 // Check for existance of file via wxFTP::FileExists(...)
919 if ( FileExists(fileName
) )
923 // First try "SIZE" command using BINARY(IMAGE) transfermode
924 // Especially UNIX ftp-servers distinguish between the different
925 // transfermodes and reports different filesizes accordingly.
926 // The BINARY size is the interesting one: How much memory
927 // will we need to hold this file?
928 TransferMode oldTransfermode
= m_currentTransfermode
;
929 SetTransferMode(BINARY
);
930 command
<< _T("SIZE ") << fileName
;
932 bool ok
= CheckCommand(command
, '2');
936 // The answer should be one line: "213 <filesize>\n"
937 // 213 is File Status (STD9)
938 // "SIZE" is not described anywhere..? It works on most servers
940 if ( wxSscanf(GetLastResult().c_str(), _T("%i %i"),
941 &statuscode
, &filesize
) == 2 )
943 // We've gotten a good reply.
948 // Something bad happened.. A "2yz" reply with no size
954 // Set transfermode back to the original. Only the "SIZE"-command
955 // is dependant on transfermode
956 if ( oldTransfermode
!= NONE
)
958 SetTransferMode(oldTransfermode
);
961 // this is not a direct else clause.. The size command might return an
962 // invalid "2yz" reply
965 // The server didn't understand the "SIZE"-command or it
966 // returned an invalid reply.
967 // We now try to get details for the file with a "LIST"-command
968 // and then parse the output from there..
969 wxArrayString fileList
;
970 if ( GetList(fileList
, fileName
, true) )
972 if ( !fileList
.IsEmpty() )
974 // We _should_ only get one line in return, but just to be
975 // safe we run through the line(s) returned and look for a
976 // substring containing the name we are looking for. We
977 // stop the iteration at the first occurrence of the
978 // filename. The search is not case-sensitive.
979 bool foundIt
= false;
982 for ( i
= 0; !foundIt
&& i
< fileList
.GetCount(); i
++ )
984 foundIt
= fileList
[i
].Upper().Contains(fileName
.Upper());
989 // The index i points to the first occurrence of
990 // fileName in the array Now we have to find out what
991 // format the LIST has returned. There are two
992 // "schools": Unix-like
994 // '-rw-rw-rw- owner group size month day time filename'
998 // 'date size filename'
1000 // check if the first character is '-'. This would
1001 // indicate Unix-style (this also limits this function
1002 // to searching for files, not directories)
1003 if ( fileList
[i
].Mid(0, 1) == _T("-") )
1006 if ( wxSscanf(fileList
[i
].c_str(),
1007 _T("%*s %*s %*s %*s %i %*s %*s %*s %*s"),
1010 // Hmm... Invalid response
1011 wxLogTrace(FTP_TRACE_MASK
,
1012 _T("Invalid LIST response"));
1015 else // Windows-style response (?)
1017 if ( wxSscanf(fileList
[i
].c_str(),
1018 _T("%*s %*s %i %*s"),
1021 // something bad happened..?
1022 wxLogTrace(FTP_TRACE_MASK
,
1023 _T("Invalid or unknown LIST response"));
1032 // filesize might still be -1 when exiting
1036 #endif // wxUSE_PROTOCOL_FTP