context: //
  // System.Net.FtpWebRequest.cs
  //
  // Authors:
  //	Carlos Alberto Cortez (calberto.cortez@gmail.com)
  //
  // (c) Copyright 2006 Novell, Inc. (http://www.novell.com)
  //
  
  using System;
  using System.IO;
  using System.Net.Sockets;
  using System.Text;
  using System.Threading;
  using System.Net.Cache;
  using System.Security.Cryptography.X509Certificates;
  using System.Net;
  using System.Net.Security;
  using System.Security.Authentication;
  
  namespace System.Net
  {
  	public sealed class FtpWebRequest : WebRequest
  	{
  		Uri requestUri;
  		string file_name; // By now, used for upload
  		ServicePoint servicePoint;
  		Stream origDataStream;
  		Stream dataStream;
  		Stream controlStream;
  		StreamReader controlReader;
  		NetworkCredential credentials;
  		IPHostEntry hostEntry;
  		IPEndPoint localEndPoint;
  		IWebProxy proxy;
  		int timeout = 100000;
  		int rwTimeout = 300000;
  		long offset = 0;
  		bool binary = true;
  		bool enableSsl = false;
  		bool usePassive = true;
  		bool keepAlive = false;
  		string method = WebRequestMethods.Ftp.DownloadFile;
  		string renameTo;
  		object locker = new object ();
  		
  		RequestState requestState = RequestState.Before;
  		FtpAsyncResult asyncResult;
  		FtpWebResponse ftpResponse;
  		Stream requestStream;
  		string initial_path;
  
  		const string ChangeDir = "CWD";
  		const string UserCommand = "USER";
  		const string PasswordCommand = "PASS";
  		const string TypeCommand = "TYPE";
  		const string PassiveCommand = "PASV";
  		const string PortCommand = "PORT";
  		const string AbortCommand = "ABOR";
  		const string AuthCommand = "AUTH";
  		const string RestCommand = "REST";
  		const string RenameFromCommand = "RNFR";
  		const string RenameToCommand = "RNTO";
  		const string QuitCommand = "QUIT";
  		const string EOL = "\r\n"; // Special end of line
  
  		enum RequestState
  		{
  			Before,
  			Scheduled,
  			Connecting,
  			Authenticating,
  			OpeningData,
  			TransferInProgress,
  			Finished,
  			Aborted,
  			Error
  		}
  
  		// sorted commands
  		static readonly string [] supportedCommands = new string [] {
  			WebRequestMethods.Ftp.AppendFile, // APPE
  			WebRequestMethods.Ftp.DeleteFile, // DELE
  			WebRequestMethods.Ftp.ListDirectoryDetails, // LIST
  			WebRequestMethods.Ftp.GetDateTimestamp, // MDTM
  			WebRequestMethods.Ftp.MakeDirectory, // MKD
  			WebRequestMethods.Ftp.ListDirectory, // NLST
  			WebRequestMethods.Ftp.PrintWorkingDirectory, // PWD
  			WebRequestMethods.Ftp.Rename, // RENAME
  			WebRequestMethods.Ftp.DownloadFile, // RETR
  			WebRequestMethods.Ftp.RemoveDirectory, // RMD
  			WebRequestMethods.Ftp.GetFileSize, // SIZE
  			WebRequestMethods.Ftp.UploadFile, // STOR
  			WebRequestMethods.Ftp.UploadFileWithUniqueName // STUR
  			};
  
  		internal FtpWebRequest (Uri uri) 
  		{
  			this.requestUri = uri;
  			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 ();
  			}
  			set {
  				throw new NotSupportedException ();
  			}
  		}
  
  		public override long ContentLength {
  			get {
  				return 0;
  			} 
  			set {
  				// DO nothing
  			}
  		}
  
  		public long ContentOffset {
  			get {
  				return offset;
  			}
  			set {
  				CheckRequestStarted ();
  				if (value < 0)
  					throw new ArgumentOutOfRangeException ();
  
  				offset = value;
  			}
  		}
  
  		public override ICredentials Credentials {
  			get {
  				return credentials;
  			}
  			set {
  				CheckRequestStarted ();
  				if (value == null)
  					throw new ArgumentNullException ();
  				if (!(value is NetworkCredential))
  					throw new ArgumentException ();
  
  				credentials = value as NetworkCredential;
  			}
  		}
  
  #if !NET_2_1
  		[MonoTODO]
  		public static new RequestCachePolicy DefaultCachePolicy
  		{
  			get {
  				throw GetMustImplement ();
  			}
  			set {
  				throw GetMustImplement ();
  			}
  		}
  #endif
  
  		public bool EnableSsl {
  			get {
  				return enableSsl;
  			}
  			set {
  				CheckRequestStarted ();
  				enableSsl = value;
  			}
  		}
  
  		[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;
  			}
  		}
  
  		public override string Method {
  			get {
  				return method;
  			}
  			set {
  				CheckRequestStarted ();
  				if (value == null)
  					throw new ArgumentNullException ("Method string cannot be null");
  
  				if (value.Length == 0 || Array.BinarySearch (supportedCommands, value) < 0)
  					throw new ArgumentException ("Method not supported", "value");
  				
  				method = value;
  			}
  		}
  
  		public override bool PreAuthenticate {
  			get {
  				throw new NotSupportedException ();
  			}
  			set {
  				throw new NotSupportedException ();
  			}
  		}
  
  		public override IWebProxy Proxy {
  			get {
  				return proxy;
  			}
  			set {
  				CheckRequestStarted ();
  				proxy = value;
  			}
  		}
  
  		public int ReadWriteTimeout {
  			get {
  				return rwTimeout;
  			}
  			set {
  				CheckRequestStarted ();
  
  				if (value < - 1)
  					throw new ArgumentOutOfRangeException ();
  				else
  					rwTimeout = value;
  			}
  		}
  
  		public string RenameTo {
  			get {
  				return renameTo;
  			}
  			set {
  				CheckRequestStarted ();
  				if (value == null || value.Length == 0)
  					throw new ArgumentException ("RenameTo value can't be null or empty", "RenameTo");
  
  				renameTo = value;
  			}
  		}
  
  		public override Uri RequestUri {
  			get {
  				return requestUri;
  			}
  		}
  
  		public ServicePoint ServicePoint {
  			get {
  				return GetServicePoint ();
  			}
  		}
  
  		public bool UsePassive {
  			get {
  				return usePassive;
  			}
  			set {
  				CheckRequestStarted ();
  				usePassive = value;
  			}
  		}
  
  		[MonoTODO]
  		public override bool UseDefaultCredentials
  		{
  			get {
  				throw GetMustImplement ();
  			}
  			set {
  				throw GetMustImplement ();
  			}
  		}
  		
  		public bool UseBinary {
  			get {
  				return binary;
  			} set {
  				CheckRequestStarted ();
  				binary = value;
  			}
  		}
  
  		public override int Timeout {
  			get {
  				return timeout;
  			}
  			set {
  				CheckRequestStarted ();
  
  				if (value < -1)
  					throw new ArgumentOutOfRangeException ();
  				else
  					timeout = value;
  			}
  		}
  
  		string DataType {
  			get {
  				return binary ? "I" : "A";
  			}
  		}
  
  		RequestState State {
  			get {
  				lock (locker) {
  					return requestState;
  				}
  			}
  
  			set {
  				lock (locker) {
  					CheckIfAborted ();
  					CheckFinalState ();
  					requestState = value;
  				}
  			}
  		}
  
  		public override void Abort () {
  			lock (locker) {
  				if (State == RequestState.TransferInProgress) {
  					/*FtpStatus status = */
  					SendCommand (false, AbortCommand);
  				}
  
  				if (!InFinalState ()) {
  					State = RequestState.Aborted;
  					ftpResponse = new FtpWebResponse (this, requestUri, method, FtpStatusCode.FileActionAborted, "Aborted by request");
  				}
  			}
  		}
  
  		public override IAsyncResult BeginGetResponse (AsyncCallback callback, object state) {
  			if (asyncResult != null && !asyncResult.IsCompleted) {
  				throw new InvalidOperationException ("Cannot re-call BeginGetRequestStream/BeginGetResponse while a previous call is still in progress");
  			}
  
  			CheckIfAborted ();
  			
  			asyncResult = new FtpAsyncResult (callback, state);
  
  			lock (locker) {
  				if (InFinalState ())
  					asyncResult.SetCompleted (true, ftpResponse);
  				else {
  					if (State == RequestState.Before)
  						State = RequestState.Scheduled;
  
  					Thread thread = new Thread (ProcessRequest);
  					thread.Start ();
  				}
  			}
  
  			return asyncResult;
  		}
  
  		public override WebResponse EndGetResponse (IAsyncResult asyncResult) {
  			if (asyncResult == null)
  				throw new ArgumentNullException ("AsyncResult cannot be null!");
  
  			if (!(asyncResult is FtpAsyncResult) || asyncResult != this.asyncResult)
  				throw new ArgumentException ("AsyncResult is from another request!");
  
  			FtpAsyncResult asyncFtpResult = (FtpAsyncResult) asyncResult;
  			if (!asyncFtpResult.WaitUntilComplete (timeout, false)) {
  				Abort ();
  				throw new WebException ("Transfer timed out.", WebExceptionStatus.Timeout);
  			}
  
  			CheckIfAborted ();
  
  			asyncResult = null;
  
  			if (asyncFtpResult.GotException)
  				throw asyncFtpResult.Exception;
  
  			return asyncFtpResult.Response;
  		}
  
  		public override WebResponse GetResponse () {
  			IAsyncResult asyncResult = BeginGetResponse (null, null);
  			return EndGetResponse (asyncResult);
  		}
  
  		public override IAsyncResult BeginGetRequestStream (AsyncCallback callback, object state) {
  			if (method != WebRequestMethods.Ftp.UploadFile && method != WebRequestMethods.Ftp.UploadFileWithUniqueName &&
  					method != WebRequestMethods.Ftp.AppendFile)
  				throw new ProtocolViolationException ();
  
  			lock (locker) {
  				CheckIfAborted ();
  
  				if (State != RequestState.Before)
  					throw new InvalidOperationException ("Cannot re-call BeginGetRequestStream/BeginGetResponse while a previous call is still in progress");
  
  				State = RequestState.Scheduled;
  			}
  
  			asyncResult = new FtpAsyncResult (callback, state);
  			Thread thread = new Thread (ProcessRequest);
  			thread.Start ();
  
  			return asyncResult;
  		}
  
  		public override Stream EndGetRequestStream (IAsyncResult asyncResult) {
  			if (asyncResult == null)
  				throw new ArgumentNullException ("asyncResult");
  
  			if (!(asyncResult is FtpAsyncResult))
  				throw new ArgumentException ("asyncResult");
  
  			if (State == RequestState.Aborted) {
  				throw new WebException ("Request aborted", WebExceptionStatus.RequestCanceled);
  			}
  			
  			if (asyncResult != this.asyncResult)
  				throw new ArgumentException ("AsyncResult is from another request!");
  
  			FtpAsyncResult res = (FtpAsyncResult) asyncResult;
  
  			if (!res.WaitUntilComplete (timeout, false)) {
  				Abort ();
  				throw new WebException ("Request timed out");
  			}
  
  			if (res.GotException)
  				throw res.Exception;
  
  			return res.Stream;
  		}
  
  		public override Stream GetRequestStream () {
  			IAsyncResult asyncResult = BeginGetRequestStream (null, null);
  			return EndGetRequestStream (asyncResult);
  		}
  		
  		ServicePoint GetServicePoint ()
  		{
  			if (servicePoint == null)
  				servicePoint = ServicePointManager.FindServicePoint (requestUri, proxy);
  
  			return servicePoint;
  		}
  
  		// Probably move some code of command connection here
  		void ResolveHost ()
  		{
  			CheckIfAborted ();
  			hostEntry = GetServicePoint ().HostEntry;
  
  			if (hostEntry == null) {
  				ftpResponse.UpdateStatus (new FtpStatus(FtpStatusCode.ActionAbortedLocalProcessingError, "Cannot resolve server name"));
  				throw new WebException ("The remote server name could not be resolved: " + requestUri,
  					null, WebExceptionStatus.NameResolutionFailure, ftpResponse);
  			}
  		}
  
  		void ProcessRequest () {
  
  			if (State == RequestState.Scheduled) {
  				ftpResponse = new FtpWebResponse (this, requestUri, method, keepAlive);
  
  				try {
  					ProcessMethod ();
  					//State = RequestState.Finished;
  					//finalResponse = ftpResponse;
  					asyncResult.SetCompleted (false, ftpResponse);
  				}
  				catch (Exception e) {
  					if (!GetServicePoint ().UsesProxy)
  						State = RequestState.Error;
  					SetCompleteWithError (e);
  				}
  			}
  			else {
  				if (InProgress ()) {
  					FtpStatus status = GetResponseStatus ();
  
  					ftpResponse.UpdateStatus (status);
  
  					if (ftpResponse.IsFinal ()) {
  						State = RequestState.Finished;
  					}
  				}
  
  				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);
  
  				UriBuilder initialBuilder = new UriBuilder () {
  					Scheme  = "ftp",
  					Host    = "dummy-host",
  					Path    = initial_path,
  				};
  				Uri initial = initialBuilder.Uri;
  				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 ()
  		{
  			ServicePoint sp = GetServicePoint ();
  			if (sp.UsesProxy) {
  				if (method != WebRequestMethods.Ftp.DownloadFile)
  					throw new NotSupportedException ("FTP+proxy only supports RETR");
  
  				HttpWebRequest req = (HttpWebRequest) WebRequest.Create (proxy.GetProxy (requestUri));
  				req.Address = requestUri;
  				requestState = RequestState.Finished;
  				WebResponse response = req.GetResponse ();
  				ftpResponse.Stream = new FtpDataStream (this, response.GetResponseStream (), true);
  				ftpResponse.StatusCode = FtpStatusCode.CommandOK;
  				return;
  			}
  			State = RequestState.Connecting;
  
  			ResolveHost ();
  
  			OpenControlConnection ();
  			CWDAndSetFileName (requestUri);
  			SetType ();
  
  			switch (method) {
  			// Open data connection and receive data
  			case WebRequestMethods.Ftp.DownloadFile:
  			case WebRequestMethods.Ftp.ListDirectory:
  			case WebRequestMethods.Ftp.ListDirectoryDetails:
  				DownloadData ();
  				break;
  			// Open data connection and send data
  			case WebRequestMethods.Ftp.AppendFile:
  			case WebRequestMethods.Ftp.UploadFile:
  			case WebRequestMethods.Ftp.UploadFileWithUniqueName:
  				UploadData ();
  				break;
  			// Get info from control connection
  			case WebRequestMethods.Ftp.GetFileSize:
  			case WebRequestMethods.Ftp.GetDateTimestamp:
  			case WebRequestMethods.Ftp.PrintWorkingDirectory:
  			case WebRequestMethods.Ftp.MakeDirectory:
  			case WebRequestMethods.Ftp.Rename:
  			case WebRequestMethods.Ftp.DeleteFile:
  				ProcessSimpleMethod ();
  				break;
  			default: // What to do here?
  				throw new Exception (String.Format ("Support for command {0} not implemented yet", method));
  			}
  
  			CheckIfAborted ();
  		}
  
  		private void CloseControlConnection () {
  			if (controlStream != null) {
  				SendCommand (QuitCommand);
  				controlStream.Close ();
  				controlStream = null;
  			}
  		}
  
  		internal void CloseDataConnection () {
  			if(origDataStream != null) {
  				origDataStream.Close ();
  				origDataStream = null;
  			}
  		}
  
  		private void CloseConnection () {
  			CloseControlConnection ();
  			CloseDataConnection ();
  		}
  		
  		void ProcessSimpleMethod ()
  		{
  			State = RequestState.TransferInProgress;
  			
  			FtpStatus status;
  			
  			if (method == WebRequestMethods.Ftp.PrintWorkingDirectory)
  				method = "PWD";
  
  
gt: 			if (method == WebRequestMethods.Ftp.Rename)