Ftp.cpp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. /******************************************************************************/
  2. #include "stdafx.h"
  3. namespace EE{
  4. #define FTP_WAIT_TIME (8*1000) // 8 seconds
  5. #define FTP_INTERRUPT 1 // if separate long transfers into few smaller ones
  6. /******************************************************************************/
  7. enum
  8. {
  9. LO=-2, // logged in
  10. ER=-1, // error
  11. NUMLOGIN=9, // 9 login sequences
  12. };
  13. static SByte LogInSequence[NUMLOGIN][18]=
  14. {
  15. // this array stores all of the login sequences for the various firewalls in blocks of 3 numbers
  16. // 1st number is command to send
  17. // 2nd number is next point in login sequence array if 200 series response is received from server as the result of the command
  18. // 3rd number is next point in login sequence array if 300 series response is received from server as the result of the command
  19. { 0,LO,3, 1,LO, 6, 2,LO,ER }, // no firewall
  20. { 3, 6,3, 4, 6,ER, 5,ER, 9, 0,LO,12, 1,LO,15, 2,LO,ER}, // SITE hostname
  21. { 3, 6,3, 4, 6,ER, 6,LO, 9, 1,LO,12, 2,LO,ER }, // USER after login
  22. { 7, 3,3, 0,LO, 6, 1,LO, 9, 2,LO,ER }, // proxy OPEN
  23. { 3, 6,3, 4, 6,ER, 0,LO, 9, 1,LO,12, 2,LO,ER }, // Transparent
  24. { 6,LO,3, 1,LO, 6, 2,LO,ER }, // USER with no login
  25. { 8, 6,3, 4, 6,ER, 0,LO, 9, 1,LO,12, 2,LO,ER }, // USER fireID@remotehost
  26. { 9,ER,3, 1,LO, 6, 2,LO,ER }, // USER remoteID@remotehost fireID
  27. {10,LO,3, 11,LO, 6, 2,LO,ER }, // USER remoteID@fireID@remotehost
  28. };
  29. /******************************************************************************/
  30. Ftp::Ftp()
  31. {
  32. _binary =false;
  33. _abort =false;
  34. _timeout =false;
  35. _timeouts=false;
  36. _progress= 0;
  37. _total =-1;
  38. _port = 0;
  39. }
  40. /******************************************************************************/
  41. Bool Ftp::login(C Str8 &host, C Str8 &user, C Str8 &password, Bool ignore_auth_result)
  42. {
  43. logout();
  44. T._host =host;
  45. T._user =user;
  46. T._password=password;
  47. CChar8 *url=host;
  48. Bool secure=false;
  49. if(StartsPath(url, "ftps://"))
  50. {
  51. url+=7; T._port=21; // 990 = is the implicit FTPS port however not working on one tested server, so 21 with explicit is used
  52. secure=true;
  53. }else
  54. if(StartsPath(url, "ftp://"))
  55. {
  56. url+=6; T._port=21; // 21 = default FTP port
  57. }else // assume FTP
  58. {
  59. T._port=21; // 21 = default FTP port
  60. }
  61. {
  62. Char8 *account=null,
  63. *fw_user=null,
  64. *fw_pass=null;
  65. Str8 host_port=url; if(_port!=21){host_port+=':'; host_port+=_port;} // add port to the host
  66. // connect
  67. SockAddr addr; if(!addr.setHost(url, _port))goto error;
  68. _socket.createTcp(addr);
  69. if(_socket.connect (addr)!=Socket::CONNECTED)goto error;
  70. if(response(FTP_WAIT_TIME).first()!='2')goto error;
  71. if(secure)
  72. {
  73. // send command
  74. Char8 reply=command("AUTH TLS").first(); if(reply!='2' && reply!='3')goto error;
  75. if(!_socket.secure(url))goto error;
  76. _socket.setDefaultFunc();
  77. for(; !_abort; )switch(_socket.handshake())
  78. {
  79. case SecureSocket::OK: goto ok;
  80. case SecureSocket::NEED_FLUSH: _socket.flush(DOWNLOAD_WAIT_TIME); break; // flush quickly and break to check '_abort'
  81. case SecureSocket::NEED_WAIT : _socket.wait (DOWNLOAD_WAIT_TIME); break; // wait quickly and break to check '_abort'
  82. case SecureSocket::BAD_CERT: if(ignore_auth_result)goto ok; goto error;
  83. default: goto error;
  84. }
  85. ok:;
  86. }
  87. // go through appropriate log in procedure
  88. for(Int i=0; ; )
  89. {
  90. Str8 cmd;
  91. switch(LogInSequence[0][i])
  92. {
  93. case 0: cmd=S+"USER "+user ; break;
  94. case 1: cmd=S+"PASS "+password; break;
  95. case 2: cmd=S+"ACCT "+account ; break;
  96. case 3: cmd=S+"USER "+fw_user ; break;
  97. case 4: cmd=S+"PASS "+fw_pass ; break;
  98. case 5: cmd=S+"SITE "+host_port; break;
  99. case 6: cmd=S+"USER "+user+'@'+host_port; break;
  100. case 7: cmd=S+"OPEN "+host_port ; break;
  101. case 8: cmd=S+"USER "+fw_user +'@'+host_port ; break;
  102. case 9: cmd=S+"USER "+user +'@'+host_port+' '+fw_user; break;
  103. case 10: cmd=S+"USER "+user +'@'+fw_user+'@'+host_port; break;
  104. case 11: cmd=S+"PASS "+password+'@'+fw_pass ; break;
  105. }
  106. // send command
  107. Char8 reply=command(cmd).first(); if(reply!='2' && reply!='3')goto error;
  108. // get next command
  109. switch(i=LogInSequence[0][i + (reply-'0')-1])
  110. {
  111. case ER: goto error;
  112. case LO: if(!changeDir("/"))goto error; return true;
  113. }
  114. }
  115. }
  116. error:
  117. logout(); return false;
  118. }
  119. void Ftp::logout()
  120. {
  121. if(is())command("QUIT");
  122. _socket.del();
  123. _binary =false;
  124. _abort =false;
  125. _timeout =false;
  126. _timeouts=false;
  127. _progress= 0;
  128. _total =-1;
  129. _port = 0;
  130. _response.clear();
  131. _host .clear();
  132. _user .clear();
  133. _password.clear();
  134. }
  135. Bool Ftp::reconnect() // warning: this does not preserve the binary mode !!
  136. {
  137. if(_host.is())
  138. {
  139. Str8 host, user, password; // move to temps because 'login' calls 'logout' which clears these values
  140. Swap(T._host , host);
  141. Swap(T._user , user);
  142. Swap(T._password, password);
  143. return login(host, user, password);
  144. }
  145. return true;
  146. }
  147. /******************************************************************************/
  148. Bool Ftp::send(SecureSocket &socket, CPtr data, Int size)
  149. {
  150. Int sent=0;
  151. for(; sent<size && !_abort; )
  152. {
  153. if( !socket.flush(FTP_WAIT_TIME))break;
  154. Int s=socket.send ((Byte*)data+sent, size-sent);
  155. if( s>0)sent+=s;else switch(s)
  156. {
  157. case SecureSocket::NEED_WAIT : if(socket.wait(FTP_WAIT_TIME))continue; goto error;
  158. case SecureSocket::NEED_FLUSH: continue; goto error; // just continue because there's 'flush' at the start of the loop
  159. default : goto error;
  160. }
  161. }
  162. error:
  163. return sent==size;
  164. }
  165. static void AdjustTime(Int &time, UInt &start_time)
  166. {
  167. if(time>0)
  168. {
  169. UInt cur_time=Time.curTimeMs();
  170. Int delta=cur_time-start_time; // this code was tested OK for UInt overflow, if 'start_time' is UINT_MAX and 'cur_time' is 0, then 'delta' will be 1
  171. start_time=cur_time;
  172. MAX(time-=delta, 0); // don't wait infinity
  173. }
  174. }
  175. Str8 Ftp::response(Int time)
  176. {
  177. UInt start_time; if(time>0)start_time=Time.curTimeMs();
  178. Memt<Char8, 2*1024> out;
  179. try_again:
  180. // get single response
  181. Int line_pos=0;
  182. {
  183. try_next_line:
  184. Int new_line =TextPosI(_response()+line_pos, '\n');
  185. if( new_line>=0)
  186. {
  187. new_line+=line_pos+1; // go to next character after '\n'
  188. if(_response[line_pos+3]==' ') // we've found a full response
  189. {
  190. out.setNum(new_line+1); Set(out.data(), _response, out.elms()); _response.remove(0, new_line); return out.data();
  191. }
  192. line_pos=new_line;
  193. goto try_next_line;
  194. }
  195. }
  196. // read from server
  197. wait_again:
  198. if(_socket.wait(time))
  199. {
  200. Char8 buf[10*1024];
  201. Int r=_socket.receive(buf, SIZE(buf)-1);
  202. AdjustTime(time, start_time);
  203. if(r>0)
  204. {
  205. buf[r]='\0';
  206. _response+=buf;
  207. goto try_again;
  208. }else switch(r)
  209. {
  210. case SecureSocket::NEED_WAIT : goto wait_again; // just continue because there's 'wait' at the start of the loop
  211. case SecureSocket::NEED_FLUSH: if(_socket.flush(time)){AdjustTime(time, start_time); goto wait_again;} break;
  212. }
  213. }
  214. // return what we've got
  215. out.setNum(_response.length()+1); Set(out.data(), _response, out.elms()); _response.clear(); return out.data();
  216. }
  217. Str8 Ftp::command(Str8 cmd)
  218. {
  219. _abort=false;
  220. cmd+="\r\n";
  221. if(send(_socket, cmd(), cmd.length()))return response(_timeout ? 0 : FTP_WAIT_TIME);
  222. return S;
  223. }
  224. Bool Ftp::mode(Bool binary)
  225. {
  226. if(binary!=T._binary)if(command(binary ? "TYPE I" : "TYPE A N").first()=='2')T._binary=binary;
  227. return binary==T._binary;
  228. }
  229. /******************************************************************************/
  230. Bool Ftp::noop()
  231. {
  232. return is() ? command("NOOP").first()=='2' : false;
  233. }
  234. Long Ftp::fileSize(C Str &file)
  235. {
  236. if(is())
  237. {
  238. Str8 size=command(S+"SIZE "+UnixPathUTF8(file));
  239. if( size.length()>=5)return TextLong(size()+4);
  240. }
  241. return -1;
  242. }
  243. Bool Ftp::fileTime(C Str &file, DateTime &dt)
  244. {
  245. if(is())
  246. {
  247. Str8 time=command(S+"MDTM "+UnixPathUTF8(file));
  248. if( time.length()>=18)
  249. {
  250. Char8 temp[5];
  251. CChar8 *src=time()+4;
  252. Set(temp, src, 4+1); src+=4; dt.year =TextInt(temp);
  253. Set(temp, src, 2+1); src+=2; dt.month =TextInt(temp);
  254. Set(temp, src, 2+1); src+=2; dt.day =TextInt(temp);
  255. Set(temp, src, 2+1); src+=2; dt.hour =TextInt(temp);
  256. Set(temp, src, 2+1); src+=2; dt.minute=TextInt(temp);
  257. Set(temp, src, 2+1); src+=2; dt.second=TextInt(temp);
  258. return true;
  259. }
  260. }
  261. dt.zero(); return false;
  262. }
  263. Bool Ftp::rename(C Str &src, C Str &dest)
  264. {
  265. if(is())
  266. {
  267. if( command(S+"RNFR "+UnixPathUTF8(src ).tailSlash(false)).first()=='3')
  268. return command(S+"RNTO "+UnixPathUTF8(dest).tailSlash(false)).first()=='2';
  269. }
  270. return false;
  271. }
  272. Bool Ftp::removeFile(C Str &file)
  273. {
  274. if(is())return command(S+"DELE "+UnixPathUTF8(file)).first()=='2';
  275. return false;
  276. }
  277. Bool Ftp::removeDir(C Str &dir)
  278. {
  279. if(is())return command(S+"RMD "+UnixPathUTF8(dir).tailSlash(false)).first()=='2';
  280. return false;
  281. }
  282. Bool Ftp::createDir(C Str &dir)
  283. {
  284. if(is())return command(S+"MKD "+UnixPathUTF8(dir).tailSlash(false)).first()=='2';
  285. return false;
  286. }
  287. Bool Ftp::changeDir(C Str &dir)
  288. {
  289. if(is())return command(S+"CWD "+UnixPathUTF8(dir).tailSlash(false)).first()=='2';
  290. return false;
  291. }
  292. /******************************************************************************/
  293. Bool Ftp::connect(SecureSocket &transfer, C Str &ftp_file, CChar8 *cmd, Long offset, Bool passive)
  294. {
  295. Str8 ftp_file8=UnixPathUTF8(ftp_file).tailSlash(false);
  296. if(passive)
  297. {
  298. Str8 addr=command("PASV"); // sample reply: "227 Entering Passive Mode (174,142,230,146,135,63)\r\n"
  299. if(CChar8 *t =TextPos(addr, '('))
  300. {
  301. t++;
  302. Byte ip[4], port[2];
  303. CalcValue val;
  304. t=_SkipChar(TextValue(t, val)); ip [0]=val.asUInt();
  305. t=_SkipChar(TextValue(t, val)); ip [1]=val.asUInt();
  306. t=_SkipChar(TextValue(t, val)); ip [2]=val.asUInt();
  307. t=_SkipChar(TextValue(t, val)); ip [3]=val.asUInt();
  308. t=_SkipChar(TextValue(t, val)); port[0]=val.asUInt();
  309. t=_SkipChar(TextValue(t, val)); port[1]=val.asUInt();
  310. SockAddr addr; addr.setIp4Port(VecB4(ip[0], ip[1], ip[2], ip[3]).u, (port[0]<<8)|port[1]);
  311. if(transfer.createTcp(addr))
  312. if(transfer.connect (addr)==Socket::CONNECTED)
  313. {
  314. // set offset
  315. if(offset)if(!_abort && command(S+"REST "+offset).first()!='3')return false;
  316. // initialize transfer
  317. return !_abort && command(S+cmd+ftp_file8).first()=='1';
  318. }
  319. }
  320. }else
  321. {
  322. // create server
  323. Socket server ; server .createTcp(GetDualStackSocket()); server.bind(SockAddr().setServer(0)); server.listen(); // creating an IPv6 socket when 'DualStackSocket' is not supported will result in FTP failing to connect (this was tested)
  324. SockAddr ftp_addr; ftp_addr.setFrom (_socket);
  325. Int port=server.port();
  326. // connect
  327. if(!_abort && command(Replace(S+"PORT "+ftp_addr.ip4Text()+','+(port>>8)+','+(port&0xFF), '.', ',')).first()=='2')
  328. {
  329. // set offset
  330. if(offset)if(!_abort && command(S+"REST "+offset).first()!='3')return false;
  331. // initialize transfer
  332. if(!_abort && command(S+cmd+ftp_file8).first()=='1')
  333. if(server.wait(FTP_WAIT_TIME)) // wait until connection arrives
  334. {
  335. SockAddr transfer_addr;
  336. return server.accept(transfer, transfer_addr);
  337. }
  338. }
  339. }
  340. return false;
  341. }
  342. Bool Ftp::transfer(File &file, C Str &ftp_file, CChar8 *cmd, Long offset, Bool passive, Bool send, Bool binary, Cipher *cipher)
  343. {
  344. _abort =false;
  345. _progress= 0;
  346. _total =-1;
  347. if(is() && !_abort && mode(binary))
  348. {
  349. SecureSocket transfer; if(!_abort && connect(transfer, ftp_file, cmd, offset, passive))
  350. {
  351. Byte buf[1024*32];
  352. Bool ok=true;
  353. UInt start_time; if(_timeouts)start_time=Time.curTimeMs();
  354. if(send) // send data
  355. {
  356. _total=file.left();
  357. for(; !_abort && !file.end(); )
  358. {
  359. Int size=file.getReturnSize(buf, Min(SIZEI(buf), file.left()));
  360. if(cipher)cipher->encrypt(buf, buf, size, offset+_progress);
  361. if(!T.send(transfer, buf, size)){ok=false; break;}
  362. _progress+=size;
  363. #if FTP_INTERRUPT
  364. // interrupt in case server tends to timeout
  365. if(_timeouts && !_abort && !file.end() && Time.curTimeMs()-start_time>=50*1000) // if 50 seconds have passed, this code was tested OK for UInt overflow
  366. if(Equal(cmd, "STOR ")
  367. || Equal(cmd, "APPE "))
  368. {
  369. if(!transfer.flush(FTP_WAIT_TIME)){ok=false; break;}
  370. transfer.del();
  371. if(response(FTP_WAIT_TIME).first()=='2' // check if current transfer was ok
  372. && connect(transfer, ftp_file, "APPE ", 0, passive))start_time=Time.curTimeMs();else{ok=false; break;} // continue in appending mode
  373. }
  374. #endif
  375. }
  376. if(!transfer.flush(FTP_WAIT_TIME))ok=false;
  377. }else // receive data
  378. {
  379. for(; !_abort; )
  380. {
  381. if(!transfer.wait(FTP_WAIT_TIME)){ok=false; break;}
  382. Int size =transfer.receive(buf, SIZE(buf));
  383. if( size<=0)break;
  384. if(cipher)cipher->decrypt(buf, buf, size, offset+_progress);
  385. file.put(buf, size);
  386. _progress+=size;
  387. }
  388. }
  389. transfer.del();
  390. Char8 resp=response(FTP_WAIT_TIME).first();
  391. if(ok && resp!='2')
  392. {
  393. ok=false;
  394. if(!resp) // "no response" = timeout, which means that this may have succeeded but we've just lost connection
  395. {
  396. if(Equal(cmd, "STOR ")) // if we were uploading (can't do this for appending, because we would need to check initial size of the file on the server to verify how much we've uploaded)
  397. {
  398. Long total=_total; // remember this because it will get cleared in reconnection
  399. _timeout=true; // we're currently timed out
  400. if(reconnect())
  401. {
  402. _timeouts=true; // mark this connection as tending to time out
  403. if(total==fileSize(ftp_file)) // if uploaded data is all that we wanted to send
  404. ok=true; // set as ok
  405. }
  406. }else
  407. if(Equal(cmd, "RETR ")) // if we were downloading
  408. {
  409. Long progress=_progress; // remember this because it will get cleared in reconnection
  410. _timeout=true; // we're currently timed out
  411. if(reconnect())
  412. {
  413. _timeouts=true; // mark this connection as tending to time out
  414. if(progress==fileSize(ftp_file)-offset) // if downloaded data is all that we wanted to receive
  415. ok=true; // set as ok
  416. }
  417. }
  418. }
  419. }
  420. _abort =false;
  421. _progress= 0;
  422. _total =-1;
  423. return ok;
  424. }
  425. }
  426. return false;
  427. }
  428. Bool Ftp::download(C Str &src, File &dest, Long offset, Bool passive, Cipher * src_cipher) {return transfer(dest, src , "RETR ", offset, passive, false, true, src_cipher);}
  429. Bool Ftp:: upload( File &src, C Str &dest , Bool passive, Cipher *dest_cipher) {return transfer(src , dest, "STOR ", 0, passive, true , true, dest_cipher);}
  430. Bool Ftp:: append( File &src, C Str &dest , Bool passive ) {return transfer(src , dest, "APPE ", 0, passive, true , true );}
  431. /******************************************************************************/
  432. Bool Ftp::listFiles(C Str &path, MemPtr<FtpFile> files, Bool passive)
  433. {
  434. files.clear();
  435. if(is())
  436. {
  437. File f; f.writeMem();
  438. if(transfer(f, path, "LIST ", 0, passive, false, false))
  439. {
  440. DateTime dt; dt.getUTC();
  441. Memt<Char8> data; data.setNum(f.size()+1); f.pos(0); f.get(data.data(), f.size()); data[f.size()]=0;
  442. for(Char8 *line=data.data(); line; )
  443. {
  444. CChar8 *t=line; if(line=(Char8*)TextPos(line, '\n'))line++; if(line){line[-1]=0; if(line[-2]=='\r')line[-2]=0;}
  445. if(*t=='-' || *t=='d' || *t=='l')
  446. {
  447. FtpFile f;
  448. switch(*t)
  449. {
  450. case '-': f.type=FSTD_FILE; break;
  451. case 'd': f.type=FSTD_DIR ; break;
  452. case 'l': f.type=FSTD_LINK; break;
  453. }
  454. t=_SkipWhiteChars(TextPos(t, ' '));
  455. t=_SkipWhiteChars(TextPos(t, ' '));
  456. t=_SkipWhiteChars(TextPos(t, ' '));
  457. t=_SkipWhiteChars(TextPos(t, ' '));
  458. CalcValue val;
  459. f.size=TextLong(t);
  460. t=_SkipWhiteChars(TextPos(t, ' '));
  461. if(Starts(t, "Jan"))f.modify_time_utc.month= 1;else
  462. if(Starts(t, "Feb"))f.modify_time_utc.month= 2;else
  463. if(Starts(t, "Mar"))f.modify_time_utc.month= 3;else
  464. if(Starts(t, "Apr"))f.modify_time_utc.month= 4;else
  465. if(Starts(t, "May"))f.modify_time_utc.month= 5;else
  466. if(Starts(t, "Jun"))f.modify_time_utc.month= 6;else
  467. if(Starts(t, "Jul"))f.modify_time_utc.month= 7;else
  468. if(Starts(t, "Aug"))f.modify_time_utc.month= 8;else
  469. if(Starts(t, "Sep"))f.modify_time_utc.month= 9;else
  470. if(Starts(t, "Oct"))f.modify_time_utc.month=10;else
  471. if(Starts(t, "Nov"))f.modify_time_utc.month=11;else
  472. if(Starts(t, "Dec"))f.modify_time_utc.month=12;else continue;
  473. t=_SkipWhiteChars(TextPos (t, ' '));
  474. t=_SkipWhiteChars(TextValue(t, val)); f.modify_time_utc.day=val.asUInt();
  475. if(Char8 *next=(Char8*)TextPos(t, ' '))
  476. {
  477. *next=0; next=(Char8*)_SkipWhiteChars(next+1);
  478. if(CChar8 *minute=TextPos(t, ':'))
  479. {
  480. TextValue(t , val); f.modify_time_utc.hour =val.asUInt();
  481. TextValue(minute+1, val); f.modify_time_utc.minute=val.asUInt();
  482. f.modify_time_utc.year =dt .year; // if the year is not specified then it means that date is from this year
  483. }else
  484. {
  485. TextValue(t, val); f.modify_time_utc.year=val.asUInt();
  486. }
  487. if(!Equal(next, ".")
  488. && !Equal(next, ".."))
  489. {
  490. f.name=FromUTF8(next);
  491. files.add(f);
  492. }
  493. }
  494. }
  495. }
  496. return true;
  497. }
  498. }
  499. return false;
  500. }
  501. Bool Ftp::listNames(C Str &path, MemPtr<Str> names, Bool passive)
  502. {
  503. names.clear();
  504. if(is())
  505. {
  506. File f; f.writeMem();
  507. if(transfer(f, path, "NLST ", 0, passive, false, false))
  508. {
  509. Memt<Char8> data; data.setNum(f.size()+1); f.pos(0); f.get(data.data(), f.size()); data[f.size()]=0;
  510. for(Char8 *line=data.data(); line; )
  511. {
  512. Char8 *t=line; if(line=(Char8*)TextPos(line, '\n'))line++; if(line){line[-1]=0; if(line[-2]=='\r')line[-2]=0;}
  513. if( Is (t )
  514. && !Equal(t, "." )
  515. && !Equal(t, ".."))names.add(FromUTF8(t));
  516. }
  517. return true;
  518. }
  519. }
  520. return false;
  521. }
  522. /******************************************************************************/
  523. void Ftp::abort() {_abort=true;}
  524. /******************************************************************************/
  525. }
  526. /******************************************************************************/