1 // Licensed to the .NET Foundation under one or more agreements. 2 // The .NET Foundation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System.Collections; 6 using System.Diagnostics; 7 using System.Globalization; 8 using System.IO; 9 using System.Net.Sockets; 10 using System.Text; 11 12 namespace System.Net 13 { 14 internal enum FtpLoginState : byte 15 { 16 NotLoggedIn, 17 LoggedIn, 18 LoggedInButNeedsRelogin, 19 ReloginFailed 20 }; 21 22 /// <summary> 23 /// <para> 24 /// The FtpControlStream class implements a basic FTP connection, 25 /// This means basic command sending and parsing. 26 /// </para> 27 /// </summary> 28 internal class FtpControlStream : CommandStream 29 { 30 private Socket _dataSocket; 31 private IPEndPoint _passiveEndPoint; 32 private TlsStream _tlsStream; 33 34 private StringBuilder _bannerMessage; 35 private StringBuilder _welcomeMessage; 36 private StringBuilder _exitMessage; 37 private WeakReference _credentials; 38 private string _currentTypeSetting = string.Empty; 39 40 private long _contentLength = -1; 41 private DateTime _lastModified; 42 private bool _dataHandshakeStarted = false; 43 private string _loginDirectory = null; 44 private string _establishedServerDirectory = null; 45 private string _requestedServerDirectory = null; 46 private Uri _responseUri; 47 48 private FtpLoginState _loginState = FtpLoginState.NotLoggedIn; 49 50 internal FtpStatusCode StatusCode; 51 internal string StatusLine; 52 53 internal NetworkCredential Credentials 54 { 55 get 56 { 57 if (_credentials != null && _credentials.IsAlive) 58 { 59 return (NetworkCredential)_credentials.Target; 60 } 61 else 62 { 63 return null; 64 } 65 } 66 set 67 { 68 if (_credentials == null) 69 { 70 _credentials = new WeakReference(null); 71 } 72 _credentials.Target = value; 73 } 74 } 75 76 private static readonly AsyncCallback s_acceptCallbackDelegate = new AsyncCallback(AcceptCallback); 77 private static readonly AsyncCallback s_connectCallbackDelegate = new AsyncCallback(ConnectCallback); 78 private static readonly AsyncCallback s_SSLHandshakeCallback = new AsyncCallback(SSLHandshakeCallback); 79 FtpControlStream(TcpClient client)80 internal FtpControlStream(TcpClient client) 81 : base(client) 82 { 83 } 84 85 /// <summary> 86 /// <para>Closes the connecting socket to generate an error.</para> 87 /// </summary> AbortConnect()88 internal void AbortConnect() 89 { 90 Socket socket = _dataSocket; 91 if (socket != null) 92 { 93 try 94 { 95 socket.Close(); 96 } 97 catch (ObjectDisposedException) 98 { 99 } 100 } 101 } 102 103 /// <summary> 104 /// <para>Provides a wrapper for the async accept operations 105 /// </summary> AcceptCallback(IAsyncResult asyncResult)106 private static void AcceptCallback(IAsyncResult asyncResult) 107 { 108 FtpControlStream connection = (FtpControlStream)asyncResult.AsyncState; 109 Socket listenSocket = connection._dataSocket; 110 try 111 { 112 connection._dataSocket = listenSocket.EndAccept(asyncResult); 113 if (!connection.ServerAddress.Equals(((IPEndPoint)connection._dataSocket.RemoteEndPoint).Address)) 114 { 115 connection._dataSocket.Close(); 116 throw new WebException(SR.net_ftp_active_address_different, WebExceptionStatus.ProtocolError); 117 } 118 connection.ContinueCommandPipeline(); 119 } 120 catch (Exception e) 121 { 122 connection.CloseSocket(); 123 connection.InvokeRequestCallback(e); 124 } 125 finally 126 { 127 listenSocket.Close(); 128 } 129 } 130 131 /// <summary> 132 /// <para>Provides a wrapper for the async accept operations</para> 133 /// </summary> ConnectCallback(IAsyncResult asyncResult)134 private static void ConnectCallback(IAsyncResult asyncResult) 135 { 136 FtpControlStream connection = (FtpControlStream)asyncResult.AsyncState; 137 try 138 { 139 connection._dataSocket.EndConnect(asyncResult); 140 connection.ContinueCommandPipeline(); 141 } 142 catch (Exception e) 143 { 144 connection.CloseSocket(); 145 connection.InvokeRequestCallback(e); 146 } 147 } 148 SSLHandshakeCallback(IAsyncResult asyncResult)149 private static void SSLHandshakeCallback(IAsyncResult asyncResult) 150 { 151 FtpControlStream connection = (FtpControlStream)asyncResult.AsyncState; 152 try 153 { 154 connection._tlsStream.EndAuthenticateAsClient(asyncResult); 155 connection.ContinueCommandPipeline(); 156 } 157 catch (Exception e) 158 { 159 connection.CloseSocket(); 160 connection.InvokeRequestCallback(e); 161 } 162 } 163 164 // Creates a FtpDataStream object, constructs a TLS stream if needed. 165 // In case SSL and ASYNC we delay sigaling the user stream until the handshake is done. QueueOrCreateFtpDataStream(ref Stream stream)166 private PipelineInstruction QueueOrCreateFtpDataStream(ref Stream stream) 167 { 168 if (_dataSocket == null) 169 throw new InternalException(); 170 171 // 172 // Re-entered pipeline with completed read on the TlsStream 173 // 174 if (_tlsStream != null) 175 { 176 stream = new FtpDataStream(_tlsStream, (FtpWebRequest)_request, IsFtpDataStreamWriteable()); 177 _tlsStream = null; 178 return PipelineInstruction.GiveStream; 179 } 180 181 NetworkStream networkStream = new NetworkStream(_dataSocket, true); 182 183 if (UsingSecureStream) 184 { 185 FtpWebRequest request = (FtpWebRequest)_request; 186 187 TlsStream tlsStream = new TlsStream(networkStream, _dataSocket, request.RequestUri.Host, request.ClientCertificates); 188 networkStream = tlsStream; 189 190 if (_isAsync) 191 { 192 _tlsStream = tlsStream; 193 194 tlsStream.BeginAuthenticateAsClient(s_SSLHandshakeCallback, this); 195 return PipelineInstruction.Pause; 196 } 197 else 198 { 199 tlsStream.AuthenticateAsClient(); 200 } 201 } 202 203 stream = new FtpDataStream(networkStream, (FtpWebRequest)_request, IsFtpDataStreamWriteable()); 204 return PipelineInstruction.GiveStream; 205 } 206 ClearState()207 protected override void ClearState() 208 { 209 _contentLength = -1; 210 _lastModified = DateTime.MinValue; 211 _responseUri = null; 212 _dataHandshakeStarted = false; 213 StatusCode = FtpStatusCode.Undefined; 214 StatusLine = null; 215 216 _dataSocket = null; 217 _passiveEndPoint = null; 218 _tlsStream = null; 219 220 base.ClearState(); 221 } 222 223 // This is called by underlying base class code, each time a new response is received from the wire or a protocol stage is resumed. 224 // This function controls the setting up of a data socket/connection, and of saving off the server responses. PipelineCallback(PipelineEntry entry, ResponseDescription response, bool timeout, ref Stream stream)225 protected override PipelineInstruction PipelineCallback(PipelineEntry entry, ResponseDescription response, bool timeout, ref Stream stream) 226 { 227 if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"Command:{entry?.Command} Description:{response?.StatusDescription}"); 228 229 // null response is not expected 230 if (response == null) 231 return PipelineInstruction.Abort; 232 233 FtpStatusCode status = (FtpStatusCode)response.Status; 234 235 // 236 // Update global "current status" for FtpWebRequest 237 // 238 if (status != FtpStatusCode.ClosingControl) 239 { 240 // A 221 status won't be reflected on the user FTP response 241 // Anything else will (by design?) 242 StatusCode = status; 243 StatusLine = response.StatusDescription; 244 } 245 246 // If the status code is outside the range defined in RFC (1xx to 5xx) throw 247 if (response.InvalidStatusCode) 248 throw new WebException(SR.net_InvalidStatusCode, WebExceptionStatus.ProtocolError); 249 250 // Update the banner message if any, this is a little hack because the "entry" param is null 251 if (_index == -1) 252 { 253 if (status == FtpStatusCode.SendUserCommand) 254 { 255 _bannerMessage = new StringBuilder(); 256 _bannerMessage.Append(StatusLine); 257 return PipelineInstruction.Advance; 258 } 259 else if (status == FtpStatusCode.ServiceTemporarilyNotAvailable) 260 { 261 return PipelineInstruction.Reread; 262 } 263 else 264 throw GenerateException(status, response.StatusDescription, null); 265 } 266 267 // 268 // Check for the result of our attempt to use UTF8 269 // 270 if (entry.Command == "OPTS utf8 on\r\n") 271 { 272 if (response.PositiveCompletion) 273 { 274 Encoding = Encoding.UTF8; 275 } 276 else 277 { 278 Encoding = Encoding.Default; 279 } 280 return PipelineInstruction.Advance; 281 } 282 283 // If we are already logged in and the server returns 530 then 284 // the server does not support re-issuing a USER command, 285 // tear down the connection and start all over again 286 if (entry.Command.IndexOf("USER") != -1) 287 { 288 // The server may not require a password for this user, so bypass the password command 289 if (status == FtpStatusCode.LoggedInProceed) 290 { 291 _loginState = FtpLoginState.LoggedIn; 292 _index++; 293 } 294 } 295 296 // 297 // Throw on an error with possible recovery option 298 // 299 if (response.TransientFailure || response.PermanentFailure) 300 { 301 if (status == FtpStatusCode.ServiceNotAvailable) 302 { 303 MarkAsRecoverableFailure(); 304 } 305 throw GenerateException(status, response.StatusDescription, null); 306 } 307 308 if (_loginState != FtpLoginState.LoggedIn 309 && entry.Command.IndexOf("PASS") != -1) 310 { 311 // Note the fact that we logged in 312 if (status == FtpStatusCode.NeedLoginAccount || status == FtpStatusCode.LoggedInProceed) 313 _loginState = FtpLoginState.LoggedIn; 314 else 315 throw GenerateException(status, response.StatusDescription, null); 316 } 317 318 // 319 // Parse special cases 320 // 321 if (entry.HasFlag(PipelineEntryFlags.CreateDataConnection) && (response.PositiveCompletion || response.PositiveIntermediate)) 322 { 323 bool isSocketReady; 324 PipelineInstruction result = QueueOrCreateDataConection(entry, response, timeout, ref stream, out isSocketReady); 325 if (!isSocketReady) 326 return result; 327 // otherwise we have a stream to create 328 } 329 // 330 // This is part of the above case and it's all about giving data stream back 331 // 332 if (status == FtpStatusCode.OpeningData || status == FtpStatusCode.DataAlreadyOpen) 333 { 334 if (_dataSocket == null) 335 { 336 return PipelineInstruction.Abort; 337 } 338 if (!entry.HasFlag(PipelineEntryFlags.GiveDataStream)) 339 { 340 _abortReason = SR.Format(SR.net_ftp_invalid_status_response, status, entry.Command); 341 return PipelineInstruction.Abort; 342 } 343 344 // Parse out the Content length, if we can 345 TryUpdateContentLength(response.StatusDescription); 346 347 // Parse out the file name, when it is returned and use it for our ResponseUri 348 FtpWebRequest request = (FtpWebRequest)_request; 349 if (request.MethodInfo.ShouldParseForResponseUri) 350 { 351 TryUpdateResponseUri(response.StatusDescription, request); 352 } 353 354 return QueueOrCreateFtpDataStream(ref stream); 355 } 356 357 358 // 359 // Parse responses by status code exclusivelly 360 // 361 362 // Update welcome message 363 if (status == FtpStatusCode.LoggedInProceed) 364 { 365 _welcomeMessage.Append(StatusLine); 366 } 367 // OR set the user response ExitMessage 368 else if (status == FtpStatusCode.ClosingControl) 369 { 370 _exitMessage.Append(response.StatusDescription); 371 // And close the control stream socket on "QUIT" 372 CloseSocket(); 373 } 374 // OR set us up for SSL/TLS, after this we'll be writing securely 375 else if (status == FtpStatusCode.ServerWantsSecureSession) 376 { 377 // If NetworkStream is a TlsStream, then this must be in the async callback 378 // from completing the SSL handshake. 379 // So just let the pipeline continue. 380 if (!(NetworkStream is TlsStream)) 381 { 382 FtpWebRequest request = (FtpWebRequest)_request; 383 TlsStream tlsStream = new TlsStream(NetworkStream, Socket, request.RequestUri.Host, request.ClientCertificates); 384 385 if (_isAsync) 386 { 387 tlsStream.BeginAuthenticateAsClient(ar => 388 { 389 try 390 { 391 tlsStream.EndAuthenticateAsClient(ar); 392 NetworkStream = tlsStream; 393 this.ContinueCommandPipeline(); 394 } 395 catch (Exception e) 396 { 397 this.CloseSocket(); 398 this.InvokeRequestCallback(e); 399 } 400 }, null); 401 402 return PipelineInstruction.Pause; 403 } 404 else 405 { 406 tlsStream.AuthenticateAsClient(); 407 NetworkStream = tlsStream; 408 } 409 } 410 } 411 // OR parse out the file size or file time, usually a result of sending SIZE/MDTM commands 412 else if (status == FtpStatusCode.FileStatus) 413 { 414 FtpWebRequest request = (FtpWebRequest)_request; 415 if (entry.Command.StartsWith("SIZE ")) 416 { 417 _contentLength = GetContentLengthFrom213Response(response.StatusDescription); 418 } 419 else if (entry.Command.StartsWith("MDTM ")) 420 { 421 _lastModified = GetLastModifiedFrom213Response(response.StatusDescription); 422 } 423 } 424 // OR parse out our login directory 425 else if (status == FtpStatusCode.PathnameCreated) 426 { 427 if (entry.Command == "PWD\r\n" && !entry.HasFlag(PipelineEntryFlags.UserCommand)) 428 { 429 _loginDirectory = GetLoginDirectory(response.StatusDescription); 430 } 431 } 432 // Asserting we have some positive response 433 else 434 { 435 // We only use CWD to reset ourselves back to the login directory. 436 if (entry.Command.IndexOf("CWD") != -1) 437 { 438 _establishedServerDirectory = _requestedServerDirectory; 439 } 440 } 441 442 // Intermediate responses require rereading 443 if (response.PositiveIntermediate || (!UsingSecureStream && entry.Command == "AUTH TLS\r\n")) 444 { 445 return PipelineInstruction.Reread; 446 } 447 448 return PipelineInstruction.Advance; 449 } 450 451 /// <summary> 452 /// <para>Creates an array of commands, that will be sent to the server</para> 453 /// </summary> BuildCommandsList(WebRequest req)454 protected override PipelineEntry[] BuildCommandsList(WebRequest req) 455 { 456 bool resetLoggedInState = false; 457 FtpWebRequest request = (FtpWebRequest)req; 458 459 if (NetEventSource.IsEnabled) NetEventSource.Info(this); 460 461 _responseUri = request.RequestUri; 462 ArrayList commandList = new ArrayList(); 463 464 if (request.EnableSsl && !UsingSecureStream) 465 { 466 commandList.Add(new PipelineEntry(FormatFtpCommand("AUTH", "TLS"))); 467 // According to RFC we need to re-authorize with USER/PASS after we re-authenticate. 468 resetLoggedInState = true; 469 } 470 471 if (resetLoggedInState) 472 { 473 _loginDirectory = null; 474 _establishedServerDirectory = null; 475 _requestedServerDirectory = null; 476 _currentTypeSetting = string.Empty; 477 if (_loginState == FtpLoginState.LoggedIn) 478 _loginState = FtpLoginState.LoggedInButNeedsRelogin; 479 } 480 481 if (_loginState != FtpLoginState.LoggedIn) 482 { 483 Credentials = request.Credentials.GetCredential(request.RequestUri, "basic"); 484 _welcomeMessage = new StringBuilder(); 485 _exitMessage = new StringBuilder(); 486 487 string domainUserName = string.Empty; 488 string password = string.Empty; 489 490 if (Credentials != null) 491 { 492 domainUserName = Credentials.UserName; 493 string domain = Credentials.Domain; 494 if (!string.IsNullOrEmpty(domain)) 495 { 496 domainUserName = domain + "\\" + domainUserName; 497 } 498 499 password = Credentials.Password; 500 } 501 502 if (domainUserName.Length == 0 && password.Length == 0) 503 { 504 domainUserName = "anonymous"; 505 password = "anonymous@"; 506 } 507 508 commandList.Add(new PipelineEntry(FormatFtpCommand("USER", domainUserName))); 509 commandList.Add(new PipelineEntry(FormatFtpCommand("PASS", password), PipelineEntryFlags.DontLogParameter)); 510 511 // If SSL, always configure data channel encryption after authentication to maximum RFC compatibility. The RFC allows for 512 // PBSZ/PROT commands to come either before or after the USER/PASS, but some servers require USER/PASS immediately after 513 // the AUTH TLS command. 514 if (request.EnableSsl && !UsingSecureStream) 515 { 516 commandList.Add(new PipelineEntry(FormatFtpCommand("PBSZ", "0"))); 517 commandList.Add(new PipelineEntry(FormatFtpCommand("PROT", "P"))); 518 } 519 520 commandList.Add(new PipelineEntry(FormatFtpCommand("OPTS", "utf8 on"))); 521 commandList.Add(new PipelineEntry(FormatFtpCommand("PWD", null))); 522 } 523 524 GetPathOption getPathOption = GetPathOption.Normal; 525 526 if (request.MethodInfo.HasFlag(FtpMethodFlags.DoesNotTakeParameter)) 527 { 528 getPathOption = GetPathOption.AssumeNoFilename; 529 } 530 else if (request.MethodInfo.HasFlag(FtpMethodFlags.ParameterIsDirectory)) 531 { 532 getPathOption = GetPathOption.AssumeFilename; 533 } 534 535 string requestPath; 536 string requestDirectory; 537 string requestFilename; 538 539 GetPathInfo(getPathOption, request.RequestUri, out requestPath, out requestDirectory, out requestFilename); 540 541 if (requestFilename.Length == 0 && request.MethodInfo.HasFlag(FtpMethodFlags.TakesParameter)) 542 throw new WebException(SR.net_ftp_invalid_uri); 543 544 // We optimize for having the current working directory staying at the login directory. This ensure that 545 // our relative paths work right and reduces unnecessary CWD commands. 546 // Usually, we don't change the working directory except for some FTP commands. If necessary, 547 // we need to reset our working directory back to the login directory. 548 if (_establishedServerDirectory != null && _loginDirectory != null && _establishedServerDirectory != _loginDirectory) 549 { 550 commandList.Add(new PipelineEntry(FormatFtpCommand("CWD", _loginDirectory), PipelineEntryFlags.UserCommand)); 551 _requestedServerDirectory = _loginDirectory; 552 } 553 554 // For most commands, we don't need to navigate to the directory since we pass in the full 555 // path as part of the FTP protocol command. However, some commands require it. 556 if (request.MethodInfo.HasFlag(FtpMethodFlags.MustChangeWorkingDirectoryToPath) && requestDirectory.Length > 0) 557 { 558 commandList.Add(new PipelineEntry(FormatFtpCommand("CWD", requestDirectory), PipelineEntryFlags.UserCommand)); 559 _requestedServerDirectory = requestDirectory; 560 } 561 562 if (!request.MethodInfo.IsCommandOnly) 563 { 564 string requestedTypeSetting = request.UseBinary ? "I" : "A"; 565 if (_currentTypeSetting != requestedTypeSetting) 566 { 567 commandList.Add(new PipelineEntry(FormatFtpCommand("TYPE", requestedTypeSetting))); 568 _currentTypeSetting = requestedTypeSetting; 569 } 570 571 if (request.UsePassive) 572 { 573 string passiveCommand = (ServerAddress.AddressFamily == AddressFamily.InterNetwork) ? "PASV" : "EPSV"; 574 commandList.Add(new PipelineEntry(FormatFtpCommand(passiveCommand, null), PipelineEntryFlags.CreateDataConnection)); 575 } 576 else 577 { 578 string portCommand = (ServerAddress.AddressFamily == AddressFamily.InterNetwork) ? "PORT" : "EPRT"; 579 CreateFtpListenerSocket(request); 580 commandList.Add(new PipelineEntry(FormatFtpCommand(portCommand, GetPortCommandLine(request)))); 581 } 582 583 if (request.ContentOffset > 0) 584 { 585 // REST command must always be the last sent before the main file command is sent. 586 commandList.Add(new PipelineEntry(FormatFtpCommand("REST", request.ContentOffset.ToString(CultureInfo.InvariantCulture)))); 587 } 588 } 589 590 PipelineEntryFlags flags = PipelineEntryFlags.UserCommand; 591 if (!request.MethodInfo.IsCommandOnly) 592 { 593 flags |= PipelineEntryFlags.GiveDataStream; 594 if (!request.UsePassive) 595 flags |= PipelineEntryFlags.CreateDataConnection; 596 } 597 598 if (request.MethodInfo.Operation == FtpOperation.Rename) 599 { 600 string baseDir = (requestDirectory == string.Empty) 601 ? string.Empty : requestDirectory + "/"; 602 commandList.Add(new PipelineEntry(FormatFtpCommand("RNFR", baseDir + requestFilename), flags)); 603 604 string renameTo; 605 if (!string.IsNullOrEmpty(request.RenameTo) 606 && request.RenameTo.StartsWith("/", StringComparison.OrdinalIgnoreCase)) 607 { 608 renameTo = request.RenameTo; // Absolute path 609 } 610 else 611 { 612 renameTo = baseDir + request.RenameTo; // Relative path 613 } 614 commandList.Add(new PipelineEntry(FormatFtpCommand("RNTO", renameTo), flags)); 615 } 616 else if (request.MethodInfo.HasFlag(FtpMethodFlags.DoesNotTakeParameter)) 617 { 618 commandList.Add(new PipelineEntry(FormatFtpCommand(request.Method, string.Empty), flags)); 619 } 620 else if (request.MethodInfo.HasFlag(FtpMethodFlags.MustChangeWorkingDirectoryToPath)) 621 { 622 commandList.Add(new PipelineEntry(FormatFtpCommand(request.Method, requestFilename), flags)); 623 } 624 else 625 { 626 commandList.Add(new PipelineEntry(FormatFtpCommand(request.Method, requestPath), flags)); 627 } 628 629 commandList.Add(new PipelineEntry(FormatFtpCommand("QUIT", null))); 630 631 return (PipelineEntry[])commandList.ToArray(typeof(PipelineEntry)); 632 } 633 QueueOrCreateDataConection(PipelineEntry entry, ResponseDescription response, bool timeout, ref Stream stream, out bool isSocketReady)634 private PipelineInstruction QueueOrCreateDataConection(PipelineEntry entry, ResponseDescription response, bool timeout, ref Stream stream, out bool isSocketReady) 635 { 636 isSocketReady = false; 637 if (_dataHandshakeStarted) 638 { 639 isSocketReady = true; 640 return PipelineInstruction.Pause; //if we already started then this is re-entering into the callback where we proceed with the stream 641 } 642 643 _dataHandshakeStarted = true; 644 645 // Handle passive responses by parsing the port and later doing a Connect(...) 646 bool isPassive = false; 647 int port = -1; 648 if (entry.Command == "PASV\r\n" || entry.Command == "EPSV\r\n") 649 { 650 if (!response.PositiveCompletion) 651 { 652 _abortReason = SR.Format(SR.net_ftp_server_failed_passive, response.Status); 653 return PipelineInstruction.Abort; 654 } 655 if (entry.Command == "PASV\r\n") 656 { 657 port = GetPortV4(response.StatusDescription); 658 } 659 else 660 { 661 port = GetPortV6(response.StatusDescription); 662 } 663 664 isPassive = true; 665 } 666 667 if (isPassive) 668 { 669 if (port == -1) 670 { 671 NetEventSource.Fail(this, "'port' not set."); 672 } 673 674 try 675 { 676 _dataSocket = CreateFtpDataSocket((FtpWebRequest)_request, Socket); 677 } 678 catch (ObjectDisposedException) 679 { 680 throw ExceptionHelper.RequestAbortedException; 681 } 682 683 IPEndPoint localEndPoint = new IPEndPoint(((IPEndPoint)Socket.LocalEndPoint).Address, 0); 684 _dataSocket.Bind(localEndPoint); 685 686 _passiveEndPoint = new IPEndPoint(ServerAddress, port); 687 } 688 689 PipelineInstruction result; 690 691 if (_passiveEndPoint != null) 692 { 693 IPEndPoint passiveEndPoint = _passiveEndPoint; 694 _passiveEndPoint = null; 695 if (NetEventSource.IsEnabled) NetEventSource.Info(this, "starting Connect()"); 696 if (_isAsync) 697 { 698 _dataSocket.BeginConnect(passiveEndPoint, s_connectCallbackDelegate, this); 699 result = PipelineInstruction.Pause; 700 } 701 else 702 { 703 _dataSocket.Connect(passiveEndPoint); 704 result = PipelineInstruction.Advance; // for passive mode we end up going to the next command 705 } 706 } 707 else 708 { 709 if (NetEventSource.IsEnabled) NetEventSource.Info(this, "starting Accept()"); 710 711 if (_isAsync) 712 { 713 _dataSocket.BeginAccept(s_acceptCallbackDelegate, this); 714 result = PipelineInstruction.Pause; 715 } 716 else 717 { 718 Socket listenSocket = _dataSocket; 719 try 720 { 721 _dataSocket = _dataSocket.Accept(); 722 if (!ServerAddress.Equals(((IPEndPoint)_dataSocket.RemoteEndPoint).Address)) 723 { 724 _dataSocket.Close(); 725 throw new WebException(SR.net_ftp_active_address_different, WebExceptionStatus.ProtocolError); 726 } 727 isSocketReady = true; // for active mode we end up creating a stream before advancing the pipeline 728 result = PipelineInstruction.Pause; 729 } 730 finally 731 { 732 listenSocket.Close(); 733 } 734 } 735 } 736 return result; 737 } 738 739 private enum GetPathOption 740 { 741 Normal, 742 AssumeFilename, 743 AssumeNoFilename 744 } 745 746 /// <summary> 747 /// <para>Gets the path component of the Uri</para> 748 /// </summary> GetPathInfo(GetPathOption pathOption, Uri uri, out string path, out string directory, out string filename)749 private static void GetPathInfo(GetPathOption pathOption, 750 Uri uri, 751 out string path, 752 out string directory, 753 out string filename) 754 { 755 path = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped); 756 int index = path.LastIndexOf('/'); 757 758 if (pathOption == GetPathOption.AssumeFilename && 759 index != -1 && index == path.Length - 1) 760 { 761 // Remove last '/' and continue normal processing 762 path = path.Substring(0, path.Length - 1); 763 index = path.LastIndexOf('/'); 764 } 765 766 // split path into directory and filename 767 if (pathOption == GetPathOption.AssumeNoFilename) 768 { 769 directory = path; 770 filename = string.Empty; 771 } 772 else 773 { 774 directory = path.Substring(0, index + 1); 775 filename = path.Substring(index + 1, path.Length - (index + 1)); 776 } 777 778 // strip off trailing '/' on directory if present 779 if (directory.Length > 1 && directory[directory.Length - 1] == '/') 780 directory = directory.Substring(0, directory.Length - 1); 781 } 782 783 // 784 /// <summary> 785 /// <para>Formats an IP address (contained in a UInt32) to a FTP style command string</para> 786 /// </summary> FormatAddress(IPAddress address, int Port)787 private String FormatAddress(IPAddress address, int Port) 788 { 789 byte[] localAddressInBytes = address.GetAddressBytes(); 790 791 // produces a string in FTP IPAddress/Port encoding (a1, a2, a3, a4, p1, p2), for sending as a parameter 792 // to the port command. 793 StringBuilder sb = new StringBuilder(32); 794 foreach (byte element in localAddressInBytes) 795 { 796 sb.Append(element); 797 sb.Append(','); 798 } 799 sb.Append(Port / 256); 800 sb.Append(','); 801 sb.Append(Port % 256); 802 return sb.ToString(); 803 } 804 805 /// <summary> 806 /// <para>Formats an IP address (v6) to a FTP style command string 807 /// Looks something in this form: |2|1080::8:800:200C:417A|5282| <para> 808 /// |2|4567::0123:5678:0123:5678|0123| 809 /// </summary> FormatAddressV6(IPAddress address, int port)810 private string FormatAddressV6(IPAddress address, int port) 811 { 812 StringBuilder sb = new StringBuilder(43); // based on max size of IPv6 address + port + seperators 813 String addressString = address.ToString(); 814 sb.Append("|2|"); 815 sb.Append(addressString); 816 sb.Append('|'); 817 sb.Append(port.ToString(NumberFormatInfo.InvariantInfo)); 818 sb.Append('|'); 819 return sb.ToString(); 820 } 821 822 internal long ContentLength 823 { 824 get 825 { 826 return _contentLength; 827 } 828 } 829 830 internal DateTime LastModified 831 { 832 get 833 { 834 return _lastModified; 835 } 836 } 837 838 internal Uri ResponseUri 839 { 840 get 841 { 842 return _responseUri; 843 } 844 } 845 846 /// <summary> 847 /// <para>Returns the server message sent before user credentials are sent</para> 848 /// </summary> 849 internal string BannerMessage 850 { 851 get 852 { 853 return (_bannerMessage != null) ? _bannerMessage.ToString() : null; 854 } 855 } 856 857 /// <summary> 858 /// <para>Returns the server message sent after user credentials are sent</para> 859 /// </summary> 860 internal string WelcomeMessage 861 { 862 get 863 { 864 return (_welcomeMessage != null) ? _welcomeMessage.ToString() : null; 865 } 866 } 867 868 /// <summary> 869 /// <para>Returns the exit sent message on shutdown</para> 870 /// </summary> 871 internal string ExitMessage 872 { 873 get 874 { 875 return (_exitMessage != null) ? _exitMessage.ToString() : null; 876 } 877 } 878 879 /// <summary> 880 /// <para>Parses a response string for content length</para> 881 /// </summary> GetContentLengthFrom213Response(string responseString)882 private long GetContentLengthFrom213Response(string responseString) 883 { 884 string[] parsedList = responseString.Split(new char[] { ' ' }); 885 if (parsedList.Length < 2) 886 throw new FormatException(SR.Format(SR.net_ftp_response_invalid_format, responseString)); 887 return Convert.ToInt64(parsedList[1], NumberFormatInfo.InvariantInfo); 888 } 889 890 /// <summary> 891 /// <para>Parses a response string for last modified time</para> 892 /// </summary> GetLastModifiedFrom213Response(string str)893 private DateTime GetLastModifiedFrom213Response(string str) 894 { 895 DateTime dateTime = _lastModified; 896 string[] parsedList = str.Split(new char[] { ' ', '.' }); 897 if (parsedList.Length < 2) 898 { 899 return dateTime; 900 } 901 string dateTimeLine = parsedList[1]; 902 if (dateTimeLine.Length < 14) 903 { 904 return dateTime; 905 } 906 int year = Convert.ToInt32(dateTimeLine.Substring(0, 4), NumberFormatInfo.InvariantInfo); 907 int month = Convert.ToInt16(dateTimeLine.Substring(4, 2), NumberFormatInfo.InvariantInfo); 908 int day = Convert.ToInt16(dateTimeLine.Substring(6, 2), NumberFormatInfo.InvariantInfo); 909 int hour = Convert.ToInt16(dateTimeLine.Substring(8, 2), NumberFormatInfo.InvariantInfo); 910 int minute = Convert.ToInt16(dateTimeLine.Substring(10, 2), NumberFormatInfo.InvariantInfo); 911 int second = Convert.ToInt16(dateTimeLine.Substring(12, 2), NumberFormatInfo.InvariantInfo); 912 int millisecond = 0; 913 if (parsedList.Length > 2) 914 { 915 millisecond = Convert.ToInt16(parsedList[2], NumberFormatInfo.InvariantInfo); 916 } 917 try 918 { 919 dateTime = new DateTime(year, month, day, hour, minute, second, millisecond); 920 dateTime = dateTime.ToLocalTime(); // must be handled in local time 921 } 922 catch (ArgumentOutOfRangeException) 923 { 924 } 925 catch (ArgumentException) 926 { 927 } 928 return dateTime; 929 } 930 931 /// <summary> 932 /// <para>Attempts to find the response Uri 933 /// Typical string looks like this, need to get trailing filename 934 /// "150 Opening BINARY mode data connection for FTP46.tmp."</para> 935 /// </summary> TryUpdateResponseUri(string str, FtpWebRequest request)936 private void TryUpdateResponseUri(string str, FtpWebRequest request) 937 { 938 Uri baseUri = request.RequestUri; 939 // 940 // Not sure what we are doing here but I guess the logic is IIS centric 941 // 942 int start = str.IndexOf("for "); 943 if (start == -1) 944 return; 945 start += 4; 946 int end = str.LastIndexOf('('); 947 if (end == -1) 948 end = str.Length; 949 if (end <= start) 950 return; 951 952 string filename = str.Substring(start, end - start); 953 filename = filename.TrimEnd(new char[] { ' ', '.', '\r', '\n' }); 954 // Do minimal escaping that we need to get a valid Uri 955 // when combined with the baseUri 956 string escapedFilename; 957 escapedFilename = filename.Replace("%", "%25"); 958 escapedFilename = escapedFilename.Replace("#", "%23"); 959 960 // help us out if the user forgot to add a slash to the directory name 961 string orginalPath = baseUri.AbsolutePath; 962 if (orginalPath.Length > 0 && orginalPath[orginalPath.Length - 1] != '/') 963 { 964 UriBuilder uriBuilder = new UriBuilder(baseUri); 965 uriBuilder.Path = orginalPath + "/"; 966 baseUri = uriBuilder.Uri; 967 } 968 969 Uri newUri; 970 if (!Uri.TryCreate(baseUri, escapedFilename, out newUri)) 971 { 972 throw new FormatException(SR.Format(SR.net_ftp_invalid_response_filename, filename)); 973 } 974 else 975 { 976 if (!baseUri.IsBaseOf(newUri) || 977 baseUri.Segments.Length != newUri.Segments.Length - 1) 978 { 979 throw new FormatException(SR.Format(SR.net_ftp_invalid_response_filename, filename)); 980 } 981 else 982 { 983 _responseUri = newUri; 984 } 985 } 986 } 987 988 /// <summary> 989 /// <para>Parses a response string for content length</para> 990 /// </summary> TryUpdateContentLength(string str)991 private void TryUpdateContentLength(string str) 992 { 993 int pos1 = str.LastIndexOf("("); 994 if (pos1 != -1) 995 { 996 int pos2 = str.IndexOf(" bytes)."); 997 if (pos2 != -1 && pos2 > pos1) 998 { 999 pos1++; 1000 long result; 1001 if (Int64.TryParse(str.Substring(pos1, pos2 - pos1), 1002 NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite, 1003 NumberFormatInfo.InvariantInfo, out result)) 1004 { 1005 _contentLength = result; 1006 } 1007 } 1008 } 1009 } 1010 1011 /// <summary> 1012 /// <para>Parses a response string for our login dir in " "</para> 1013 /// </summary> GetLoginDirectory(string str)1014 private string GetLoginDirectory(string str) 1015 { 1016 int firstQuote = str.IndexOf('"'); 1017 int lastQuote = str.LastIndexOf('"'); 1018 if (firstQuote != -1 && lastQuote != -1 && firstQuote != lastQuote) 1019 { 1020 return str.Substring(firstQuote + 1, lastQuote - firstQuote - 1); 1021 } 1022 else 1023 { 1024 return String.Empty; 1025 } 1026 } 1027 1028 /// <summary> 1029 /// <para>Parses a response string for a port number</para> 1030 /// </summary> GetPortV4(string responseString)1031 private int GetPortV4(string responseString) 1032 { 1033 string[] parsedList = responseString.Split(new char[] { ' ', '(', ',', ')' }); 1034 1035 // We need at least the status code and the port 1036 if (parsedList.Length <= 7) 1037 { 1038 throw new FormatException(SR.Format(SR.net_ftp_response_invalid_format, responseString)); 1039 } 1040 1041 int index = parsedList.Length - 1; 1042 // skip the last non-number token (e.g. terminating '.') 1043 if (!Char.IsNumber(parsedList[index], 0)) 1044 index--; 1045 1046 int port = Convert.ToByte(parsedList[index--], NumberFormatInfo.InvariantInfo); 1047 port = port | 1048 (Convert.ToByte(parsedList[index--], NumberFormatInfo.InvariantInfo) << 8); 1049 1050 return port; 1051 } 1052 1053 /// <summary> 1054 /// <para>Parses a response string for a port number</para> 1055 /// </summary> GetPortV6(string responseString)1056 private int GetPortV6(string responseString) 1057 { 1058 int pos1 = responseString.LastIndexOf("("); 1059 int pos2 = responseString.LastIndexOf(")"); 1060 if (pos1 == -1 || pos2 <= pos1) 1061 throw new FormatException(SR.Format(SR.net_ftp_response_invalid_format, responseString)); 1062 1063 // addressInfo will contain a string of format "|||<tcp-port>|" 1064 string addressInfo = responseString.Substring(pos1 + 1, pos2 - pos1 - 1); 1065 1066 string[] parsedList = addressInfo.Split(new char[] { '|' }); 1067 if (parsedList.Length < 4) 1068 throw new FormatException(SR.Format(SR.net_ftp_response_invalid_format, responseString)); 1069 1070 return Convert.ToInt32(parsedList[3], NumberFormatInfo.InvariantInfo); 1071 } 1072 1073 /// <summary> 1074 /// <para>Creates the Listener socket</para> 1075 /// </summary> CreateFtpListenerSocket(FtpWebRequest request)1076 private void CreateFtpListenerSocket(FtpWebRequest request) 1077 { 1078 // Gets an IPEndPoint for the local host for the data socket to bind to. 1079 IPEndPoint epListener = new IPEndPoint(((IPEndPoint)Socket.LocalEndPoint).Address, 0); 1080 try 1081 { 1082 _dataSocket = CreateFtpDataSocket(request, Socket); 1083 } 1084 catch (ObjectDisposedException) 1085 { 1086 throw ExceptionHelper.RequestAbortedException; 1087 } 1088 1089 // Binds the data socket to the local end point. 1090 _dataSocket.Bind(epListener); 1091 _dataSocket.Listen(1); // Put the dataSocket in Listen mode 1092 } 1093 1094 /// <summary> 1095 /// <para>Builds a command line to send to the server with proper port and IP address of client</para> 1096 /// </summary> GetPortCommandLine(FtpWebRequest request)1097 private string GetPortCommandLine(FtpWebRequest request) 1098 { 1099 try 1100 { 1101 // retrieves the IP address of the local endpoint 1102 IPEndPoint localEP = (IPEndPoint)_dataSocket.LocalEndPoint; 1103 if (ServerAddress.AddressFamily == AddressFamily.InterNetwork) 1104 { 1105 return FormatAddress(localEP.Address, localEP.Port); 1106 } 1107 else if (ServerAddress.AddressFamily == AddressFamily.InterNetworkV6) 1108 { 1109 return FormatAddressV6(localEP.Address, localEP.Port); 1110 } 1111 else 1112 { 1113 throw new InternalException(); 1114 } 1115 } 1116 catch (Exception e) 1117 { 1118 throw GenerateException(SR.net_ftp_protocolerror, WebExceptionStatus.ProtocolError, e); // could not open data connection 1119 } 1120 } 1121 1122 /// <summary> 1123 /// <para>Formats a simple FTP command + parameter in correct pre-wire format</para> 1124 /// </summary> FormatFtpCommand(string command, string parameter)1125 private string FormatFtpCommand(string command, string parameter) 1126 { 1127 StringBuilder stringBuilder = new StringBuilder(command.Length + ((parameter != null) ? parameter.Length : 0) + 3 /*size of ' ' \r\n*/); 1128 stringBuilder.Append(command); 1129 if (!string.IsNullOrEmpty(parameter)) 1130 { 1131 stringBuilder.Append(' '); 1132 stringBuilder.Append(parameter); 1133 } 1134 stringBuilder.Append("\r\n"); 1135 return stringBuilder.ToString(); 1136 } 1137 1138 /// <summary> 1139 /// <para> 1140 /// This will handle either connecting to a port or listening for one 1141 /// </para> 1142 /// </summary> CreateFtpDataSocket(FtpWebRequest request, Socket templateSocket)1143 protected Socket CreateFtpDataSocket(FtpWebRequest request, Socket templateSocket) 1144 { 1145 // Safe to be called under an Assert. 1146 Socket socket = new Socket(templateSocket.AddressFamily, templateSocket.SocketType, templateSocket.ProtocolType); 1147 return socket; 1148 } 1149 CheckValid(ResponseDescription response, ref int validThrough, ref int completeLength)1150 protected override bool CheckValid(ResponseDescription response, ref int validThrough, ref int completeLength) 1151 { 1152 if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"CheckValid({response.StatusBuffer})"); 1153 1154 // If the response is less than 4 bytes long, it is too short to tell, so return true, valid so far. 1155 if (response.StatusBuffer.Length < 4) 1156 { 1157 return true; 1158 } 1159 string responseString = response.StatusBuffer.ToString(); 1160 1161 // Otherwise, if there is no status code for this response yet, get one. 1162 if (response.Status == ResponseDescription.NoStatus) 1163 { 1164 // If the response does not start with three digits, then it is not a valid response from an FTP server. 1165 if (!(Char.IsDigit(responseString[0]) && Char.IsDigit(responseString[1]) && Char.IsDigit(responseString[2]) && (responseString[3] == ' ' || responseString[3] == '-'))) 1166 { 1167 return false; 1168 } 1169 else 1170 { 1171 response.StatusCodeString = responseString.Substring(0, 3); 1172 response.Status = Convert.ToInt16(response.StatusCodeString, NumberFormatInfo.InvariantInfo); 1173 } 1174 1175 // IF a hyphen follows the status code on the first line of the response, then we have a multiline response coming. 1176 if (responseString[3] == '-') 1177 { 1178 response.Multiline = true; 1179 } 1180 } 1181 1182 // If a complete line of response has been received from the server, then see if the 1183 // overall response is complete. 1184 // If this was not a multiline response, then the response is complete at the end of the line. 1185 1186 // If this was a multiline response (indicated by three digits followed by a '-' in the first line), 1187 // then we see if the last line received started with the same three digits followed by a space. 1188 // If it did, then this is the sign of a complete multiline response. 1189 // If the line contained three other digits followed by the response, then this is a violation of the 1190 // FTP protocol for multiline responses. 1191 // All other cases indicate that the response is not yet complete. 1192 int index = 0; 1193 while ((index = responseString.IndexOf("\r\n", validThrough)) != -1) // gets the end line. 1194 { 1195 int lineStart = validThrough; 1196 validThrough = index + 2; // validThrough now marks the end of the line being examined. 1197 if (!response.Multiline) 1198 { 1199 completeLength = validThrough; 1200 return true; 1201 } 1202 1203 if (responseString.Length > lineStart + 4) 1204 { 1205 // If the first three characters of the response line currently being examined 1206 // match the status code, then if they are followed by a space, then we 1207 // have reached the end of the reply. 1208 if (responseString.Substring(lineStart, 3) == response.StatusCodeString) 1209 { 1210 if (responseString[lineStart + 3] == ' ') 1211 { 1212 completeLength = validThrough; 1213 return true; 1214 } 1215 } 1216 } 1217 } 1218 return true; 1219 } 1220 1221 /// <summary> 1222 /// <para>Determines whether the stream we return is Writeable or Readable</para> 1223 /// </summary> IsFtpDataStreamWriteable()1224 private TriState IsFtpDataStreamWriteable() 1225 { 1226 FtpWebRequest request = _request as FtpWebRequest; 1227 if (request != null) 1228 { 1229 if (request.MethodInfo.IsUpload) 1230 { 1231 return TriState.True; 1232 } 1233 else if (request.MethodInfo.IsDownload) 1234 { 1235 return TriState.False; 1236 } 1237 } 1238 return TriState.Unspecified; 1239 } 1240 } // class FtpControlStream 1241 } // namespace System.Net 1242 1243