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