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