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