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         auto b = new ubyte[_bufferSize];
181         while ( __controlChannel && __controlChannel.isConnected && buffer.length < bufferLimit ) {
182             debug(requests) trace("Wait on control channel");
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             if ( _verbosity >= 1 ) {
197                 (cast(string)b[0..rc]).
198                     splitLines.
199                     each!(l=>writefln("< %s", l));
200             }
201             buffer ~= b[0..rc];
202             if ( buffer.endsWith('\n') ){
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         //static if ( __traits(compiles, cast(ubyte[])content) ) {
388         //    auto data = cast(ubyte[])content;
389         //    auto b = new ubyte[_bufferSize];
390         //    for(size_t pos = 0; pos < data.length;) {
391         //        auto chunk = data.take(_bufferSize).array;
392         //        auto rc = dataStream.send(chunk);
393         //        if ( rc <= 0 ) {
394         //            debug(requests) trace("done");
395         //            break;
396         //        }
397         //        debug(requests) tracef("sent %d bytes to data channel", rc);
398         //        pos += rc;
399         //    }
400         //} else {
401         //    while (!content.empty) {
402         //        auto chunk = content.front;
403         //        debug(requests) trace("ftp posting %d of data chunk".format(chunk.length));
404         //        auto rc = dataStream.send(chunk);
405         //        if ( rc <= 0 ) {
406         //            debug(requests) trace("done");
407         //            break;
408         //        }
409         //        content.popFront;
410         //    }
411         //}
412         dataStream.close();
413         dataStream = null;
414         response = serverResponse(_controlChannel);
415         code = responseToCode(response);
416         if ( code/100 == 2 ) {
417             debug(requests) tracef("Successfully uploaded %d bytes", uploaded);
418         }
419         _response.code = code;
420         return _response;
421     }
422 
423     FTPResponse get(string uri = null) {
424         enforce( uri || _uri.host, "FTP URL undefined");
425         string response;
426         ushort code;
427 
428         _response = new FTPResponse;
429         _contentReceived = 0;
430         _method = "GET";
431 
432         _response._startedAt = Clock.currTime;
433         scope(exit) {
434             _response._finishedAt = Clock.currTime;
435         }
436 
437         if ( uri ) {
438             handleChangeURI(uri);
439         }
440 
441         _response.uri = _uri;
442         _response.finalURI = _uri;
443 
444         _controlChannel = _cm.get(_uri.scheme, _uri.host, _uri.port);
445         
446         if ( !_controlChannel ) {
447             _controlChannel = new TCPStream();
448             _controlChannel.bind(_bind);
449             _controlChannel.connect(_uri.host, _uri.port, _timeout);
450             if ( auto purged_connection = _cm.put(_uri.scheme, _uri.host, _uri.port, _controlChannel) )
451             {
452                 debug(requests) tracef("closing purged connection %s", purged_connection);
453                 purged_connection.close();
454             }
455             _response._connectedAt = Clock.currTime;
456             response = serverResponse(_controlChannel);
457             _responseHistory ~= response;
458             
459             code = responseToCode(response);
460             debug(requests) tracef("Server initial response: %s", response);
461             if ( code/100 > 2 ) {
462                 _response.code = code;
463                 return _response;
464             }
465             // Log in
466             string user, pass;
467             if ( _authenticator ) {
468                 user = _authenticator.userName();
469                 pass = _authenticator.password();
470             }
471             else{
472                 user = _uri.username.length ? _uri.username : "anonymous";
473                 pass = _uri.password.length ? _uri.password : "requests@";
474             }
475             debug(requests) tracef("Use %s:%s%s as username:password", user, pass[0], replicate("-", pass.length-1));
476             
477             code = sendCmdGetResponse("USER " ~ user ~ "\r\n", _controlChannel);
478             if ( code/100 > 3 ) {
479                 _response.code = code;
480                 return _response;
481             } else if ( code/100 == 3) {
482                 
483                 code = sendCmdGetResponse("PASS " ~ pass ~ "\r\n", _controlChannel);
484                 if ( code/100 > 2 ) {
485                     _response.code = code;
486                     return _response;
487                 }
488             }
489         }
490         else {
491             _response._connectedAt = Clock.currTime;
492         }
493 
494         code = sendCmdGetResponse("PWD\r\n", _controlChannel);
495         string pwd;
496         if ( code/100 == 2 ) {
497             // like '257 "/home/testuser"'
498             auto a = _responseHistory[$-1].split();
499             if ( a.length > 1 ) {
500                 pwd = a[1].chompPrefix(`"`).chomp(`"`);
501             }
502         }
503         scope (exit) {
504             if ( pwd && _controlChannel && !_useStreaming ) {
505                 sendCmdGetResponse("CWD " ~ pwd ~ "\r\n", _controlChannel);
506             }
507         }
508         
509         auto path = dirName(_uri.path);
510         if ( path != "/") {
511             path = path.chompPrefix("/");
512         }
513         code = sendCmdGetResponse("CWD " ~ path ~ "\r\n", _controlChannel);
514         if ( code/100 > 2 ) {
515             _response.code = code;
516             return _response;
517         }
518 
519         code = sendCmdGetResponse("TYPE I\r\n", _controlChannel);
520         if ( code/100 > 2 ) {
521             _response.code = code;
522             return _response;
523         }
524         
525         code = sendCmdGetResponse("SIZE " ~ baseName(_uri.path) ~ "\r\n", _controlChannel);
526         if ( code/100 == 2 ) {
527             // something like 
528             // 213 229355520
529             auto s = _responseHistory[$-1].findSplitAfter(" ");
530             if ( s.length ) {
531                 try {
532                     _contentLength = to!long(s[1]);
533                 } catch (ConvException) {
534                     debug(requests) trace("Failed to convert string %s to file size".format(s[1]));
535                 }
536             }
537         }
538 
539         if ( _maxContentLength > 0 && _contentLength > _maxContentLength ) {
540             throw new RequestException("maxContentLength exceeded for ftp data");
541         }
542 
543         code = sendCmdGetResponse("PASV\r\n", _controlChannel);
544         if ( code/100 > 2 ) {
545             _response.code = code;
546             return _response;
547         }
548         // something like  "227 Entering Passive Mode (132,180,15,2,210,187)" expected
549         // in last response.
550         // Cut anything between ( and )
551         auto v = _responseHistory[$-1].findSplitBefore(")")[0].findSplitAfter("(")[1];
552         string host;
553         ushort port;
554         try {
555             ubyte a1,a2,a3,a4,p1,p2;
556             formattedRead(v, "%d,%d,%d,%d,%d,%d", &a1, &a2, &a3, &a4, &p1, &p2);
557             host = std.format.format("%d.%d.%d.%d", a1, a2, a3, a4);
558             port = (p1<<8) + p2;
559         } catch (FormatException e) {
560             error("Failed to parse ", v);
561             _response.code = 500;
562             return _response;
563         }
564         
565         auto dataStream = new TCPStream();
566         scope (exit ) {
567             if ( dataStream !is null && !_response._receiveAsRange.activated ) {
568                 dataStream.close();
569             }
570         }
571         dataStream.bind(_bind);
572         dataStream.connect(host, port, _timeout);
573         
574         code = sendCmdGetResponse("RETR " ~ baseName(_uri.path) ~ "\r\n", _controlChannel);
575         if ( code/100 > 1 ) {
576             _response.code = code;
577             return _response;
578         }
579         dataStream.readTimeout = _timeout;
580         while ( true ) {
581             auto b = new ubyte[_bufferSize];
582             auto rc = dataStream.receive(b);
583             if ( rc <= 0 ) {
584                 debug(requests) trace("done");
585                 break;
586             }
587             debug(requests) tracef("got %d bytes from data channel", rc);
588 
589             _contentReceived += rc;
590             _response._responseBody.putNoCopy(b[0..rc]);
591 
592             if ( _maxContentLength && _response._responseBody.length >= _maxContentLength ) {
593                 throw new RequestException("maxContentLength exceeded for ftp data");
594             }
595             if ( _useStreaming ) {
596                 debug(requests) trace("ftp uses streaming");
597 
598                 auto __maxContentLength = _maxContentLength;
599                 auto __contentLength = _contentLength;
600                 auto __contentReceived = _contentReceived;
601                 auto __bufferSize = _bufferSize;
602                 auto __dataStream = dataStream;
603                 auto __controlChannel = _controlChannel;
604 
605                 _response._contentLength = _contentLength;
606                 _response.receiveAsRange.activated = true;
607                 _response.receiveAsRange.data.length = 0;
608                 _response.receiveAsRange.data = _response._responseBody.data;
609                 _response.receiveAsRange.read = delegate ubyte[] () {
610                     Buffer!ubyte result;
611                     while(true) {
612                         // check if we received everything we need
613                         if ( __maxContentLength > 0 && __contentReceived >= __maxContentLength ) 
614                         {
615                             throw new RequestException("ContentLength > maxContentLength (%d>%d)".
616                                 format(__contentLength, __maxContentLength));
617                         }
618                         // have to continue
619                         auto b = new ubyte[__bufferSize];
620                         ptrdiff_t read;
621                         try {
622                             read = __dataStream.receive(b);
623                         }
624                         catch (Exception e) {
625                             throw new RequestException("streaming_in error reading from socket", __FILE__, __LINE__, e);
626                         }
627 
628                         if ( read > 0 ) {
629                             _response._contentReceived += read;
630                             __contentReceived += read;
631                             result.putNoCopy(b[0..read]);
632                             return result.data;
633                         }
634                         if ( read == 0 ) {
635                             debug(requests) tracef("streaming_in: server closed connection");
636                             __dataStream.close();
637                             code = responseToCode(serverResponse(__controlChannel));
638                             if ( code/100 == 2 ) {
639                                 debug(requests) tracef("Successfully received %d bytes", _response._responseBody.length);
640                             }
641                             _response.code = code;
642                             sendCmdGetResponse("CWD " ~ pwd ~ "\r\n", __controlChannel);
643                             break;
644                         }
645                     }
646                     return result.data;
647                 };
648                 debug(requests) tracef("leave streaming get");
649                 return _response;
650             }
651         }
652         dataStream.close();
653         response = serverResponse(_controlChannel);
654         code = responseToCode(response);
655         if ( code/100 == 2 ) {
656             debug(requests) tracef("Successfully received %d bytes", _response._responseBody.length);
657         }
658         _response.code = code;
659         return _response;
660     }
661 
662     FTPResponse execute(Request r)
663     {
664         string method = r.method;
665         _uri = r.uri();
666         _authenticator = r.authenticator;
667         _maxContentLength = r.maxContentLength;
668         _useStreaming = r.useStreaming;
669         _verbosity = r.verbosity;
670         _cm = r.cm;
671         _postData = r.postData;
672         _bufferSize = r.bufferSize;
673         _proxy = r.proxy;
674         _bind = r.bind;
675         _timeout = r.timeout;
676 
677         if ( method == "GET" )
678         {
679             return get();
680         }
681         if ( method == "POST" )
682         {
683             return post();
684         }
685         assert(0, "Can't handle method %s for ftp request".format(method));
686     }
687 }
688 
689 //package unittest {
690 //    import std.process;
691 //
692 //    globalLogLevel(LogLevel.info);
693 //    bool unreliable_network = environment.get("UNRELIABLENETWORK", "false") == "true";
694 //
695 //    info("testing ftp");
696 //    auto rq = FTPRequest();
697 //    info("ftp post ", "ftp://speedtest.tele2.net/upload/TEST.TXT");
698 //    auto rs = rq.post("ftp://speedtest.tele2.net/upload/TEST.TXT", "test, ignore please\n".representation);
699 //    assert(unreliable_network || rs.code == 226);
700 //    info("ftp get  ", "ftp://speedtest.tele2.net/nonexistent", ", in same session.");
701 //    rs = rq.get("ftp://speedtest.tele2.net/nonexistent");
702 //    assert(unreliable_network || rs.code != 226);
703 //    info("ftp get  ", "ftp://speedtest.tele2.net/1KB.zip", ", in same session.");
704 //    rs = rq.get("ftp://speedtest.tele2.net/1KB.zip");
705 //    assert(unreliable_network || rs.code == 226);
706 //    assert(unreliable_network || rs.responseBody.length == 1024);
707 //    info("ftp post ", "ftp://speedtest.tele2.net/upload/TEST.TXT");
708 //    rs = rq.post("ftp://speedtest.tele2.net/upload/TEST.TXT", "another test, ignore please\n".representation);
709 //    assert(unreliable_network || rs.code == 226);
710 //    info("ftp get  ", "ftp://ftp.iij.ad.jp/pub/FreeBSD/README.TXT");
711 //    try {
712 //        rs = rq.get("ftp://ftp.iij.ad.jp/pub/FreeBSD/README.TXT");
713 //    }
714 //    catch (ConnectError e)
715 //    {
716 //    }
717 //    assert(unreliable_network || rs.code == 226);
718 //    info("ftp get  ftp://ftp.iij.ad.jp/pub/FreeBSD/README.TXT with authenticator");
719 //    rq.authenticator = new FtpAuthentication("anonymous", "requests@");
720 //    try {
721 //        rs = rq.get("ftp://ftp.iij.ad.jp/pub/FreeBSD/README.TXT");
722 //    }
723 //    catch (ConnectError e)
724 //    {
725 //    }
726 //    assert(unreliable_network || rs.code == 226);
727 //    assert(unreliable_network || rs.finalURI.path == "/pub/FreeBSD/README.TXT");
728 //    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");
729 //    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");
730 //    info("testing ftp - done.");
731 //}