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