//
// (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;
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 ();
FtpAsyncResult asyncResult;
FtpWebResponse ftpResponse;
Stream requestStream;
+ string initial_path;
const string ChangeDir = "CWD";
const string UserCommand = "USER";
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;
}
}
usePassive = value;
}
}
+
+ [MonoTODO]
+ public override bool UseDefaultCredentials
+ {
+ get {
+ throw GetMustImplement ();
+ }
+ set {
+ throw GetMustImplement ();
+ }
+ }
public bool UseBinary {
get {
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");
}
}
}
void ProcessRequest () {
if (State == RequestState.Scheduled) {
- ftpResponse = new FtpWebResponse (requestUri, method, keepAlive);
+ ftpResponse = new FtpWebResponse (this, requestUri, method, keepAlive);
try {
ProcessMethod ();
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;
ResolveHost ();
OpenControlConnection ();
+ CWDAndSetFileName (requestUri);
+ SetType ();
switch (method) {
// Open data connection and receive data
case WebRequestMethods.Ftp.PrintWorkingDirectory:
case WebRequestMethods.Ftp.MakeDirectory:
case WebRequestMethods.Ftp.Rename:
+ case WebRequestMethods.Ftp.DeleteFile:
ProcessSimpleMethod ();
break;
default: // What to do here?
}
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 () {
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;
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;
}
OpenDataConnection ();
State = RequestState.TransferInProgress;
- requestStream = new FtpDataStream (this, dataSocket, false);
+ requestStream = new FtpDataStream (this, dataStream, false);
asyncResult.Stream = requestStream;
}
{
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 ()
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);
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
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);
State = RequestState.Finished;
FtpStatus status = GetResponseStatus ();
-
ftpResponse.UpdateStatus (status);
-
+ if(!keepAlive)
+ CloseConnection ();
+ }
+
+ internal void OperationCompleted ()
+ {
if(!keepAlive)
CloseConnection ();
}
}
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;
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 {
}
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;
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)
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
if (InFinalState ())
throw new InvalidOperationException ("Cannot change final state");
}
-
- class EmptyStream : MemoryStream
- {
- internal EmptyStream ()
- : base (new byte [0], false) {
- }
- }
}
}