New tests.
[mono.git] / mcs / class / System / System.Net / FtpWebRequest.cs
index 413c6b0cd6eeb2b047144488c827f245439ac510..f766fc29a50d88d7e00fc3b7482bee4cabdbe176 100644 (file)
@@ -6,28 +6,28 @@
 //
 // (c) Copyright 2006 Novell, Inc. (http://www.novell.com)
 //
+#if NET_2_0
 
 using System;
 using System.IO;
 using System.Net.Sockets;
 using System.Text;
 using System.Threading;
-#if NET_2_0
 using System.Net.Cache;
-#endif
+using System.Security.Cryptography.X509Certificates;
 using System.Net;
-
-#if NET_2_0
+using System.Net.Security;
+using System.Security.Authentication;
 
 namespace System.Net
 {
-       [Serializable]
-       public class FtpWebRequest : WebRequest
+       public sealed class FtpWebRequest : WebRequest
        {
                Uri requestUri;
+               string file_name; // By now, used for upload
                ServicePoint servicePoint;
-               Socket dataSocket;
-               NetworkStream controlStream;
+               Stream dataStream;
+               Stream controlStream;
                StreamReader controlReader;
                NetworkCredential credentials;
                IPHostEntry hostEntry;
@@ -39,7 +39,7 @@ namespace System.Net
                bool binary = true;
                bool enableSsl = false;
                bool usePassive = true;
-               bool keepAlive = true;
+               bool keepAlive = false;
                string method = WebRequestMethods.Ftp.DownloadFile;
                string renameTo;
                object locker = new object ();
@@ -48,6 +48,7 @@ namespace System.Net
                FtpAsyncResult asyncResult;
                FtpWebResponse ftpResponse;
                Stream requestStream;
+               string initial_path;
 
                const string ChangeDir = "CWD";
                const string UserCommand = "USER";
@@ -99,6 +100,33 @@ namespace System.Net
                        this.proxy = GlobalProxySelection.Select;
                }
 
+               static Exception GetMustImplement ()
+               {
+                       return new NotImplementedException ();
+               }
+               
+               [MonoTODO]
+               public X509CertificateCollection ClientCertificates
+               {
+                       get {
+                               throw GetMustImplement ();
+                       }
+                       set {
+                               throw GetMustImplement ();
+                       }
+               }
+               
+               [MonoTODO]
+               public override string ConnectionGroupName
+               {
+                       get {
+                               throw GetMustImplement ();
+                       }
+                       set {
+                               throw GetMustImplement ();
+                       }
+               }
+
                public override string ContentType {
                        get {
                                throw new NotSupportedException ();
@@ -145,6 +173,17 @@ namespace System.Net
                        }
                }
 
+               [MonoTODO]
+               public static new RequestCachePolicy DefaultCachePolicy
+               {
+                       get {
+                               throw GetMustImplement ();
+                       }
+                       set {
+                               throw GetMustImplement ();
+                       }
+               }
+
                public bool EnableSsl {
                        get {
                                return enableSsl;
@@ -155,13 +194,25 @@ namespace System.Net
                        }
                }
 
+               [MonoTODO]
+               public override WebHeaderCollection Headers
+               {
+                       get {
+                               throw GetMustImplement ();
+                       }
+                       set {
+                               throw GetMustImplement ();
+                       }
+               }
+
+               [MonoTODO ("We don't support KeepAlive = true")]
                public bool KeepAlive {
                        get {
                                return keepAlive;
                        }
                        set {
                                CheckRequestStarted ();
-                               keepAlive = value;
+                               //keepAlive = value;
                        }
                }
 
@@ -251,6 +302,17 @@ namespace System.Net
                                usePassive = value;
                        }
                }
+
+               [MonoTODO]
+               public override bool UseDefaultCredentials
+               {
+                       get {
+                               throw GetMustImplement ();
+                       }
+                       set {
+                               throw GetMustImplement ();
+                       }
+               }
                
                public bool UseBinary {
                        get {
@@ -306,7 +368,7 @@ namespace System.Net
 
                                if (!InFinalState ()) {
                                        State = RequestState.Aborted;
-                                       ftpResponse = new FtpWebResponse (requestUri, method, FtpStatusCode.FileActionAborted, "Aborted by request");
+                                       ftpResponse = new FtpWebResponse (this, requestUri, method, FtpStatusCode.FileActionAborted, "Aborted by request");
                                }
                        }
                }
@@ -440,7 +502,7 @@ namespace System.Net
                void ProcessRequest () {
 
                        if (State == RequestState.Scheduled) {
-                               ftpResponse = new FtpWebResponse (requestUri, method, keepAlive);
+                               ftpResponse = new FtpWebResponse (this, requestUri, method, keepAlive);
 
                                try {
                                        ProcessMethod ();
@@ -467,7 +529,53 @@ namespace System.Net
                                asyncResult.SetCompleted (false, ftpResponse);
                        }
                }
-               
+
+               void SetType ()
+               {
+                       if (binary) {
+                               FtpStatus status = SendCommand (TypeCommand, DataType);
+                               if ((int) status.StatusCode < 200 || (int) status.StatusCode >= 300)
+                                       throw CreateExceptionFromResponse (status);
+                       }
+               }
+
+               string GetRemoteFolderPath (Uri uri)
+               {
+                       string result;
+                       string local_path = Uri.UnescapeDataString (uri.LocalPath);
+                       if (initial_path == null || initial_path == "/") {
+                               result = local_path;
+                       } else {
+                               if (local_path [0] == '/')
+                                       local_path = local_path.Substring (1);
+
+                               Uri initial = new Uri ("ftp://dummy-host" + initial_path);
+                               result = new Uri (initial, local_path).LocalPath;
+                       }
+
+                       int last = result.LastIndexOf ('/');
+                       if (last == -1)
+                               return null;
+
+                       return result.Substring (0, last + 1);
+               }
+
+               void CWDAndSetFileName (Uri uri)
+               {
+                       string remote_folder = GetRemoteFolderPath (uri);
+                       FtpStatus status;
+                       if (remote_folder != null) {
+                               status = SendCommand (ChangeDir, remote_folder);
+                               if ((int) status.StatusCode < 200 || (int) status.StatusCode >= 300)
+                                       throw CreateExceptionFromResponse (status);
+
+                               int last = uri.LocalPath.LastIndexOf ('/');
+                               if (last >= 0) {
+                                       file_name = Uri.UnescapeDataString (uri.LocalPath.Substring (last + 1));
+                               }
+                       }
+               }
+
                void ProcessMethod ()
                {
                        State = RequestState.Connecting;
@@ -475,6 +583,8 @@ namespace System.Net
                        ResolveHost ();
 
                        OpenControlConnection ();
+                       CWDAndSetFileName (requestUri);
+                       SetType ();
 
                        switch (method) {
                        // Open data connection and receive data
@@ -495,6 +605,7 @@ namespace System.Net
                        case WebRequestMethods.Ftp.PrintWorkingDirectory:
                        case WebRequestMethods.Ftp.MakeDirectory:
                        case WebRequestMethods.Ftp.Rename:
+                       case WebRequestMethods.Ftp.DeleteFile:
                                ProcessSimpleMethod ();
                                break;
                        default: // What to do here?
@@ -505,13 +616,18 @@ namespace System.Net
                }
 
                private void CloseControlConnection () {
-                       SendCommand (QuitCommand);
-                       controlStream.Close ();
+                       if (controlStream != null) {
+                               SendCommand (QuitCommand);
+                               controlStream.Close ();
+                               controlStream = null;
+                       }
                }
 
                private void CloseDataConnection () {
-                       if(dataSocket != null)
-                               dataSocket.Close ();
+                       if(dataStream != null) {
+                               dataStream.Close ();
+                               dataStream = null;
+                       }
                }
 
                private void CloseConnection () {
@@ -526,14 +642,14 @@ namespace System.Net
                        FtpStatus status;
                        
                        if (method == WebRequestMethods.Ftp.PrintWorkingDirectory)
-                               method = ChangeDir;
+                               method = "PWD";
 
                        if (method == WebRequestMethods.Ftp.Rename)
                                method = RenameFromCommand;
                        
-                       status = SendCommand (method, requestUri.LocalPath);
+                       status = SendCommand (method, file_name);
 
-                       ftpResponse.Stream = new EmptyStream ();
+                       ftpResponse.Stream = Stream.Null;
                        
                        string desc = status.StatusDescription;
 
@@ -585,9 +701,13 @@ namespace System.Net
                                if (status.StatusCode != FtpStatusCode.FileActionOK)
                                        throw CreateExceptionFromResponse (status);
                                break;
+                       case WebRequestMethods.Ftp.DeleteFile:
+                               if (status.StatusCode != FtpStatusCode.FileActionOK)  {
+                                       throw CreateExceptionFromResponse (status);
+                               }
+                               break;
                        }
 
-                       ftpResponse.UpdateStatus (status);
                        State = RequestState.Finished;
                }
 
@@ -598,7 +718,7 @@ namespace System.Net
                        OpenDataConnection ();
 
                        State = RequestState.TransferInProgress;
-                       requestStream = new FtpDataStream (this, dataSocket, false);
+                       requestStream = new FtpDataStream (this, dataStream, false);
                        asyncResult.Stream = requestStream;
                }
 
@@ -606,18 +726,10 @@ namespace System.Net
                {
                        State = RequestState.OpeningData;
 
-                       // Handle content offset
-                       if (offset > 0) {
-                               FtpStatus status = SendCommand (RestCommand, offset.ToString ());
-
-                               if (status.StatusCode != FtpStatusCode.FileCommandPending)
-                                       throw CreateExceptionFromResponse (status);
-                       }
-
                        OpenDataConnection ();
 
                        State = RequestState.TransferInProgress;
-                       ftpResponse.Stream = new FtpDataStream (this, dataSocket, true);
+                       ftpResponse.Stream = new FtpDataStream (this, dataStream, true);
                }
 
                void CheckRequestStarted ()
@@ -628,22 +740,32 @@ namespace System.Net
 
                void OpenControlConnection ()
                {
+                       Exception exception = null;
                        Socket sock = null;
                        foreach (IPAddress address in hostEntry.AddressList) {
                                sock = new Socket (address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
-                               try {
-                                       sock.Connect (new IPEndPoint (address, requestUri.Port));
-                                       localEndPoint = (IPEndPoint) sock.LocalEndPoint;
-                                       break;
-                               } catch (SocketException) {
+
+                               IPEndPoint remote = new IPEndPoint (address, requestUri.Port);
+
+                               if (!ServicePoint.CallEndPointDelegate (sock, remote)) {
                                        sock.Close ();
                                        sock = null;
+                               } else {
+                                       try {
+                                               sock.Connect (remote);
+                                               localEndPoint = (IPEndPoint) sock.LocalEndPoint;
+                                               break;
+                                       } catch (SocketException exc) {
+                                               exception = exc;
+                                               sock.Close ();
+                                               sock = null;
+                                       }
                                }
                        }
 
                        // Couldn't connect to any address
                        if (sock == null)
-                               throw new WebException ("Unable to connect to remote server", null,
+                               throw new WebException ("Unable to connect to remote server", exception,
                                                WebExceptionStatus.UnknownError, ftpResponse);
 
                        controlStream = new NetworkStream (sock);
@@ -652,6 +774,32 @@ namespace System.Net
                        State = RequestState.Authenticating;
 
                        Authenticate ();
+                       FtpStatus status = SendCommand ("OPTS", "utf8", "on");
+                       // ignore status for OPTS
+                       status = SendCommand (WebRequestMethods.Ftp.PrintWorkingDirectory);
+                       initial_path = GetInitialPath (status);
+               }
+
+               static string GetInitialPath (FtpStatus status)
+               {
+                       int s = (int) status.StatusCode;
+                       if (s < 200 || s > 300 || status.StatusDescription.Length <= 4)
+                               throw new WebException ("Error getting current directory: " + status.StatusDescription, null,
+                                               WebExceptionStatus.UnknownError, null);
+
+                       string msg = status.StatusDescription.Substring (4);
+                       if (msg [0] == '"') {
+                               int next_quote = msg.IndexOf ('\"', 1);
+                               if (next_quote == -1)
+                                       throw new WebException ("Error getting current directory: PWD -> " + status.StatusDescription, null,
+                                                               WebExceptionStatus.UnknownError, null);
+
+                               msg = msg.Substring (1, next_quote - 1);
+                       }
+
+                       if (!msg.EndsWith ("/"))
+                               msg += "/";
+                       return msg;
                }
 
                // Probably we could do better having here a regex
@@ -714,7 +862,7 @@ namespace System.Net
 
                Exception CreateExceptionFromResponse (FtpStatus status)
                {
-                       FtpWebResponse ftpResponse = new FtpWebResponse (requestUri, method, status);
+                       FtpWebResponse ftpResponse = new FtpWebResponse (this, requestUri, method, status);
                        
                        WebException exc = new WebException ("Server returned an error: " + status.StatusDescription, 
                                null, WebExceptionStatus.ProtocolError, ftpResponse);
@@ -729,9 +877,13 @@ namespace System.Net
 
                        State = RequestState.Finished;
                        FtpStatus status = GetResponseStatus ();
-
                        ftpResponse.UpdateStatus (status);
-                       
+                       if(!keepAlive)
+                               CloseConnection ();
+               }
+
+               internal void OperationCompleted ()
+               {
                        if(!keepAlive)
                                CloseConnection ();
                }
@@ -769,7 +921,7 @@ namespace System.Net
                        }
 
                        IPEndPoint ep = (IPEndPoint) sock.LocalEndPoint;
-                       string ipString = ep.Address.ToString ().Replace (".", ",");
+                       string ipString = ep.Address.ToString ().Replace ('.', ',');
                        int h1 = ep.Port >> 8; // ep.Port / 256
                        int h2 = ep.Port % 256;
 
@@ -790,24 +942,27 @@ namespace System.Net
                        
                        Socket s = InitDataConnection ();
 
-                       // TODO - Check that this command is only used for data connection based commands
-                       if (method != WebRequestMethods.Ftp.ListDirectory && method != WebRequestMethods.Ftp.ListDirectoryDetails) {
-                               status = SendCommand (TypeCommand, DataType);
-                               
-                               if (status.StatusCode != FtpStatusCode.CommandOK)
+                       // Handle content offset
+                       if (offset > 0) {
+                               status = SendCommand (RestCommand, offset.ToString ());
+                               if (status.StatusCode != FtpStatusCode.FileCommandPending)
                                        throw CreateExceptionFromResponse (status);
                        }
 
-                       if(method != WebRequestMethods.Ftp.UploadFileWithUniqueName)
-                               status = SendCommand (method, Uri.UnescapeDataString (requestUri.LocalPath));
-                       else
+                       if (method != WebRequestMethods.Ftp.ListDirectory && method != WebRequestMethods.Ftp.ListDirectoryDetails &&
+                           method != WebRequestMethods.Ftp.UploadFileWithUniqueName) {
+                               status = SendCommand (method, file_name);
+                       } else {
                                status = SendCommand (method);
+                       }
 
                        if (status.StatusCode != FtpStatusCode.OpeningData && status.StatusCode != FtpStatusCode.DataAlreadyOpen)
                                throw CreateExceptionFromResponse (status);
 
                        if (usePassive) {
-                               dataSocket = s;
+                               dataStream = new NetworkStream (s, false);
+                               if (EnableSsl)
+                                       ChangeToSSLSocket (ref dataStream);
                        }
                        else {
 
@@ -825,18 +980,16 @@ namespace System.Net
                                }
 
                                s.Close ();
-                               dataSocket = incoming;
-                       }
-
-                       if (EnableSsl) {
-                               InitiateSecureConnection (ref controlStream);
-                               controlReader = new StreamReader (controlStream, Encoding.ASCII);
+                               dataStream = new NetworkStream (incoming, false);
+                               if (EnableSsl)
+                                       ChangeToSSLSocket (ref dataStream);
                        }
 
                        ftpResponse.UpdateStatus (status);
                }
 
-               void Authenticate () {
+               void Authenticate ()
+               {
                        string username = null;
                        string password = null;
                        string domain = null;
@@ -861,6 +1014,17 @@ namespace System.Net
                        if (EnableSsl) {
                                InitiateSecureConnection (ref controlStream);
                                controlReader = new StreamReader (controlStream, Encoding.ASCII);
+                               status = SendCommand ("PBSZ", "0");
+                               int st = (int) status.StatusCode;
+                               if (st < 200 || st >= 300)
+                                       throw CreateExceptionFromResponse (status);
+                               // TODO: what if "PROT P" is denied by the server? What does MS do?
+                               status = SendCommand ("PROT", "P");
+                               st = (int) status.StatusCode;
+                               if (st < 200 || st >= 300)
+                                       throw CreateExceptionFromResponse (status);
+
+                               status = new FtpStatus (FtpStatusCode.SendUserCommand, "");
                        }
                        
                        if (status.StatusCode != FtpStatusCode.SendUserCommand)
@@ -907,49 +1071,89 @@ namespace System.Net
                        if(!waitResponse)
                                return null;
                        
-                       return GetResponseStatus ();
+                       FtpStatus result = GetResponseStatus ();
+                       if (ftpResponse != null)
+                               ftpResponse.UpdateStatus (result);
+                       return result;
                }
 
+               internal static FtpStatus ServiceNotAvailable ()
+               {
+                       return new FtpStatus (FtpStatusCode.ServiceNotAvailable, Locale.GetText ("Invalid response from server"));
+               }
+               
                internal FtpStatus GetResponseStatus ()
                {
                        while (true) {
-                               string responseString = null;
+                               string response = null;
 
                                try {
-                                       responseString = controlReader.ReadLine ();
+                                       response = controlReader.ReadLine ();
+                               } catch (IOException) {
                                }
-                               catch (IOException) {
-                                       // controlReader.Close ();
-                               }
-
-                               if (responseString == null || responseString.Length < 3)
-                                       return new FtpStatus(FtpStatusCode.ServiceNotAvailable, "Invalid response from server");
 
-                               string codeString = responseString.Substring (0, 3);
+                               if (response == null || response.Length < 3)
+                                       return ServiceNotAvailable ();
 
                                int code;
-                               if (!Int32.TryParse (codeString, out code))
-                                       return new FtpStatus (FtpStatusCode.ServiceNotAvailable, "Invalid response from server");
-
-                               if (responseString.Length < 4 || responseString [3] != '-')
-                                       return new FtpStatus ((FtpStatusCode) code, responseString);
+                               if (!Int32.TryParse (response.Substring (0, 3), out code))
+                                       return ServiceNotAvailable ();
+
+                               if (response.Length > 3 && response [3] == '-'){
+                                       string line = null;
+                                       string find = code.ToString() + ' ';
+                                       while (true){
+                                               try {
+                                                       line = controlReader.ReadLine();
+                                               } catch (IOException) {
+                                               }
+                                               if (line == null)
+                                                       return ServiceNotAvailable ();
+                                               
+                                               response += Environment.NewLine + line;
+
+                                               if (line.StartsWith(find, StringComparison.Ordinal))
+                                                       break;
+                                       } 
+                               }
+                               return new FtpStatus ((FtpStatusCode) code, response);
                        }
                }
 
-               private void InitiateSecureConnection (ref NetworkStream stream) {
+               private void InitiateSecureConnection (ref Stream stream) {
                        FtpStatus status = SendCommand (AuthCommand, "TLS");
-
-                       if (status.StatusCode != FtpStatusCode.ServerWantsSecureSession) {
+                       if (status.StatusCode != FtpStatusCode.ServerWantsSecureSession)
                                throw CreateExceptionFromResponse (status);
-                       }
 
                        ChangeToSSLSocket (ref stream);
                }
 
-               internal static bool ChangeToSSLSocket (ref NetworkStream stream) {
+#if SECURITY_DEP
+               RemoteCertificateValidationCallback callback = delegate (object sender,
+                                                                        X509Certificate certificate,
+                                                                        X509Chain chain,
+                                                                        SslPolicyErrors sslPolicyErrors) {
+                       // honor any exciting callback defined on ServicePointManager
+                       if (ServicePointManager.ServerCertificateValidationCallback != null)
+                               return ServicePointManager.ServerCertificateValidationCallback (sender, certificate, chain, sslPolicyErrors);
+                       // otherwise provide our own
+                       if (sslPolicyErrors != SslPolicyErrors.None)
+                               throw new InvalidOperationException ("SSL authentication error: " + sslPolicyErrors);
+                       return true;
+                       };
+#endif
+
+               internal bool ChangeToSSLSocket (ref Stream stream) {
 #if TARGET_JVM
                        stream.ChangeToSSLSocket ();
                        return true;
+#elif SECURITY_DEP
+                       SslStream sslStream = new SslStream (stream, true, callback, null);
+                       //sslStream.AuthenticateAsClient (Host, this.ClientCertificates, SslProtocols.Default, false);
+                       //TODO: client certificates
+                       sslStream.AuthenticateAsClient (requestUri.Host, null, SslProtocols.Default, false);
+                       stream = sslStream;
+                       return true;
 #else
                        throw new NotImplementedException ();
 #endif
@@ -972,13 +1176,6 @@ namespace System.Net
                        if (InFinalState ())
                                throw new InvalidOperationException ("Cannot change final state");
                }
-
-               class EmptyStream : MemoryStream
-               {
-                       internal EmptyStream ()
-                               : base (new byte [0], false) {
-                       }
-               }
        }
 }