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