2 // System.Net.FtpWebRequest.cs
5 // Carlos Alberto Cortez (calberto.cortez@gmail.com)
7 // (c) Copyright 2006 Novell, Inc. (http://www.novell.com)
12 using System.Net.Sockets;
14 using System.Threading;
16 using System.Net.Cache;
17 using System.Security.Cryptography.X509Certificates;
25 public sealed class FtpWebRequest : WebRequest
28 ServicePoint servicePoint;
30 NetworkStream controlStream;
31 StreamReader controlReader;
32 NetworkCredential credentials;
33 IPHostEntry hostEntry;
34 IPEndPoint localEndPoint;
37 int rwTimeout = 300000;
40 bool enableSsl = false;
41 bool usePassive = true;
42 bool keepAlive = true;
43 string method = WebRequestMethods.Ftp.DownloadFile;
45 object locker = new object ();
47 RequestState requestState = RequestState.Before;
48 FtpAsyncResult asyncResult;
49 FtpWebResponse ftpResponse;
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
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
96 internal FtpWebRequest (Uri uri)
98 this.requestUri = uri;
99 this.proxy = GlobalProxySelection.Select;
103 static Exception GetMustImplement ()
105 return new NotImplementedException ();
109 public X509CertificateCollection ClientCertificates
112 throw GetMustImplement ();
115 throw GetMustImplement ();
120 public override string ConnectionGroupName
123 throw GetMustImplement ();
126 throw GetMustImplement ();
131 public override string ContentType {
133 throw new NotSupportedException ();
136 throw new NotSupportedException ();
140 public override long ContentLength {
149 public long ContentOffset {
154 CheckRequestStarted ();
156 throw new ArgumentOutOfRangeException ();
162 public override ICredentials Credentials {
167 CheckRequestStarted ();
169 throw new ArgumentNullException ();
170 if (!(value is NetworkCredential))
171 throw new ArgumentException ();
173 credentials = value as NetworkCredential;
179 public static RequestCachePolicy DefaultCachePolicy
182 throw GetMustImplement ();
185 throw GetMustImplement ();
190 public bool EnableSsl {
195 CheckRequestStarted ();
202 public override WebHeaderCollection Headers
205 throw GetMustImplement ();
208 throw GetMustImplement ();
213 public bool KeepAlive {
218 CheckRequestStarted ();
223 public override string Method {
228 CheckRequestStarted ();
230 throw new ArgumentNullException ("Method string cannot be null");
232 if (value.Length == 0 || Array.BinarySearch (supportedCommands, value) < 0)
233 throw new ArgumentException ("Method not supported", "value");
239 public override bool PreAuthenticate {
241 throw new NotSupportedException ();
244 throw new NotSupportedException ();
248 public override IWebProxy Proxy {
253 CheckRequestStarted ();
255 throw new ArgumentNullException ();
261 public int ReadWriteTimeout {
266 CheckRequestStarted ();
269 throw new ArgumentOutOfRangeException ();
275 public string RenameTo {
280 CheckRequestStarted ();
281 if (value == null || value.Length == 0)
282 throw new ArgumentException ("RenameTo value can't be null or empty", "RenameTo");
288 public override Uri RequestUri {
294 public ServicePoint ServicePoint {
296 return GetServicePoint ();
300 public bool UsePassive {
305 CheckRequestStarted ();
312 public override bool UseDefaultCredentials
315 throw GetMustImplement ();
318 throw GetMustImplement ();
323 public bool UseBinary {
327 CheckRequestStarted ();
332 public override int Timeout {
337 CheckRequestStarted ();
340 throw new ArgumentOutOfRangeException ();
348 return binary ? "I" : "A";
363 requestState = value;
368 public override void Abort () {
370 if (State == RequestState.TransferInProgress) {
371 /*FtpStatus status = */
372 SendCommand (false, AbortCommand);
375 if (!InFinalState ()) {
376 State = RequestState.Aborted;
377 ftpResponse = new FtpWebResponse (requestUri, method, FtpStatusCode.FileActionAborted, "Aborted by request");
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");
389 asyncResult = new FtpAsyncResult (callback, state);
393 asyncResult.SetCompleted (true, ftpResponse);
395 if (State == RequestState.Before)
396 State = RequestState.Scheduled;
398 Thread thread = new Thread (ProcessRequest);
406 public override WebResponse EndGetResponse (IAsyncResult asyncResult) {
407 if (asyncResult == null)
408 throw new ArgumentNullException ("AsyncResult cannot be null!");
410 if (!(asyncResult is FtpAsyncResult) || asyncResult != this.asyncResult)
411 throw new ArgumentException ("AsyncResult is from another request!");
413 FtpAsyncResult asyncFtpResult = (FtpAsyncResult) asyncResult;
414 if (!asyncFtpResult.WaitUntilComplete (timeout, false)) {
416 throw new WebException ("Transfer timed out.", WebExceptionStatus.Timeout);
423 if (asyncFtpResult.GotException)
424 throw asyncFtpResult.Exception;
426 return asyncFtpResult.Response;
429 public override WebResponse GetResponse () {
430 IAsyncResult asyncResult = BeginGetResponse (null, null);
431 return EndGetResponse (asyncResult);
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 ();
442 if (State != RequestState.Before)
443 throw new InvalidOperationException ("Cannot re-call BeginGetRequestStream/BeginGetResponse while a previous call is still in progress");
445 State = RequestState.Scheduled;
448 asyncResult = new FtpAsyncResult (callback, state);
449 Thread thread = new Thread (ProcessRequest);
455 public override Stream EndGetRequestStream (IAsyncResult asyncResult) {
456 if (asyncResult == null)
457 throw new ArgumentNullException ("asyncResult");
459 if (!(asyncResult is FtpAsyncResult))
460 throw new ArgumentException ("asyncResult");
462 if (State == RequestState.Aborted) {
463 throw new WebException ("Request aborted", WebExceptionStatus.RequestCanceled);
466 if (asyncResult != this.asyncResult)
467 throw new ArgumentException ("AsyncResult is from another request!");
469 FtpAsyncResult res = (FtpAsyncResult) asyncResult;
471 if (!res.WaitUntilComplete (timeout, false)) {
473 throw new WebException ("Request timed out");
476 if (res.GotException)
482 public override Stream GetRequestStream () {
483 IAsyncResult asyncResult = BeginGetRequestStream (null, null);
484 return EndGetRequestStream (asyncResult);
487 ServicePoint GetServicePoint ()
489 if (servicePoint == null)
490 servicePoint = ServicePointManager.FindServicePoint (requestUri, proxy);
495 // Probably move some code of command connection here
499 hostEntry = GetServicePoint ().HostEntry;
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);
508 void ProcessRequest () {
510 if (State == RequestState.Scheduled) {
511 ftpResponse = new FtpWebResponse (requestUri, method, keepAlive);
515 //State = RequestState.Finished;
516 //finalResponse = ftpResponse;
517 asyncResult.SetCompleted (false, ftpResponse);
519 catch (Exception e) {
520 State = RequestState.Error;
521 SetCompleteWithError (e);
526 FtpStatus status = GetResponseStatus ();
528 ftpResponse.UpdateStatus (status);
530 if (ftpResponse.IsFinal ()) {
531 State = RequestState.Finished;
535 asyncResult.SetCompleted (false, ftpResponse);
539 void ProcessMethod ()
541 State = RequestState.Connecting;
545 OpenControlConnection ();
548 // Open data connection and receive data
549 case WebRequestMethods.Ftp.DownloadFile:
550 case WebRequestMethods.Ftp.ListDirectory:
551 case WebRequestMethods.Ftp.ListDirectoryDetails:
554 // Open data connection and send data
555 case WebRequestMethods.Ftp.AppendFile:
556 case WebRequestMethods.Ftp.UploadFile:
557 case WebRequestMethods.Ftp.UploadFileWithUniqueName:
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 ();
568 default: // What to do here?
569 throw new Exception (String.Format ("Support for command {0} not implemented yet", method));
575 private void CloseControlConnection () {
576 SendCommand (QuitCommand);
577 controlStream.Close ();
580 private void CloseDataConnection () {
581 if(dataSocket != null)
585 private void CloseConnection () {
586 CloseControlConnection ();
587 CloseDataConnection ();
590 void ProcessSimpleMethod ()
592 State = RequestState.TransferInProgress;
596 if (method == WebRequestMethods.Ftp.PrintWorkingDirectory)
599 if (method == WebRequestMethods.Ftp.Rename)
600 method = RenameFromCommand;
602 status = SendCommand (method, requestUri.LocalPath);
604 ftpResponse.Stream = new EmptyStream ();
606 string desc = status.StatusDescription;
609 case WebRequestMethods.Ftp.GetFileSize: {
610 if (status.StatusCode != FtpStatusCode.FileStatus)
611 throw CreateExceptionFromResponse (status);
615 for (i = 4, len = 0; i < desc.Length && Char.IsDigit (desc [i]); i++, len++)
619 throw new WebException ("Bad format for server response in " + method);
621 if (!Int64.TryParse (desc.Substring (4, len), out size))
622 throw new WebException ("Bad format for server response in " + method);
624 ftpResponse.contentLength = size;
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);
632 case WebRequestMethods.Ftp.MakeDirectory:
633 if (status.StatusCode != FtpStatusCode.PathnameCreated)
634 throw CreateExceptionFromResponse (status);
637 method = WebRequestMethods.Ftp.PrintWorkingDirectory;
639 if (status.StatusCode != FtpStatusCode.FileActionOK)
640 throw CreateExceptionFromResponse (status);
642 status = SendCommand (method);
644 if (status.StatusCode != FtpStatusCode.PathnameCreated)
645 throw CreateExceptionFromResponse (status);
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);
658 ftpResponse.UpdateStatus (status);
659 State = RequestState.Finished;
664 State = RequestState.OpeningData;
666 OpenDataConnection ();
668 State = RequestState.TransferInProgress;
669 requestStream = new FtpDataStream (this, dataSocket, false);
670 asyncResult.Stream = requestStream;
675 State = RequestState.OpeningData;
677 // Handle content offset
679 FtpStatus status = SendCommand (RestCommand, offset.ToString ());
681 if (status.StatusCode != FtpStatusCode.FileCommandPending)
682 throw CreateExceptionFromResponse (status);
685 OpenDataConnection ();
687 State = RequestState.TransferInProgress;
688 ftpResponse.Stream = new FtpDataStream (this, dataSocket, true);
691 void CheckRequestStarted ()
693 if (State != RequestState.Before)
694 throw new InvalidOperationException ("There is a request currently in progress");
697 void OpenControlConnection ()
700 foreach (IPAddress address in hostEntry.AddressList) {
701 sock = new Socket (address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
703 sock.Connect (new IPEndPoint (address, requestUri.Port));
704 localEndPoint = (IPEndPoint) sock.LocalEndPoint;
706 } catch (SocketException) {
712 // Couldn't connect to any address
714 throw new WebException ("Unable to connect to remote server", null,
715 WebExceptionStatus.UnknownError, ftpResponse);
717 controlStream = new NetworkStream (sock);
718 controlReader = new StreamReader (controlStream, Encoding.ASCII);
720 State = RequestState.Authenticating;
725 // Probably we could do better having here a regex
726 Socket SetupPassiveConnection (string statusDescription)
728 // Current response string
729 string response = statusDescription;
730 if (response.Length < 4)
731 throw new WebException ("Cannot open passive data connection");
733 // Look for first digit after code
735 for (i = 3; i < response.Length && !Char.IsDigit (response [i]); i++)
737 if (i >= response.Length)
738 throw new WebException ("Cannot open passive data connection");
741 string [] digits = response.Substring (i).Split (new char [] {','}, 6);
742 if (digits.Length != 6)
743 throw new WebException ("Cannot open passive data connection");
745 // Clean non-digits at the end of last element
747 for (j = digits [5].Length - 1; j >= 0 && !Char.IsDigit (digits [5][j]); j--)
750 throw new WebException ("Cannot open passive data connection");
752 digits [5] = digits [5].Substring (0, j + 1);
756 ip = IPAddress.Parse (String.Join (".", digits, 0, 4));
757 } catch (FormatException) {
758 throw new WebException ("Cannot open passive data connection");
763 if (!Int32.TryParse (digits [4], out p1) || !Int32.TryParse (digits [5], out p2))
764 throw new WebException ("Cannot open passive data connection");
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");
771 IPEndPoint ep = new IPEndPoint (ip, port);
772 Socket sock = new Socket (ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
775 } catch (SocketException) {
777 throw new WebException ("Cannot open passive data connection");
783 Exception CreateExceptionFromResponse (FtpStatus status)
785 FtpWebResponse ftpResponse = new FtpWebResponse (requestUri, method, status);
787 WebException exc = new WebException ("Server returned an error: " + status.StatusDescription,
788 null, WebExceptionStatus.ProtocolError, ftpResponse);
792 // Here we could also get a server error, so be cautious
793 internal void SetTransferCompleted ()
798 State = RequestState.Finished;
799 FtpStatus status = GetResponseStatus ();
801 ftpResponse.UpdateStatus (status);
807 void SetCompleteWithError (Exception exc)
809 if (asyncResult != null) {
810 asyncResult.SetCompleted (false, exc);
814 Socket InitDataConnection ()
819 status = SendCommand (PassiveCommand);
820 if (status.StatusCode != FtpStatusCode.EnteringPassive) {
821 throw CreateExceptionFromResponse (status);
824 return SetupPassiveConnection (status.StatusDescription);
827 // Open a socket to listen the server's connection
828 Socket sock = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
830 sock.Bind (new IPEndPoint (localEndPoint.Address, 0));
831 sock.Listen (1); // We only expect a connection from server
833 } catch (SocketException e) {
836 throw new WebException ("Couldn't open listening socket on client", e);
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;
844 string portParam = ipString + "," + h1 + "," + h2;
845 status = SendCommand (PortCommand, portParam);
847 if (status.StatusCode != FtpStatusCode.CommandOK) {
849 throw (CreateExceptionFromResponse (status));
855 void OpenDataConnection ()
859 Socket s = InitDataConnection ();
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);
865 if (status.StatusCode != FtpStatusCode.CommandOK)
866 throw CreateExceptionFromResponse (status);
869 if(method != WebRequestMethods.Ftp.UploadFileWithUniqueName)
870 status = SendCommand (method, Uri.UnescapeDataString (requestUri.LocalPath));
872 status = SendCommand (method);
874 if (status.StatusCode != FtpStatusCode.OpeningData && status.StatusCode != FtpStatusCode.DataAlreadyOpen)
875 throw CreateExceptionFromResponse (status);
882 // Active connection (use Socket.Blocking to true)
883 Socket incoming = null;
885 incoming = s.Accept ();
887 catch (SocketException) {
889 if (incoming != null)
892 throw new ProtocolViolationException ("Server commited a protocol violation.");
896 dataSocket = incoming;
900 InitiateSecureConnection (ref controlStream);
901 controlReader = new StreamReader (controlStream, Encoding.ASCII);
904 ftpResponse.UpdateStatus (status);
907 void Authenticate () {
908 string username = null;
909 string password = null;
910 string domain = null;
912 if (credentials != null) {
913 username = credentials.UserName;
914 password = credentials.Password;
915 domain = credentials.Domain;
918 if (username == null)
919 username = "anonymous";
920 if (password == null)
921 password = "@anonymous";
922 if (!string.IsNullOrEmpty (domain))
923 username = domain + '\\' + username;
925 // Connect to server and get banner message
926 FtpStatus status = GetResponseStatus ();
927 ftpResponse.BannerMessage = status.StatusDescription;
930 InitiateSecureConnection (ref controlStream);
931 controlReader = new StreamReader (controlStream, Encoding.ASCII);
934 if (status.StatusCode != FtpStatusCode.SendUserCommand)
935 throw CreateExceptionFromResponse (status);
937 status = SendCommand (UserCommand, username);
939 switch (status.StatusCode) {
940 case FtpStatusCode.SendPasswordCommand:
941 status = SendCommand (PasswordCommand, password);
942 if (status.StatusCode != FtpStatusCode.LoggedInProceed)
943 throw CreateExceptionFromResponse (status);
945 case FtpStatusCode.LoggedInProceed:
948 throw CreateExceptionFromResponse (status);
951 ftpResponse.WelcomeMessage = status.StatusDescription;
952 ftpResponse.UpdateStatus (status);
955 FtpStatus SendCommand (string command, params string [] parameters) {
956 return SendCommand (true, command, parameters);
959 FtpStatus SendCommand (bool waitResponse, string command, params string [] parameters)
962 string commandString = command;
963 if (parameters.Length > 0)
964 commandString += " " + String.Join (" ", parameters);
966 commandString += EOL;
967 cmd = Encoding.ASCII.GetBytes (commandString);
969 controlStream.Write (cmd, 0, cmd.Length);
970 } catch (IOException) {
971 //controlStream.Close ();
972 return new FtpStatus(FtpStatusCode.ServiceNotAvailable, "Write failed");
978 return GetResponseStatus ();
981 internal FtpStatus GetResponseStatus ()
984 string responseString = null;
987 responseString = controlReader.ReadLine ();
989 catch (IOException) {
990 // controlReader.Close ();
993 if (responseString == null || responseString.Length < 3)
994 return new FtpStatus(FtpStatusCode.ServiceNotAvailable, "Invalid response from server");
996 string codeString = responseString.Substring (0, 3);
999 if (!Int32.TryParse (codeString, out code))
1000 return new FtpStatus (FtpStatusCode.ServiceNotAvailable, "Invalid response from server");
1002 if (responseString.Length < 4 || responseString [3] != '-')
1003 return new FtpStatus ((FtpStatusCode) code, responseString);
1007 private void InitiateSecureConnection (ref NetworkStream stream) {
1008 FtpStatus status = SendCommand (AuthCommand, "TLS");
1010 if (status.StatusCode != FtpStatusCode.ServerWantsSecureSession) {
1011 throw CreateExceptionFromResponse (status);
1014 ChangeToSSLSocket (ref stream);
1017 internal static bool ChangeToSSLSocket (ref NetworkStream stream) {
1019 stream.ChangeToSSLSocket ();
1022 throw new NotImplementedException ();
1026 bool InFinalState () {
1027 return (State == RequestState.Aborted || State == RequestState.Error || State == RequestState.Finished);
1030 bool InProgress () {
1031 return (State != RequestState.Before && !InFinalState ());
1034 internal void CheckIfAborted () {
1035 if (State == RequestState.Aborted)
1036 throw new WebException ("Request aborted", WebExceptionStatus.RequestCanceled);
1039 void CheckFinalState () {
1040 if (InFinalState ())
1041 throw new InvalidOperationException ("Cannot change final state");
1044 class EmptyStream : MemoryStream
1046 internal EmptyStream ()
1047 : base (new byte [0], false) {