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