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