use default timeout of 1 minute
[wxWidgets.git] / src / common / ftp.cpp
1 /////////////////////////////////////////////////////////////////////////////
2 // Name: 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 // Created: 07/07/1997
11 // RCS-ID: $Id$
12 // Copyright: (c) 1997, 1998 Guilhem Lavaux
13 // Licence: wxWindows license
14 /////////////////////////////////////////////////////////////////////////////
15
16 // ============================================================================
17 // declarations
18 // ============================================================================
19
20 #ifdef __GNUG__
21 #pragma implementation "ftp.h"
22 #endif
23
24 // ----------------------------------------------------------------------------
25 // headers
26 // ----------------------------------------------------------------------------
27
28 // For compilers that support precompilation, includes "wx.h".
29 #include "wx/wxprec.h"
30
31 #ifdef __BORLANDC__
32 #pragma hdrstop
33 #endif
34
35 #if wxUSE_SOCKETS && wxUSE_STREAMS
36
37 #ifndef WX_PRECOMP
38 #include <stdlib.h>
39 #include "wx/string.h"
40 #include "wx/utils.h"
41 #include "wx/log.h"
42 #include "wx/intl.h"
43 #endif // WX_PRECOMP
44
45 #include "wx/sckaddr.h"
46 #include "wx/socket.h"
47 #include "wx/url.h"
48 #include "wx/sckstrm.h"
49 #include "wx/protocol/protocol.h"
50 #include "wx/protocol/ftp.h"
51
52 #if defined(__WXMAC__)
53 #include "/wx/mac/macsock.h"
54 #endif
55
56 #ifndef __MWERKS__
57 #include <memory.h>
58 #endif
59
60 // ----------------------------------------------------------------------------
61 // constants
62 // ----------------------------------------------------------------------------
63
64 // the length of FTP status code (3 digits)
65 static const size_t LEN_CODE = 3;
66
67 // ----------------------------------------------------------------------------
68 // macros
69 // ----------------------------------------------------------------------------
70
71 IMPLEMENT_DYNAMIC_CLASS(wxFTP, wxProtocol)
72 IMPLEMENT_PROTOCOL(wxFTP, wxT("ftp"), wxT("ftp"), TRUE)
73
74 // ============================================================================
75 // implementation
76 // ============================================================================
77
78 // ----------------------------------------------------------------------------
79 // wxFTP constructor and destructor
80 // ----------------------------------------------------------------------------
81
82 wxFTP::wxFTP()
83 {
84 m_lastError = wxPROTO_NOERR;
85 m_streaming = FALSE;
86 m_modeSet = FALSE;
87
88 m_user = wxT("anonymous");
89 m_passwd << wxGetUserId() << wxT('@') << wxGetFullHostName();
90
91 SetNotify(0);
92 SetFlags(wxSOCKET_NONE);
93 }
94
95 wxFTP::~wxFTP()
96 {
97 if ( m_streaming )
98 {
99 (void)Abort();
100 }
101
102 Close();
103 }
104
105 // ----------------------------------------------------------------------------
106 // wxFTP connect and login methods
107 // ----------------------------------------------------------------------------
108
109 bool wxFTP::Connect(wxSockAddress& addr, bool WXUNUSED(wait))
110 {
111 if ( !wxProtocol::Connect(addr) )
112 {
113 m_lastError = wxPROTO_NETERR;
114 return FALSE;
115 }
116
117 if ( !m_user )
118 {
119 m_lastError = wxPROTO_CONNERR;
120 return FALSE;
121 }
122
123 // we should have 220 welcome message
124 if ( !CheckResult('2') )
125 {
126 Close();
127 return FALSE;
128 }
129
130 wxString command;
131 command.Printf(wxT("USER %s"), m_user.c_str());
132 char rc = SendCommand(command);
133 if ( rc == '2' )
134 {
135 // 230 return: user accepted without password
136 return TRUE;
137 }
138
139 if ( rc != '3' )
140 {
141 Close();
142 return FALSE;
143 }
144
145 command.Printf(wxT("PASS %s"), m_passwd.c_str());
146 if ( !CheckCommand(command, '2') )
147 {
148 Close();
149 return FALSE;
150 }
151
152 return TRUE;
153 }
154
155 bool wxFTP::Connect(const wxString& host)
156 {
157 wxIPV4address addr;
158 addr.Hostname(host);
159 addr.Service(wxT("ftp"));
160
161 return Connect(addr);
162 }
163
164 bool wxFTP::Close()
165 {
166 if ( m_streaming )
167 {
168 m_lastError = wxPROTO_STREAMING;
169 return FALSE;
170 }
171
172 if ( IsConnected() )
173 {
174 if ( !CheckCommand(wxT("QUIT"), '2') )
175 {
176 wxLogDebug(_T("Failed to close connection gracefully."));
177 }
178 }
179
180 return wxSocketClient::Close();
181 }
182
183 // ============================================================================
184 // low level methods
185 // ============================================================================
186
187 // ----------------------------------------------------------------------------
188 // Send command to FTP server
189 // ----------------------------------------------------------------------------
190
191 char wxFTP::SendCommand(const wxString& command)
192 {
193 if ( m_streaming )
194 {
195 m_lastError = wxPROTO_STREAMING;
196 return 0;
197 }
198
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())
202 {
203 m_lastError = wxPROTO_NETERR;
204 return 0;
205 }
206
207 wxLogTrace(_T("ftp"), _T("==> %s"), command.c_str());
208
209 return GetResult();
210 }
211
212 // ----------------------------------------------------------------------------
213 // Recieve servers reply
214 // ----------------------------------------------------------------------------
215
216 char wxFTP::GetResult()
217 {
218 wxString code;
219
220 // m_lastResult will contain the entire server response, possibly on
221 // multiple lines
222 m_lastResult.clear();
223
224 // we handle multiline replies here according to RFC 959: it says that a
225 // reply may either be on 1 line of the form "xyz ..." or on several lines
226 // in whuch case it looks like
227 // xyz-...
228 // ...
229 // xyz ...
230 // and the intermeidate lines may start with xyz or not
231 bool badReply = FALSE;
232 bool firstLine = TRUE;
233 bool endOfReply = FALSE;
234 while ( !endOfReply && !badReply )
235 {
236 wxString line;
237 m_lastError = ReadLine(line);
238 if ( m_lastError )
239 return 0;
240
241 if ( !m_lastResult.empty() )
242 {
243 // separate from last line
244 m_lastResult += _T('\n');
245 }
246
247 m_lastResult += line;
248
249 // unless this is an intermediate line of a multiline reply, it must
250 // contain the code in the beginning and '-' or ' ' following it
251 if ( line.Len() < LEN_CODE + 1 )
252 {
253 if ( firstLine )
254 {
255 badReply = TRUE;
256 }
257 else
258 {
259 wxLogTrace(_T("ftp"), _T("<== %s %s"),
260 code.c_str(), line.c_str());
261 }
262 }
263 else // line has at least 4 chars
264 {
265 // this is the char which tells us what we're dealing with
266 wxChar chMarker = line.GetChar(LEN_CODE);
267
268 if ( firstLine )
269 {
270 code = wxString(line, LEN_CODE);
271 wxLogTrace(_T("ftp"), _T("<== %s %s"),
272 code.c_str(), line.c_str() + LEN_CODE + 1);
273
274 switch ( chMarker )
275 {
276 case _T(' '):
277 endOfReply = TRUE;
278 break;
279
280 case _T('-'):
281 firstLine = FALSE;
282 break;
283
284 default:
285 // unexpected
286 badReply = TRUE;
287 }
288 }
289 else // subsequent line of multiline reply
290 {
291 if ( wxStrncmp(line, code, LEN_CODE) == 0 )
292 {
293 if ( chMarker == _T(' ') )
294 {
295 endOfReply = TRUE;
296 }
297
298 wxLogTrace(_T("ftp"), _T("<== %s %s"),
299 code.c_str(), line.c_str() + LEN_CODE + 1);
300 }
301 else
302 {
303 // just part of reply
304 wxLogTrace(_T("ftp"), _T("<== %s %s"),
305 code.c_str(), line.c_str());
306 }
307 }
308 }
309 }
310
311 if ( badReply )
312 {
313 wxLogDebug(_T("Broken FTP server: '%s' is not a valid reply."),
314 m_lastResult.c_str());
315
316 m_lastError = wxPROTO_PROTERR;
317
318 return 0;
319 }
320
321 // if we got here we must have a non empty code string
322 return code[0u];
323 }
324
325 // ----------------------------------------------------------------------------
326 // wxFTP simple commands
327 // ----------------------------------------------------------------------------
328
329 bool wxFTP::SetTransferMode(TransferMode transferMode)
330 {
331 wxString mode;
332 switch ( transferMode )
333 {
334 default:
335 wxFAIL_MSG(_T("unknown FTP transfer mode"));
336 // fall through
337
338 case BINARY:
339 mode = _T('I');
340 break;
341
342 case ASCII:
343 mode = _T('A');
344 break;
345 }
346
347 if ( !DoSimpleCommand(_T("TYPE"), mode) )
348 {
349 wxLogError(_("Failed to set FTP transfer mode to %s."),
350 transferMode == ASCII ? _("ASCII") : _("binary"));
351
352 return FALSE;
353 }
354
355 m_modeSet = TRUE;
356
357 return TRUE;
358 }
359
360 bool wxFTP::DoSimpleCommand(const wxChar *command, const wxString& arg)
361 {
362 wxString fullcmd = command;
363 if ( !arg.empty() )
364 {
365 fullcmd << _T(' ') << arg;
366 }
367
368 if ( !CheckCommand(fullcmd, '2') )
369 {
370 wxLogDebug(_T("FTP command '%s' failed."), fullcmd.c_str());
371
372 return FALSE;
373 }
374
375 return TRUE;
376 }
377
378 bool wxFTP::ChDir(const wxString& dir)
379 {
380 // some servers might not understand ".." if they use different directory
381 // tree conventions, but they always understand CDUP - should we use it if
382 // dir == ".."? OTOH, do such servers (still) exist?
383
384 return DoSimpleCommand(_T("CWD"), dir);
385 }
386
387 bool wxFTP::MkDir(const wxString& dir)
388 {
389 return DoSimpleCommand(_T("MKD"), dir);
390 }
391
392 bool wxFTP::RmDir(const wxString& dir)
393 {
394 return DoSimpleCommand(_T("RMD"), dir);
395 }
396
397 wxString wxFTP::Pwd()
398 {
399 wxString path;
400
401 if ( CheckCommand(wxT("PWD"), '2') )
402 {
403 // the result is at least that long if CheckCommand() succeeded
404 const wxChar *p = m_lastResult.c_str() + LEN_CODE + 1;
405 if ( *p != _T('"') )
406 {
407 wxLogDebug(_T("Missing starting quote in reply for PWD: %s"), p);
408 }
409 else
410 {
411 for ( p++; *p; p++ )
412 {
413 if ( *p == _T('"') )
414 {
415 // check if the quote is doubled
416 p++;
417 if ( !*p || *p != _T('"') )
418 {
419 // no, this is the end
420 break;
421 }
422 //else: yes, it is: this is an embedded quote in the
423 // filename, treat as normal char
424 }
425
426 path += *p;
427 }
428
429 if ( !*p )
430 {
431 wxLogDebug(_T("Missing ending quote in reply for PWD: %s"),
432 m_lastResult.c_str() + LEN_CODE + 1);
433 }
434 }
435 }
436 else
437 {
438 wxLogDebug(_T("FTP PWD command failed."));
439 }
440
441 return path;
442 }
443
444 bool wxFTP::Rename(const wxString& src, const wxString& dst)
445 {
446 wxString str;
447
448 str = wxT("RNFR ") + src;
449 if ( !CheckCommand(str, '3') )
450 return FALSE;
451
452 str = wxT("RNTO ") + dst;
453
454 return CheckCommand(str, '2');
455 }
456
457 bool wxFTP::RmFile(const wxString& path)
458 {
459 wxString str;
460 str = wxT("DELE ") + path;
461
462 return CheckCommand(str, '2');
463 }
464
465 // ----------------------------------------------------------------------------
466 // wxFTP download and upload
467 // ----------------------------------------------------------------------------
468
469 class wxInputFTPStream : public wxSocketInputStream
470 {
471 public:
472 wxInputFTPStream(wxFTP *ftp, wxSocketBase *sock)
473 : wxSocketInputStream(*sock)
474 {
475 m_ftp = ftp;
476
477 // FIXME make the timeout configurable
478
479 // set a shorter than default timeout
480 m_i_socket->SetTimeout(60); // 1 minute
481 }
482
483 size_t GetSize() const { return m_ftpsize; }
484
485 virtual ~wxInputFTPStream()
486 {
487 delete m_i_socket;
488
489 if ( LastError() == wxStream_NOERROR )
490 {
491 // wait for "226 transfer completed"
492 m_ftp->CheckResult('2');
493
494 m_ftp->m_streaming = FALSE;
495 }
496 else
497 {
498 m_ftp->Abort();
499 }
500 }
501
502 wxFTP *m_ftp;
503 size_t m_ftpsize;
504 };
505
506 class wxOutputFTPStream : public wxSocketOutputStream
507 {
508 public:
509 wxOutputFTPStream(wxFTP *ftp_clt, wxSocketBase *sock)
510 : wxSocketOutputStream(*sock), m_ftp(ftp_clt)
511 {
512 }
513
514 virtual ~wxOutputFTPStream(void)
515 {
516 if ( IsOk() )
517 {
518 // close data connection first, this will generate "transfer
519 // completed" reply
520 delete m_o_socket;
521
522 // read this reply
523 m_ftp->CheckResult('2');
524
525 m_ftp->m_streaming = FALSE;
526 }
527 else
528 {
529 // abort data connection first
530 m_ftp->Abort();
531
532 // and close it after
533 delete m_o_socket;
534 }
535 }
536
537 wxFTP *m_ftp;
538 };
539
540 wxSocketClient *wxFTP::GetPort()
541 {
542 int a[6];
543
544 if ( !DoSimpleCommand(_T("PASV")) )
545 {
546 wxLogError(_("The FTP server doesn't support passive mode."));
547
548 return NULL;
549 }
550
551 const char *addrStart = wxStrchr(m_lastResult, _T('('));
552 if ( !addrStart )
553 {
554 m_lastError = wxPROTO_PROTERR;
555
556 return NULL;
557 }
558
559 const char *addrEnd = wxStrchr(addrStart, _T(')'));
560 if ( !addrEnd )
561 {
562 m_lastError = wxPROTO_PROTERR;
563
564 return NULL;
565 }
566
567 wxString straddr(addrStart + 1, addrEnd);
568
569 wxSscanf(straddr, wxT("%d,%d,%d,%d,%d,%d"),
570 &a[2],&a[3],&a[4],&a[5],&a[0],&a[1]);
571
572 wxUint32 hostaddr = (wxUint16)a[5] << 24 |
573 (wxUint16)a[4] << 16 |
574 (wxUint16)a[3] << 8 |
575 a[2];
576 wxUint16 port = (wxUint16)a[0] << 8 | a[1];
577
578 wxIPV4address addr;
579 addr.Hostname(hostaddr);
580 addr.Service(port);
581
582 wxSocketClient *client = new wxSocketClient();
583 if ( !client->Connect(addr) )
584 {
585 delete client;
586 return NULL;
587 }
588
589 client->Notify(FALSE);
590
591 return client;
592 }
593
594 bool wxFTP::Abort()
595 {
596 if ( !m_streaming )
597 return TRUE;
598
599 m_streaming = FALSE;
600 if ( !CheckCommand(wxT("ABOR"), '4') )
601 return FALSE;
602
603 return CheckResult('2');
604 }
605
606 wxInputStream *wxFTP::GetInputStream(const wxString& path)
607 {
608 int pos_size;
609 wxInputFTPStream *in_stream;
610
611 if ( !m_modeSet && !SetTransferMode(BINARY) )
612 return NULL;
613
614 wxSocketClient *sock = GetPort();
615
616 if ( !sock )
617 {
618 m_lastError = wxPROTO_NETERR;
619 return NULL;
620 }
621
622 wxString tmp_str = wxT("RETR ") + wxURL::ConvertFromURI(path);
623 if ( !CheckCommand(tmp_str, '1') )
624 return NULL;
625
626 m_streaming = TRUE;
627
628 in_stream = new wxInputFTPStream(this, sock);
629
630 pos_size = m_lastResult.Index(wxT('('));
631 if ( pos_size != wxNOT_FOUND )
632 {
633 wxString str_size = m_lastResult(pos_size+1, m_lastResult.Index(wxT(')'))-1);
634
635 in_stream->m_ftpsize = wxAtoi(WXSTRINGCAST str_size);
636 }
637
638 sock->SetFlags(wxSOCKET_WAITALL);
639
640 return in_stream;
641 }
642
643 wxOutputStream *wxFTP::GetOutputStream(const wxString& path)
644 {
645 if ( !m_modeSet && !SetTransferMode(BINARY) )
646 return NULL;
647
648 wxSocketClient *sock = GetPort();
649
650 wxString tmp_str = wxT("STOR ") + path;
651 if ( !CheckCommand(tmp_str, '1') )
652 return FALSE;
653
654 m_streaming = TRUE;
655
656 return new wxOutputFTPStream(this, sock);
657 }
658
659 // ----------------------------------------------------------------------------
660 // FTP directory listing
661 // ----------------------------------------------------------------------------
662
663 bool wxFTP::GetList(wxArrayString& files,
664 const wxString& wildcard,
665 bool details)
666 {
667 wxSocketBase *sock = GetPort();
668 if (!sock)
669 return FALSE;
670
671 // NLST : List of Filenames (including Directory's !)
672 // LIST : depending on BS of FTP-Server
673 // - Unix : result like "ls" command
674 // - Windows : like "dir" command
675 // - others : ?
676 wxString line(details ? _T("LIST") : _T("NLST"));
677 if ( !!wildcard )
678 {
679 line << _T(' ') << wildcard;
680 }
681
682 if (!CheckCommand(line, '1'))
683 {
684 return FALSE;
685 }
686 files.Empty();
687 while ( ReadLine(sock, line) == wxPROTO_NOERR )
688 {
689 files.Add(line);
690 }
691 delete sock;
692
693 // the file list should be terminated by "226 Transfer complete""
694 if ( !CheckResult('2') )
695 return FALSE;
696
697 return TRUE;
698 }
699
700 #ifdef WXWIN_COMPATIBILITY_2
701 // deprecated
702 wxList *wxFTP::GetList(const wxString& wildcard, bool details)
703 {
704 wxSocketBase *sock = GetPort();
705 if (!sock)
706 return FALSE;
707 wxList *file_list = new wxList;
708 wxString line;
709 // NLST : List of Filenames (including Directory's !)
710 // LIST : depending on BS of FTP-Server
711 // - Unix : result like "ls" command
712 // - Windows : like "dir" command
713 // - others : ?
714 if (!details)
715 line = _T("NLST"); // Default
716 else
717 line = _T("LIST");
718 if (!wildcard.IsNull())
719 line += wildcard;
720 if (!CheckCommand(line, '1'))
721 {
722 delete sock;
723 delete file_list;
724 return NULL;
725 }
726 while (GetLine(sock, line) == wxPROTO_NOERR)
727 {
728 file_list->Append((wxObject *)(new wxString(line)));
729 }
730 if (!CheckResult('2'))
731 {
732 delete sock;
733 file_list->DeleteContents(TRUE);
734 delete file_list;
735 return NULL;
736 }
737 return file_list;
738 }
739 #endif // WXWIN_COMPATIBILITY_2
740
741 #endif
742 // wxUSE_SOCKETS