[System] Fixes UdpClient.Receive with IPv6 endpoint
[mono.git] / mcs / class / System / System.Net / FtpWebRequest.cs
1 //
2 // System.Net.FtpWebRequest.cs
3 //
4 // Authors:
5 //      Carlos Alberto Cortez (calberto.cortez@gmail.com)
6 //
7 // (c) Copyright 2006 Novell, Inc. (http://www.novell.com)
8 //
9
10 using System;
11 using System.IO;
12 using System.Net.Sockets;
13 using System.Text;
14 using System.Threading;
15 using System.Net.Cache;
16 using System.Security.Cryptography.X509Certificates;
17 using System.Net;
18 using System.Net.Security;
19 using System.Security.Authentication;
20
21 namespace System.Net
22 {
23         public sealed class FtpWebRequest : WebRequest
24         {
25                 Uri requestUri;
26                 string file_name; // By now, used for upload
27                 ServicePoint servicePoint;
28                 Stream origDataStream;
29                 Stream dataStream;
30                 Stream controlStream;
31                 StreamReader controlReader;
32                 NetworkCredential credentials;
33                 IPHostEntry hostEntry;
34                 IPEndPoint localEndPoint;
35                 IWebProxy proxy;
36                 int timeout = 100000;
37                 int rwTimeout = 300000;
38                 long offset = 0;
39                 bool binary = true;
40                 bool enableSsl = false;
41                 bool usePassive = true;
42                 bool keepAlive = false;
43                 string method = WebRequestMethods.Ftp.DownloadFile;
44                 string renameTo;
45                 object locker = new object ();
46                 
47                 RequestState requestState = RequestState.Before;
48                 FtpAsyncResult asyncResult;
49                 FtpWebResponse ftpResponse;
50                 Stream requestStream;
51                 string initial_path;
52
53                 const string ChangeDir = "CWD";
54                 const string UserCommand = "USER";
55                 const string PasswordCommand = "PASS";
56                 const string TypeCommand = "TYPE";
57                 const string PassiveCommand = "PASV";
58                 const string PortCommand = "PORT";
59                 const string AbortCommand = "ABOR";
60                 const string AuthCommand = "AUTH";
61                 const string RestCommand = "REST";
62                 const string RenameFromCommand = "RNFR";
63                 const string RenameToCommand = "RNTO";
64                 const string QuitCommand = "QUIT";
65                 const string EOL = "\r\n"; // Special end of line
66
67                 enum RequestState
68                 {
69                         Before,
70                         Scheduled,
71                         Connecting,
72                         Authenticating,
73                         OpeningData,
74                         TransferInProgress,
75                         Finished,
76                         Aborted,
77                         Error
78                 }
79
80                 // sorted commands
81                 static readonly string [] supportedCommands = new string [] {
82                         WebRequestMethods.Ftp.AppendFile, // APPE
83                         WebRequestMethods.Ftp.DeleteFile, // DELE
84                         WebRequestMethods.Ftp.ListDirectoryDetails, // LIST
85                         WebRequestMethods.Ftp.GetDateTimestamp, // MDTM
86                         WebRequestMethods.Ftp.MakeDirectory, // MKD
87                         WebRequestMethods.Ftp.ListDirectory, // NLST
88                         WebRequestMethods.Ftp.PrintWorkingDirectory, // PWD
89                         WebRequestMethods.Ftp.Rename, // RENAME
90                         WebRequestMethods.Ftp.DownloadFile, // RETR
91                         WebRequestMethods.Ftp.RemoveDirectory, // RMD
92                         WebRequestMethods.Ftp.GetFileSize, // SIZE
93                         WebRequestMethods.Ftp.UploadFile, // STOR
94                         WebRequestMethods.Ftp.UploadFileWithUniqueName // STUR
95                         };
96
97                 Encoding dataEncoding = Encoding.UTF8;
98
99                 internal FtpWebRequest (Uri uri) 
100                 {
101                         this.requestUri = uri;
102                         this.proxy = GlobalProxySelection.Select;
103                 }
104
105                 static Exception GetMustImplement ()
106                 {
107                         return new NotImplementedException ();
108                 }
109                 
110                 [MonoTODO]
111                 public X509CertificateCollection ClientCertificates
112                 {
113                         get {
114                                 throw GetMustImplement ();
115                         }
116                         set {
117                                 throw GetMustImplement ();
118                         }
119                 }
120                 
121                 [MonoTODO]
122                 public override string ConnectionGroupName
123                 {
124                         get {
125                                 throw GetMustImplement ();
126                         }
127                         set {
128                                 throw GetMustImplement ();
129                         }
130                 }
131
132                 public override string ContentType {
133                         get {
134                                 throw new NotSupportedException ();
135                         }
136                         set {
137                                 throw new NotSupportedException ();
138                         }
139                 }
140
141                 public override long ContentLength {
142                         get {
143                                 return 0;
144                         } 
145                         set {
146                                 // DO nothing
147                         }
148                 }
149
150                 public long ContentOffset {
151                         get {
152                                 return offset;
153                         }
154                         set {
155                                 CheckRequestStarted ();
156                                 if (value < 0)
157                                         throw new ArgumentOutOfRangeException ();
158
159                                 offset = value;
160                         }
161                 }
162
163                 public override ICredentials Credentials {
164                         get {
165                                 return credentials;
166                         }
167                         set {
168                                 CheckRequestStarted ();
169                                 if (value == null)
170                                         throw new ArgumentNullException ();
171                                 if (!(value is NetworkCredential))
172                                         throw new ArgumentException ();
173
174                                 credentials = value as NetworkCredential;
175                         }
176                 }
177
178 #if !NET_2_1
179                 [MonoTODO]
180                 public static new RequestCachePolicy DefaultCachePolicy
181                 {
182                         get {
183                                 throw GetMustImplement ();
184                         }
185                         set {
186                                 throw GetMustImplement ();
187                         }
188                 }
189 #endif
190
191                 public bool EnableSsl {
192                         get {
193                                 return enableSsl;
194                         }
195                         set {
196                                 CheckRequestStarted ();
197                                 enableSsl = value;
198                         }
199                 }
200
201                 [MonoTODO]
202                 public override WebHeaderCollection Headers
203                 {
204                         get {
205                                 throw GetMustImplement ();
206                         }
207                         set {
208                                 throw GetMustImplement ();
209                         }
210                 }
211
212                 [MonoTODO ("We don't support KeepAlive = true")]
213                 public bool KeepAlive {
214                         get {
215                                 return keepAlive;
216                         }
217                         set {
218                                 CheckRequestStarted ();
219                                 //keepAlive = value;
220                         }
221                 }
222
223                 public override string Method {
224                         get {
225                                 return method;
226                         }
227                         set {
228                                 CheckRequestStarted ();
229                                 if (value == null)
230                                         throw new ArgumentNullException ("Method string cannot be null");
231
232                                 if (value.Length == 0 || Array.BinarySearch (supportedCommands, value) < 0)
233                                         throw new ArgumentException ("Method not supported", "value");
234                                 
235                                 method = value;
236                         }
237                 }
238
239                 public override bool PreAuthenticate {
240                         get {
241                                 throw new NotSupportedException ();
242                         }
243                         set {
244                                 throw new NotSupportedException ();
245                         }
246                 }
247
248                 public override IWebProxy Proxy {
249                         get {
250                                 return proxy;
251                         }
252                         set {
253                                 CheckRequestStarted ();
254                                 proxy = value;
255                         }
256                 }
257
258                 public int ReadWriteTimeout {
259                         get {
260                                 return rwTimeout;
261                         }
262                         set {
263                                 CheckRequestStarted ();
264
265                                 if (value < - 1)
266                                         throw new ArgumentOutOfRangeException ();
267                                 else
268                                         rwTimeout = value;
269                         }
270                 }
271
272                 public string RenameTo {
273                         get {
274                                 return renameTo;
275                         }
276                         set {
277                                 CheckRequestStarted ();
278                                 if (value == null || value.Length == 0)
279                                         throw new ArgumentException ("RenameTo value can't be null or empty", "RenameTo");
280
281                                 renameTo = value;
282                         }
283                 }
284
285                 public override Uri RequestUri {
286                         get {
287                                 return requestUri;
288                         }
289                 }
290
291                 public ServicePoint ServicePoint {
292                         get {
293                                 return GetServicePoint ();
294                         }
295                 }
296
297                 public bool UsePassive {
298                         get {
299                                 return usePassive;
300                         }
301                         set {
302                                 CheckRequestStarted ();
303                                 usePassive = value;
304                         }
305                 }
306
307                 [MonoTODO]
308                 public override bool UseDefaultCredentials
309                 {
310                         get {
311                                 throw GetMustImplement ();
312                         }
313                         set {
314                                 throw GetMustImplement ();
315                         }
316                 }
317                 
318                 public bool UseBinary {
319                         get {
320                                 return binary;
321                         } set {
322                                 CheckRequestStarted ();
323                                 binary = value;
324                         }
325                 }
326
327                 public override int Timeout {
328                         get {
329                                 return timeout;
330                         }
331                         set {
332                                 CheckRequestStarted ();
333
334                                 if (value < -1)
335                                         throw new ArgumentOutOfRangeException ();
336                                 else
337                                         timeout = value;
338                         }
339                 }
340
341                 string DataType {
342                         get {
343                                 return binary ? "I" : "A";
344                         }
345                 }
346
347                 RequestState State {
348                         get {
349                                 lock (locker) {
350                                         return requestState;
351                                 }
352                         }
353
354                         set {
355                                 lock (locker) {
356                                         CheckIfAborted ();
357                                         CheckFinalState ();
358                                         requestState = value;
359                                 }
360                         }
361                 }
362
363                 public override void Abort () {
364                         lock (locker) {
365                                 if (State == RequestState.TransferInProgress) {
366                                         /*FtpStatus status = */
367                                         SendCommand (false, AbortCommand);
368                                 }
369
370                                 if (!InFinalState ()) {
371                                         State = RequestState.Aborted;
372                                         ftpResponse = new FtpWebResponse (this, requestUri, method, FtpStatusCode.FileActionAborted, "Aborted by request");
373                                 }
374                         }
375                 }
376
377                 public override IAsyncResult BeginGetResponse (AsyncCallback callback, object state) {
378                         if (asyncResult != null && !asyncResult.IsCompleted) {
379                                 throw new InvalidOperationException ("Cannot re-call BeginGetRequestStream/BeginGetResponse while a previous call is still in progress");
380                         }
381
382                         CheckIfAborted ();
383                         
384                         asyncResult = new FtpAsyncResult (callback, state);
385
386                         lock (locker) {
387                                 if (InFinalState ())
388                                         asyncResult.SetCompleted (true, ftpResponse);
389                                 else {
390                                         if (State == RequestState.Before)
391                                                 State = RequestState.Scheduled;
392
393                                         Thread thread = new Thread (ProcessRequest);
394                                         thread.Start ();
395                                 }
396                         }
397
398                         return asyncResult;
399                 }
400
401                 public override WebResponse EndGetResponse (IAsyncResult asyncResult) {
402                         if (asyncResult == null)
403                                 throw new ArgumentNullException ("AsyncResult cannot be null!");
404
405                         if (!(asyncResult is FtpAsyncResult) || asyncResult != this.asyncResult)
406                                 throw new ArgumentException ("AsyncResult is from another request!");
407
408                         FtpAsyncResult asyncFtpResult = (FtpAsyncResult) asyncResult;
409                         if (!asyncFtpResult.WaitUntilComplete (timeout, false)) {
410                                 Abort ();
411                                 throw new WebException ("Transfer timed out.", WebExceptionStatus.Timeout);
412                         }
413
414                         CheckIfAborted ();
415
416                         asyncResult = null;
417
418                         if (asyncFtpResult.GotException)
419                                 throw asyncFtpResult.Exception;
420
421                         return asyncFtpResult.Response;
422                 }
423
424                 public override WebResponse GetResponse () {
425                         IAsyncResult asyncResult = BeginGetResponse (null, null);
426                         return EndGetResponse (asyncResult);
427                 }
428
429                 public override IAsyncResult BeginGetRequestStream (AsyncCallback callback, object state) {
430                         if (method != WebRequestMethods.Ftp.UploadFile && method != WebRequestMethods.Ftp.UploadFileWithUniqueName &&
431                                         method != WebRequestMethods.Ftp.AppendFile)
432                                 throw new ProtocolViolationException ();
433
434                         lock (locker) {
435                                 CheckIfAborted ();
436
437                                 if (State != RequestState.Before)
438                                         throw new InvalidOperationException ("Cannot re-call BeginGetRequestStream/BeginGetResponse while a previous call is still in progress");
439
440                                 State = RequestState.Scheduled;
441                         }
442
443                         asyncResult = new FtpAsyncResult (callback, state);
444                         Thread thread = new Thread (ProcessRequest);
445                         thread.Start ();
446
447                         return asyncResult;
448                 }
449
450                 public override Stream EndGetRequestStream (IAsyncResult asyncResult) {
451                         if (asyncResult == null)
452                                 throw new ArgumentNullException ("asyncResult");
453
454                         if (!(asyncResult is FtpAsyncResult))
455                                 throw new ArgumentException ("asyncResult");
456
457                         if (State == RequestState.Aborted) {
458                                 throw new WebException ("Request aborted", WebExceptionStatus.RequestCanceled);
459                         }
460                         
461                         if (asyncResult != this.asyncResult)
462                                 throw new ArgumentException ("AsyncResult is from another request!");
463
464                         FtpAsyncResult res = (FtpAsyncResult) asyncResult;
465
466                         if (!res.WaitUntilComplete (timeout, false)) {
467                                 Abort ();
468                                 throw new WebException ("Request timed out");
469                         }
470
471                         if (res.GotException)
472                                 throw res.Exception;
473
474                         return res.Stream;
475                 }
476
477                 public override Stream GetRequestStream () {
478                         IAsyncResult asyncResult = BeginGetRequestStream (null, null);
479                         return EndGetRequestStream (asyncResult);
480                 }
481                 
482                 ServicePoint GetServicePoint ()
483                 {
484                         if (servicePoint == null)
485                                 servicePoint = ServicePointManager.FindServicePoint (requestUri, proxy);
486
487                         return servicePoint;
488                 }
489
490                 // Probably move some code of command connection here
491                 void ResolveHost ()
492                 {
493                         CheckIfAborted ();
494                         hostEntry = GetServicePoint ().HostEntry;
495
496                         if (hostEntry == null) {
497                                 ftpResponse.UpdateStatus (new FtpStatus(FtpStatusCode.ActionAbortedLocalProcessingError, "Cannot resolve server name"));
498                                 throw new WebException ("The remote server name could not be resolved: " + requestUri,
499                                         null, WebExceptionStatus.NameResolutionFailure, ftpResponse);
500                         }
501                 }
502
503                 void ProcessRequest () {
504
505                         if (State == RequestState.Scheduled) {
506                                 ftpResponse = new FtpWebResponse (this, requestUri, method, keepAlive);
507
508                                 try {
509                                         ProcessMethod ();
510                                         //State = RequestState.Finished;
511                                         //finalResponse = ftpResponse;
512                                         asyncResult.SetCompleted (false, ftpResponse);
513                                 }
514                                 catch (Exception e) {
515                                         if (!GetServicePoint ().UsesProxy)
516                                                 State = RequestState.Error;
517                                         SetCompleteWithError (e);
518                                 }
519                         }
520                         else {
521                                 if (InProgress ()) {
522                                         FtpStatus status = GetResponseStatus ();
523
524                                         ftpResponse.UpdateStatus (status);
525
526                                         if (ftpResponse.IsFinal ()) {
527                                                 State = RequestState.Finished;
528                                         }
529                                 }
530
531                                 asyncResult.SetCompleted (false, ftpResponse);
532                         }
533                 }
534
535                 void SetType ()
536                 {
537                         if (binary) {
538                                 FtpStatus status = SendCommand (TypeCommand, DataType);
539                                 if ((int) status.StatusCode < 200 || (int) status.StatusCode >= 300)
540                                         throw CreateExceptionFromResponse (status);
541                         }
542                 }
543
544                 string GetRemoteFolderPath (Uri uri)
545                 {
546                         string result;
547                         string local_path = Uri.UnescapeDataString (uri.LocalPath);
548                         if (initial_path == null || initial_path == "/") {
549                                 result = local_path;
550                         } else {
551                                 if (local_path [0] == '/')
552                                         local_path = local_path.Substring (1);
553
554                                 UriBuilder initialBuilder = new UriBuilder () {
555                                         Scheme  = "ftp",
556                                         Host    = "dummy-host",
557                                         Path    = initial_path,
558                                 };
559                                 Uri initial = initialBuilder.Uri;
560                                 result = new Uri (initial, local_path).LocalPath;
561                         }
562
563                         int last = result.LastIndexOf ('/');
564                         if (last == -1)
565                                 return null;
566
567                         return result.Substring (0, last + 1);
568                 }
569
570                 void CWDAndSetFileName (Uri uri)
571                 {
572                         string remote_folder = GetRemoteFolderPath (uri);
573                         FtpStatus status;
574                         if (remote_folder != null) {
575                                 status = SendCommand (ChangeDir, remote_folder);
576                                 if ((int) status.StatusCode < 200 || (int) status.StatusCode >= 300)
577                                         throw CreateExceptionFromResponse (status);
578
579                                 int last = uri.LocalPath.LastIndexOf ('/');
580                                 if (last >= 0) {
581                                         file_name = Uri.UnescapeDataString (uri.LocalPath.Substring (last + 1));
582                                 }
583                         }
584                 }
585
586                 void ProcessMethod ()
587                 {
588                         ServicePoint sp = GetServicePoint ();
589                         if (sp.UsesProxy) {
590                                 if (method != WebRequestMethods.Ftp.DownloadFile)
591                                         throw new NotSupportedException ("FTP+proxy only supports RETR");
592
593                                 HttpWebRequest req = (HttpWebRequest) WebRequest.Create (proxy.GetProxy (requestUri));
594                                 req.Address = requestUri;
595                                 requestState = RequestState.Finished;
596                                 WebResponse response = req.GetResponse ();
597                                 ftpResponse.Stream = new FtpDataStream (this, response.GetResponseStream (), true);
598                                 ftpResponse.StatusCode = FtpStatusCode.CommandOK;
599                                 return;
600                         }
601                         State = RequestState.Connecting;
602
603                         ResolveHost ();
604
605                         OpenControlConnection ();
606                         CWDAndSetFileName (requestUri);
607                         SetType ();
608
609                         switch (method) {
610                         // Open data connection and receive data
611                         case WebRequestMethods.Ftp.DownloadFile:
612                         case WebRequestMethods.Ftp.ListDirectory:
613                         case WebRequestMethods.Ftp.ListDirectoryDetails:
614                                 DownloadData ();
615                                 break;
616                         // Open data connection and send data
617                         case WebRequestMethods.Ftp.AppendFile:
618                         case WebRequestMethods.Ftp.UploadFile:
619                         case WebRequestMethods.Ftp.UploadFileWithUniqueName:
620                                 UploadData ();
621                                 break;
622                         // Get info from control connection
623                         case WebRequestMethods.Ftp.GetFileSize:
624                         case WebRequestMethods.Ftp.GetDateTimestamp:
625                         case WebRequestMethods.Ftp.PrintWorkingDirectory:
626                         case WebRequestMethods.Ftp.MakeDirectory:
627                         case WebRequestMethods.Ftp.Rename:
628                         case WebRequestMethods.Ftp.DeleteFile:
629                                 ProcessSimpleMethod ();
630                                 break;
631                         default: // What to do here?
632                                 throw new Exception (String.Format ("Support for command {0} not implemented yet", method));
633                         }
634
635                         CheckIfAborted ();
636                 }
637
638                 private void CloseControlConnection () {
639                         if (controlStream != null) {
640                                 SendCommand (QuitCommand);
641                                 controlStream.Close ();
642                                 controlStream = null;
643                         }
644                 }
645
646                 internal void CloseDataConnection () {
647                         if(origDataStream != null) {
648                                 origDataStream.Close ();
649                                 origDataStream = null;
650                         }
651                 }
652
653                 private void CloseConnection () {
654                         CloseControlConnection ();
655                         CloseDataConnection ();
656                 }
657                 
658                 void ProcessSimpleMethod ()
659                 {
660                         State = RequestState.TransferInProgress;
661                         
662                         FtpStatus status;
663                         
664                         if (method == WebRequestMethods.Ftp.PrintWorkingDirectory)
665                                 method = "PWD";
666
667                         if (method == WebRequestMethods.Ftp.Rename)
668                                 method = RenameFromCommand;
669                         
670                         status = SendCommand (method, file_name);
671
672                         ftpResponse.Stream = Stream.Null;
673                         
674                         string desc = status.StatusDescription;
675
676                         switch (method) {
677                         case WebRequestMethods.Ftp.GetFileSize: {
678                                         if (status.StatusCode != FtpStatusCode.FileStatus)
679                                                 throw CreateExceptionFromResponse (status);
680
681                                         int i, len;
682                                         long size;
683                                         for (i = 4, len = 0; i < desc.Length && Char.IsDigit (desc [i]); i++, len++)
684                                                 ;
685
686                                         if (len == 0)
687                                                 throw new WebException ("Bad format for server response in " + method);
688
689                                         if (!Int64.TryParse (desc.Substring (4, len), out size))
690                                                 throw new WebException ("Bad format for server response in " + method);
691
692                                         ftpResponse.contentLength = size;
693                                 }
694                                 break;
695                         case WebRequestMethods.Ftp.GetDateTimestamp:
696                                 if (status.StatusCode != FtpStatusCode.FileStatus)
697                                         throw CreateExceptionFromResponse (status);
698                                 ftpResponse.LastModified = DateTime.ParseExact (desc.Substring (4), "yyyyMMddHHmmss", null);
699                                 break;
700                         case WebRequestMethods.Ftp.MakeDirectory:
701                                 if (status.StatusCode != FtpStatusCode.PathnameCreated)
702                                         throw CreateExceptionFromResponse (status);
703                                 break;
704                         case ChangeDir:
705                                 method = WebRequestMethods.Ftp.PrintWorkingDirectory;
706
707                                 if (status.StatusCode != FtpStatusCode.FileActionOK)
708                                         throw CreateExceptionFromResponse (status);
709
710                                 status = SendCommand (method);
711
712                                 if (status.StatusCode != FtpStatusCode.PathnameCreated)
713                                         throw CreateExceptionFromResponse (status);
714                                 break;
715                         case RenameFromCommand:
716                                 method = WebRequestMethods.Ftp.Rename;
717                                 if (status.StatusCode != FtpStatusCode.FileCommandPending) 
718                                         throw CreateExceptionFromResponse (status);
719                                 // Pass an empty string if RenameTo wasn't specified
720                                 status = SendCommand (RenameToCommand, renameTo != null ? renameTo : String.Empty);
721                                 if (status.StatusCode != FtpStatusCode.FileActionOK)
722                                         throw CreateExceptionFromResponse (status);
723                                 break;
724                         case WebRequestMethods.Ftp.DeleteFile:
725                                 if (status.StatusCode != FtpStatusCode.FileActionOK)  {
726                                         throw CreateExceptionFromResponse (status);
727                                 }
728                                 break;
729                         }
730
731                         State = RequestState.Finished;
732                 }
733
734                 void UploadData ()
735                 {
736                         State = RequestState.OpeningData;
737
738                         OpenDataConnection ();
739
740                         State = RequestState.TransferInProgress;
741                         requestStream = new FtpDataStream (this, dataStream, false);
742                         asyncResult.Stream = requestStream;
743                 }
744
745                 void DownloadData ()
746                 {
747                         State = RequestState.OpeningData;
748
749                         OpenDataConnection ();
750
751                         State = RequestState.TransferInProgress;
752                         ftpResponse.Stream = new FtpDataStream (this, dataStream, true);
753                 }
754
755                 void CheckRequestStarted ()
756                 {
757                         if (State != RequestState.Before)
758                                 throw new InvalidOperationException ("There is a request currently in progress");
759                 }
760
761                 void OpenControlConnection ()
762                 {
763                         Exception exception = null;
764                         Socket sock = null;
765                         foreach (IPAddress address in hostEntry.AddressList) {
766                                 sock = new Socket (address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
767
768                                 IPEndPoint remote = new IPEndPoint (address, requestUri.Port);
769
770                                 if (!ServicePoint.CallEndPointDelegate (sock, remote)) {
771                                         sock.Close ();
772                                         sock = null;
773                                 } else {
774                                         try {
775                                                 sock.Connect (remote);
776                                                 localEndPoint = (IPEndPoint) sock.LocalEndPoint;
777                                                 break;
778                                         } catch (SocketException exc) {
779                                                 exception = exc;
780                                                 sock.Close ();
781                                                 sock = null;
782                                         }
783                                 }
784                         }
785
786                         // Couldn't connect to any address
787                         if (sock == null)
788                                 throw new WebException ("Unable to connect to remote server", exception,
789                                                 WebExceptionStatus.UnknownError, ftpResponse);
790
791                         controlStream = new NetworkStream (sock);
792                         controlReader = new StreamReader (controlStream, Encoding.ASCII);
793
794                         State = RequestState.Authenticating;
795
796                         Authenticate ();
797                         FtpStatus status = SendCommand ("OPTS", "utf8", "on");
798                         if ((int)status.StatusCode < 200 || (int)status.StatusCode > 300)
799                                 dataEncoding = Encoding.Default;
800                         else
801                                 dataEncoding = Encoding.UTF8;
802
803                         status = SendCommand (WebRequestMethods.Ftp.PrintWorkingDirectory);
804                         initial_path = GetInitialPath (status);
805                 }
806
807                 static string GetInitialPath (FtpStatus status)
808                 {
809                         int s = (int) status.StatusCode;
810                         if (s < 200 || s > 300 || status.StatusDescription.Length <= 4)
811                                 throw new WebException ("Error getting current directory: " + status.StatusDescription, null,
812                                                 WebExceptionStatus.UnknownError, null);
813
814                         string msg = status.StatusDescription.Substring (4);
815                         if (msg [0] == '"') {
816                                 int next_quote = msg.IndexOf ('\"', 1);
817                                 if (next_quote == -1)
818                                         throw new WebException ("Error getting current directory: PWD -> " + status.StatusDescription, null,
819                                                                 WebExceptionStatus.UnknownError, null);
820
821                                 msg = msg.Substring (1, next_quote - 1);
822                         }
823
824                         if (!msg.EndsWith ("/"))
825                                 msg += "/";
826                         return msg;
827                 }
828
829                 // Probably we could do better having here a regex
830                 Socket SetupPassiveConnection (string statusDescription)
831                 {
832                         // Current response string
833                         string response = statusDescription;
834                         if (response.Length < 4)
835                                 throw new WebException ("Cannot open passive data connection");
836                         
837                         // Look for first digit after code
838                         int i;
839                         for (i = 3; i < response.Length && !Char.IsDigit (response [i]); i++)
840                                 ;
841                         if (i >= response.Length)
842                                 throw new WebException ("Cannot open passive data connection");
843
844                         // Get six elements
845                         string [] digits = response.Substring (i).Split (new char [] {','}, 6);
846                         if (digits.Length != 6)
847                                 throw new WebException ("Cannot open passive data connection");
848
849                         // Clean non-digits at the end of last element
850                         int j;
851                         for (j = digits [5].Length - 1; j >= 0 && !Char.IsDigit (digits [5][j]); j--)
852                                 ;
853                         if (j < 0)
854                                 throw new WebException ("Cannot open passive data connection");
855                         
856                         digits [5] = digits [5].Substring (0, j + 1);
857
858                         IPAddress ip;
859                         try {
860                                 ip = IPAddress.Parse (String.Join (".", digits, 0, 4));
861                         } catch (FormatException) {
862                                 throw new WebException ("Cannot open passive data connection");
863                         }
864
865                         // Get the port
866                         int p1, p2, port;
867                         if (!Int32.TryParse (digits [4], out p1) || !Int32.TryParse (digits [5], out p2))
868                                 throw new WebException ("Cannot open passive data connection");
869
870                         port = (p1 << 8) + p2; // p1 * 256 + p2
871                         //port = p1 * 256 + p2;
872                         if (port < IPEndPoint.MinPort || port > IPEndPoint.MaxPort)
873                                 throw new WebException ("Cannot open passive data connection");
874
875                         IPEndPoint ep = new IPEndPoint (ip, port);
876                         Socket sock = new Socket (ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
877                         try {
878                                 sock.Connect (ep);
879                         } catch (SocketException) {
880                                 sock.Close ();
881                                 throw new WebException ("Cannot open passive data connection");
882                         }
883
884                         return sock;
885                 }
886
887                 Exception CreateExceptionFromResponse (FtpStatus status)
888                 {
889                         FtpWebResponse ftpResponse = new FtpWebResponse (this, requestUri, method, status);
890                         
891                         WebException exc = new WebException ("Server returned an error: " + status.StatusDescription, 
892                                 null, WebExceptionStatus.ProtocolError, ftpResponse);
893                         return exc;
894                 }
895                 
896                 // Here we could also get a server error, so be cautious
897                 internal void SetTransferCompleted ()
898                 {
899                         if (InFinalState ())
900                                 return;
901
902                         State = RequestState.Finished;
903                         FtpStatus status = GetResponseStatus ();
904                         ftpResponse.UpdateStatus (status);
905                         if(!keepAlive)
906                                 CloseConnection ();
907                 }
908
909                 internal void OperationCompleted ()
910                 {
911                         if(!keepAlive)
912                                 CloseConnection ();
913                 }
914
915                 void SetCompleteWithError (Exception exc)
916                 {
917                         if (asyncResult != null) {
918                                 asyncResult.SetCompleted (false, exc);
919                         }
920                 }
921
922                 Socket InitDataConnection ()
923                 {
924                         FtpStatus status;
925                         
926                         if (usePassive) {
927                                 status = SendCommand (PassiveCommand);
928                                 if (status.StatusCode != FtpStatusCode.EnteringPassive) {
929                                         throw CreateExceptionFromResponse (status);
930                                 }
931                                 
932                                 return SetupPassiveConnection (status.StatusDescription);
933                         }
934
935                         // Open a socket to listen the server's connection
936                         Socket sock = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
937                         try {
938                                 sock.Bind (new IPEndPoint (localEndPoint.Address, 0));
939                                 sock.Listen (1); // We only expect a connection from server
940
941                         } catch (SocketException e) {
942                                 sock.Close ();
943
944                                 throw new WebException ("Couldn't open listening socket on client", e);
945                         }
946
947                         IPEndPoint ep = (IPEndPoint) sock.LocalEndPoint;
948                         string ipString = ep.Address.ToString ().Replace ('.', ',');
949                         int h1 = ep.Port >> 8; // ep.Port / 256
950                         int h2 = ep.Port % 256;
951
952                         string portParam = ipString + "," + h1 + "," + h2;
953                         status = SendCommand (PortCommand, portParam);
954                         
955                         if (status.StatusCode != FtpStatusCode.CommandOK) {
956                                 sock.Close ();
957                                 throw (CreateExceptionFromResponse (status));
958                         }
959
960                         return sock;
961                 }
962
963                 void OpenDataConnection ()
964                 {
965                         FtpStatus status;
966                         
967                         Socket s = InitDataConnection ();
968
969                         // Handle content offset
970                         if (offset > 0) {
971                                 status = SendCommand (RestCommand, offset.ToString ());
972                                 if (status.StatusCode != FtpStatusCode.FileCommandPending)
973                                         throw CreateExceptionFromResponse (status);
974                         }
975
976                         if (method != WebRequestMethods.Ftp.ListDirectory && method != WebRequestMethods.Ftp.ListDirectoryDetails &&
977                             method != WebRequestMethods.Ftp.UploadFileWithUniqueName) {
978                                 status = SendCommand (method, file_name);
979                         } else {
980                                 status = SendCommand (method);
981                         }
982
983                         if (status.StatusCode != FtpStatusCode.OpeningData && status.StatusCode != FtpStatusCode.DataAlreadyOpen)
984                                 throw CreateExceptionFromResponse (status);
985
986                         if (usePassive) {
987                                 origDataStream = new NetworkStream (s, true);
988                                 dataStream = origDataStream;
989                                 if (EnableSsl)
990                                         ChangeToSSLSocket (ref dataStream);
991                         }
992                         else {
993
994                                 // Active connection (use Socket.Blocking to true)
995                                 Socket incoming = null;
996                                 try {
997                                         incoming = s.Accept ();
998                                 }
999                                 catch (SocketException) {
1000                                         s.Close ();
1001                                         if (incoming != null)
1002                                                 incoming.Close ();
1003
1004                                         throw new ProtocolViolationException ("Server commited a protocol violation.");
1005                                 }
1006
1007                                 s.Close ();
1008                                 origDataStream = new NetworkStream (incoming, true);
1009                                 dataStream = origDataStream;
1010                                 if (EnableSsl)
1011                                         ChangeToSSLSocket (ref dataStream);
1012                         }
1013
1014                         ftpResponse.UpdateStatus (status);
1015                 }
1016
1017                 void Authenticate ()
1018                 {
1019                         string username = null;
1020                         string password = null;
1021                         string domain = null;
1022
1023                         if (credentials != null) {
1024                                 username = credentials.UserName;
1025                                 password = credentials.Password;
1026                                 domain = credentials.Domain;
1027                         }
1028
1029                         if (username == null)
1030                                 username = "anonymous";
1031                         if (password == null)
1032                                 password = "@anonymous";
1033                         if (!string.IsNullOrEmpty (domain))
1034                                 username = domain + '\\' + username;
1035
1036                         // Connect to server and get banner message
1037                         FtpStatus status = GetResponseStatus ();
1038                         ftpResponse.BannerMessage = status.StatusDescription;
1039
1040                         if (EnableSsl) {
1041                                 InitiateSecureConnection (ref controlStream);
1042                                 controlReader = new StreamReader (controlStream, Encoding.ASCII);
1043                                 status = SendCommand ("PBSZ", "0");
1044                                 int st = (int) status.StatusCode;
1045                                 if (st < 200 || st >= 300)
1046                                         throw CreateExceptionFromResponse (status);
1047                                 // TODO: what if "PROT P" is denied by the server? What does MS do?
1048                                 status = SendCommand ("PROT", "P");
1049                                 st = (int) status.StatusCode;
1050                                 if (st < 200 || st >= 300)
1051                                         throw CreateExceptionFromResponse (status);
1052
1053                                 status = new FtpStatus (FtpStatusCode.SendUserCommand, "");
1054                         }
1055                         
1056                         if (status.StatusCode != FtpStatusCode.SendUserCommand)
1057                                 throw CreateExceptionFromResponse (status);
1058
1059                         status = SendCommand (UserCommand, username);
1060
1061                         switch (status.StatusCode) {
1062                         case FtpStatusCode.SendPasswordCommand:
1063                                 status = SendCommand (PasswordCommand, password);
1064                                 if (status.StatusCode != FtpStatusCode.LoggedInProceed)
1065                                         throw CreateExceptionFromResponse (status);
1066                                 break;
1067                         case FtpStatusCode.LoggedInProceed:
1068                                 break;
1069                         default:
1070                                 throw CreateExceptionFromResponse (status);
1071                         }
1072
1073                         ftpResponse.WelcomeMessage = status.StatusDescription;
1074                         ftpResponse.UpdateStatus (status);
1075                 }
1076
1077                 FtpStatus SendCommand (string command, params string [] parameters) {
1078                         return SendCommand (true, command, parameters);
1079                 }
1080
1081                 FtpStatus SendCommand (bool waitResponse, string command, params string [] parameters)
1082                 {
1083                         byte [] cmd;
1084                         string commandString = command;
1085                         if (parameters.Length > 0)
1086                                 commandString += " " + String.Join (" ", parameters);
1087
1088                         commandString += EOL;
1089                         cmd = dataEncoding.GetBytes (commandString);
1090                         try {
1091                                 controlStream.Write (cmd, 0, cmd.Length);
1092                         } catch (IOException) {
1093                                 //controlStream.Close ();
1094                                 return new FtpStatus(FtpStatusCode.ServiceNotAvailable, "Write failed");
1095                         }
1096
1097                         if(!waitResponse)
1098                                 return null;
1099                         
1100                         FtpStatus result = GetResponseStatus ();
1101                         if (ftpResponse != null)
1102                                 ftpResponse.UpdateStatus (result);
1103                         return result;
1104                 }
1105
1106                 internal static FtpStatus ServiceNotAvailable ()
1107                 {
1108                         return new FtpStatus (FtpStatusCode.ServiceNotAvailable, Locale.GetText ("Invalid response from server"));
1109                 }
1110                 
1111                 internal FtpStatus GetResponseStatus ()
1112                 {
1113                         while (true) {
1114                                 string response = null;
1115
1116                                 try {
1117                                         response = controlReader.ReadLine ();
1118                                 } catch (IOException) {
1119                                 }
1120
1121                                 if (response == null || response.Length < 3)
1122                                         return ServiceNotAvailable ();
1123
1124                                 int code;
1125                                 if (!Int32.TryParse (response.Substring (0, 3), out code))
1126                                         return ServiceNotAvailable ();
1127
1128                                 if (response.Length > 3 && response [3] == '-'){
1129                                         string line = null;
1130                                         string find = code.ToString() + ' ';
1131                                         while (true){
1132                                                 line = null;
1133                                                 try {
1134                                                         line = controlReader.ReadLine();
1135                                                 } catch (IOException) {
1136                                                 }
1137                                                 if (line == null)
1138                                                         return ServiceNotAvailable ();
1139                                                 
1140                                                 response += Environment.NewLine + line;
1141
1142                                                 if (line.StartsWith(find, StringComparison.Ordinal))
1143                                                         break;
1144                                         } 
1145                                 }
1146                                 return new FtpStatus ((FtpStatusCode) code, response);
1147                         }
1148                 }
1149
1150                 private void InitiateSecureConnection (ref Stream stream) {
1151                         FtpStatus status = SendCommand (AuthCommand, "TLS");
1152                         if (status.StatusCode != FtpStatusCode.ServerWantsSecureSession)
1153                                 throw CreateExceptionFromResponse (status);
1154
1155                         ChangeToSSLSocket (ref stream);
1156                 }
1157
1158 #if SECURITY_DEP
1159                 RemoteCertificateValidationCallback callback = delegate (object sender,
1160                                                                          X509Certificate certificate,
1161                                                                          X509Chain chain,
1162                                                                          SslPolicyErrors sslPolicyErrors) {
1163                         // honor any exciting callback defined on ServicePointManager
1164                         if (ServicePointManager.ServerCertificateValidationCallback != null)
1165                                 return ServicePointManager.ServerCertificateValidationCallback (sender, certificate, chain, sslPolicyErrors);
1166                         // otherwise provide our own
1167                         if (sslPolicyErrors != SslPolicyErrors.None)
1168                                 throw new InvalidOperationException ("SSL authentication error: " + sslPolicyErrors);
1169                         return true;
1170                         };
1171 #endif
1172
1173                 internal bool ChangeToSSLSocket (ref Stream stream) {
1174 #if   SECURITY_DEP
1175                         SslStream sslStream = new SslStream (stream, true, callback, null);
1176                         //sslStream.AuthenticateAsClient (Host, this.ClientCertificates, SslProtocols.Default, false);
1177                         //TODO: client certificates
1178                         sslStream.AuthenticateAsClient (requestUri.Host, null, SslProtocols.Default, false);
1179                         stream = sslStream;
1180                         return true;
1181 #else
1182                         throw new NotImplementedException ();
1183 #endif
1184                 }
1185                 
1186                 bool InFinalState () {
1187                         return (State == RequestState.Aborted || State == RequestState.Error || State == RequestState.Finished);
1188                 }
1189
1190                 bool InProgress () {
1191                         return (State != RequestState.Before && !InFinalState ());
1192                 }
1193
1194                 internal void CheckIfAborted () {
1195                         if (State == RequestState.Aborted)
1196                                 throw new WebException ("Request aborted", WebExceptionStatus.RequestCanceled);
1197                 }
1198
1199                 void CheckFinalState () {
1200                         if (InFinalState ())
1201                                 throw new InvalidOperationException ("Cannot change final state");
1202                 }
1203         }
1204 }
1205
1206