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