Blob Blame History Raw
From 2839922c259b848d7689d245a055c628754dc116 Mon Sep 17 00:00:00 2001
From: Benjamin Otte <otte@gnome.org>
Date: Mon, 15 Jun 2009 23:03:26 +0200
Subject: [PATCH 02/13] =?utf-8?q?[FTP]=20Bug=20516704=20=E2=80=93=20Be=20able=20to=20connect=20to=20an=20Active=20FTP=20Site?=
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit

Add initial support for the PORT command. Support for EPRT and a
non-ugly API are still missing.
---
 daemon/gvfsftpconnection.c |  157 +++++++++++++++++++++++++++++++++++++++++++-
 daemon/gvfsftpconnection.h |    7 ++
 daemon/gvfsftptask.c       |   56 ++++++++++++++--
 daemon/gvfsftptask.h       |    1 +
 4 files changed, 215 insertions(+), 6 deletions(-)

diff --git a/daemon/gvfsftpconnection.c b/daemon/gvfsftpconnection.c
index ac5418f..521664c 100644
--- a/daemon/gvfsftpconnection.c
+++ b/daemon/gvfsftpconnection.c
@@ -22,10 +22,12 @@
 
 #include <config.h>
 
+#include "gvfsftpconnection.h"
+
 #include <string.h>
 #include <glib/gi18n.h>
 
-#include "gvfsftpconnection.h"
+#include "gvfsbackendftp.h"
 
 /* used for identifying the connection during debugging */
 static volatile int debug_id = 0;
@@ -37,6 +39,7 @@ struct _GVfsFtpConnection
   GIOStream *        	commands;               /* ftp command stream */
   GDataInputStream *    commands_in;            /* wrapper around in stream to allow line-wise reading */
 
+  GSocket *             listen_socket;          /* socket we are listening on for active FTP connections */
   GIOStream *        	data;                   /* ftp data stream or NULL if not in use */
 
   int                   debug_id;               /* unique id for debugging purposes */
@@ -71,11 +74,22 @@ g_vfs_ftp_connection_new (GSocketConnectable *addr,
   return conn;
 }
 
+static void
+g_vfs_ftp_connection_stop_listening (GVfsFtpConnection *conn)
+{
+  if (conn->listen_socket)
+    {
+      g_object_unref (conn->listen_socket);
+      conn->listen_socket = NULL;
+    }
+}
+
 void
 g_vfs_ftp_connection_free (GVfsFtpConnection *conn)
 {
   g_return_if_fail (conn != NULL);
 
+  g_vfs_ftp_connection_stop_listening (conn);
   if (conn->data)
     g_vfs_ftp_connection_close_data_connection (conn);
 
@@ -218,6 +232,8 @@ g_vfs_ftp_connection_open_data_connection (GVfsFtpConnection *conn,
   g_return_val_if_fail (conn != NULL, FALSE);
   g_return_val_if_fail (conn->data == NULL, FALSE);
 
+  g_vfs_ftp_connection_stop_listening (conn);
+
   conn->data = G_IO_STREAM (g_socket_client_connect (conn->client,
                                                      G_SOCKET_CONNECTABLE (addr),
                                                      cancellable,
@@ -226,6 +242,145 @@ g_vfs_ftp_connection_open_data_connection (GVfsFtpConnection *conn,
   return conn->data != NULL;
 }
 
+/**
+ * g_vfs_ftp_connection_listen_data_connection:
+ * @conn: a connection
+ * @error: %NULL or location to take potential errors
+ *
+ * Initiates a listening socket that the FTP server can connect to. To accept 
+ * connections and initialize data transfers, use 
+ * g_vfs_ftp_connection_accept_data_connection().
+ * This function supports what is known as "active FTP", while
+ * g_vfs_ftp_connection_open_data_connection() is to be used for "passive FTP".
+ *
+ * Returns: the actual address the socket is listening on or %NULL on error
+ **/
+GSocketAddress *
+g_vfs_ftp_connection_listen_data_connection (GVfsFtpConnection *conn,
+                                             GError **          error)
+{
+  GSocketAddress *local, *addr;
+
+  g_return_val_if_fail (conn != NULL, NULL);
+  g_return_val_if_fail (conn->data == NULL, FALSE);
+
+  g_vfs_ftp_connection_stop_listening (conn);
+
+  local = g_socket_connection_get_local_address (G_SOCKET_CONNECTION (conn->commands), error);
+  if (local == NULL)
+    return NULL;
+
+  conn->listen_socket = g_socket_new (g_socket_address_get_family (local),
+                                      G_SOCKET_TYPE_STREAM,
+                                      G_SOCKET_PROTOCOL_TCP,
+                                      error);
+  if (conn->listen_socket == NULL)
+    return NULL;
+
+  g_assert (G_IS_INET_SOCKET_ADDRESS (local));
+  addr = g_inet_socket_address_new (g_inet_socket_address_get_address (G_INET_SOCKET_ADDRESS (local)), 0);
+  g_object_unref (local);
+
+  if (!g_socket_bind (conn->listen_socket, addr, TRUE, error) ||
+      !g_socket_listen (conn->listen_socket, error) ||
+      !(local = g_socket_get_local_address (conn->listen_socket, error)))
+    {
+      g_object_unref (addr);
+      g_vfs_ftp_connection_stop_listening (conn);
+      return NULL;
+    }
+
+  g_object_unref (addr);
+  return local;
+}
+
+static void
+cancel_timer_cb (GCancellable *orig, GCancellable *to_cancel)
+{
+  g_cancellable_cancel (to_cancel);
+}
+
+static gboolean
+cancel_cancellable (gpointer cancellable)
+{
+  g_cancellable_cancel (cancellable);
+  return FALSE;
+}
+
+/**
+ * g_vfs_ftp_connection_accept_data_connection:
+ * @conn: a listening connection
+ * @cancellable: cancellable to interrupt wait
+ * @error: %NULL or location to take a potential error
+ *
+ * Opens a data connection for @conn by accepting an incoming connection on the
+ * address it is listening on via g_vfs_ftp_connection_listen_data_connection(),
+ * which must have been called prior to this function.
+ * If this function succeeds, a data connection will have been opened, and calls
+ * to g_vfs_ftp_connection_get_data_stream() will work.
+ *
+ * Returns: %TRUE if a connection was successfully acquired
+ **/
+gboolean
+g_vfs_ftp_connection_accept_data_connection (GVfsFtpConnection *conn,
+                                             GCancellable *     cancellable,
+                                             GError **          error)
+{
+  GSocket *accepted;
+  GCancellable *timer;
+  gulong cancel_cb_id;
+  GIOCondition condition;
+
+  g_return_val_if_fail (conn != NULL, FALSE);
+  g_return_val_if_fail (conn->data == NULL, FALSE);
+  g_return_val_if_fail (G_IS_SOCKET (conn->listen_socket), FALSE);
+
+  timer = g_cancellable_new ();
+  cancel_cb_id = g_cancellable_connect (cancellable, 
+                                        G_CALLBACK (cancel_timer_cb),
+                                        timer,
+                                        NULL);
+  g_object_ref (timer);
+  g_timeout_add_seconds_full (G_PRIORITY_DEFAULT,
+                              G_VFS_FTP_TIMEOUT_IN_SECONDS,
+                              cancel_cancellable,
+                              timer,
+                              g_object_unref);
+
+  condition = g_socket_condition_wait (conn->listen_socket, G_IO_IN, timer, error);
+
+  g_cancellable_disconnect (cancellable, cancel_cb_id);
+  g_object_unref (timer);
+
+  if ((condition & G_IO_IN) == 0)
+    {
+      if (g_cancellable_is_cancelled (timer) &&
+          !g_cancellable_is_cancelled (cancellable))
+        {
+          g_clear_error (error);
+          g_set_error_literal (error, 
+                               G_IO_ERROR, G_IO_ERROR_HOST_NOT_FOUND,
+                               _("Failed to create active FTP connection. "
+                                 "Maybe your router does not support this?"));
+        }
+      else if (error && *error == NULL)
+        {
+          g_set_error_literal (error, 
+                               G_IO_ERROR, G_IO_ERROR_HOST_NOT_FOUND,
+                               _("Failed to create active FTP connection."));
+        }
+      return FALSE;
+    }
+
+  accepted = g_socket_accept (conn->listen_socket, error);
+  if (accepted == NULL)
+    return FALSE;
+
+  conn->data = G_IO_STREAM (g_socket_connection_factory_create_connection (accepted));
+  g_object_unref (accepted);
+  return TRUE;
+}
+
 void
 g_vfs_ftp_connection_close_data_connection (GVfsFtpConnection *conn)
 {
diff --git a/daemon/gvfsftpconnection.h b/daemon/gvfsftpconnection.h
index 3605f26..7d0c697 100644
--- a/daemon/gvfsftpconnection.h
+++ b/daemon/gvfsftpconnection.h
@@ -55,6 +55,13 @@ gboolean                g_vfs_ftp_connection_open_data_connection
                                                                GSocketAddress *         addr,
                                                                GCancellable *           cancellable,
                                                                GError **                error);
+GSocketAddress *        g_vfs_ftp_connection_listen_data_connection
+                                                              (GVfsFtpConnection *      conn,
+                                                               GError **                error);
+gboolean                g_vfs_ftp_connection_accept_data_connection
+                                                              (GVfsFtpConnection *      conn,
+                                                               GCancellable *           cancellable,
+                                                               GError **                error);
 void                    g_vfs_ftp_connection_close_data_connection
                                                               (GVfsFtpConnection *      conn);
 GIOStream *             g_vfs_ftp_connection_get_data_stream  (GVfsFtpConnection *      conn);
diff --git a/daemon/gvfsftptask.c b/daemon/gvfsftptask.c
index 37f2b59..879b912 100644
--- a/daemon/gvfsftptask.c
+++ b/daemon/gvfsftptask.c
@@ -799,7 +799,6 @@ g_vfs_ftp_task_setup_data_connection_pasv (GVfsFtpTask *task, GVfsFtpMethod meth
   guint status;
   gboolean success;
 
-  /* only binary transfers please */
   status = g_vfs_ftp_task_send_and_check (task, 0, NULL, NULL, &reply, "PASV");
   if (status == 0)
     return G_VFS_FTP_METHOD_ANY;
@@ -872,6 +871,45 @@ g_vfs_ftp_task_setup_data_connection_pasv (GVfsFtpTask *task, GVfsFtpMethod meth
   return G_VFS_FTP_METHOD_ANY;
 }
 
+static GVfsFtpMethod
+g_vfs_ftp_task_open_data_connection_port (GVfsFtpTask *task, GVfsFtpMethod unused)
+{
+  GSocketAddress *addr;
+  guint status, i, port;
+  char *ip_string;
+
+  /* workaround for the task not having a connection yet */
+  if (task->conn == NULL &&
+      g_vfs_ftp_task_send (task, 0, "NOOP") == 0)
+    return G_VFS_FTP_METHOD_ANY;
+
+  addr = g_vfs_ftp_connection_listen_data_connection (task->conn, &task->error);
+  if (addr == NULL)
+    return G_VFS_FTP_METHOD_ANY;
+  /* the PORT command only supports IPv4 */
+  if (g_socket_address_get_family (addr) != G_SOCKET_FAMILY_IPV4)
+    {
+      g_object_unref (addr);
+      return G_VFS_FTP_METHOD_ANY;
+    }
+
+  ip_string = g_inet_address_to_string (g_inet_socket_address_get_address (G_INET_SOCKET_ADDRESS (addr)));
+  for (i = 0; ip_string[i]; i++)
+    {
+      if (ip_string[i] == '.')
+        ip_string[i] = ',';
+    }
+  port = g_inet_socket_address_get_port (G_INET_SOCKET_ADDRESS (addr));
+
+  status = g_vfs_ftp_task_send (task, 0, "PORT %s,%u,%u", ip_string, port >> 8, port & 0xFF);
+  g_free (ip_string);
+  g_object_unref (addr);
+  if (status == 0)
+    return G_VFS_FTP_METHOD_ANY;
+  
+  return G_VFS_FTP_METHOD_PORT;
+}
+
 typedef GVfsFtpMethod (* GVfsFtpOpenDataConnectionFunc) (GVfsFtpTask *task, GVfsFtpMethod method);
 
 static GVfsFtpMethod
@@ -882,7 +920,8 @@ g_vfs_ftp_task_setup_data_connection_any (GVfsFtpTask *task, GVfsFtpMethod unuse
     GVfsFtpOpenDataConnectionFunc func;
   } funcs_ordered[] = {
     { G_VFS_FTP_FEATURE_EPSV, g_vfs_ftp_task_setup_data_connection_epsv },
-    { 0,                      g_vfs_ftp_task_setup_data_connection_pasv }
+    { 0,                      g_vfs_ftp_task_setup_data_connection_pasv },
+    { 0,                      g_vfs_ftp_task_open_data_connection_port }
   };
   GVfsFtpMethod method;
   guint i;
@@ -936,15 +975,15 @@ g_vfs_ftp_task_setup_data_connection (GVfsFtpTask *task)
     [G_VFS_FTP_METHOD_PASV] = g_vfs_ftp_task_setup_data_connection_pasv,
     [G_VFS_FTP_METHOD_PASV_ADDR] = g_vfs_ftp_task_setup_data_connection_pasv,
     [G_VFS_FTP_METHOD_EPRT] = NULL,
-    [G_VFS_FTP_METHOD_PORT] = NULL
+    [G_VFS_FTP_METHOD_PORT] = g_vfs_ftp_task_open_data_connection_port
   };
   GVfsFtpMethod method, result;
 
   g_return_if_fail (task != NULL);
 
-  /* FIXME: get the method from elsewhere */
+  task->method = G_VFS_FTP_METHOD_ANY;
+
   method = g_atomic_int_get (&task->backend->method);
-  
   g_assert (method < G_N_ELEMENTS (connect_funcs) && connect_funcs[method]);
 
   if (g_vfs_ftp_task_is_in_error (task))
@@ -973,6 +1012,7 @@ g_vfs_ftp_task_setup_data_connection (GVfsFtpTask *task)
       g_debug ("# set default data connection method from %s to %s\n",
                methods[method], methods[result]);
     }
+  task->method = result;
 }
 
 /**
@@ -989,5 +1029,11 @@ g_vfs_ftp_task_open_data_connection (GVfsFtpTask *task)
 
   if (g_vfs_ftp_task_is_in_error (task))
     return;
+
+  if (task->method == G_VFS_FTP_METHOD_EPRT ||
+      task->method == G_VFS_FTP_METHOD_PORT)
+    g_vfs_ftp_connection_accept_data_connection (task->conn, 
+                                                 task->cancellable,
+                                                 &task->error);
 }
 
diff --git a/daemon/gvfsftptask.h b/daemon/gvfsftptask.h
index 8345535..ac0bd74 100644
--- a/daemon/gvfsftptask.h
+++ b/daemon/gvfsftptask.h
@@ -47,6 +47,7 @@ struct _GVfsFtpTask
 
   GError *              error;          /* NULL or current error - will be propagated to task */
   GVfsFtpConnection *   conn;           /* connection in use by this task or NULL if none */
+  GVfsFtpMethod         method;         /* method currently in use (only valid after call to _setup_data_connection() */
 };
 
 typedef void (* GVfsFtpErrorFunc) (GVfsFtpTask *task, gpointer data);
-- 
1.6.3.2