1 module requests.ftp;
2 
3 private:
4 import std.ascii;
5 import std.algorithm;
6 import std.conv;
7 import std.datetime;
8 import std.format;
9 import std.exception;
10 import std..string;
11 import std.range;
12 import std.experimental.logger;
13 import std.stdio;
14 import std.path;
15 import std.traits;
16 import std.typecons;
17 
18 import requests.uri;
19 import requests.utils;
20 import requests.streams;
21 import requests.base;
22 import requests.request;
23 import requests.connmanager;
24 import requests.rangeadapter;
25 
26 public class FTPServerResponseError: Exception {
27     this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null) @safe pure nothrow {
28         super(message, file, line, next);
29     }
30 }
31 
32 public class FTPResponse : Response {
33 }
34 
35 public class FtpAuthentication: Auth {
36     private {
37         string   _username, _password;
38     }
39     /// Constructor.
40     /// Params:
41     /// username = username
42     /// password = password
43     ///
44     this(string username, string password) {
45         _username = username;
46         _password = password;
47     }
48     override string userName() {
49         return _username;
50     }
51     override string password() {
52         return _password;
53     }
54     override string[string] authHeaders(string domain) {
55         return null;
56     }
57 }
58 
59 enum defaultBufferSize = 8192;
60 
61 public struct FTPRequest {
62     private {
63         URI           _uri;
64         Duration      _timeout = 60.seconds;
65         uint          _verbosity = 0;
66         size_t        _bufferSize = defaultBufferSize;
67         long          _maxContentLength = 5*1024*1024*1024;
68         long          _contentLength = -1;
69         long          _contentReceived;
70         NetworkStream _controlChannel;
71         string[]      _responseHistory;
72         FTPResponse   _response;
73         bool          _useStreaming;
74         Auth           _authenticator;
75         string        _method;
76         string        _proxy;
77         string        _bind;
78         RefCounted!ConnManager      _cm;
79         InputRangeAdapter           _postData;
80     }
81     mixin(Getter_Setter!Duration("timeout"));
82     mixin(Getter_Setter!uint("verbosity"));
83     mixin(Getter_Setter!size_t("bufferSize"));
84     mixin(Getter_Setter!long("maxContentLength"));
85     mixin(Getter_Setter!bool("useStreaming"));
86     mixin(Getter("contentLength"));
87     mixin(Getter("contentReceived"));
88     mixin(Setter!Auth("authenticator"));
89     mixin(Getter_Setter!string("proxy"));
90     mixin(Getter_Setter!string("bind"));
91 
92     @property final string[] responseHistory() @safe @nogc nothrow {
93         return _responseHistory;
94     }
95     this(string uri) {
96         _uri = URI(uri);
97     }
98 
99     this(in URI uri) {
100         _uri = uri;
101     }
102 
103     ~this() {
104         //if ( _controlChannel ) {
105         //    _controlChannel.close();
106         //}
107     }
108     string toString() const {
109         return "FTPRequest(%s, %s)".format(_method, _uri.uri());
110     }
111     string format(string fmt) const {
112         import std.array;
113         import std.stdio;
114         auto a = appender!string();
115         auto f = FormatSpec!char(fmt);
116         while (f.writeUpToNextSpec(a)) {
117             switch(f.spec) {
118                 case 'h':
119                     // Remote hostname.
120                     a.put(_uri.host);
121                     break;
122                 case 'm':
123                     // method.
124                     a.put(_method);
125                     break;
126                 case 'p':
127                     // Remote port.
128                     a.put("%d".format(_uri.port));
129                     break;
130                 case 'P':
131                     // Path
132                     a.put(_uri.path);
133                     break;
134                 case 'q':
135                     // query parameters supplied with url.
136                     a.put(_uri.query);
137                     break;
138                 case 'U':
139                     a.put(_uri.uri());
140                     break;
141                 default:
142                     throw new FormatException("Unknown Request format spec " ~ f.spec);
143             }
144         }
145         return a.data();
146     }
147     ushort sendCmdGetResponse(string cmd, NetworkStream __controlChannel) {
148         debug(requests) tracef("cmd to server: %s", cmd.strip);
149         if ( _verbosity >=1 ) {
150             writefln("> %s", cmd.strip);
151         }
152         __controlChannel.send(cmd);
153         string response = serverResponse(__controlChannel);
154         _responseHistory ~= response;
155         return responseToCode(response);
156     }
157 
158     ushort responseToCode(string response) pure const @safe {
159         return to!ushort(response[0..3]);
160     }
161 
162     void handleChangeURI(in string uri) @safe {
163         // if control channel exists and new URL not match old, then close
164         URI newURI = URI(uri);
165         if ( _controlChannel && 
166             (newURI.host != _uri.host || newURI.port != _uri.port || newURI.username != _uri.username)) {
167             _controlChannel.close();
168             _controlChannel = null;
169         }
170         _uri = newURI;
171     }
172 
173     string serverResponse(NetworkStream __controlChannel) {
174         string res, buffer;
175         immutable bufferLimit = 16*1024;
176         __controlChannel.readTimeout = _timeout;
177         scope(exit) {
178             __controlChannel.readTimeout = 0.seconds;
179         }
180         debug(requests) trace("Wait on control channel");
181         auto b = new ubyte[1];
182         while ( __controlChannel && __controlChannel.isConnected && buffer.length < bufferLimit ) {
183             ptrdiff_t rc;
184             try {
185                 rc = __controlChannel.receive(b);
186             }
187             catch (Exception e) {
188                 error("Failed to read response from server");
189                 throw new FTPServerResponseError("Failed to read server responce over control channel", __FILE__, __LINE__, e);
190             }
191             //debug(requests) tracef("Got %d bytes from control socket", rc);
192             if ( rc == 0 ) {
193                 error("Failed to read response from server");
194                 throw new FTPServerResponseError("Failed to read server responce over control channel", __FILE__, __LINE__);
195             }
196             buffer ~= b[0..rc];
197             if ( buffer.endsWith('\n') ){
198                 if ( _verbosity >= 1 ) {
199                     buffer.
200                     splitLines.
201                     each!(l=>writefln("< %s", l));
202                 }
203                 auto responseLines = buffer.
204                     splitLines.
205                     filter!(l => l.length>3 && l[3]==' ' && l[0..3].all!isDigit);
206                 if ( responseLines.count > 0 ) {
207                     return responseLines.front;
208                 }
209             }
210         }
211         throw new FTPServerResponseError("Failed to read server responce over control channel");
212         assert(0);
213     }
214     ushort tryCdOrCreatePath(string[] path) {
215         /*
216          * At start we stay at original path, we have to create next path element
217          * For example:
218          * path = ["", "a", "b"] - we stay in root (path[0]), we have to cd and return ok
219          * or try to cteate "a" and cd to "a".
220          */
221         debug(requests) info("Trying to create path %s".format(path));
222         enforce(path.length>=2, "You called tryCdOrCreate, but there is nothing to create: %s".format(path));
223         auto next_dir = path[1];
224         auto code = sendCmdGetResponse("CWD " ~ next_dir ~ "\r\n", _controlChannel);
225         if ( code >= 300) {
226             // try to create, then again CWD
227             code = sendCmdGetResponse("MKD " ~ next_dir ~ "\r\n", _controlChannel);
228             if ( code > 300 ) {
229                 return code;
230             }
231             code = sendCmdGetResponse("CWD " ~ next_dir ~ "\r\n", _controlChannel);
232         }
233         if ( path.length == 2 ) {
234             return code;
235         }
236         return tryCdOrCreatePath(path[1..$]);
237     }
238 
239     FTPResponse post(R, A...)(string uri, R content, A args) 
240         if ( __traits(compiles, cast(ubyte[])content) 
241         || (rank!R == 2 && isSomeChar!(Unqual!(typeof(content.front.front)))) 
242         || (rank!R == 2 && (is(Unqual!(typeof(content.front.front)) == ubyte)))
243         )
244     {
245         if ( uri ) {
246             handleChangeURI(uri);
247         }
248         _postData = makeAdapter(content);
249         return post();
250     }
251 
252     FTPResponse post()
253     {
254         string response;
255         ushort code;
256 
257         _response = new FTPResponse;
258         _response._startedAt = Clock.currTime;
259         _method = "POST";
260 
261         scope(exit) {
262             _response._finishedAt = Clock.currTime;
263         }
264 
265         _response.uri = _uri;
266         _response.finalURI = _uri;
267 
268         _controlChannel = _cm.get(_uri.scheme, _uri.host, _uri.port);
269         
270         if ( !_controlChannel ) {
271             _controlChannel = new TCPStream();
272             _controlChannel.connect(_uri.host, _uri.port, _timeout);
273             response = serverResponse(_controlChannel);
274             _responseHistory ~= response;
275             
276             code = responseToCode(response);
277             debug(requests) tracef("Server initial response: %s", response);
278             if ( code/100 > 2 ) {
279                 _response.code = code;
280                 return _response;
281             }
282             // Log in
283             string user, pass;
284             if ( _authenticator ) {
285                 user = _authenticator.userName();
286                 pass = _authenticator.password();
287             }
288             else{
289                 user = _uri.username.length ? _uri.username : "anonymous";
290                 pass = _uri.password.length ? _uri.password : "requests@";
291             }
292             debug(requests) tracef("Use %s:%s%s as username:password", user, pass[0], replicate("-", pass.length-1));
293             
294             code = sendCmdGetResponse("USER " ~ user ~ "\r\n", _controlChannel);
295             if ( code/100 > 3 ) {
296                 _response.code = code;
297                 return _response;
298             } else if ( code/100 == 3) {
299                 
300                 code = sendCmdGetResponse("PASS " ~ pass ~ "\r\n", _controlChannel);
301                 if ( code/100 > 2 ) {
302                     _response.code = code;
303                     return _response;
304                 }
305             }
306             
307         }
308         code = sendCmdGetResponse("PWD\r\n", _controlChannel);
309         string pwd;
310         if ( code/100 == 2 ) {
311             // like '257 "/home/testuser"'
312             auto a = _responseHistory[$-1].split();
313             if ( a.length > 1 ) {
314                 pwd = a[1].chompPrefix(`"`).chomp(`"`);
315             }
316         }
317         scope (exit) {
318             if ( pwd && _controlChannel ) {
319                 sendCmdGetResponse("CWD " ~ pwd ~ "\r\n", _controlChannel);
320             }
321         }
322 
323         auto path = dirName(_uri.path);
324         if ( path != "/") {
325             path = path.chompPrefix("/");
326         }
327         code = sendCmdGetResponse("CWD " ~ path ~ "\r\n", _controlChannel);
328         if ( code == 550 ) {
329             // try to create directory end enter it
330             code = tryCdOrCreatePath(dirName(_uri.path).split('/'));
331         }
332         if ( code/100 > 2 ) {
333             _response.code = code;
334             return _response;
335         }
336 
337         code = sendCmdGetResponse("PASV\r\n", _controlChannel);
338         if ( code/100 > 2 ) {
339             _response.code = code;
340             return _response;
341         }
342         // something like  "227 Entering Passive Mode (132,180,15,2,210,187)" expected
343         // in last response.
344         // Cut anything between ( and )
345         auto v = _responseHistory[$-1].findSplitBefore(")")[0].findSplitAfter("(")[1];
346         string host;
347         ushort port;
348         try {
349             ubyte a1,a2,a3,a4,p1,p2;
350             formattedRead(v, "%d,%d,%d,%d,%d,%d", &a1, &a2, &a3, &a4, &p1, &p2);
351             host = std.format.format("%d.%d.%d.%d", a1, a2, a3, a4);
352             port = (p1<<8) + p2;
353         } catch (FormatException e) {
354             error("Failed to parse ", v);
355             _response.code = 500;
356             return _response;
357         }
358 
359         auto dataStream = new TCPStream();
360         scope (exit ) {
361             if ( dataStream !is null ) {
362                 dataStream.close();
363             }
364         }
365 
366         dataStream.connect(host, port, _timeout);
367 
368         code = sendCmdGetResponse("TYPE I\r\n", _controlChannel);
369         if ( code/100 > 2 ) {
370             _response.code = code;
371             return _response;
372         }
373 
374         code = sendCmdGetResponse("STOR " ~ baseName(_uri.path) ~ "\r\n", _controlChannel);
375         if ( code/100 > 1 ) {
376             _response.code = code;
377             return _response;
378         }
379         size_t uploaded;
380         while ( !_postData.empty ) {
381             auto chunk = _postData.front;
382             uploaded += chunk.length;
383             dataStream.send(chunk);
384             _postData.popFront;
385         }
386         debug(requests) tracef("sent");
387         dataStream.close();
388         dataStream = null;
389         response = serverResponse(_controlChannel);
390         code = responseToCode(response);
391         if ( code/100 == 2 ) {
392             debug(requests) tracef("Successfully uploaded %d bytes", uploaded);
393         }
394         _response.code = code;
395         return _response;
396     }
397     private auto connectData(string v)
398     {
399         string host;
400         ushort port;
401 
402         ubyte a1,a2,a3,a4,p1,p2;
403         formattedRead(v, "%d,%d,%d,%d,%d,%d", &a1, &a2, &a3, &a4, &p1, &p2);
404         host = std.format.format("%d.%d.%d.%d", a1, a2, a3, a4);
405         port = (p1<<8) + p2;
406 
407         auto dataStream = new TCPStream();
408         dataStream.bind(_bind);
409         dataStream.connect(host, port, _timeout);
410         return dataStream;
411     }
412 
413     FTPResponse get(string uri = null) {
414         enforce( uri || _uri.host, "FTP URL undefined");
415         string response;
416         ushort code;
417 
418         _response = new FTPResponse;
419         _contentReceived = 0;
420         _method = "GET";
421 
422         _response._startedAt = Clock.currTime;
423         scope(exit) {
424             _response._finishedAt = Clock.currTime;
425         }
426 
427         if ( uri ) {
428             handleChangeURI(uri);
429         }
430 
431         _response.uri = _uri;
432         _response.finalURI = _uri;
433 
434         _controlChannel = _cm.get(_uri.scheme, _uri.host, _uri.port);
435         
436         if ( !_controlChannel ) {
437             _controlChannel = new TCPStream();
438             _controlChannel.bind(_bind);
439             _controlChannel.connect(_uri.host, _uri.port, _timeout);
440             _cm.put(_uri.scheme, _uri.host, _uri.port, _controlChannel);
441             _response._connectedAt = Clock.currTime;
442             response = serverResponse(_controlChannel);
443             _responseHistory ~= response;
444             
445             code = responseToCode(response);
446             debug(requests) tracef("Server initial response: %s", response);
447             if ( code/100 > 2 ) {
448                 _response.code = code;
449                 return _response;
450             }
451             // Log in
452             string user, pass;
453             if ( _authenticator ) {
454                 user = _authenticator.userName();
455                 pass = _authenticator.password();
456             }
457             else{
458                 user = _uri.username.length ? _uri.username : "anonymous";
459                 pass = _uri.password.length ? _uri.password : "requests@";
460             }
461             debug(requests) tracef("Use %s:%s%s as username:password", user, pass[0], replicate("-", pass.length-1));
462             
463             code = sendCmdGetResponse("USER " ~ user ~ "\r\n", _controlChannel);
464             if ( code/100 > 3 ) {
465                 _response.code = code;
466                 return _response;
467             } else if ( code/100 == 3) {
468                 
469                 code = sendCmdGetResponse("PASS " ~ pass ~ "\r\n", _controlChannel);
470                 if ( code/100 > 2 ) {
471                     _response.code = code;
472                     return _response;
473                 }
474             }
475         }
476         else {
477             _response._connectedAt = Clock.currTime;
478         }
479 
480         code = sendCmdGetResponse("PWD\r\n", _controlChannel);
481         string pwd;
482         if ( code/100 == 2 ) {
483             // like '257 "/home/testuser"'
484             auto a = _responseHistory[$-1].split();
485             if ( a.length > 1 ) {
486                 pwd = a[1].chompPrefix(`"`).chomp(`"`);
487             }
488         }
489         scope (exit) {
490             if ( pwd && _controlChannel && !_useStreaming ) {
491                 sendCmdGetResponse("CWD " ~ pwd ~ "\r\n", _controlChannel);
492             }
493         }
494         
495         auto path = dirName(_uri.path);
496         if ( path != "/") {
497             path = path.chompPrefix("/");
498         }
499         code = sendCmdGetResponse("CWD " ~ path ~ "\r\n", _controlChannel);
500         if ( code/100 > 2 ) {
501             _response.code = code;
502             return _response;
503         }
504 
505         code = sendCmdGetResponse("TYPE I\r\n", _controlChannel);
506         if ( code/100 > 2 ) {
507             _response.code = code;
508             return _response;
509         }
510         
511         code = sendCmdGetResponse("SIZE " ~ baseName(_uri.path) ~ "\r\n", _controlChannel);
512         if ( code/100 == 2 ) {
513             // something like 
514             // 213 229355520
515             auto s = _responseHistory[$-1].findSplitAfter(" ");
516             if ( s.length ) {
517                 try {
518                     _contentLength = to!long(s[1]);
519                 } catch (ConvException) {
520                     debug(requests) trace("Failed to convert string %s to file size".format(s[1]));
521                 }
522             }
523         }
524 
525         if ( _maxContentLength > 0 && _contentLength > _maxContentLength ) {
526             throw new RequestException("maxContentLength exceeded for ftp data");
527         }
528 
529         code = sendCmdGetResponse("PASV\r\n", _controlChannel);
530         if ( code/100 > 2 ) {
531             _response.code = code;
532             return _response;
533         }
534         // something like  "227 Entering Passive Mode (132,180,15,2,210,187)" expected
535         // in last response.
536         // Cut anything between ( and )
537         auto v = _responseHistory[$-1].findSplitBefore(")")[0].findSplitAfter("(")[1];
538 
539         TCPStream dataStream;
540         try{
541             dataStream = connectData(v);
542         } catch (FormatException e) {
543             error("Failed to parse ", v);
544             _response.code = 500;
545             return _response;
546         }
547         scope (exit ) {
548             if ( dataStream !is null && !_response._receiveAsRange.activated ) {
549                 dataStream.close();
550             }
551         }
552 
553         _response._requestSentAt = Clock.currTime;
554         
555         code = sendCmdGetResponse("RETR " ~ baseName(_uri.path) ~ "\r\n", _controlChannel);
556         if ( code/100 > 1 && code/100 < 5) {
557             _response.code = code;
558             return _response;
559         }
560         if ( code/100 == 5) {
561             dataStream.close();
562             code = sendCmdGetResponse("PASV\r\n", _controlChannel);
563             if ( code/100 > 2 ) {
564                 _response.code = code;
565                 return _response;
566             }
567             v = _responseHistory[$-1].findSplitBefore(")")[0].findSplitAfter("(")[1];
568             dataStream = connectData(v);
569             code = sendCmdGetResponse("NLST " ~ _uri.path ~ "\r\n", _controlChannel);
570             if ( code/100 > 1 ) {
571                 _response.code = code;
572                 return _response;
573             }
574         }
575 
576         dataStream.readTimeout = _timeout;
577         while ( true ) {
578             auto b = new ubyte[_bufferSize];
579             auto rc = dataStream.receive(b);
580             if ( rc <= 0 ) {
581                 debug(requests) trace("done");
582                 break;
583             }
584             debug(requests) tracef("got %d bytes from data channel", rc);
585 
586             _contentReceived += rc;
587             _response._responseBody.putNoCopy(b[0..rc]);
588 
589             if ( _maxContentLength && _response._responseBody.length >= _maxContentLength ) {
590                 throw new RequestException("maxContentLength exceeded for ftp data");
591             }
592             if ( _useStreaming ) {
593                 debug(requests) trace("ftp uses streaming");
594 
595                 auto __maxContentLength = _maxContentLength;
596                 auto __contentLength = _contentLength;
597                 auto __contentReceived = _contentReceived;
598                 auto __bufferSize = _bufferSize;
599                 auto __dataStream = dataStream;
600                 auto __controlChannel = _controlChannel;
601 
602                 _response._contentLength = _contentLength;
603                 _response.receiveAsRange.activated = true;
604                 _response.receiveAsRange.data.length = 0;
605                 _response.receiveAsRange.data = _response._responseBody.data;
606                 _response.receiveAsRange.read = delegate ubyte[] () {
607                     Buffer!ubyte result;
608                     while(true) {
609                         // check if we received everything we need
610                         if ( __maxContentLength > 0 && __contentReceived >= __maxContentLength ) 
611                         {
612                             throw new RequestException("ContentLength > maxContentLength (%d>%d)".
613                                 format(__contentLength, __maxContentLength));
614                         }
615                         // have to continue
616                         auto b = new ubyte[__bufferSize];
617                         ptrdiff_t read;
618                         try {
619                             read = __dataStream.receive(b);
620                         }
621                         catch (Exception e) {
622                             throw new RequestException("streaming_in error reading from socket", __FILE__, __LINE__, e);
623                         }
624 
625                         if ( read > 0 ) {
626                             _response._contentReceived += read;
627                             __contentReceived += read;
628                             result.putNoCopy(b[0..read]);
629                             return result.data;
630                         }
631                         if ( read == 0 ) {
632                             debug(requests) tracef("streaming_in: server closed connection");
633                             __dataStream.close();
634                             code = responseToCode(serverResponse(__controlChannel));
635                             if ( code/100 == 2 ) {
636                                 debug(requests) tracef("Successfully received %d bytes", _response._responseBody.length);
637                             }
638                             _response.code = code;
639                             sendCmdGetResponse("CWD " ~ pwd ~ "\r\n", __controlChannel);
640                             break;
641                         }
642                     }
643                     return result.data;
644                 };
645                 debug(requests) tracef("leave streaming get");
646                 return _response;
647             }
648         }
649         dataStream.close();
650         response = serverResponse(_controlChannel);
651         code = responseToCode(response);
652         if ( code/100 == 2 ) {
653             debug(requests) tracef("Successfully received %d bytes", _response._responseBody.length);
654         }
655         _response.code = code;
656         return _response;
657     }
658 
659     FTPResponse execute(Request r)
660     {
661         string method = r.method;
662         _uri = r.uri();
663         _authenticator = r.authenticator;
664         _maxContentLength = r.maxContentLength;
665         _useStreaming = r.useStreaming;
666         _verbosity = r.verbosity;
667         _cm = r.cm;
668         _postData = r.postData;
669         _bufferSize = r.bufferSize;
670         _proxy = r.proxy;
671         _bind = r.bind;
672         _timeout = r.timeout;
673 
674         if ( method == "GET" )
675         {
676             return get();
677         }
678         if ( method == "POST" )
679         {
680             return post();
681         }
682         assert(0, "Can't handle method %s for ftp request".format(method));
683     }
684 }
685 
686 //package unittest {
687 //    import std.process;
688 //
689 //    globalLogLevel(LogLevel.info);
690 //    bool unreliable_network = environment.get("UNRELIABLENETWORK", "false") == "true";
691 //
692 //    info("testing ftp");
693 //    auto rq = FTPRequest();
694 //    info("ftp post ", "ftp://speedtest.tele2.net/upload/TEST.TXT");
695 //    auto rs = rq.post("ftp://speedtest.tele2.net/upload/TEST.TXT", "test, ignore please\n".representation);
696 //    assert(unreliable_network || rs.code == 226);
697 //    info("ftp get  ", "ftp://speedtest.tele2.net/nonexistent", ", in same session.");
698 //    rs = rq.get("ftp://speedtest.tele2.net/nonexistent");
699 //    assert(unreliable_network || rs.code != 226);
700 //    info("ftp get  ", "ftp://speedtest.tele2.net/1KB.zip", ", in same session.");
701 //    rs = rq.get("ftp://speedtest.tele2.net/1KB.zip");
702 //    assert(unreliable_network || rs.code == 226);
703 //    assert(unreliable_network || rs.responseBody.length == 1024);
704 //    info("ftp post ", "ftp://speedtest.tele2.net/upload/TEST.TXT");
705 //    rs = rq.post("ftp://speedtest.tele2.net/upload/TEST.TXT", "another test, ignore please\n".representation);
706 //    assert(unreliable_network || rs.code == 226);
707 //    info("ftp get  ", "ftp://ftp.iij.ad.jp/pub/FreeBSD/README.TXT");
708 //    try {
709 //        rs = rq.get("ftp://ftp.iij.ad.jp/pub/FreeBSD/README.TXT");
710 //    }
711 //    catch (ConnectError e)
712 //    {
713 //    }
714 //    assert(unreliable_network || rs.code == 226);
715 //    info("ftp get  ftp://ftp.iij.ad.jp/pub/FreeBSD/README.TXT with authenticator");
716 //    rq.authenticator = new FtpAuthentication("anonymous", "requests@");
717 //    try {
718 //        rs = rq.get("ftp://ftp.iij.ad.jp/pub/FreeBSD/README.TXT");
719 //    }
720 //    catch (ConnectError e)
721 //    {
722 //    }
723 //    assert(unreliable_network || rs.code == 226);
724 //    assert(unreliable_network || rs.finalURI.path == "/pub/FreeBSD/README.TXT");
725 //    assert(unreliable_network || rq.format("%m|%h|%p|%P|%q|%U") == "GET|ftp.iij.ad.jp|21|/pub/FreeBSD/README.TXT||ftp://ftp.iij.ad.jp/pub/FreeBSD/README.TXT");
726 //    assert(unreliable_network || rs.format("%h|%p|%P|%q|%U") == "ftp.iij.ad.jp|21|/pub/FreeBSD/README.TXT||ftp://ftp.iij.ad.jp/pub/FreeBSD/README.TXT");
727 //    info("testing ftp - done.");
728 //}