[System] Make sure FtpWebRequest processing happens in a background thread
[mono-project.git] / mcs / class / System / System.Net / FtpWebRequest.cs
blobb59249bed4253ba95b1d0117ad886c3063eaeb8b
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 //
10 #if SECURITY_DEP
11 #if MONO_SECURITY_ALIAS
12 extern alias MonoSecurity;
13 using MSI = MonoSecurity::Mono.Security.Interface;
14 #else
15 using MSI = Mono.Security.Interface;
16 #endif
17 #endif
19 using System;
20 using System.IO;
21 using System.Net.Sockets;
22 using System.Text;
23 using System.Threading;
24 using System.Net.Cache;
25 using System.Security.Cryptography.X509Certificates;
26 using System.Net;
27 using System.Net.Security;
28 using System.Security.Authentication;
29 using Mono.Net.Security;
31 namespace System.Net
33 public sealed class FtpWebRequest : WebRequest
35 Uri requestUri;
36 string file_name; // By now, used for upload
37 ServicePoint servicePoint;
38 Stream origDataStream;
39 Stream dataStream;
40 Stream controlStream;
41 StreamReader controlReader;
42 NetworkCredential credentials;
43 IPHostEntry hostEntry;
44 IPEndPoint localEndPoint;
45 IWebProxy proxy;
46 int timeout = 100000;
47 int rwTimeout = 300000;
48 long offset = 0;
49 bool binary = true;
50 bool enableSsl = false;
51 bool usePassive = true;
52 bool keepAlive = false;
53 string method = WebRequestMethods.Ftp.DownloadFile;
54 string renameTo;
55 object locker = new object ();
57 RequestState requestState = RequestState.Before;
58 FtpAsyncResult asyncResult;
59 FtpWebResponse ftpResponse;
60 Stream requestStream;
61 string initial_path;
63 const string ChangeDir = "CWD";
64 const string UserCommand = "USER";
65 const string PasswordCommand = "PASS";
66 const string TypeCommand = "TYPE";
67 const string PassiveCommand = "PASV";
68 const string PortCommand = "PORT";
69 const string AbortCommand = "ABOR";
70 const string AuthCommand = "AUTH";
71 const string RestCommand = "REST";
72 const string RenameFromCommand = "RNFR";
73 const string RenameToCommand = "RNTO";
74 const string QuitCommand = "QUIT";
75 const string EOL = "\r\n"; // Special end of line
77 enum RequestState
79 Before,
80 Scheduled,
81 Connecting,
82 Authenticating,
83 OpeningData,
84 TransferInProgress,
85 Finished,
86 Aborted,
87 Error
90 // sorted commands
91 static readonly string [] supportedCommands = new string [] {
92 WebRequestMethods.Ftp.AppendFile, // APPE
93 WebRequestMethods.Ftp.DeleteFile, // DELE
94 WebRequestMethods.Ftp.ListDirectoryDetails, // LIST
95 WebRequestMethods.Ftp.GetDateTimestamp, // MDTM
96 WebRequestMethods.Ftp.MakeDirectory, // MKD
97 WebRequestMethods.Ftp.ListDirectory, // NLST
98 WebRequestMethods.Ftp.PrintWorkingDirectory, // PWD
99 WebRequestMethods.Ftp.Rename, // RENAME
100 WebRequestMethods.Ftp.DownloadFile, // RETR
101 WebRequestMethods.Ftp.RemoveDirectory, // RMD
102 WebRequestMethods.Ftp.GetFileSize, // SIZE
103 WebRequestMethods.Ftp.UploadFile, // STOR
104 WebRequestMethods.Ftp.UploadFileWithUniqueName // STUR
107 Encoding dataEncoding = Encoding.UTF8;
109 internal FtpWebRequest (Uri uri)
111 this.requestUri = uri;
112 this.proxy = GlobalProxySelection.Select;
115 static Exception GetMustImplement ()
117 return new NotImplementedException ();
120 [MonoTODO]
121 public X509CertificateCollection ClientCertificates
123 get {
124 throw GetMustImplement ();
126 set {
127 throw GetMustImplement ();
131 [MonoTODO]
132 public override string ConnectionGroupName
134 get {
135 throw GetMustImplement ();
137 set {
138 throw GetMustImplement ();
142 public override string ContentType {
143 get {
144 throw new NotSupportedException ();
146 set {
147 throw new NotSupportedException ();
151 public override long ContentLength {
152 get {
153 return 0;
155 set {
156 // DO nothing
160 public long ContentOffset {
161 get {
162 return offset;
164 set {
165 CheckRequestStarted ();
166 if (value < 0)
167 throw new ArgumentOutOfRangeException ();
169 offset = value;
173 public override ICredentials Credentials {
174 get {
175 return credentials;
177 set {
178 CheckRequestStarted ();
179 if (value == null)
180 throw new ArgumentNullException ();
181 if (!(value is NetworkCredential))
182 throw new ArgumentException ();
184 credentials = value as NetworkCredential;
188 #if !MOBILE
189 [MonoTODO]
190 public static new RequestCachePolicy DefaultCachePolicy
192 get {
193 throw GetMustImplement ();
195 set {
196 throw GetMustImplement ();
199 #endif
201 public bool EnableSsl {
202 get {
203 return enableSsl;
205 set {
206 CheckRequestStarted ();
207 enableSsl = value;
211 [MonoTODO]
212 public override WebHeaderCollection Headers
214 get {
215 throw GetMustImplement ();
217 set {
218 throw GetMustImplement ();
222 [MonoTODO ("We don't support KeepAlive = true")]
223 public bool KeepAlive {
224 get {
225 return keepAlive;
227 set {
228 CheckRequestStarted ();
229 //keepAlive = value;
233 public override string Method {
234 get {
235 return method;
237 set {
238 CheckRequestStarted ();
239 if (value == null)
240 throw new ArgumentNullException ("Method string cannot be null");
242 if (value.Length == 0 || Array.BinarySearch (supportedCommands, value) < 0)
243 throw new ArgumentException ("Method not supported", "value");
245 method = value;
249 public override bool PreAuthenticate {
250 get {
251 throw new NotSupportedException ();
253 set {
254 throw new NotSupportedException ();
258 public override IWebProxy Proxy {
259 get {
260 return proxy;
262 set {
263 CheckRequestStarted ();
264 proxy = value;
268 public int ReadWriteTimeout {
269 get {
270 return rwTimeout;
272 set {
273 CheckRequestStarted ();
275 if (value < - 1)
276 throw new ArgumentOutOfRangeException ();
277 else
278 rwTimeout = value;
282 public string RenameTo {
283 get {
284 return renameTo;
286 set {
287 CheckRequestStarted ();
288 if (value == null || value.Length == 0)
289 throw new ArgumentException ("RenameTo value can't be null or empty", "RenameTo");
291 renameTo = value;
295 public override Uri RequestUri {
296 get {
297 return requestUri;
301 public ServicePoint ServicePoint {
302 get {
303 return GetServicePoint ();
307 public bool UsePassive {
308 get {
309 return usePassive;
311 set {
312 CheckRequestStarted ();
313 usePassive = value;
317 [MonoTODO]
318 public override bool UseDefaultCredentials
320 get {
321 throw GetMustImplement ();
323 set {
324 throw GetMustImplement ();
328 public bool UseBinary {
329 get {
330 return binary;
331 } set {
332 CheckRequestStarted ();
333 binary = value;
337 public override int Timeout {
338 get {
339 return timeout;
341 set {
342 CheckRequestStarted ();
344 if (value < -1)
345 throw new ArgumentOutOfRangeException ();
346 else
347 timeout = value;
351 string DataType {
352 get {
353 return binary ? "I" : "A";
357 RequestState State {
358 get {
359 lock (locker) {
360 return requestState;
364 set {
365 lock (locker) {
366 CheckIfAborted ();
367 CheckFinalState ();
368 requestState = value;
373 public override void Abort () {
374 lock (locker) {
375 if (State == RequestState.TransferInProgress) {
376 /*FtpStatus status = */
377 SendCommand (false, AbortCommand);
380 if (!InFinalState ()) {
381 State = RequestState.Aborted;
382 ftpResponse = new FtpWebResponse (this, requestUri, method, FtpStatusCode.FileActionAborted, "Aborted by request");
387 public override IAsyncResult BeginGetResponse (AsyncCallback callback, object state) {
388 if (asyncResult != null && !asyncResult.IsCompleted) {
389 throw new InvalidOperationException ("Cannot re-call BeginGetRequestStream/BeginGetResponse while a previous call is still in progress");
392 CheckIfAborted ();
394 asyncResult = new FtpAsyncResult (callback, state);
396 lock (locker) {
397 if (InFinalState ())
398 asyncResult.SetCompleted (true, ftpResponse);
399 else {
400 if (State == RequestState.Before)
401 State = RequestState.Scheduled;
403 Thread thread = new Thread (ProcessRequest);
404 thread.IsBackground = true;
405 thread.Start ();
409 return asyncResult;
412 public override WebResponse EndGetResponse (IAsyncResult asyncResult) {
413 if (asyncResult == null)
414 throw new ArgumentNullException ("AsyncResult cannot be null!");
416 if (!(asyncResult is FtpAsyncResult) || asyncResult != this.asyncResult)
417 throw new ArgumentException ("AsyncResult is from another request!");
419 FtpAsyncResult asyncFtpResult = (FtpAsyncResult) asyncResult;
420 if (!asyncFtpResult.WaitUntilComplete (timeout, false)) {
421 Abort ();
422 throw new WebException ("Transfer timed out.", WebExceptionStatus.Timeout);
425 CheckIfAborted ();
427 asyncResult = null;
429 if (asyncFtpResult.GotException)
430 throw asyncFtpResult.Exception;
432 return asyncFtpResult.Response;
435 public override WebResponse GetResponse () {
436 IAsyncResult asyncResult = BeginGetResponse (null, null);
437 return EndGetResponse (asyncResult);
440 public override IAsyncResult BeginGetRequestStream (AsyncCallback callback, object state) {
441 if (method != WebRequestMethods.Ftp.UploadFile && method != WebRequestMethods.Ftp.UploadFileWithUniqueName &&
442 method != WebRequestMethods.Ftp.AppendFile)
443 throw new ProtocolViolationException ();
445 lock (locker) {
446 CheckIfAborted ();
448 if (State != RequestState.Before)
449 throw new InvalidOperationException ("Cannot re-call BeginGetRequestStream/BeginGetResponse while a previous call is still in progress");
451 State = RequestState.Scheduled;
454 asyncResult = new FtpAsyncResult (callback, state);
455 Thread thread = new Thread (ProcessRequest);
456 thread.IsBackground = true;
457 thread.Start ();
459 return asyncResult;
462 public override Stream EndGetRequestStream (IAsyncResult asyncResult) {
463 if (asyncResult == null)
464 throw new ArgumentNullException ("asyncResult");
466 if (!(asyncResult is FtpAsyncResult))
467 throw new ArgumentException ("asyncResult");
469 if (State == RequestState.Aborted) {
470 throw new WebException ("Request aborted", WebExceptionStatus.RequestCanceled);
473 if (asyncResult != this.asyncResult)
474 throw new ArgumentException ("AsyncResult is from another request!");
476 FtpAsyncResult res = (FtpAsyncResult) asyncResult;
478 if (!res.WaitUntilComplete (timeout, false)) {
479 Abort ();
480 throw new WebException ("Request timed out");
483 if (res.GotException)
484 throw res.Exception;
486 return res.Stream;
489 public override Stream GetRequestStream () {
490 IAsyncResult asyncResult = BeginGetRequestStream (null, null);
491 return EndGetRequestStream (asyncResult);
494 ServicePoint GetServicePoint ()
496 if (servicePoint == null)
497 servicePoint = ServicePointManager.FindServicePoint (requestUri, proxy);
499 return servicePoint;
502 // Probably move some code of command connection here
503 void ResolveHost ()
505 CheckIfAborted ();
506 hostEntry = GetServicePoint ().HostEntry;
508 if (hostEntry == null) {
509 ftpResponse.UpdateStatus (new FtpStatus(FtpStatusCode.ActionAbortedLocalProcessingError, "Cannot resolve server name"));
510 throw new WebException ("The remote server name could not be resolved: " + requestUri,
511 null, WebExceptionStatus.NameResolutionFailure, ftpResponse);
515 void ProcessRequest () {
517 if (State == RequestState.Scheduled) {
518 ftpResponse = new FtpWebResponse (this, requestUri, method, keepAlive);
520 try {
521 ProcessMethod ();
522 //State = RequestState.Finished;
523 //finalResponse = ftpResponse;
524 asyncResult.SetCompleted (false, ftpResponse);
526 catch (Exception e) {
527 if (!GetServicePoint ().UsesProxy)
528 State = RequestState.Error;
529 SetCompleteWithError (e);
532 else {
533 if (InProgress ()) {
534 FtpStatus status = GetResponseStatus ();
536 ftpResponse.UpdateStatus (status);
538 if (ftpResponse.IsFinal ()) {
539 State = RequestState.Finished;
543 asyncResult.SetCompleted (false, ftpResponse);
547 void SetType ()
549 if (binary) {
550 FtpStatus status = SendCommand (TypeCommand, DataType);
551 if ((int) status.StatusCode < 200 || (int) status.StatusCode >= 300)
552 throw CreateExceptionFromResponse (status);
556 string GetRemoteFolderPath (Uri uri)
558 string result;
559 string local_path = Uri.UnescapeDataString (uri.LocalPath);
560 if (initial_path == null || initial_path == "/") {
561 result = local_path;
562 } else {
563 if (local_path [0] == '/')
564 local_path = local_path.Substring (1);
566 UriBuilder initialBuilder = new UriBuilder () {
567 Scheme = "ftp",
568 Host = "dummy-host",
569 Path = initial_path,
571 Uri initial = initialBuilder.Uri;
572 result = new Uri (initial, local_path).LocalPath;
575 int last = result.LastIndexOf ('/');
576 if (last == -1)
577 return null;
579 return result.Substring (0, last + 1);
582 void CWDAndSetFileName (Uri uri)
584 string remote_folder = GetRemoteFolderPath (uri);
585 FtpStatus status;
586 if (remote_folder != null) {
587 status = SendCommand (ChangeDir, remote_folder);
588 if ((int) status.StatusCode < 200 || (int) status.StatusCode >= 300)
589 throw CreateExceptionFromResponse (status);
591 int last = uri.LocalPath.LastIndexOf ('/');
592 if (last >= 0) {
593 file_name = Uri.UnescapeDataString (uri.LocalPath.Substring (last + 1));
598 void ProcessMethod ()
600 ServicePoint sp = GetServicePoint ();
601 if (sp.UsesProxy) {
602 if (method != WebRequestMethods.Ftp.DownloadFile)
603 throw new NotSupportedException ("FTP+proxy only supports RETR");
605 HttpWebRequest req = (HttpWebRequest) WebRequest.Create (proxy.GetProxy (requestUri));
606 req.Address = requestUri;
607 requestState = RequestState.Finished;
608 WebResponse response = req.GetResponse ();
609 ftpResponse.Stream = new FtpDataStream (this, response.GetResponseStream (), true);
610 ftpResponse.StatusCode = FtpStatusCode.CommandOK;
611 return;
613 State = RequestState.Connecting;
615 ResolveHost ();
617 OpenControlConnection ();
618 CWDAndSetFileName (requestUri);
619 SetType ();
621 switch (method) {
622 // Open data connection and receive data
623 case WebRequestMethods.Ftp.DownloadFile:
624 case WebRequestMethods.Ftp.ListDirectory:
625 case WebRequestMethods.Ftp.ListDirectoryDetails:
626 DownloadData ();
627 break;
628 // Open data connection and send data
629 case WebRequestMethods.Ftp.AppendFile:
630 case WebRequestMethods.Ftp.UploadFile:
631 case WebRequestMethods.Ftp.UploadFileWithUniqueName:
632 UploadData ();
633 break;
634 // Get info from control connection
635 case WebRequestMethods.Ftp.GetFileSize:
636 case WebRequestMethods.Ftp.GetDateTimestamp:
637 case WebRequestMethods.Ftp.PrintWorkingDirectory:
638 case WebRequestMethods.Ftp.MakeDirectory:
639 case WebRequestMethods.Ftp.Rename:
640 case WebRequestMethods.Ftp.DeleteFile:
641 ProcessSimpleMethod ();
642 break;
643 default: // What to do here?
644 throw new Exception (String.Format ("Support for command {0} not implemented yet", method));
647 CheckIfAborted ();
650 private void CloseControlConnection () {
651 if (controlStream != null) {
652 SendCommand (QuitCommand);
653 controlStream.Close ();
654 controlStream = null;
658 internal void CloseDataConnection () {
659 if(origDataStream != null) {
660 origDataStream.Close ();
661 origDataStream = null;
665 private void CloseConnection () {
666 CloseControlConnection ();
667 CloseDataConnection ();
670 void ProcessSimpleMethod ()
672 State = RequestState.TransferInProgress;
674 FtpStatus status;
676 if (method == WebRequestMethods.Ftp.PrintWorkingDirectory)
677 method = "PWD";
679 if (method == WebRequestMethods.Ftp.Rename)
680 method = RenameFromCommand;
682 status = SendCommand (method, file_name);
684 ftpResponse.Stream = Stream.Null;
686 string desc = status.StatusDescription;
688 switch (method) {
689 case WebRequestMethods.Ftp.GetFileSize: {
690 if (status.StatusCode != FtpStatusCode.FileStatus)
691 throw CreateExceptionFromResponse (status);
693 int i, len;
694 long size;
695 for (i = 4, len = 0; i < desc.Length && Char.IsDigit (desc [i]); i++, len++)
698 if (len == 0)
699 throw new WebException ("Bad format for server response in " + method);
701 if (!Int64.TryParse (desc.Substring (4, len), out size))
702 throw new WebException ("Bad format for server response in " + method);
704 ftpResponse.contentLength = size;
706 break;
707 case WebRequestMethods.Ftp.GetDateTimestamp:
708 if (status.StatusCode != FtpStatusCode.FileStatus)
709 throw CreateExceptionFromResponse (status);
710 ftpResponse.LastModified = DateTime.ParseExact (desc.Substring (4), "yyyyMMddHHmmss", null);
711 break;
712 case WebRequestMethods.Ftp.MakeDirectory:
713 if (status.StatusCode != FtpStatusCode.PathnameCreated)
714 throw CreateExceptionFromResponse (status);
715 break;
716 case ChangeDir:
717 method = WebRequestMethods.Ftp.PrintWorkingDirectory;
719 if (status.StatusCode != FtpStatusCode.FileActionOK)
720 throw CreateExceptionFromResponse (status);
722 status = SendCommand (method);
724 if (status.StatusCode != FtpStatusCode.PathnameCreated)
725 throw CreateExceptionFromResponse (status);
726 break;
727 case RenameFromCommand:
728 method = WebRequestMethods.Ftp.Rename;
729 if (status.StatusCode != FtpStatusCode.FileCommandPending)
730 throw CreateExceptionFromResponse (status);
731 // Pass an empty string if RenameTo wasn't specified
732 status = SendCommand (RenameToCommand, renameTo != null ? renameTo : String.Empty);
733 if (status.StatusCode != FtpStatusCode.FileActionOK)
734 throw CreateExceptionFromResponse (status);
735 break;
736 case WebRequestMethods.Ftp.DeleteFile:
737 if (status.StatusCode != FtpStatusCode.FileActionOK) {
738 throw CreateExceptionFromResponse (status);
740 break;
743 State = RequestState.Finished;
746 void UploadData ()
748 State = RequestState.OpeningData;
750 OpenDataConnection ();
752 State = RequestState.TransferInProgress;
753 requestStream = new FtpDataStream (this, dataStream, false);
754 asyncResult.Stream = requestStream;
757 void DownloadData ()
759 State = RequestState.OpeningData;
761 OpenDataConnection ();
763 State = RequestState.TransferInProgress;
764 ftpResponse.Stream = new FtpDataStream (this, dataStream, true);
767 void CheckRequestStarted ()
769 if (State != RequestState.Before)
770 throw new InvalidOperationException ("There is a request currently in progress");
773 void OpenControlConnection ()
775 Exception exception = null;
776 Socket sock = null;
777 foreach (IPAddress address in hostEntry.AddressList) {
778 sock = new Socket (address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
780 IPEndPoint remote = new IPEndPoint (address, requestUri.Port);
782 if (!ServicePoint.CallEndPointDelegate (sock, remote)) {
783 sock.Close ();
784 sock = null;
785 } else {
786 try {
787 sock.Connect (remote);
788 localEndPoint = (IPEndPoint) sock.LocalEndPoint;
789 break;
790 } catch (SocketException exc) {
791 exception = exc;
792 sock.Close ();
793 sock = null;
798 // Couldn't connect to any address
799 if (sock == null)
800 throw new WebException ("Unable to connect to remote server", exception,
801 WebExceptionStatus.UnknownError, ftpResponse);
803 controlStream = new NetworkStream (sock);
804 controlReader = new StreamReader (controlStream, Encoding.ASCII);
806 State = RequestState.Authenticating;
808 Authenticate ();
809 FtpStatus status = SendCommand ("OPTS", "utf8", "on");
810 if ((int)status.StatusCode < 200 || (int)status.StatusCode > 300)
811 dataEncoding = Encoding.Default;
812 else
813 dataEncoding = Encoding.UTF8;
815 status = SendCommand (WebRequestMethods.Ftp.PrintWorkingDirectory);
816 initial_path = GetInitialPath (status);
819 static string GetInitialPath (FtpStatus status)
821 int s = (int) status.StatusCode;
822 if (s < 200 || s > 300 || status.StatusDescription.Length <= 4)
823 throw new WebException ("Error getting current directory: " + status.StatusDescription, null,
824 WebExceptionStatus.UnknownError, null);
826 string msg = status.StatusDescription.Substring (4);
827 if (msg [0] == '"') {
828 int next_quote = msg.IndexOf ('\"', 1);
829 if (next_quote == -1)
830 throw new WebException ("Error getting current directory: PWD -> " + status.StatusDescription, null,
831 WebExceptionStatus.UnknownError, null);
833 msg = msg.Substring (1, next_quote - 1);
836 if (!msg.EndsWith ("/"))
837 msg += "/";
838 return msg;
841 // Probably we could do better having here a regex
842 Socket SetupPassiveConnection (string statusDescription)
844 // Current response string
845 string response = statusDescription;
846 if (response.Length < 4)
847 throw new WebException ("Cannot open passive data connection");
849 // Look for first digit after code
850 int i;
851 for (i = 3; i < response.Length && !Char.IsDigit (response [i]); i++)
853 if (i >= response.Length)
854 throw new WebException ("Cannot open passive data connection");
856 // Get six elements
857 string [] digits = response.Substring (i).Split (new char [] {','}, 6);
858 if (digits.Length != 6)
859 throw new WebException ("Cannot open passive data connection");
861 // Clean non-digits at the end of last element
862 int j;
863 for (j = digits [5].Length - 1; j >= 0 && !Char.IsDigit (digits [5][j]); j--)
865 if (j < 0)
866 throw new WebException ("Cannot open passive data connection");
868 digits [5] = digits [5].Substring (0, j + 1);
870 IPAddress ip;
871 try {
872 ip = IPAddress.Parse (String.Join (".", digits, 0, 4));
873 } catch (FormatException) {
874 throw new WebException ("Cannot open passive data connection");
877 // Get the port
878 int p1, p2, port;
879 if (!Int32.TryParse (digits [4], out p1) || !Int32.TryParse (digits [5], out p2))
880 throw new WebException ("Cannot open passive data connection");
882 port = (p1 << 8) + p2; // p1 * 256 + p2
883 //port = p1 * 256 + p2;
884 if (port < IPEndPoint.MinPort || port > IPEndPoint.MaxPort)
885 throw new WebException ("Cannot open passive data connection");
887 IPEndPoint ep = new IPEndPoint (ip, port);
888 Socket sock = new Socket (ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
889 try {
890 sock.Connect (ep);
891 } catch (SocketException) {
892 sock.Close ();
893 throw new WebException ("Cannot open passive data connection");
896 return sock;
899 Exception CreateExceptionFromResponse (FtpStatus status)
901 FtpWebResponse ftpResponse = new FtpWebResponse (this, requestUri, method, status);
903 WebException exc = new WebException ("Server returned an error: " + status.StatusDescription,
904 null, WebExceptionStatus.ProtocolError, ftpResponse);
905 return exc;
908 // Here we could also get a server error, so be cautious
909 internal void SetTransferCompleted ()
911 if (InFinalState ())
912 return;
914 State = RequestState.Finished;
915 FtpStatus status = GetResponseStatus ();
916 ftpResponse.UpdateStatus (status);
917 if(!keepAlive)
918 CloseConnection ();
921 internal void OperationCompleted ()
923 if(!keepAlive)
924 CloseConnection ();
927 void SetCompleteWithError (Exception exc)
929 if (asyncResult != null) {
930 asyncResult.SetCompleted (false, exc);
934 Socket InitDataConnection ()
936 FtpStatus status;
938 if (usePassive) {
939 status = SendCommand (PassiveCommand);
940 if (status.StatusCode != FtpStatusCode.EnteringPassive) {
941 throw CreateExceptionFromResponse (status);
944 return SetupPassiveConnection (status.StatusDescription);
947 // Open a socket to listen the server's connection
948 Socket sock = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
949 try {
950 sock.Bind (new IPEndPoint (localEndPoint.Address, 0));
951 sock.Listen (1); // We only expect a connection from server
953 } catch (SocketException e) {
954 sock.Close ();
956 throw new WebException ("Couldn't open listening socket on client", e);
959 IPEndPoint ep = (IPEndPoint) sock.LocalEndPoint;
960 string ipString = ep.Address.ToString ().Replace ('.', ',');
961 int h1 = ep.Port >> 8; // ep.Port / 256
962 int h2 = ep.Port % 256;
964 string portParam = ipString + "," + h1 + "," + h2;
965 status = SendCommand (PortCommand, portParam);
967 if (status.StatusCode != FtpStatusCode.CommandOK) {
968 sock.Close ();
969 throw (CreateExceptionFromResponse (status));
972 return sock;
975 void OpenDataConnection ()
977 FtpStatus status;
979 Socket s = InitDataConnection ();
981 // Handle content offset
982 if (offset > 0) {
983 status = SendCommand (RestCommand, offset.ToString ());
984 if (status.StatusCode != FtpStatusCode.FileCommandPending)
985 throw CreateExceptionFromResponse (status);
988 if (method != WebRequestMethods.Ftp.ListDirectory && method != WebRequestMethods.Ftp.ListDirectoryDetails &&
989 method != WebRequestMethods.Ftp.UploadFileWithUniqueName) {
990 status = SendCommand (method, file_name);
991 } else {
992 status = SendCommand (method);
995 if (status.StatusCode != FtpStatusCode.OpeningData && status.StatusCode != FtpStatusCode.DataAlreadyOpen)
996 throw CreateExceptionFromResponse (status);
998 if (usePassive) {
999 origDataStream = new NetworkStream (s, true);
1000 dataStream = origDataStream;
1001 if (EnableSsl)
1002 ChangeToSSLSocket (ref dataStream);
1004 else {
1006 // Active connection (use Socket.Blocking to true)
1007 Socket incoming = null;
1008 try {
1009 incoming = s.Accept ();
1011 catch (SocketException) {
1012 s.Close ();
1013 if (incoming != null)
1014 incoming.Close ();
1016 throw new ProtocolViolationException ("Server commited a protocol violation.");
1019 s.Close ();
1020 origDataStream = new NetworkStream (incoming, true);
1021 dataStream = origDataStream;
1022 if (EnableSsl)
1023 ChangeToSSLSocket (ref dataStream);
1026 ftpResponse.UpdateStatus (status);
1029 void Authenticate ()
1031 string username = null;
1032 string password = null;
1033 string domain = null;
1035 if (credentials != null) {
1036 username = credentials.UserName;
1037 password = credentials.Password;
1038 domain = credentials.Domain;
1041 if (username == null)
1042 username = "anonymous";
1043 if (password == null)
1044 password = "@anonymous";
1045 if (!string.IsNullOrEmpty (domain))
1046 username = domain + '\\' + username;
1048 // Connect to server and get banner message
1049 FtpStatus status = GetResponseStatus ();
1050 ftpResponse.BannerMessage = status.StatusDescription;
1052 if (EnableSsl) {
1053 InitiateSecureConnection (ref controlStream);
1054 controlReader = new StreamReader (controlStream, Encoding.ASCII);
1055 status = SendCommand ("PBSZ", "0");
1056 int st = (int) status.StatusCode;
1057 if (st < 200 || st >= 300)
1058 throw CreateExceptionFromResponse (status);
1059 // TODO: what if "PROT P" is denied by the server? What does MS do?
1060 status = SendCommand ("PROT", "P");
1061 st = (int) status.StatusCode;
1062 if (st < 200 || st >= 300)
1063 throw CreateExceptionFromResponse (status);
1065 status = new FtpStatus (FtpStatusCode.SendUserCommand, "");
1068 if (status.StatusCode != FtpStatusCode.SendUserCommand)
1069 throw CreateExceptionFromResponse (status);
1071 status = SendCommand (UserCommand, username);
1073 switch (status.StatusCode) {
1074 case FtpStatusCode.SendPasswordCommand:
1075 status = SendCommand (PasswordCommand, password);
1076 if (status.StatusCode != FtpStatusCode.LoggedInProceed)
1077 throw CreateExceptionFromResponse (status);
1078 break;
1079 case FtpStatusCode.LoggedInProceed:
1080 break;
1081 default:
1082 throw CreateExceptionFromResponse (status);
1085 ftpResponse.WelcomeMessage = status.StatusDescription;
1086 ftpResponse.UpdateStatus (status);
1089 FtpStatus SendCommand (string command, params string [] parameters) {
1090 return SendCommand (true, command, parameters);
1093 FtpStatus SendCommand (bool waitResponse, string command, params string [] parameters)
1095 byte [] cmd;
1096 string commandString = command;
1097 if (parameters.Length > 0)
1098 commandString += " " + String.Join (" ", parameters);
1100 commandString += EOL;
1101 cmd = dataEncoding.GetBytes (commandString);
1102 try {
1103 controlStream.Write (cmd, 0, cmd.Length);
1104 } catch (IOException) {
1105 //controlStream.Close ();
1106 return new FtpStatus(FtpStatusCode.ServiceNotAvailable, "Write failed");
1109 if(!waitResponse)
1110 return null;
1112 FtpStatus result = GetResponseStatus ();
1113 if (ftpResponse != null)
1114 ftpResponse.UpdateStatus (result);
1115 return result;
1118 internal static FtpStatus ServiceNotAvailable ()
1120 return new FtpStatus (FtpStatusCode.ServiceNotAvailable, Locale.GetText ("Invalid response from server"));
1123 internal FtpStatus GetResponseStatus ()
1125 while (true) {
1126 string response = null;
1128 try {
1129 response = controlReader.ReadLine ();
1130 } catch (IOException) {
1133 if (response == null || response.Length < 3)
1134 return ServiceNotAvailable ();
1136 int code;
1137 if (!Int32.TryParse (response.Substring (0, 3), out code))
1138 return ServiceNotAvailable ();
1140 if (response.Length > 3 && response [3] == '-'){
1141 string line = null;
1142 string find = code.ToString() + ' ';
1143 while (true){
1144 line = null;
1145 try {
1146 line = controlReader.ReadLine();
1147 } catch (IOException) {
1149 if (line == null)
1150 return ServiceNotAvailable ();
1152 response += Environment.NewLine + line;
1154 if (line.StartsWith(find, StringComparison.Ordinal))
1155 break;
1158 return new FtpStatus ((FtpStatusCode) code, response);
1162 private void InitiateSecureConnection (ref Stream stream) {
1163 FtpStatus status = SendCommand (AuthCommand, "TLS");
1164 if (status.StatusCode != FtpStatusCode.ServerWantsSecureSession)
1165 throw CreateExceptionFromResponse (status);
1167 ChangeToSSLSocket (ref stream);
1170 internal bool ChangeToSSLSocket (ref Stream stream) {
1171 #if SECURITY_DEP
1172 var provider = MonoTlsProviderFactory.GetProviderInternal ();
1173 var settings = MSI.MonoTlsSettings.CopyDefaultSettings ();
1174 settings.UseServicePointManagerCallback = true;
1175 var sslStream = provider.CreateSslStream (stream, true, settings);
1176 sslStream.AuthenticateAsClient (requestUri.Host, null, SslProtocols.Default, false);
1177 stream = sslStream.AuthenticatedStream;
1178 return true;
1179 #else
1180 throw new NotImplementedException ();
1181 #endif
1184 bool InFinalState () {
1185 return (State == RequestState.Aborted || State == RequestState.Error || State == RequestState.Finished);
1188 bool InProgress () {
1189 return (State != RequestState.Before && !InFinalState ());
1192 internal void CheckIfAborted () {
1193 if (State == RequestState.Aborted)
1194 throw new WebException ("Request aborted", WebExceptionStatus.RequestCanceled);
1197 void CheckFinalState () {
1198 if (InFinalState ())
1199 throw new InvalidOperationException ("Cannot change final state");