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 //}