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;
24 public class FtpWebRequest : WebRequest
27 ServicePoint servicePoint;
29 NetworkStream controlStream;
30 StreamReader controlReader;
31 NetworkCredential credentials;
32 IPHostEntry hostEntry;
33 IPEndPoint localEndPoint;
36 int rwTimeout = 300000;
40 bool requestInProgress;
41 bool usePassive = true;
42 bool keepAlive = true;
44 bool transferCompleted;
45 bool gotRequestStream;
46 string method = WebRequestMethods.Ftp.DownloadFile;
48 object locker = new object ();
50 FtpStatusCode statusCode;
51 string statusDescription = String.Empty;
53 FtpAsyncResult asyncRead;
54 FtpAsyncResult asyncWrite;
56 FtpWebResponse ftpResponse;
57 Stream requestStream = Stream.Null;
59 const string UserCommand = "USER";
60 const string PasswordCommand = "PASS";
61 const string TypeCommand = "TYPE";
62 const string PassiveCommand = "PASV";
63 const string PortCommand = "PORT";
64 const string AbortCommand = "ABOR";
65 const string AuthCommand = "AUTH";
66 const string RestCommand = "REST";
67 const string RenameFromCommand = "RNFR";
68 const string RenameToCommand = "RNTO";
69 const string EOL = "\r\n"; // Special end of line
72 static readonly string [] supportedCommands = new string [] {
73 WebRequestMethods.Ftp.AppendFile, // APPE
74 WebRequestMethods.Ftp.DeleteFile, // DELE
75 WebRequestMethods.Ftp.ListDirectoryDetails, // LIST
76 WebRequestMethods.Ftp.GetDateTimestamps, // MDTM
77 WebRequestMethods.Ftp.MakeDirectory, // MKD
78 WebRequestMethods.Ftp.ListDirectory, // NLST
79 WebRequestMethods.Ftp.PrintWorkingDirectory, // PWD
80 WebRequestMethods.Ftp.Rename, // RENAME
81 WebRequestMethods.Ftp.DownloadFile, // RETR
82 WebRequestMethods.Ftp.RemoveDirectory, // RMD
83 WebRequestMethods.Ftp.GetFileSize, // SIZE
84 WebRequestMethods.Ftp.UploadFile, // STOR
85 WebRequestMethods.Ftp.UploadFileWithUniqueName // STUR
88 internal FtpWebRequest (Uri uri)
90 this.requestUri = uri;
91 this.proxy = GlobalProxySelection.Select;
94 public override string ContentType {
96 throw new NotSupportedException ();
99 throw new NotSupportedException ();
103 public override long ContentLength {
112 public long ContentOffset {
117 CheckRequestStarted ();
119 throw new ArgumentOutOfRangeException ();
125 public override ICredentials Credentials {
130 CheckRequestStarted ();
132 throw new ArgumentNullException ();
133 if (!(value is NetworkCredential))
134 throw new ArgumentException ();
136 credentials = value as NetworkCredential;
140 public bool EnableSsl {
145 CheckRequestStarted ();
150 public bool KeepAlive {
155 CheckRequestStarted ();
160 public override string Method {
165 CheckRequestStarted ();
167 throw new ArgumentNullException ("method");
169 if (value.Length == 0 || Array.BinarySearch (supportedCommands, value) < 0)
170 throw new ArgumentException ("Method not supported", "value");
176 public override bool PreAuthenticate {
178 throw new NotSupportedException ();
181 throw new NotSupportedException ();
185 public override IWebProxy Proxy {
190 CheckRequestStarted ();
192 throw new ArgumentNullException ();
198 public int ReadWriteTimeout {
203 CheckRequestStarted ();
206 throw new ArgumentOutOfRangeException ();
212 public string RenameTo {
217 CheckRequestStarted ();
218 if (value == null || value.Length == 0)
219 throw new ArgumentException ("RenameTo value can't be null or empty", "RenameTo");
225 public override Uri RequestUri {
231 public ServicePoint ServicePoint {
233 return GetServicePoint ();
237 public bool UsePassive {
242 CheckRequestStarted ();
247 public bool UseBinary {
251 CheckRequestStarted ();
256 public override int Timeout {
261 CheckRequestStarted ();
264 throw new ArgumentOutOfRangeException ();
272 return binary ? "I" : "A";
276 ServicePoint GetServicePoint ()
278 if (servicePoint == null)
279 servicePoint = ServicePointManager.FindServicePoint (requestUri, proxy);
284 // Probably move some code of command connection here
287 hostEntry = GetServicePoint ().HostEntry;
288 if (hostEntry == null)
294 public override void Abort ()
296 FtpStatusCode status = SendCommand (AbortCommand);
297 if (status != FtpStatusCode.ClosingData)
298 throw CreateExceptionFromResponse (); // Probably ignore it by now
301 if (asyncRead != null) {
302 FtpAsyncResult r = asyncRead;
303 WebException wexc = new WebException ("Request aborted", WebExceptionStatus.RequestCanceled);
304 r.SetCompleted (false, wexc);
308 if (asyncWrite != null) {
309 FtpAsyncResult r = asyncWrite;
310 WebException wexc = new WebException ("Request aborted", WebExceptionStatus.RequestCanceled);
311 r.SetCompleted (false, wexc);
317 void ProcessRequest ()
319 ftpResponse = new FtpWebResponse (requestUri, method, keepAlive);
321 if (!ResolveHost ()) {
322 SetResponseError (new WebException ("The remote server name could not be resolved: " + requestUri,
323 null, WebExceptionStatus.NameResolutionFailure, ftpResponse));
327 if (!OpenControlConnection ())
331 // Open data connection and receive data
332 case WebRequestMethods.Ftp.DownloadFile:
333 case WebRequestMethods.Ftp.ListDirectory:
334 case WebRequestMethods.Ftp.ListDirectoryDetails:
337 // Open data connection and send data
338 case WebRequestMethods.Ftp.AppendFile:
339 case WebRequestMethods.Ftp.UploadFile:
340 case WebRequestMethods.Ftp.UploadFileWithUniqueName:
343 // Get info from control connection
344 case WebRequestMethods.Ftp.GetFileSize:
345 case WebRequestMethods.Ftp.GetDateTimestamps:
346 GetInfoFromControl ();
348 case WebRequestMethods.Ftp.Rename:
351 case WebRequestMethods.Ftp.MakeDirectory:
352 ProcessSimpleRequest ();
354 default: // What to do here?
355 throw new Exception ("Support for command not implemented yet");
359 // Currently I use this only for MKD
360 // (Commands that don't need any parsing in command connection
361 // for open data connection)
362 void ProcessSimpleRequest ()
364 if (SendCommand (method, requestUri.LocalPath) != FtpStatusCode.PathnameCreated) {
365 asyncRead.SetCompleted (true, CreateExceptionFromResponse ());
369 asyncRead.SetCompleted (true, ftpResponse);
372 // It would be good to have a SetCompleted method for
373 // settting asyncRead as completed (some code is here and there, repeated)
374 void GetInfoFromControl ()
376 FtpStatusCode status = SendCommand (method, requestUri.LocalPath);
377 if (status != FtpStatusCode.FileStatus) {
378 asyncRead.SetCompleted (true, CreateExceptionFromResponse ());
382 string desc = statusDescription;
383 Console.WriteLine ("Desc = " + desc);
384 if (method == WebRequestMethods.Ftp.GetFileSize) {
387 for (i = 4, len = 0; i < desc.Length && Char.IsDigit (desc [i]); i++, len++)
391 asyncRead.SetCompleted (true, new WebException ("Bad format for server response in " + method));
395 if (!Int64.TryParse (desc.Substring (4, len), out size)) {
396 asyncRead.SetCompleted (true, new WebException ("Bad format for server response in " + method));
400 ftpResponse.contentLength = size;
401 asyncRead.SetCompleted (true, ftpResponse);
405 if (method == WebRequestMethods.Ftp.GetDateTimestamps) {
406 // Here parse the format the date time (different formats)
407 asyncRead.SetCompleted (true, ftpResponse);
411 throw new Exception ("You shouldn't reach this point");
416 FtpStatusCode status = SendCommand (RenameFromCommand, requestUri.LocalPath);
417 if (status == FtpStatusCode.FileCommandPending) {
418 // Pass an empty string if RenameTo wasn't specified
419 status = SendCommand (RenameToCommand, renameTo != null ? renameTo : String.Empty);
421 if (status == FtpStatusCode.FileActionOK) {
422 ftpResponse.UpdateStatus (statusCode, statusDescription);
423 asyncRead.SetCompleted (true, ftpResponse);
428 ftpResponse.UpdateStatus (statusCode, statusDescription);
429 asyncRead.SetCompleted (true, CreateExceptionFromResponse ());
434 if (gotRequestStream) {
435 if (GetResponseCode () != FtpStatusCode.ClosingData)
436 asyncRead.SetCompleted (true, CreateExceptionFromResponse ());
441 if (!OpenDataConnection ())
444 gotRequestStream = true;
445 requestStream = new FtpDataStream (this, dataSocket, false);
446 asyncWrite.SetCompleted (true, requestStream);
451 FtpStatusCode status;
453 // Handle content offset
455 status = SendCommand (RestCommand, offset.ToString ());
456 if (status != FtpStatusCode.FileCommandPending) {
457 asyncRead.SetCompleted (true, CreateExceptionFromResponse ());
462 if (!OpenDataConnection ())
465 ftpResponse.Stream = new FtpDataStream (this, dataSocket, true);
466 ftpResponse.StatusDescription = statusDescription;
467 ftpResponse.StatusCode = statusCode;
468 asyncRead.SetCompleted (true, ftpResponse);
471 public override IAsyncResult BeginGetResponse (AsyncCallback callback, object state)
474 throw new WebException ("Request was previously aborted.");
476 Monitor.Enter (this);
477 if (asyncRead != null) {
479 throw new InvalidOperationException ();
482 requestInProgress = true;
483 asyncRead = new FtpAsyncResult (callback, state);
484 Thread thread = new Thread (ProcessRequest);
491 public override WebResponse EndGetResponse (IAsyncResult asyncResult)
493 if (asyncResult == null)
494 throw new ArgumentNullException ("asyncResult");
496 if (!(asyncResult is FtpAsyncResult) || asyncResult != asyncRead)
497 throw new ArgumentException ("asyncResult");
499 FtpAsyncResult asyncFtpResult = (FtpAsyncResult) asyncResult;
500 if (!asyncFtpResult.WaitUntilComplete (timeout, false)) {
502 throw new WebException ("Transfer timed out.", WebExceptionStatus.Timeout);
505 if (asyncFtpResult.GotException)
506 throw asyncFtpResult.Exception;
508 return asyncFtpResult.Response;
511 public override WebResponse GetResponse ()
513 IAsyncResult asyncResult = BeginGetResponse (null, null);
514 return EndGetResponse (asyncResult);
517 public override IAsyncResult BeginGetRequestStream (AsyncCallback callback, object state)
520 throw new WebException ("Request was previously aborted.");
522 if (method != WebRequestMethods.Ftp.UploadFile && method != WebRequestMethods.Ftp.UploadFileWithUniqueName &&
523 method != WebRequestMethods.Ftp.AppendFile)
524 throw new ProtocolViolationException ();
527 if (asyncWrite != null || asyncRead != null)
528 throw new InvalidOperationException ();
530 requestInProgress = true;
531 asyncWrite = new FtpAsyncResult (callback, state);
532 Thread thread = new Thread (ProcessRequest);
539 public override Stream EndGetRequestStream (IAsyncResult asyncResult)
541 if (asyncResult == null)
542 throw new ArgumentNullException ("asyncResult");
544 if (!(asyncResult is FtpAsyncResult))
545 throw new ArgumentException ("asyncResult");
547 FtpAsyncResult res = (FtpAsyncResult) asyncResult;
548 if (!res.WaitUntilComplete (timeout, false)) {
550 throw new WebException ("Request timeod out");
553 if (res.GotException)
559 public override Stream GetRequestStream ()
561 IAsyncResult asyncResult = BeginGetRequestStream (null, null);
562 return EndGetRequestStream (asyncResult);
565 void CheckRequestStarted ()
567 if (requestInProgress)
568 throw new InvalidOperationException ("request in progress");
571 bool OpenControlConnection ()
574 foreach (IPAddress address in hostEntry.AddressList) {
575 sock = new Socket (address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
577 sock.Connect (new IPEndPoint (address, requestUri.Port));
578 localEndPoint = (IPEndPoint) sock.LocalEndPoint;
580 } catch (SocketException e) {
586 // Couldn't connect to any address
588 SetResponseError (new WebException ("Unable to connect to remote server", null,
589 WebExceptionStatus.UnknownError, ftpResponse));
593 controlStream = new NetworkStream (sock);
594 controlReader = new StreamReader (controlStream, Encoding.ASCII);
596 if (!Authenticate ()) {
597 SetResponseError (CreateExceptionFromResponse ());
604 // Probably we could do better having here a regex
605 Socket SetupPassiveConnection ()
607 // Current response string
608 string response = statusDescription;
609 if (response.Length < 4)
612 // Look for first digit after code
614 for (i = 3; i < response.Length && !Char.IsDigit (response [i]); i++)
616 if (i >= response.Length)
620 string [] digits = response.Substring (i).Split (new char [] {','}, 6);
621 if (digits.Length != 6)
624 // Clean non-digits at the end of last element
626 for (j = digits [5].Length - 1; j >= 0 && !Char.IsDigit (digits [5][j]); j--)
631 digits [5] = digits [5].Substring (0, j + 1);
635 ip = IPAddress.Parse (String.Join (".", digits, 0, 4));
636 } catch (FormatException) {
642 if (!Int32.TryParse (digits [4], out p1) || !Int32.TryParse (digits [5], out p2))
645 port = (p1 << 8) + p2; // p1 * 256 + p2
646 //port = p1 * 256 + p2;
647 if (port < IPEndPoint.MinPort || port > IPEndPoint.MaxPort)
650 IPEndPoint ep = new IPEndPoint (ip, port);
651 Socket sock = new Socket (ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
654 } catch (SocketException exc) {
662 Exception CreateExceptionFromResponse ()
664 WebException exc = new WebException ("Server returned an error: " + statusDescription, null,
665 WebExceptionStatus.ProtocolError, ftpResponse);
669 // Here we could also get a server error, so be cautious
670 internal void SetTransferCompleted ()
672 if (transferCompleted)
675 transferCompleted = true;
677 FtpStatusCode status = GetResponseCode ();
678 ftpResponse.StatusCode = status;
679 ftpResponse.StatusDescription = statusDescription;
682 internal void SetResponseError (Exception exc)
684 FtpAsyncResult ar = asyncRead;
688 ar.SetCompleted (true, exc);
692 Socket InitDataConnection ()
694 FtpStatusCode status;
697 status = SendCommand (PassiveCommand);
698 if (status != FtpStatusCode.EnteringPassive) {
699 SetResponseError (CreateExceptionFromResponse ());
703 Socket retval = SetupPassiveConnection ();
705 SetResponseError (new WebException ("Couldn't setup passive connection"));
710 // Open a socket to listen the server's connection
711 Socket sock = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
713 sock.Bind (new IPEndPoint (localEndPoint.Address, 0));
714 sock.Listen (1); // We only expect a connection from server
716 } catch (SocketException e) {
719 SetResponseError (new WebException ("Couldn't open listening socket on client", e));
723 IPEndPoint ep = (IPEndPoint) sock.LocalEndPoint;
724 string ipString = ep.Address.ToString ().Replace (".", ",");
725 int h1 = ep.Port >> 8; // ep.Port / 256
726 int h2 = ep.Port % 256;
728 string portParam = ipString + "," + h1 + "," + h2;
729 status = SendCommand (PortCommand, portParam);
730 if (status != FtpStatusCode.CommandOK) {
733 SetResponseError (CreateExceptionFromResponse ());
740 bool OpenDataConnection ()
742 FtpStatusCode status;
743 Socket s = InitDataConnection ();
747 // TODO - Check that this command is only used for data connection based commands
748 if (method != WebRequestMethods.Ftp.ListDirectory && method != WebRequestMethods.Ftp.ListDirectoryDetails) {
749 status = SendCommand (TypeCommand, DataType);
751 if (status != FtpStatusCode.CommandOK) {
752 SetResponseError (CreateExceptionFromResponse ());
757 status = SendCommand (method, requestUri.LocalPath);
758 if (status != FtpStatusCode.OpeningData) {
759 SetResponseError (CreateExceptionFromResponse ());
768 // Active connection (use Socket.Blocking to true)
769 Socket incoming = null;
771 incoming = s.Accept ();
772 } catch (SocketException e) {
774 if (incoming != null)
777 SetResponseError (new ProtocolViolationException ("Server commited a protocol violation."));
782 dataSocket = incoming;
786 // Take in count 'account' case
789 string username = null;
790 string password = null;
792 if (credentials != null) {
793 username = credentials.UserName;
794 password = credentials.Password;
795 // account = credentials.Domain;
798 if (username == null)
799 username = "anonymous";
800 if (password == null)
801 password = "@anonymous";
803 // Connect to server and get banner message
804 FtpStatusCode status = GetResponseCode ();
805 ftpResponse.BannerMessage = statusDescription;
806 if (status != FtpStatusCode.SendUserCommand)
809 status = SendCommand (UserCommand, username);
810 if (status == FtpStatusCode.LoggedInProceed) {
811 ftpResponse.WelcomeMessage = statusDescription;
814 if (status == FtpStatusCode.SendPasswordCommand) {
815 status = SendCommand (PasswordCommand, password);
816 if (status == FtpStatusCode.LoggedInProceed) {
817 ftpResponse.WelcomeMessage = statusDescription;
827 FtpStatusCode SendCommand (string command, params string [] parameters)
830 string commandString = command;
831 if (parameters.Length > 0)
832 commandString += " " + String.Join (" ", parameters);
834 commandString += EOL;
835 cmd = Encoding.ASCII.GetBytes (commandString);
837 controlStream.Write (cmd, 0, cmd.Length);
838 } catch (IOException) {
839 //controlStream.Close ();
840 return FtpStatusCode.ServiceNotAvalaible;
843 return GetResponseCode ();
846 internal FtpStatusCode GetResponseCode ()
848 string responseString = null;
850 responseString = controlReader.ReadLine ();
851 } catch (IOException exc) {
852 // controlReader.Close ();
855 if (responseString == null || responseString.Length < 3)
856 return FtpStatusCode.ServiceNotAvalaible;
858 string codeString = responseString.Substring (0, 3);
860 if (!Int32.TryParse (codeString, out code))
861 return FtpStatusCode.ServiceNotAvalaible;
863 statusDescription = responseString;
864 return statusCode = (FtpStatusCode) code;