Merge pull request #205 from m3rlinez/master
[mono.git] / mcs / class / System / System.Net / FtpWebRequest.cs
1 //
2 // System.Net.FtpWebRequest.cs
3 //
4 // Authors:
5 //      Carlos Alberto Cortez (calberto.cortez@gmail.com)
6 //
7 // (c) Copyright 2006 Novell, Inc. (http://www.novell.com)
8 //
9 #if NET_2_0
10
11 using System;
12 using System.IO;
13 using System.Net.Sockets;
14 using System.Text;
15 using System.Threading;
16 using System.Net.Cache;
17 using System.Security.Cryptography.X509Certificates;
18 using System.Net;
19 using System.Net.Security;
20 using System.Security.Authentication;
21
22 namespace System.Net
23 {
24         public sealed class FtpWebRequest : WebRequest
25         {
26                 Uri requestUri;
27                 string file_name; // By now, used for upload
28                 ServicePoint servicePoint;
29                 Stream origDataStream;
30                 Stream dataStream;
31                 Stream controlStream;
32                 StreamReader controlReader;
33                 NetworkCredential credentials;
34                 IPHostEntry hostEntry;
35                 IPEndPoint localEndPoint;
36                 IWebProxy proxy;
37                 int timeout = 100000;
38                 int rwTimeout = 300000;
39                 long offset = 0;
40                 bool binary = true;
41                 bool enableSsl = false;
42                 bool usePassive = true;
43                 bool keepAlive = false;
44                 string method = WebRequestMethods.Ftp.DownloadFile;
45                 string renameTo;
46                 object locker = new object ();
47                 
48                 RequestState requestState = RequestState.Before;
49                 FtpAsyncResult asyncResult;
50                 FtpWebResponse ftpResponse;
51                 Stream requestStream;
52                 string initial_path;
53
54                 const string ChangeDir = "CWD";
55                 const string UserCommand = "USER";
56                 const string PasswordCommand = "PASS";
57                 const string TypeCommand = "TYPE";
58                 const string PassiveCommand = "PASV";
59                 const string PortCommand = "PORT";
60                 const string AbortCommand = "ABOR";
61                 const string AuthCommand = "AUTH";
62                 const string RestCommand = "REST";
63                 const string RenameFromCommand = "RNFR";
64                 const string RenameToCommand = "RNTO";
65                 const string QuitCommand = "QUIT";
66                 const string EOL = "\r\n"; // Special end of line
67
68                 enum RequestState
69                 {
70                         Before,
71                         Scheduled,
72                         Connecting,
73                         Authenticating,
74                         OpeningData,
75                         TransferInProgress,
76                         Finished,
77                         Aborted,
78                         Error
79                 }
80
81                 // sorted commands
82                 static readonly string [] supportedCommands = new string [] {
83                         WebRequestMethods.Ftp.AppendFile, // APPE
84                         WebRequestMethods.Ftp.DeleteFile, // DELE
85                         WebRequestMethods.Ftp.ListDirectoryDetails, // LIST
86                         WebRequestMethods.Ftp.GetDateTimestamp, // MDTM
87                         WebRequestMethods.Ftp.MakeDirectory, // MKD
88                         WebRequestMethods.Ftp.ListDirectory, // NLST
89                         WebRequestMethods.Ftp.PrintWorkingDirectory, // PWD
90                         WebRequestMethods.Ftp.Rename, // RENAME
91                         WebRequestMethods.Ftp.DownloadFile, // RETR
92                         WebRequestMethods.Ftp.RemoveDirectory, // RMD
93                         WebRequestMethods.Ftp.GetFileSize, // SIZE
94                         WebRequestMethods.Ftp.UploadFile, // STOR
95                         WebRequestMethods.Ftp.UploadFileWithUniqueName // STUR
96                         };
97
98                 internal FtpWebRequest (Uri uri) 
99                 {
100                         this.requestUri = uri;
101                         this.proxy = GlobalProxySelection.Select;
102                 }
103
104                 static Exception GetMustImplement ()
105                 {
106                         return new NotImplementedException ();
107                 }
108                 
109                 [MonoTODO]
110                 public X509CertificateCollection ClientCertificates
111                 {
112                         get {
113                                 throw GetMustImplement ();
114                         }
115                         set {
116                                 throw GetMustImplement ();
117                         }
118                 }
119                 
120                 [MonoTODO]
121                 public override string ConnectionGroupName
122                 {
123                         get {
124                                 throw GetMustImplement ();
125                         }
126                         set {
127                                 throw GetMustImplement ();
128                         }
129                 }
130
131                 public override string ContentType {
132                         get {
133                                 throw new NotSupportedException ();
134                         }
135                         set {
136                                 throw new NotSupportedException ();
137                         }
138                 }
139
140                 public override long ContentLength {
141                         get {
142                                 return 0;
143                         } 
144                         set {
145                                 // DO nothing
146                         }
147                 }
148
149                 public long ContentOffset {
150                         get {
151                                 return offset;
152                         }
153                         set {
154                                 CheckRequestStarted ();
155                                 if (value < 0)
156                                         throw new ArgumentOutOfRangeException ();
157
158                                 offset = value;
159                         }
160                 }
161
162                 public override ICredentials Credentials {
163                         get {
164                                 return credentials;
165                         }
166                         set {
167                                 CheckRequestStarted ();
168                                 if (value == null)
169                                         throw new ArgumentNullException ();
170                                 if (!(value is NetworkCredential))
171                                         throw new ArgumentException ();
172
173                                 credentials = value as NetworkCredential;
174                         }
175                 }
176
177                 [MonoTODO]
178                 public static new RequestCachePolicy DefaultCachePolicy
179                 {
180                         get {
181                                 throw GetMustImplement ();
182                         }
183                         set {
184                                 throw GetMustImplement ();
185                         }
186                 }
187
188                 public bool EnableSsl {
189                         get {
190                                 return enableSsl;
191                         }
192                         set {
193                                 CheckRequestStarted ();
194                                 enableSsl = value;
195                         }
196                 }
197
198                 [MonoTODO]
199                 public override WebHeaderCollection Headers
200                 {
201                         get {
202                                 throw GetMustImplement ();
203                         }
204                         set {
205                                 throw GetMustImplement ();
206                         }
207                 }
208
209                 [MonoTODO ("We don't support KeepAlive = true")]
210                 public bool KeepAlive {
211                         get {
212                                 return keepAlive;
213                         }
214                         set {
215                                 CheckRequestStarted ();
216                                 //keepAlive = value;
217                         }
218                 }
219
220                 public override string Method {
221                         get {
222                                 return method;
223                         }
224                         set {
225                                 CheckRequestStarted ();
226                                 if (value == null)
227                                         throw new ArgumentNullException ("Method string cannot be null");
228
229                                 if (value.Length == 0 || Array.BinarySearch (supportedCommands, value) < 0)
230                                         throw new ArgumentException ("Method not supported", "value");
231                                 
232                                 method = value;
233                         }
234                 }
235
236                 public override bool PreAuthenticate {
237                         get {
238                                 throw new NotSupportedException ();
239                         }
240                         set {
241                                 throw new NotSupportedException ();
242                         }
243                 }
244
245                 public override IWebProxy Proxy {
246                         get {
247                                 return proxy;
248                         }
249                         set {
250                                 CheckRequestStarted ();
251                                 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 (this, 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 (this, 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                                         if (!GetServicePoint ().UsesProxy)
513                                                 State = RequestState.Error;
514                                         SetCompleteWithError (e);
515                                 }
516                         }
517                         else {
518                                 if (InProgress ()) {
519                                         FtpStatus status = GetResponseStatus ();
520
521                                         ftpResponse.UpdateStatus (status);
522
523                                         if (ftpResponse.IsFinal ()) {
524                                                 State = RequestState.Finished;
525                                         }
526                                 }
527
528                                 asyncResult.SetCompleted (false, ftpResponse);
529                         }
530                 }
531
532                 void SetType ()
533                 {
534                         if (binary) {
535                                 FtpStatus status = SendCommand (TypeCommand, DataType);
536                                 if ((int) status.StatusCode < 200 || (int) status.StatusCode >= 300)
537                                         throw CreateExceptionFromResponse (status);
538                         }
539                 }
540
541                 string GetRemoteFolderPath (Uri uri)
542                 {
543                         string result;
544                         string local_path = Uri.UnescapeDataString (uri.LocalPath);
545                         if (initial_path == null || initial_path == "/") {
546                                 result = local_path;
547                         } else {
548                                 if (local_path [0] == '/')
549                                         local_path = local_path.Substring (1);
550
551                                 Uri initial = new Uri ("ftp://dummy-host" + initial_path);
552                                 result = new Uri (initial, local_path).LocalPath;
553                         }
554
555                         int last = result.LastIndexOf ('/');
556                         if (last == -1)
557                                 return null;
558
559                         return result.Substring (0, last + 1);
560                 }
561
562                 void CWDAndSetFileName (Uri uri)
563                 {
564                         string remote_folder = GetRemoteFolderPath (uri);
565                         FtpStatus status;
566                         if (remote_folder != null) {
567                                 status = SendCommand (ChangeDir, remote_folder);
568                                 if ((int) status.StatusCode < 200 || (int) status.StatusCode >= 300)
569                                         throw CreateExceptionFromResponse (status);
570
571                                 int last = uri.LocalPath.LastIndexOf ('/');
572                                 if (last >= 0) {
573                                         file_name = Uri.UnescapeDataString (uri.LocalPath.Substring (last + 1));
574                                 }
575                         }
576                 }
577
578                 void ProcessMethod ()
579                 {
580                         ServicePoint sp = GetServicePoint ();
581                         if (sp.UsesProxy) {
582                                 if (method != WebRequestMethods.Ftp.DownloadFile)
583                                         throw new NotSupportedException ("FTP+proxy only supports RETR");
584
585                                 HttpWebRequest req = (HttpWebRequest) WebRequest.Create (proxy.GetProxy (requestUri));
586                                 req.Address = requestUri;
587                                 requestState = RequestState.Finished;
588                                 WebResponse response = req.GetResponse ();
589                                 ftpResponse.Stream = new FtpDataStream (this, response.GetResponseStream (), true);
590                                 ftpResponse.StatusCode = FtpStatusCode.CommandOK;
591                                 return;
592                         }
593                         State = RequestState.Connecting;
594
595                         ResolveHost ();
596
597                         OpenControlConnection ();
598                         CWDAndSetFileName (requestUri);
599                         SetType ();
600
601                         switch (method) {
602                         // Open data connection and receive data
603                         case WebRequestMethods.Ftp.DownloadFile:
604                         case WebRequestMethods.Ftp.ListDirectory:
605                         case WebRequestMethods.Ftp.ListDirectoryDetails:
606                                 DownloadData ();
607                                 break;
608                         // Open data connection and send data
609                         case WebRequestMethods.Ftp.AppendFile:
610                         case WebRequestMethods.Ftp.UploadFile:
611                         case WebRequestMethods.Ftp.UploadFileWithUniqueName:
612                                 UploadData ();
613                                 break;
614                         // Get info from control connection
615                         case WebRequestMethods.Ftp.GetFileSize:
616                         case WebRequestMethods.Ftp.GetDateTimestamp:
617                         case WebRequestMethods.Ftp.PrintWorkingDirectory:
618                         case WebRequestMethods.Ftp.MakeDirectory:
619                         case WebRequestMethods.Ftp.Rename:
620                         case WebRequestMethods.Ftp.DeleteFile:
621                                 ProcessSimpleMethod ();
622                                 break;
623                         default: // What to do here?
624                                 throw new Exception (String.Format ("Support for command {0} not implemented yet", method));
625                         }
626
627                         CheckIfAborted ();
628                 }
629
630                 private void CloseControlConnection () {
631                         if (controlStream != null) {
632                                 SendCommand (QuitCommand);
633                                 controlStream.Close ();
634                                 controlStream = null;
635                         }
636                 }
637
638                 internal void CloseDataConnection () {
639                         if(origDataStream != null) {
640                                 origDataStream.Close ();
641                                 origDataStream = null;
642                         }
643                 }
644
645                 private void CloseConnection () {
646                         CloseControlConnection ();
647                         CloseDataConnection ();
648                 }
649                 
650                 void ProcessSimpleMethod ()
651                 {
652                         State = RequestState.TransferInProgress;
653                         
654                         FtpStatus status;
655                         
656                         if (method == WebRequestMethods.Ftp.PrintWorkingDirectory)
657                                 method = "PWD";
658
659                         if (method == WebRequestMethods.Ftp.Rename)
660                                 method = RenameFromCommand;
661                         
662                         status = SendCommand (method, file_name);
663
664                         ftpResponse.Stream = Stream.Null;
665                         
666                         string desc = status.StatusDescription;
667
668                         switch (method) {
669                         case WebRequestMethods.Ftp.GetFileSize: {
670                                         if (status.StatusCode != FtpStatusCode.FileStatus)
671                                                 throw CreateExceptionFromResponse (status);
672
673                                         int i, len;
674                                         long size;
675                                         for (i = 4, len = 0; i < desc.Length && Char.IsDigit (desc [i]); i++, len++)
676                                                 ;
677
678                                         if (len == 0)
679                                                 throw new WebException ("Bad format for server response in " + method);
680
681                                         if (!Int64.TryParse (desc.Substring (4, len), out size))
682                                                 throw new WebException ("Bad format for server response in " + method);
683
684                                         ftpResponse.contentLength = size;
685                                 }
686                                 break;
687                         case WebRequestMethods.Ftp.GetDateTimestamp:
688                                 if (status.StatusCode != FtpStatusCode.FileStatus)
689                                         throw CreateExceptionFromResponse (status);
690                                 ftpResponse.LastModified = DateTime.ParseExact (desc.Substring (4), "yyyyMMddHHmmss", null);
691                                 break;
692                         case WebRequestMethods.Ftp.MakeDirectory:
693                                 if (status.StatusCode != FtpStatusCode.PathnameCreated)
694                                         throw CreateExceptionFromResponse (status);
695                                 break;
696                         case ChangeDir:
697                                 method = WebRequestMethods.Ftp.PrintWorkingDirectory;
698
699                                 if (status.StatusCode != FtpStatusCode.FileActionOK)
700                                         throw CreateExceptionFromResponse (status);
701
702                                 status = SendCommand (method);
703
704                                 if (status.StatusCode != FtpStatusCode.PathnameCreated)
705                                         throw CreateExceptionFromResponse (status);
706                                 break;
707                         case RenameFromCommand:
708                                 method = WebRequestMethods.Ftp.Rename;
709                                 if (status.StatusCode != FtpStatusCode.FileCommandPending) 
710                                         throw CreateExceptionFromResponse (status);
711                                 // Pass an empty string if RenameTo wasn't specified
712                                 status = SendCommand (RenameToCommand, renameTo != null ? renameTo : String.Empty);
713                                 if (status.StatusCode != FtpStatusCode.FileActionOK)
714                                         throw CreateExceptionFromResponse (status);
715                                 break;
716                         case WebRequestMethods.Ftp.DeleteFile:
717                                 if (status.StatusCode != FtpStatusCode.FileActionOK)  {
718                                         throw CreateExceptionFromResponse (status);
719                                 }
720                                 break;
721                         }
722
723                         State = RequestState.Finished;
724                 }
725
726                 void UploadData ()
727                 {
728                         State = RequestState.OpeningData;
729
730                         OpenDataConnection ();
731
732                         State = RequestState.TransferInProgress;
733                         requestStream = new FtpDataStream (this, dataStream, false);
734                         asyncResult.Stream = requestStream;
735                 }
736
737                 void DownloadData ()
738                 {
739                         State = RequestState.OpeningData;
740
741                         OpenDataConnection ();
742
743                         State = RequestState.TransferInProgress;
744                         ftpResponse.Stream = new FtpDataStream (this, dataStream, true);
745                 }
746
747                 void CheckRequestStarted ()
748                 {
749                         if (State != RequestState.Before)
750                                 throw new InvalidOperationException ("There is a request currently in progress");
751                 }
752
753                 void OpenControlConnection ()
754                 {
755                         Exception exception = null;
756                         Socket sock = null;
757                         foreach (IPAddress address in hostEntry.AddressList) {
758                                 sock = new Socket (address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
759
760                                 IPEndPoint remote = new IPEndPoint (address, requestUri.Port);
761
762                                 if (!ServicePoint.CallEndPointDelegate (sock, remote)) {
763                                         sock.Close ();
764                                         sock = null;
765                                 } else {
766                                         try {
767                                                 sock.Connect (remote);
768                                                 localEndPoint = (IPEndPoint) sock.LocalEndPoint;
769                                                 break;
770                                         } catch (SocketException exc) {
771                                                 exception = exc;
772                                                 sock.Close ();
773                                                 sock = null;
774                                         }
775                                 }
776                         }
777
778                         // Couldn't connect to any address
779                         if (sock == null)
780                                 throw new WebException ("Unable to connect to remote server", exception,
781                                                 WebExceptionStatus.UnknownError, ftpResponse);
782
783                         controlStream = new NetworkStream (sock);
784                         controlReader = new StreamReader (controlStream, Encoding.ASCII);
785
786                         State = RequestState.Authenticating;
787
788                         Authenticate ();
789                         FtpStatus status = SendCommand ("OPTS", "utf8", "on");
790                         // ignore status for OPTS
791                         status = SendCommand (WebRequestMethods.Ftp.PrintWorkingDirectory);
792                         initial_path = GetInitialPath (status);
793                 }
794
795                 static string GetInitialPath (FtpStatus status)
796                 {
797                         int s = (int) status.StatusCode;
798                         if (s < 200 || s > 300 || status.StatusDescription.Length <= 4)
799                                 throw new WebException ("Error getting current directory: " + status.StatusDescription, null,
800                                                 WebExceptionStatus.UnknownError, null);
801
802                         string msg = status.StatusDescription.Substring (4);
803                         if (msg [0] == '"') {
804                                 int next_quote = msg.IndexOf ('\"', 1);
805                                 if (next_quote == -1)
806                                         throw new WebException ("Error getting current directory: PWD -> " + status.StatusDescription, null,
807                                                                 WebExceptionStatus.UnknownError, null);
808
809                                 msg = msg.Substring (1, next_quote - 1);
810                         }
811
812                         if (!msg.EndsWith ("/"))
813                                 msg += "/";
814                         return msg;
815                 }
816
817                 // Probably we could do better having here a regex
818                 Socket SetupPassiveConnection (string statusDescription)
819                 {
820                         // Current response string
821                         string response = statusDescription;
822                         if (response.Length < 4)
823                                 throw new WebException ("Cannot open passive data connection");
824                         
825                         // Look for first digit after code
826                         int i;
827                         for (i = 3; i < response.Length && !Char.IsDigit (response [i]); i++)
828                                 ;
829                         if (i >= response.Length)
830                                 throw new WebException ("Cannot open passive data connection");
831
832                         // Get six elements
833                         string [] digits = response.Substring (i).Split (new char [] {','}, 6);
834                         if (digits.Length != 6)
835                                 throw new WebException ("Cannot open passive data connection");
836
837                         // Clean non-digits at the end of last element
838                         int j;
839                         for (j = digits [5].Length - 1; j >= 0 && !Char.IsDigit (digits [5][j]); j--)
840                                 ;
841                         if (j < 0)
842                                 throw new WebException ("Cannot open passive data connection");
843                         
844                         digits [5] = digits [5].Substring (0, j + 1);
845
846                         IPAddress ip;
847                         try {
848                                 ip = IPAddress.Parse (String.Join (".", digits, 0, 4));
849                         } catch (FormatException) {
850                                 throw new WebException ("Cannot open passive data connection");
851                         }
852
853                         // Get the port
854                         int p1, p2, port;
855                         if (!Int32.TryParse (digits [4], out p1) || !Int32.TryParse (digits [5], out p2))
856                                 throw new WebException ("Cannot open passive data connection");
857
858                         port = (p1 << 8) + p2; // p1 * 256 + p2
859                         //port = p1 * 256 + p2;
860                         if (port < IPEndPoint.MinPort || port > IPEndPoint.MaxPort)
861                                 throw new WebException ("Cannot open passive data connection");
862
863                         IPEndPoint ep = new IPEndPoint (ip, port);
864                         Socket sock = new Socket (ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
865                         try {
866                                 sock.Connect (ep);
867                         } catch (SocketException) {
868                                 sock.Close ();
869                                 throw new WebException ("Cannot open passive data connection");
870                         }
871
872                         return sock;
873                 }
874
875                 Exception CreateExceptionFromResponse (FtpStatus status)
876                 {
877                         FtpWebResponse ftpResponse = new FtpWebResponse (this, requestUri, method, status);
878                         
879                         WebException exc = new WebException ("Server returned an error: " + status.StatusDescription, 
880                                 null, WebExceptionStatus.ProtocolError, ftpResponse);
881                         return exc;
882                 }
883                 
884                 // Here we could also get a server error, so be cautious
885                 internal void SetTransferCompleted ()
886                 {
887                         if (InFinalState ())
888                                 return;
889
890                         State = RequestState.Finished;
891                         FtpStatus status = GetResponseStatus ();
892                         ftpResponse.UpdateStatus (status);
893                         if(!keepAlive)
894                                 CloseConnection ();
895                 }
896
897                 internal void OperationCompleted ()
898                 {
899                         if(!keepAlive)
900                                 CloseConnection ();
901                 }
902
903                 void SetCompleteWithError (Exception exc)
904                 {
905                         if (asyncResult != null) {
906                                 asyncResult.SetCompleted (false, exc);
907                         }
908                 }
909
910                 Socket InitDataConnection ()
911                 {
912                         FtpStatus status;
913                         
914                         if (usePassive) {
915                                 status = SendCommand (PassiveCommand);
916                                 if (status.StatusCode != FtpStatusCode.EnteringPassive) {
917                                         throw CreateExceptionFromResponse (status);
918                                 }
919                                 
920                                 return SetupPassiveConnection (status.StatusDescription);
921                         }
922
923                         // Open a socket to listen the server's connection
924                         Socket sock = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
925                         try {
926                                 sock.Bind (new IPEndPoint (localEndPoint.Address, 0));
927                                 sock.Listen (1); // We only expect a connection from server
928
929                         } catch (SocketException e) {
930                                 sock.Close ();
931
932                                 throw new WebException ("Couldn't open listening socket on client", e);
933                         }
934
935                         IPEndPoint ep = (IPEndPoint) sock.LocalEndPoint;
936                         string ipString = ep.Address.ToString ().Replace ('.', ',');
937                         int h1 = ep.Port >> 8; // ep.Port / 256
938                         int h2 = ep.Port % 256;
939
940                         string portParam = ipString + "," + h1 + "," + h2;
941                         status = SendCommand (PortCommand, portParam);
942                         
943                         if (status.StatusCode != FtpStatusCode.CommandOK) {
944                                 sock.Close ();
945                                 throw (CreateExceptionFromResponse (status));
946                         }
947
948                         return sock;
949                 }
950
951                 void OpenDataConnection ()
952                 {
953                         FtpStatus status;
954                         
955                         Socket s = InitDataConnection ();
956
957                         // Handle content offset
958                         if (offset > 0) {
959                                 status = SendCommand (RestCommand, offset.ToString ());
960                                 if (status.StatusCode != FtpStatusCode.FileCommandPending)
961                                         throw CreateExceptionFromResponse (status);
962                         }
963
964                         if (method != WebRequestMethods.Ftp.ListDirectory && method != WebRequestMethods.Ftp.ListDirectoryDetails &&
965                             method != WebRequestMethods.Ftp.UploadFileWithUniqueName) {
966                                 status = SendCommand (method, file_name);
967                         } else {
968                                 status = SendCommand (method);
969                         }
970
971                         if (status.StatusCode != FtpStatusCode.OpeningData && status.StatusCode != FtpStatusCode.DataAlreadyOpen)
972                                 throw CreateExceptionFromResponse (status);
973
974                         if (usePassive) {
975                                 origDataStream = new NetworkStream (s, true);
976                                 dataStream = origDataStream;
977                                 if (EnableSsl)
978                                         ChangeToSSLSocket (ref dataStream);
979                         }
980                         else {
981
982                                 // Active connection (use Socket.Blocking to true)
983                                 Socket incoming = null;
984                                 try {
985                                         incoming = s.Accept ();
986                                 }
987                                 catch (SocketException) {
988                                         s.Close ();
989                                         if (incoming != null)
990                                                 incoming.Close ();
991
992                                         throw new ProtocolViolationException ("Server commited a protocol violation.");
993                                 }
994
995                                 s.Close ();
996                                 origDataStream = new NetworkStream (incoming, true);
997                                 dataStream = origDataStream;
998                                 if (EnableSsl)
999                                         ChangeToSSLSocket (ref dataStream);
1000                         }
1001
1002                         ftpResponse.UpdateStatus (status);
1003                 }
1004
1005                 void Authenticate ()
1006                 {
1007                         string username = null;
1008                         string password = null;
1009                         string domain = null;
1010
1011                         if (credentials != null) {
1012                                 username = credentials.UserName;
1013                                 password = credentials.Password;
1014                                 domain = credentials.Domain;
1015                         }
1016
1017                         if (username == null)
1018                                 username = "anonymous";
1019                         if (password == null)
1020                                 password = "@anonymous";
1021                         if (!string.IsNullOrEmpty (domain))
1022                                 username = domain + '\\' + username;
1023
1024                         // Connect to server and get banner message
1025                         FtpStatus status = GetResponseStatus ();
1026                         ftpResponse.BannerMessage = status.StatusDescription;
1027
1028                         if (EnableSsl) {
1029                                 InitiateSecureConnection (ref controlStream);
1030                                 controlReader = new StreamReader (controlStream, Encoding.ASCII);
1031                                 status = SendCommand ("PBSZ", "0");
1032                                 int st = (int) status.StatusCode;
1033                                 if (st < 200 || st >= 300)
1034                                         throw CreateExceptionFromResponse (status);
1035                                 // TODO: what if "PROT P" is denied by the server? What does MS do?
1036                                 status = SendCommand ("PROT", "P");
1037                                 st = (int) status.StatusCode;
1038                                 if (st < 200 || st >= 300)
1039                                         throw CreateExceptionFromResponse (status);
1040
1041                                 status = new FtpStatus (FtpStatusCode.SendUserCommand, "");
1042                         }
1043                         
1044                         if (status.StatusCode != FtpStatusCode.SendUserCommand)
1045                                 throw CreateExceptionFromResponse (status);
1046
1047                         status = SendCommand (UserCommand, username);
1048
1049                         switch (status.StatusCode) {
1050                         case FtpStatusCode.SendPasswordCommand:
1051                                 status = SendCommand (PasswordCommand, password);
1052                                 if (status.StatusCode != FtpStatusCode.LoggedInProceed)
1053                                         throw CreateExceptionFromResponse (status);
1054                                 break;
1055                         case FtpStatusCode.LoggedInProceed:
1056                                 break;
1057                         default:
1058                                 throw CreateExceptionFromResponse (status);
1059                         }
1060
1061                         ftpResponse.WelcomeMessage = status.StatusDescription;
1062                         ftpResponse.UpdateStatus (status);
1063                 }
1064
1065                 FtpStatus SendCommand (string command, params string [] parameters) {
1066                         return SendCommand (true, command, parameters);
1067                 }
1068
1069                 FtpStatus SendCommand (bool waitResponse, string command, params string [] parameters)
1070                 {
1071                         byte [] cmd;
1072                         string commandString = command;
1073                         if (parameters.Length > 0)
1074                                 commandString += " " + String.Join (" ", parameters);
1075
1076                         commandString += EOL;
1077                         cmd = Encoding.ASCII.GetBytes (commandString);
1078                         try {
1079                                 controlStream.Write (cmd, 0, cmd.Length);
1080                         } catch (IOException) {
1081                                 //controlStream.Close ();
1082                                 return new FtpStatus(FtpStatusCode.ServiceNotAvailable, "Write failed");
1083                         }
1084
1085                         if(!waitResponse)
1086                                 return null;
1087                         
1088                         FtpStatus result = GetResponseStatus ();
1089                         if (ftpResponse != null)
1090                                 ftpResponse.UpdateStatus (result);
1091                         return result;
1092                 }
1093
1094                 internal static FtpStatus ServiceNotAvailable ()
1095                 {
1096                         return new FtpStatus (FtpStatusCode.ServiceNotAvailable, Locale.GetText ("Invalid response from server"));
1097                 }
1098                 
1099                 internal FtpStatus GetResponseStatus ()
1100                 {
1101                         while (true) {
1102                                 string response = null;
1103
1104                                 try {
1105                                         response = controlReader.ReadLine ();
1106                                 } catch (IOException) {
1107                                 }
1108
1109                                 if (response == null || response.Length < 3)
1110                                         return ServiceNotAvailable ();
1111
1112                                 int code;
1113                                 if (!Int32.TryParse (response.Substring (0, 3), out code))
1114                                         return ServiceNotAvailable ();
1115
1116                                 if (response.Length > 3 && response [3] == '-'){
1117                                         string line = null;
1118                                         string find = code.ToString() + ' ';
1119                                         while (true){
1120                                                 line = null;
1121                                                 try {
1122                                                         line = controlReader.ReadLine();
1123                                                 } catch (IOException) {
1124                                                 }
1125                                                 if (line == null)
1126                                                         return ServiceNotAvailable ();
1127                                                 
1128                                                 response += Environment.NewLine + line;
1129
1130                                                 if (line.StartsWith(find, StringComparison.Ordinal))
1131                                                         break;
1132                                         } 
1133                                 }
1134                                 return new FtpStatus ((FtpStatusCode) code, response);
1135                         }
1136                 }
1137
1138                 private void InitiateSecureConnection (ref Stream stream) {
1139                         FtpStatus status = SendCommand (AuthCommand, "TLS");
1140                         if (status.StatusCode != FtpStatusCode.ServerWantsSecureSession)
1141                                 throw CreateExceptionFromResponse (status);
1142
1143                         ChangeToSSLSocket (ref stream);
1144                 }
1145
1146 #if SECURITY_DEP
1147                 RemoteCertificateValidationCallback callback = delegate (object sender,
1148                                                                          X509Certificate certificate,
1149                                                                          X509Chain chain,
1150                                                                          SslPolicyErrors sslPolicyErrors) {
1151                         // honor any exciting callback defined on ServicePointManager
1152                         if (ServicePointManager.ServerCertificateValidationCallback != null)
1153                                 return ServicePointManager.ServerCertificateValidationCallback (sender, certificate, chain, sslPolicyErrors);
1154                         // otherwise provide our own
1155                         if (sslPolicyErrors != SslPolicyErrors.None)
1156                                 throw new InvalidOperationException ("SSL authentication error: " + sslPolicyErrors);
1157                         return true;
1158                         };
1159 #endif
1160
1161                 internal bool ChangeToSSLSocket (ref Stream stream) {
1162 #if TARGET_JVM
1163                         stream.ChangeToSSLSocket ();
1164                         return true;
1165 #elif SECURITY_DEP
1166                         SslStream sslStream = new SslStream (stream, true, callback, null);
1167                         //sslStream.AuthenticateAsClient (Host, this.ClientCertificates, SslProtocols.Default, false);
1168                         //TODO: client certificates
1169                         sslStream.AuthenticateAsClient (requestUri.Host, null, SslProtocols.Default, false);
1170                         stream = sslStream;
1171                         return true;
1172 #else
1173                         throw new NotImplementedException ();
1174 #endif
1175                 }
1176                 
1177                 bool InFinalState () {
1178                         return (State == RequestState.Aborted || State == RequestState.Error || State == RequestState.Finished);
1179                 }
1180
1181                 bool InProgress () {
1182                         return (State != RequestState.Before && !InFinalState ());
1183                 }
1184
1185                 internal void CheckIfAborted () {
1186                         if (State == RequestState.Aborted)
1187                                 throw new WebException ("Request aborted", WebExceptionStatus.RequestCanceled);
1188                 }
1189
1190                 void CheckFinalState () {
1191                         if (InFinalState ())
1192                                 throw new InvalidOperationException ("Cannot change final state");
1193                 }
1194         }
1195 }
1196
1197 #endif
1198