//
// (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
-
-#if NET_2_0
+using System.Security.Cryptography.X509Certificates;
+using System.Net;
+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;
IWebProxy proxy;
int timeout = 100000;
int rwTimeout = 300000;
- long offset;
+ long offset = 0;
bool binary = true;
- bool enableSsl;
- bool requestInProgress;
+ bool enableSsl = false;
bool usePassive = true;
- bool keepAlive = true;
- bool aborted;
- bool transferCompleted;
- bool gotRequestStream;
+ bool keepAlive = false;
string method = WebRequestMethods.Ftp.DownloadFile;
string renameTo;
object locker = new object ();
-
- FtpStatusCode statusCode;
- string statusDescription = String.Empty;
-
- FtpAsyncResult asyncRead;
- FtpAsyncResult asyncWrite;
-
+
+ RequestState requestState = RequestState.Before;
+ FtpAsyncResult asyncResult;
FtpWebResponse ftpResponse;
- Stream requestStream = Stream.Null;
+ Stream requestStream;
+ string initial_path;
+ const string ChangeDir = "CWD";
const string UserCommand = "USER";
const string PasswordCommand = "PASS";
const string TypeCommand = "TYPE";
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
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 ();
}
}
+ [MonoTODO]
+ public static new RequestCachePolicy DefaultCachePolicy
+ {
+ get {
+ throw GetMustImplement ();
+ }
+ set {
+ throw GetMustImplement ();
+ }
+ }
+
public bool EnableSsl {
get {
return enableSsl;
}
}
+ [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;
}
}
set {
CheckRequestStarted ();
if (value == null)
- throw new ArgumentNullException ("method");
+ throw new ArgumentNullException ("Method string cannot be null");
if (value.Length == 0 || Array.BinarySearch (supportedCommands, value) < 0)
throw new ArgumentException ("Method not supported", "value");
usePassive = value;
}
}
+
+ [MonoTODO]
+ public override bool UseDefaultCredentials
+ {
+ get {
+ throw GetMustImplement ();
+ }
+ set {
+ throw GetMustImplement ();
+ }
+ }
public bool UseBinary {
get {
}
}
- ServicePoint GetServicePoint ()
- {
- if (servicePoint == null)
- servicePoint = ServicePointManager.FindServicePoint (requestUri, proxy);
+ RequestState State {
+ get {
+ lock (locker) {
+ return requestState;
+ }
+ }
- return servicePoint;
+ set {
+ lock (locker) {
+ CheckIfAborted ();
+ CheckFinalState ();
+ requestState = value;
+ }
+ }
}
- // Probably move some code of command connection here
- bool ResolveHost ()
- {
- hostEntry = GetServicePoint ().HostEntry;
- if (hostEntry == null)
- return false;
-
- return true;
- }
+ public override void Abort () {
+ lock (locker) {
+ if (State == RequestState.TransferInProgress) {
+ /*FtpStatus status = */
+ SendCommand (false, AbortCommand);
+ }
- public override void Abort ()
- {
- FtpStatusCode status = SendCommand (AbortCommand);
- if (status != FtpStatusCode.ClosingData)
- throw CreateExceptionFromResponse (); // Probably ignore it by now
+ if (!InFinalState ()) {
+ State = RequestState.Aborted;
+ ftpResponse = new FtpWebResponse (this, requestUri, method, FtpStatusCode.FileActionAborted, "Aborted by request");
+ }
+ }
+ }
- aborted = true;
- if (asyncRead != null) {
- FtpAsyncResult r = asyncRead;
- WebException wexc = new WebException ("Request aborted", WebExceptionStatus.RequestCanceled);
- r.SetCompleted (false, wexc);
- r.DoCallback ();
- asyncRead = null;
+ 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");
}
- if (asyncWrite != null) {
- FtpAsyncResult r = asyncWrite;
- WebException wexc = new WebException ("Request aborted", WebExceptionStatus.RequestCanceled);
- r.SetCompleted (false, wexc);
- r.DoCallback ();
- asyncWrite = null;
+
+ 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;
}
- void ProcessRequest ()
- {
- ftpResponse = new FtpWebResponse (requestUri, method, keepAlive);
+ public override WebResponse EndGetResponse (IAsyncResult asyncResult) {
+ if (asyncResult == null)
+ throw new ArgumentNullException ("AsyncResult cannot be null!");
- if (!ResolveHost ()) {
- SetResponseError (new WebException ("The remote server name could not be resolved: " + requestUri,
- null, WebExceptionStatus.NameResolutionFailure, ftpResponse));
- return;
- }
-
- if (!OpenControlConnection ())
- return;
+ if (!(asyncResult is FtpAsyncResult) || asyncResult != this.asyncResult)
+ throw new ArgumentException ("AsyncResult is from another request!");
- 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:
- GetInfoFromControl ();
- break;
- case WebRequestMethods.Ftp.Rename:
- RenameFile ();
- break;
- case WebRequestMethods.Ftp.MakeDirectory:
- ProcessSimpleRequest ();
- break;
- default: // What to do here?
- throw new Exception ("Support for command not implemented yet");
- }
- }
-
- // Currently I use this only for MKD
- // (Commands that don't need any parsing in command connection
- // for open data connection)
- void ProcessSimpleRequest ()
- {
- if (SendCommand (method, requestUri.LocalPath) != FtpStatusCode.PathnameCreated) {
- asyncRead.SetCompleted (true, CreateExceptionFromResponse ());
- return;
+ FtpAsyncResult asyncFtpResult = (FtpAsyncResult) asyncResult;
+ if (!asyncFtpResult.WaitUntilComplete (timeout, false)) {
+ Abort ();
+ throw new WebException ("Transfer timed out.", WebExceptionStatus.Timeout);
}
- asyncRead.SetCompleted (true, ftpResponse);
+ CheckIfAborted ();
+
+ asyncResult = null;
+
+ if (asyncFtpResult.GotException)
+ throw asyncFtpResult.Exception;
+
+ return asyncFtpResult.Response;
}
- // It would be good to have a SetCompleted method for
- // settting asyncRead as completed (some code is here and there, repeated)
- void GetInfoFromControl ()
- {
- FtpStatusCode status = SendCommand (method, requestUri.LocalPath);
- if (status != FtpStatusCode.FileStatus) {
- asyncRead.SetCompleted (true, CreateExceptionFromResponse ());
- return;
+ 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;
}
- string desc = statusDescription;
- Console.WriteLine ("Desc = " + desc);
- if (method == WebRequestMethods.Ftp.GetFileSize) {
- int i, len;
- long size;
- for (i = 4, len = 0; i < desc.Length && Char.IsDigit (desc [i]); i++, len++)
- ;
+ asyncResult = new FtpAsyncResult (callback, state);
+ Thread thread = new Thread (ProcessRequest);
+ thread.Start ();
- if (len == 0) {
- asyncRead.SetCompleted (true, new WebException ("Bad format for server response in " + method));
- return;
- }
+ return asyncResult;
+ }
- if (!Int64.TryParse (desc.Substring (4, len), out size)) {
- asyncRead.SetCompleted (true, new WebException ("Bad format for server response in " + method));
- return;
- }
+ public override Stream EndGetRequestStream (IAsyncResult asyncResult) {
+ if (asyncResult == null)
+ throw new ArgumentNullException ("asyncResult");
- ftpResponse.contentLength = size;
- asyncRead.SetCompleted (true, ftpResponse);
- return;
+ if (!(asyncResult is FtpAsyncResult))
+ throw new ArgumentException ("asyncResult");
+
+ if (State == RequestState.Aborted) {
+ throw new WebException ("Request aborted", WebExceptionStatus.RequestCanceled);
}
- if (method == WebRequestMethods.Ftp.GetDateTimestamp) {
- // Here parse the format the date time (different formats)
- asyncRead.SetCompleted (true, ftpResponse);
- return;
+ 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");
}
- throw new Exception ("You shouldn't reach this point");
+ if (res.GotException)
+ throw res.Exception;
+
+ return res.Stream;
}
- void RenameFile ()
+ public override Stream GetRequestStream () {
+ IAsyncResult asyncResult = BeginGetRequestStream (null, null);
+ return EndGetRequestStream (asyncResult);
+ }
+
+ ServicePoint GetServicePoint ()
{
- FtpStatusCode status = SendCommand (RenameFromCommand, requestUri.LocalPath);
- if (status == FtpStatusCode.FileCommandPending) {
- // Pass an empty string if RenameTo wasn't specified
- status = SendCommand (RenameToCommand, renameTo != null ? renameTo : String.Empty);
-
- if (status == FtpStatusCode.FileActionOK) {
- ftpResponse.UpdateStatus (statusCode, statusDescription);
- asyncRead.SetCompleted (true, ftpResponse);
- return;
- }
- }
+ if (servicePoint == null)
+ servicePoint = ServicePointManager.FindServicePoint (requestUri, proxy);
- ftpResponse.UpdateStatus (statusCode, statusDescription);
- asyncRead.SetCompleted (true, CreateExceptionFromResponse ());
+ return servicePoint;
}
- void UploadData ()
+ // Probably move some code of command connection here
+ void ResolveHost ()
{
- if (gotRequestStream) {
- if (GetResponseCode () != FtpStatusCode.ClosingData)
- asyncRead.SetCompleted (true, CreateExceptionFromResponse ());
-
- return;
- }
-
- if (!OpenDataConnection ())
- return;
+ CheckIfAborted ();
+ hostEntry = GetServicePoint ().HostEntry;
- gotRequestStream = true;
- requestStream = new FtpDataStream (this, dataSocket, false);
- asyncWrite.SetCompleted (true, requestStream);
+ 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 DownloadData ()
- {
- FtpStatusCode status;
+ void ProcessRequest () {
- // Handle content offset
- if (offset > 0) {
- status = SendCommand (RestCommand, offset.ToString ());
- if (status != FtpStatusCode.FileCommandPending) {
- asyncRead.SetCompleted (true, CreateExceptionFromResponse ());
- return;
+ 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) {
+ State = RequestState.Error;
+ SetCompleteWithError (e);
}
}
+ else {
+ if (InProgress ()) {
+ FtpStatus status = GetResponseStatus ();
- if (!OpenDataConnection ())
- return;
+ ftpResponse.UpdateStatus (status);
+
+ if (ftpResponse.IsFinal ()) {
+ State = RequestState.Finished;
+ }
+ }
- ftpResponse.Stream = new FtpDataStream (this, dataSocket, true);
- ftpResponse.StatusDescription = statusDescription;
- ftpResponse.StatusCode = statusCode;
- asyncRead.SetCompleted (true, ftpResponse);
+ asyncResult.SetCompleted (false, ftpResponse);
+ }
}
- public override IAsyncResult BeginGetResponse (AsyncCallback callback, object state)
+ void SetType ()
{
- if (aborted)
- throw new WebException ("Request was previously aborted.");
-
- Monitor.Enter (this);
- if (asyncRead != null) {
- Monitor.Exit (this);
- throw new InvalidOperationException ();
+ if (binary) {
+ FtpStatus status = SendCommand (TypeCommand, DataType);
+ if ((int) status.StatusCode < 200 || (int) status.StatusCode >= 300)
+ throw CreateExceptionFromResponse (status);
}
+ }
- requestInProgress = true;
- asyncRead = new FtpAsyncResult (callback, state);
- Thread thread = new Thread (ProcessRequest);
- thread.Start ();
+ 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;
+ }
- Monitor.Exit (this);
- return asyncRead;
+ int last = result.LastIndexOf ('/');
+ if (last == -1)
+ return null;
+
+ return result.Substring (0, last + 1);
}
- public override WebResponse EndGetResponse (IAsyncResult asyncResult)
+ void CWDAndSetFileName (Uri uri)
{
- if (asyncResult == null)
- throw new ArgumentNullException ("asyncResult");
+ 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));
+ }
+ }
+ }
- if (!(asyncResult is FtpAsyncResult) || asyncResult != asyncRead)
- throw new ArgumentException ("asyncResult");
+ void ProcessMethod ()
+ {
+ State = RequestState.Connecting;
- FtpAsyncResult asyncFtpResult = (FtpAsyncResult) asyncResult;
- if (!asyncFtpResult.WaitUntilComplete (timeout, false)) {
- Abort ();
- throw new WebException ("Transfer timed out.", WebExceptionStatus.Timeout);
+ 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));
}
- if (asyncFtpResult.GotException)
- throw asyncFtpResult.Exception;
+ CheckIfAborted ();
+ }
- return asyncFtpResult.Response;
+ private void CloseControlConnection () {
+ if (controlStream != null) {
+ SendCommand (QuitCommand);
+ controlStream.Close ();
+ controlStream = null;
+ }
}
- public override WebResponse GetResponse ()
- {
- IAsyncResult asyncResult = BeginGetResponse (null, null);
- return EndGetResponse (asyncResult);
+ private void CloseDataConnection () {
+ if(dataStream != null) {
+ dataStream.Close ();
+ dataStream = null;
+ }
}
- public override IAsyncResult BeginGetRequestStream (AsyncCallback callback, object state)
+ private void CloseConnection () {
+ CloseControlConnection ();
+ CloseDataConnection ();
+ }
+
+ void ProcessSimpleMethod ()
{
- if (aborted)
- throw new WebException ("Request was previously aborted.");
+ State = RequestState.TransferInProgress;
- if (method != WebRequestMethods.Ftp.UploadFile && method != WebRequestMethods.Ftp.UploadFileWithUniqueName &&
- method != WebRequestMethods.Ftp.AppendFile)
- throw new ProtocolViolationException ();
+ FtpStatus status;
+
+ if (method == WebRequestMethods.Ftp.PrintWorkingDirectory)
+ method = "PWD";
- lock (locker) {
- if (asyncWrite != null || asyncRead != null)
- throw new InvalidOperationException ();
-
- requestInProgress = true;
- asyncWrite = new FtpAsyncResult (callback, state);
- Thread thread = new Thread (ProcessRequest);
- thread.Start ();
+ if (method == WebRequestMethods.Ftp.Rename)
+ method = RenameFromCommand;
+
+ status = SendCommand (method, file_name);
- return asyncWrite;
- }
- }
+ ftpResponse.Stream = Stream.Null;
+
+ string desc = status.StatusDescription;
- public override Stream EndGetRequestStream (IAsyncResult asyncResult)
- {
- if (asyncResult == null)
- throw new ArgumentNullException ("asyncResult");
+ switch (method) {
+ case WebRequestMethods.Ftp.GetFileSize: {
+ if (status.StatusCode != FtpStatusCode.FileStatus)
+ throw CreateExceptionFromResponse (status);
- if (!(asyncResult is FtpAsyncResult))
- throw new ArgumentException ("asyncResult");
+ int i, len;
+ long size;
+ for (i = 4, len = 0; i < desc.Length && Char.IsDigit (desc [i]); i++, len++)
+ ;
- FtpAsyncResult res = (FtpAsyncResult) asyncResult;
- if (!res.WaitUntilComplete (timeout, false)) {
- Abort ();
- throw new WebException ("Request timeod out");
+ if (len == 0)
+ throw new WebException ("Bad format for server response in " + method);
+
+ if (!Int64.TryParse (desc.Substring (4, len), out size))
+ throw new WebException ("Bad format for server response in " + method);
+
+ ftpResponse.contentLength = size;
+ }
+ break;
+ case WebRequestMethods.Ftp.GetDateTimestamp:
+ if (status.StatusCode != FtpStatusCode.FileStatus)
+ throw CreateExceptionFromResponse (status);
+ ftpResponse.LastModified = DateTime.ParseExact (desc.Substring (4), "yyyyMMddHHmmss", null);
+ break;
+ case WebRequestMethods.Ftp.MakeDirectory:
+ if (status.StatusCode != FtpStatusCode.PathnameCreated)
+ throw CreateExceptionFromResponse (status);
+ break;
+ case ChangeDir:
+ method = WebRequestMethods.Ftp.PrintWorkingDirectory;
+
+ if (status.StatusCode != FtpStatusCode.FileActionOK)
+ throw CreateExceptionFromResponse (status);
+
+ status = SendCommand (method);
+
+ if (status.StatusCode != FtpStatusCode.PathnameCreated)
+ throw CreateExceptionFromResponse (status);
+ break;
+ case RenameFromCommand:
+ method = WebRequestMethods.Ftp.Rename;
+ if (status.StatusCode != FtpStatusCode.FileCommandPending)
+ throw CreateExceptionFromResponse (status);
+ // Pass an empty string if RenameTo wasn't specified
+ status = SendCommand (RenameToCommand, renameTo != null ? renameTo : String.Empty);
+ if (status.StatusCode != FtpStatusCode.FileActionOK)
+ throw CreateExceptionFromResponse (status);
+ break;
+ case WebRequestMethods.Ftp.DeleteFile:
+ if (status.StatusCode != FtpStatusCode.FileActionOK) {
+ throw CreateExceptionFromResponse (status);
+ }
+ break;
}
- if (res.GotException)
- throw res.Exception;
+ State = RequestState.Finished;
+ }
- return res.Stream;
+ void UploadData ()
+ {
+ State = RequestState.OpeningData;
+
+ OpenDataConnection ();
+
+ State = RequestState.TransferInProgress;
+ requestStream = new FtpDataStream (this, dataStream, false);
+ asyncResult.Stream = requestStream;
}
- public override Stream GetRequestStream ()
+ void DownloadData ()
{
- IAsyncResult asyncResult = BeginGetRequestStream (null, null);
- return EndGetRequestStream (asyncResult);
+ State = RequestState.OpeningData;
+
+ OpenDataConnection ();
+
+ State = RequestState.TransferInProgress;
+ ftpResponse.Stream = new FtpDataStream (this, dataStream, true);
}
void CheckRequestStarted ()
{
- if (requestInProgress)
- throw new InvalidOperationException ("request in progress");
+ if (State != RequestState.Before)
+ throw new InvalidOperationException ("There is a request currently in progress");
}
- bool OpenControlConnection ()
+ 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 e) {
+
+ 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) {
- SetResponseError (new WebException ("Unable to connect to remote server", null,
- WebExceptionStatus.UnknownError, ftpResponse));
- return false;
- }
+ if (sock == null)
+ throw new WebException ("Unable to connect to remote server", exception,
+ WebExceptionStatus.UnknownError, ftpResponse);
controlStream = new NetworkStream (sock);
controlReader = new StreamReader (controlStream, Encoding.ASCII);
- if (!Authenticate ()) {
- SetResponseError (CreateExceptionFromResponse ());
- return false;
+ 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);
}
- return true;
+ if (!msg.EndsWith ("/"))
+ msg += "/";
+ return msg;
}
// Probably we could do better having here a regex
- Socket SetupPassiveConnection ()
+ Socket SetupPassiveConnection (string statusDescription)
{
// Current response string
string response = statusDescription;
if (response.Length < 4)
- return null;
+ throw new WebException ("Cannot open passive data connection");
// Look for first digit after code
int i;
for (i = 3; i < response.Length && !Char.IsDigit (response [i]); i++)
;
if (i >= response.Length)
- return null;
+ throw new WebException ("Cannot open passive data connection");
// Get six elements
string [] digits = response.Substring (i).Split (new char [] {','}, 6);
if (digits.Length != 6)
- return null;
+ throw new WebException ("Cannot open passive data connection");
// Clean non-digits at the end of last element
int j;
for (j = digits [5].Length - 1; j >= 0 && !Char.IsDigit (digits [5][j]); j--)
;
if (j < 0)
- return null;
+ throw new WebException ("Cannot open passive data connection");
digits [5] = digits [5].Substring (0, j + 1);
try {
ip = IPAddress.Parse (String.Join (".", digits, 0, 4));
} catch (FormatException) {
- return null;
+ throw new WebException ("Cannot open passive data connection");
}
// Get the port
int p1, p2, port;
if (!Int32.TryParse (digits [4], out p1) || !Int32.TryParse (digits [5], out p2))
- return null;
+ throw new WebException ("Cannot open passive data connection");
port = (p1 << 8) + p2; // p1 * 256 + p2
//port = p1 * 256 + p2;
if (port < IPEndPoint.MinPort || port > IPEndPoint.MaxPort)
- return null;
+ throw new WebException ("Cannot open passive data connection");
IPEndPoint ep = new IPEndPoint (ip, port);
Socket sock = new Socket (ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try {
sock.Connect (ep);
- } catch (SocketException exc) {
+ } catch (SocketException) {
sock.Close ();
- return null;
+ throw new WebException ("Cannot open passive data connection");
}
return sock;
}
- Exception CreateExceptionFromResponse ()
+ Exception CreateExceptionFromResponse (FtpStatus status)
{
- WebException exc = new WebException ("Server returned an error: " + statusDescription, null,
- WebExceptionStatus.ProtocolError, ftpResponse);
+ FtpWebResponse ftpResponse = new FtpWebResponse (this, requestUri, method, status);
+
+ WebException exc = new WebException ("Server returned an error: " + status.StatusDescription,
+ null, WebExceptionStatus.ProtocolError, ftpResponse);
return exc;
}
// Here we could also get a server error, so be cautious
internal void SetTransferCompleted ()
{
- if (transferCompleted)
+ if (InFinalState ())
return;
-
- transferCompleted = true;
-
- FtpStatusCode status = GetResponseCode ();
- ftpResponse.StatusCode = status;
- ftpResponse.StatusDescription = statusDescription;
+
+ State = RequestState.Finished;
+ FtpStatus status = GetResponseStatus ();
+ ftpResponse.UpdateStatus (status);
+ if(!keepAlive)
+ CloseConnection ();
}
- internal void SetResponseError (Exception exc)
+ internal void OperationCompleted ()
{
- FtpAsyncResult ar = asyncRead;
- if (ar == null)
- ar = asyncWrite;
+ if(!keepAlive)
+ CloseConnection ();
+ }
- ar.SetCompleted (true, exc);
- ar.DoCallback ();
+ void SetCompleteWithError (Exception exc)
+ {
+ if (asyncResult != null) {
+ asyncResult.SetCompleted (false, exc);
+ }
}
Socket InitDataConnection ()
{
- FtpStatusCode status;
+ FtpStatus status;
if (usePassive) {
status = SendCommand (PassiveCommand);
- if (status != FtpStatusCode.EnteringPassive) {
- SetResponseError (CreateExceptionFromResponse ());
- return null;
+ if (status.StatusCode != FtpStatusCode.EnteringPassive) {
+ throw CreateExceptionFromResponse (status);
}
- Socket retval = SetupPassiveConnection ();
- if (retval == null)
- SetResponseError (new WebException ("Couldn't setup passive connection"));
-
- return retval;
+ return SetupPassiveConnection (status.StatusDescription);
}
// Open a socket to listen the server's connection
} catch (SocketException e) {
sock.Close ();
- SetResponseError (new WebException ("Couldn't open listening socket on client", e));
- return null;
+ throw new WebException ("Couldn't open listening socket on client", e);
}
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;
string portParam = ipString + "," + h1 + "," + h2;
status = SendCommand (PortCommand, portParam);
- if (status != FtpStatusCode.CommandOK) {
+
+ if (status.StatusCode != FtpStatusCode.CommandOK) {
sock.Close ();
-
- SetResponseError (CreateExceptionFromResponse ());
- return null;
+ throw (CreateExceptionFromResponse (status));
}
return sock;
}
- bool OpenDataConnection ()
+ void OpenDataConnection ()
{
- FtpStatusCode status;
+ FtpStatus status;
+
Socket s = InitDataConnection ();
- if (s == null)
- return false;
- // 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 != FtpStatusCode.CommandOK) {
- SetResponseError (CreateExceptionFromResponse ());
- return false;
- }
+ // Handle content offset
+ if (offset > 0) {
+ status = SendCommand (RestCommand, offset.ToString ());
+ if (status.StatusCode != FtpStatusCode.FileCommandPending)
+ throw CreateExceptionFromResponse (status);
}
- status = SendCommand (method, requestUri.LocalPath);
- if (status != FtpStatusCode.OpeningData) {
- SetResponseError (CreateExceptionFromResponse ());
- return false;
+ 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;
- return true;
+ dataStream = new NetworkStream (s, false);
+ if (EnableSsl)
+ ChangeToSSLSocket (ref dataStream);
}
+ else {
+
+ // Active connection (use Socket.Blocking to true)
+ Socket incoming = null;
+ try {
+ incoming = s.Accept ();
+ }
+ catch (SocketException) {
+ s.Close ();
+ if (incoming != null)
+ incoming.Close ();
+
+ throw new ProtocolViolationException ("Server commited a protocol violation.");
+ }
- // Active connection (use Socket.Blocking to true)
- Socket incoming = null;
- try {
- incoming = s.Accept ();
- } catch (SocketException e) {
s.Close ();
- if (incoming != null)
- incoming.Close ();
-
- SetResponseError (new ProtocolViolationException ("Server commited a protocol violation."));
- return false;
- }
+ dataStream = new NetworkStream (incoming, false);
+ if (EnableSsl)
+ ChangeToSSLSocket (ref dataStream);
+ }
- s.Close ();
- dataSocket = incoming;
- return true;
+ ftpResponse.UpdateStatus (status);
}
- // Take in count 'account' case
- bool Authenticate ()
+ void Authenticate ()
{
string username = null;
string password = null;
-
+ string domain = null;
+
if (credentials != null) {
username = credentials.UserName;
password = credentials.Password;
- // account = credentials.Domain;
+ domain = credentials.Domain;
}
if (username == null)
username = "anonymous";
if (password == null)
password = "@anonymous";
+ if (!string.IsNullOrEmpty (domain))
+ username = domain + '\\' + username;
// Connect to server and get banner message
- FtpStatusCode status = GetResponseCode ();
- ftpResponse.BannerMessage = statusDescription;
- if (status != FtpStatusCode.SendUserCommand)
- return false;
+ FtpStatus status = GetResponseStatus ();
+ ftpResponse.BannerMessage = status.StatusDescription;
+
+ 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)
+ throw CreateExceptionFromResponse (status);
status = SendCommand (UserCommand, username);
- if (status == FtpStatusCode.LoggedInProceed) {
- ftpResponse.WelcomeMessage = statusDescription;
- return true;
- }
- if (status == FtpStatusCode.SendPasswordCommand) {
- status = SendCommand (PasswordCommand, password);
- if (status == FtpStatusCode.LoggedInProceed) {
- ftpResponse.WelcomeMessage = statusDescription;
- return true;
- }
- return false;
+ switch (status.StatusCode) {
+ case FtpStatusCode.SendPasswordCommand:
+ status = SendCommand (PasswordCommand, password);
+ if (status.StatusCode != FtpStatusCode.LoggedInProceed)
+ throw CreateExceptionFromResponse (status);
+ break;
+ case FtpStatusCode.LoggedInProceed:
+ break;
+ default:
+ throw CreateExceptionFromResponse (status);
}
- return false;
+ ftpResponse.WelcomeMessage = status.StatusDescription;
+ ftpResponse.UpdateStatus (status);
+ }
+
+ FtpStatus SendCommand (string command, params string [] parameters) {
+ return SendCommand (true, command, parameters);
}
- FtpStatusCode SendCommand (string command, params string [] parameters)
+ FtpStatus SendCommand (bool waitResponse, string command, params string [] parameters)
{
byte [] cmd;
string commandString = command;
controlStream.Write (cmd, 0, cmd.Length);
} catch (IOException) {
//controlStream.Close ();
- return FtpStatusCode.ServiceNotAvailable;
+ return new FtpStatus(FtpStatusCode.ServiceNotAvailable, "Write failed");
}
- return GetResponseCode ();
+ if(!waitResponse)
+ return null;
+
+ FtpStatus result = GetResponseStatus ();
+ if (ftpResponse != null)
+ ftpResponse.UpdateStatus (result);
+ return result;
}
- internal FtpStatusCode GetResponseCode ()
+ internal static FtpStatus ServiceNotAvailable ()
{
- string responseString = null;
- try {
- responseString = controlReader.ReadLine ();
- } catch (IOException exc) {
- // controlReader.Close ();
+ return new FtpStatus (FtpStatusCode.ServiceNotAvailable, Locale.GetText ("Invalid response from server"));
+ }
+
+ internal FtpStatus GetResponseStatus ()
+ {
+ while (true) {
+ string response = null;
+
+ try {
+ response = controlReader.ReadLine ();
+ } catch (IOException) {
+ }
+
+ if (response == null || response.Length < 3)
+ return ServiceNotAvailable ();
+
+ int code;
+ 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 Stream stream) {
+ FtpStatus status = SendCommand (AuthCommand, "TLS");
+ if (status.StatusCode != FtpStatusCode.ServerWantsSecureSession)
+ throw CreateExceptionFromResponse (status);
+
+ ChangeToSSLSocket (ref 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
- if (responseString == null || responseString.Length < 3)
- return FtpStatusCode.ServiceNotAvailable;
+ 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
+ }
+
+ bool InFinalState () {
+ return (State == RequestState.Aborted || State == RequestState.Error || State == RequestState.Finished);
+ }
- string codeString = responseString.Substring (0, 3);
- int code;
- if (!Int32.TryParse (codeString, out code))
- return FtpStatusCode.ServiceNotAvailable;
+ bool InProgress () {
+ return (State != RequestState.Before && !InFinalState ());
+ }
- statusDescription = responseString;
- return statusCode = (FtpStatusCode) code;
+ internal void CheckIfAborted () {
+ if (State == RequestState.Aborted)
+ throw new WebException ("Request aborted", WebExceptionStatus.RequestCanceled);
}
+ void CheckFinalState () {
+ if (InFinalState ())
+ throw new InvalidOperationException ("Cannot change final state");
+ }
}
}