[gvfs] ftp: Implement TLS support



commit dccf4aeea3f94251062836f2c6f5f37219c64054
Author: Ross Lagerwall <rosslagerwall gmail com>
Date:   Thu Feb 26 22:30:44 2015 +0000

    ftp: Implement TLS support
    
    Implement TLS support (aka explicit ftps).  This is done by using a
    different URL scheme, ftps, so that it is only used if explicitly
    specified.
    
    Although the protocol allows transparently upgrading a normal
    connection to a secure one, there are several problems with this.
    FEAT is needed to determine support for it but some servers do not allow
    this before login.  Some servers are configured to allow AUTH TLS but
    have firewalls that block data connections because they can't inspect
    the traffic.  Servers may disallow TLS on the data connection, making it
    unclear to the user how secure the connection is.  Finally, there may be
    verification errors which need to be presented to the user, and these
    are unexpected because they did not choose to use ftps.
    
    Making secure ftp opt-in as a separate URL scheme side-steps most of
    these issues as well as ensuring there are no regressions for normal
    ftp.  When using ftps, we assume that the server implements AUTH TLS so
    the connection is secured before login. It is also assumed that TLS for
    data connections is allowed, so both control and data connection are
    secure before any information is transferred.
    Verification errors are presented during mounting.  If the identity
    changes on subsequent reconnections, the connection is aborted.
    While presenting verification errors to the user in this way is perhaps
    not the best way of handling security, it is fairly standard.
    
    The implementation has been successfully tested on vsftpd, ProFTPD,
    Pure-FTPd, IIS, and FileZilla Server.
    
    Based on a patch by Benjamin Otte.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=526582

 configure.ac               |    2 +-
 daemon/Makefile.am         |    6 +-
 daemon/ftps.mount.in       |    7 ++
 daemon/gvfsbackendftp.c    |   38 ++++++++++++-
 daemon/gvfsbackendftp.h    |    8 +++
 daemon/gvfsftpconnection.c |  133 ++++++++++++++++++++++++++++++++++++++++++--
 daemon/gvfsftpconnection.h |   18 ++++++
 daemon/gvfsftptask.c       |   75 ++++++++++++++++++++++++-
 daemon/gvfsftptask.h       |    4 +
 9 files changed, 278 insertions(+), 13 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index d219aba..9a03466 100644
--- a/configure.ac
+++ b/configure.ac
@@ -61,7 +61,7 @@ GTK_DOC_CHECK
 DISTCHECK_CONFIGURE_FLAGS="--enable-gtk-doc"
 AC_SUBST(DISTCHECK_CONFIGURE_FLAGS)
 
-PKG_CHECK_MODULES(GLIB, glib-2.0 >= 2.43.2 gobject-2.0 gmodule-no-export-2.0 gio-unix-2.0 gio-2.0 )
+PKG_CHECK_MODULES(GLIB, glib-2.0 >= 2.45.0 gobject-2.0 gmodule-no-export-2.0 gio-unix-2.0 gio-2.0 )
 
 PKG_CHECK_MODULES(DBUS, dbus-1)
 
diff --git a/daemon/Makefile.am b/daemon/Makefile.am
index 384f18c..2d52f0a 100644
--- a/daemon/Makefile.am
+++ b/daemon/Makefile.am
@@ -45,8 +45,8 @@ service_DATA = gvfs-daemon.service
 
 libexec_PROGRAMS=gvfsd gvfsd-sftp gvfsd-trash gvfsd-computer gvfsd-burn gvfsd-localtest gvfsd-ftp 
gvfsd-network
 
-mount_in_files = sftp.mount.in ftp.mount.in trash.mount.in computer.mount.in burn.mount.in 
localtest.mount.in network.mount.in
-mount_DATA =  sftp.mount ftp.mount trash.mount computer.mount burn.mount localtest.mount network.mount
+mount_in_files = sftp.mount.in ftp.mount.in ftps.mount.in trash.mount.in computer.mount.in burn.mount.in 
localtest.mount.in network.mount.in
+mount_DATA =  sftp.mount ftp.mount ftps.mount trash.mount computer.mount burn.mount localtest.mount 
network.mount
 
 mount_in_files +=recent.mount.in
 if USE_GTK
@@ -276,7 +276,7 @@ gvfsd_ftp_CPPFLAGS = \
        -DBACKEND_HEADER=gvfsbackendftp.h \
        -DDEFAULT_BACKEND_TYPE=ftp \
        -DMAX_JOB_THREADS=10 \
-       -DBACKEND_TYPES='"ftp", G_VFS_TYPE_BACKEND_FTP,'
+       -DBACKEND_TYPES='"ftp", G_VFS_TYPE_BACKEND_FTP, "ftps", G_VFS_TYPE_BACKEND_FTP,'
 
 gvfsd_ftp_LDADD = $(libraries)
 
diff --git a/daemon/ftps.mount.in b/daemon/ftps.mount.in
new file mode 100644
index 0000000..06ae835
--- /dev/null
+++ b/daemon/ftps.mount.in
@@ -0,0 +1,7 @@
+[Mount]
+Type=ftps
+Exec= libexecdir@/gvfsd-ftp
+AutoMount=false
+Scheme=ftps
+DefaultPort=21
+HostnameIsInetAddress=true
diff --git a/daemon/gvfsbackendftp.c b/daemon/gvfsbackendftp.c
index 5fe056b..388c4c8 100644
--- a/daemon/gvfsbackendftp.c
+++ b/daemon/gvfsbackendftp.c
@@ -393,6 +393,9 @@ g_vfs_backend_ftp_finalize (GObject *object)
   g_free (ftp->user);
   g_free (ftp->password);
 
+  g_clear_object (&ftp->server_identity);
+  g_clear_object (&ftp->certificate);
+
   if (G_OBJECT_CLASS (g_vfs_backend_ftp_parent_class)->finalize)
     (*G_OBJECT_CLASS (g_vfs_backend_ftp_parent_class)->finalize) (object);
 }
@@ -404,6 +407,23 @@ g_vfs_backend_ftp_init (GVfsBackendFtp *ftp)
   g_cond_init (&ftp->cond);
 }
 
+/* If the initial connection has a verification error, display the certificate
+ * to the user and ask whether to proceed. */
+static gboolean
+initial_certificate_cb (GTlsConnection *conn,
+                        GTlsCertificate *certificate,
+                        GTlsCertificateFlags errors,
+                        gpointer user_data)
+{
+  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (user_data);
+
+  /* Save the certificate and result for reconnections. */
+  ftp->certificate = g_object_ref (certificate);
+  ftp->certificate_errors = errors;
+
+  return gvfs_accept_certificate (ftp->mount_source, certificate, errors);
+}
+
 static void
 do_mount (GVfsBackend *backend,
           GVfsJobMount *job,
@@ -435,6 +455,19 @@ restart:
 
   /* send pre-login commands */
   g_vfs_ftp_task_receive (&task, 0, NULL);
+
+  /* Secure the initial connection if necessary. This may result in a prompt
+   * for the user. */
+  ftp->mount_source = mount_source;
+  if (ftp->use_tls &&
+      !g_vfs_ftp_task_enable_tls (&task, initial_certificate_cb, ftp))
+    {
+      ftp->mount_source = NULL;
+      g_vfs_ftp_task_done (&task);
+      return;
+    }
+  ftp->mount_source = NULL;
+
   if (!g_vfs_backend_ftp_uses_workaround (ftp, G_VFS_FTP_WORKAROUND_FEAT_AFTER_LOGIN) &&
       !gvfs_backend_ftp_determine_features (&task))
     {
@@ -619,7 +652,7 @@ try_login:
       g_free (prompt);
     }
 
-  mount_spec = g_mount_spec_new ("ftp");
+  mount_spec = g_mount_spec_new (ftp->use_tls ? "ftps" : "ftp");
   g_mount_spec_set (mount_spec, "host", g_network_address_get_hostname (addr));
   if (port != 21)
     {
@@ -691,6 +724,9 @@ try_mount (GVfsBackend *backend,
     ftp->host_display_name = g_strdup (host);
   else
     ftp->host_display_name = g_strdup_printf ("%s:%u", host, port);
+  ftp->use_tls = strcmp (g_mount_spec_get_type (mount_spec), "ftps") == 0;
+  if (ftp->use_tls)
+    ftp->server_identity = g_object_ref (ftp->addr);
 
   return FALSE;
 }
diff --git a/daemon/gvfsbackendftp.h b/daemon/gvfsbackendftp.h
index f24d413..bd98a79 100644
--- a/daemon/gvfsbackendftp.h
+++ b/daemon/gvfsbackendftp.h
@@ -25,6 +25,7 @@
 
 #include <gvfsbackend.h>
 #include <gmountspec.h>
+#include <gio/gio.h>
 
 G_BEGIN_DECLS
 
@@ -90,6 +91,13 @@ struct _GVfsBackendFtp
   char *                password;              /* password or NULL for anonymous */
   char *                host_display_name;
 
+  /* ftps support */
+  gboolean              use_tls;
+  GSocketConnectable *  server_identity;        /* Server identity used for verification */
+  GTlsCertificate *     certificate;            /* Initial server certificate */
+  GTlsCertificateFlags  certificate_errors;     /* Errors received during TLS handshake */
+  GMountSource *        mount_source;           /* Only used during do_mount */
+
   GVfsFtpSystem         system;                 /* the system from the SYST response */
   int                   features;               /* GVfsFtpFeatures that are supported */
   int                   workarounds;            /* GVfsFtpWorkarounds in use - int because it's atomic */
diff --git a/daemon/gvfsftpconnection.c b/daemon/gvfsftpconnection.c
index edfffac..217d798 100644
--- a/daemon/gvfsftpconnection.c
+++ b/daemon/gvfsftpconnection.c
@@ -37,6 +37,7 @@ struct _GVfsFtpConnection
   GSocketClient *       client;                 /* socket client used for opening connections */
 
   GIOStream *          commands;               /* ftp command stream */
+  GSocketConnection *   connection;             /* original connection */
   GDataInputStream *    commands_in;            /* wrapper around in stream to allow line-wise reading */
   gboolean              waiting_for_reply;           /* TRUE if a command was sent but no reply received yet 
*/
 
@@ -62,6 +63,19 @@ enable_keepalive (GSocketConnection *conn)
   g_socket_set_keepalive (g_socket_connection_get_socket (conn), TRUE);
 }
 
+static void
+create_input_stream (GVfsFtpConnection *conn)
+{
+  if (conn->commands_in)
+    {
+      g_filter_input_stream_set_close_base_stream (G_FILTER_INPUT_STREAM (conn->commands_in), FALSE);
+      g_object_unref (conn->commands_in);
+    }
+
+  conn->commands_in = G_DATA_INPUT_STREAM (g_data_input_stream_new (g_io_stream_get_input_stream 
(conn->commands)));
+  g_data_input_stream_set_newline_type (conn->commands_in, G_DATA_STREAM_NEWLINE_TYPE_CR_LF);
+}
+
 GVfsFtpConnection *
 g_vfs_ftp_connection_new (GSocketConnectable *addr,
                           GCancellable *      cancellable,
@@ -85,9 +99,9 @@ g_vfs_ftp_connection_new (GSocketConnectable *addr,
       return NULL;
     }
 
-  enable_keepalive (G_SOCKET_CONNECTION (conn->commands));
-  conn->commands_in = G_DATA_INPUT_STREAM (g_data_input_stream_new (g_io_stream_get_input_stream 
(conn->commands)));
-  g_data_input_stream_set_newline_type (conn->commands_in, G_DATA_STREAM_NEWLINE_TYPE_CR_LF);
+  conn->connection = G_SOCKET_CONNECTION (conn->commands);
+  enable_keepalive (conn->connection);
+  create_input_stream (conn);
   /* The first thing that needs to happen is receiving the welcome message */
   conn->waiting_for_reply = TRUE;
 
@@ -249,7 +263,60 @@ g_vfs_ftp_connection_get_address (GVfsFtpConnection *conn, GError **error)
 {
   g_return_val_if_fail (conn != NULL, NULL);
 
-  return g_socket_connection_get_remote_address (G_SOCKET_CONNECTION (conn->commands), error);
+  return g_socket_connection_get_remote_address (conn->connection, error);
+}
+
+/**
+ * g_vfs_ftp_connection_data_connection_enable_tls:
+ * @conn: a connection with an active control connection
+ * @server_identity: address of the server used to verify the certificate
+ * @cb: callback called if there's a verification error
+ * @user_data: user data passed to @cb
+ * @cancellable: cancellable to interrupt wait
+ * @error: %NULL or location to take a potential error
+ *
+ * Tries to enable TLS on the given @connection's data connection. If setting
+ * up TLS fails, %FALSE will be returned and @error will be set.
+ *
+ * Returns: %TRUE on success, %FALSE otherwise.
+ **/
+gboolean
+g_vfs_ftp_connection_data_connection_enable_tls (GVfsFtpConnection  *conn,
+                                                 GSocketConnectable *server_identity,
+                                                 CertificateCallback cb,
+                                                 gpointer            user_data,
+                                                 GCancellable *      cancellable,
+                                                 GError **           error)
+{
+  GIOStream *secure;
+
+  g_return_val_if_fail (conn != NULL, FALSE);
+  g_return_val_if_fail (conn->commands != NULL, FALSE);
+
+  secure = g_tls_client_connection_new (conn->data,
+                                        server_identity,
+                                        error);
+  if (secure == NULL)
+    return FALSE;
+
+  g_object_unref (conn->data);
+  conn->data = secure;
+
+  g_tls_client_connection_copy_session_state (G_TLS_CLIENT_CONNECTION (secure),
+                                              G_TLS_CLIENT_CONNECTION (conn->commands));
+
+  g_signal_connect (secure, "accept-certificate", G_CALLBACK (cb), user_data);
+
+  if (!g_tls_connection_handshake (G_TLS_CONNECTION (secure),
+                                   cancellable,
+                                   error))
+    {
+      /* Close here to be sure it won't get used anymore */
+      g_io_stream_close (secure, cancellable, NULL);
+      return FALSE;
+    }
+
+  return TRUE;
 }
 
 gboolean
@@ -295,7 +362,7 @@ g_vfs_ftp_connection_listen_data_connection (GVfsFtpConnection *conn,
 
   g_vfs_ftp_connection_stop_listening (conn);
 
-  local = g_socket_connection_get_local_address (G_SOCKET_CONNECTION (conn->commands), error);
+  local = g_socket_connection_get_local_address (conn->connection, error);
   if (local == NULL)
     return NULL;
 
@@ -480,7 +547,7 @@ g_vfs_ftp_connection_is_usable (GVfsFtpConnection *conn)
     return FALSE;
 
   cond = G_IO_ERR | G_IO_HUP | G_IO_IN;
-  cond = g_socket_condition_check (g_socket_connection_get_socket (G_SOCKET_CONNECTION (conn->commands)), 
cond);
+  cond = g_socket_condition_check (g_socket_connection_get_socket (conn->connection), cond);
   if (cond)
     {
       g_debug ("##%2d ##  connection unusuable: %s%s%s\r\n", conn->debug_id,
@@ -493,3 +560,57 @@ g_vfs_ftp_connection_is_usable (GVfsFtpConnection *conn)
   return TRUE;
 }
 
+/**
+ * g_vfs_ftp_connection_enable_tls:
+ * @conn: a connection without an active data connection
+ * @server_identity: address of the server used to verify the certificate
+ * @cb: callback called if there's a verification error
+ * @user_data: user data passed to @cb
+ * @cancellable: cancellable to interrupt wait
+ * @error: %NULL or location to take a potential error
+ *
+ * Tries to enable TLS on the given @connection. If setting up TLS fails,
+ * %FALSE will be returned and @error will be set. When this function fails,
+ * you need to check if the connection is still usable. It might have been
+ * closed.
+ *
+ * Returns: %TRUE on success, %FALSE otherwise.
+ **/
+gboolean
+g_vfs_ftp_connection_enable_tls (GVfsFtpConnection * conn,
+                                 GSocketConnectable *server_identity,
+                                 CertificateCallback cb,
+                                 gpointer            user_data,
+                                 GCancellable *      cancellable,
+                                 GError **           error)
+{
+  GIOStream *secure;
+
+  g_return_val_if_fail (conn != NULL, FALSE);
+  g_return_val_if_fail (conn->data == NULL, FALSE);
+  g_return_val_if_fail (!conn->waiting_for_reply, FALSE);
+  g_return_val_if_fail (g_buffered_input_stream_get_available (G_BUFFERED_INPUT_STREAM (conn->commands_in)) 
== 0, FALSE);
+
+  secure = g_tls_client_connection_new (conn->commands,
+                                        server_identity,
+                                        error);
+  if (secure == NULL)
+    return FALSE;
+
+  g_object_unref (conn->commands);
+  conn->commands = secure;
+  create_input_stream (conn);
+
+  g_signal_connect (secure, "accept-certificate", G_CALLBACK (cb), user_data);
+
+  if (!g_tls_connection_handshake (G_TLS_CONNECTION (secure),
+                                   cancellable,
+                                   error))
+    {
+      /* Close here to be sure it won't get used anymore */
+      g_io_stream_close (secure, cancellable, NULL);
+      return FALSE;
+    }
+
+  return TRUE;
+}
diff --git a/daemon/gvfsftpconnection.h b/daemon/gvfsftpconnection.h
index a413959..f1ddab3 100644
--- a/daemon/gvfsftpconnection.h
+++ b/daemon/gvfsftpconnection.h
@@ -30,6 +30,11 @@ G_BEGIN_DECLS
 
 typedef struct _GVfsFtpConnection GVfsFtpConnection;
 
+typedef gboolean      (*CertificateCallback)                  (GTlsConnection *         conn,
+                                                               GTlsCertificate *        peer_cert,
+                                                               GTlsCertificateFlags     errors,
+                                                               gpointer                 user_data);
+
 GVfsFtpConnection *     g_vfs_ftp_connection_new              (GSocketConnectable *     addr,
                                                                GCancellable *           cancellable,
                                                                GError **                error);
@@ -66,6 +71,19 @@ void                    g_vfs_ftp_connection_close_data_connection
                                                               (GVfsFtpConnection *      conn);
 GIOStream *             g_vfs_ftp_connection_get_data_stream  (GVfsFtpConnection *      conn);
 
+gboolean                g_vfs_ftp_connection_enable_tls       (GVfsFtpConnection *      conn,
+                                                               GSocketConnectable *     server_identity,
+                                                               CertificateCallback      cb,
+                                                               gpointer                 user_data,
+                                                               GCancellable *           cancellable,
+                                                               GError **                error);
+gboolean                g_vfs_ftp_connection_data_connection_enable_tls (GVfsFtpConnection  *conn,
+                                                                         GSocketConnectable *server_identity,
+                                                                         CertificateCallback cb,
+                                                                         gpointer            user_data,
+                                                                         GCancellable *      cancellable,
+                                                                         GError **           error);
+
 
 
 G_END_DECLS
diff --git a/daemon/gvfsftptask.c b/daemon/gvfsftptask.c
index c70262b..2209c2b 100644
--- a/daemon/gvfsftptask.c
+++ b/daemon/gvfsftptask.c
@@ -161,6 +161,28 @@ do_broadcast (GCancellable *cancellable, GCond *cond)
   g_cond_broadcast (cond);
 }
 
+/* Decide whether to allow verification errors for control and data connections
+ * after the initial connection.  The connection is only allowed if the
+ * identity is the same as for the initial connection. */
+static gboolean
+reconnect_certificate_cb (GTlsConnection *conn,
+                          GTlsCertificate *certificate,
+                          GTlsCertificateFlags errors,
+                          gpointer user_data)
+{
+  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (user_data);
+
+  /* If the verification result has changed in some way, abort the
+   * connection. */
+  if (errors != ftp->certificate_errors)
+    return FALSE;
+
+  /* Only allow the connection if the certificate presented is the same as for
+   * the initial connection which the user accepted. */
+  return ftp->certificate &&
+         g_tls_certificate_is_same (certificate, ftp->certificate);
+}
+
 /**
  * g_vfs_ftp_task_acquire_connection:
  * @task: a task without an associated connection
@@ -229,6 +251,8 @@ g_vfs_ftp_task_acquire_connection (GVfsFtpTask *task)
           if (G_LIKELY (task->conn != NULL))
             {
               g_vfs_ftp_task_receive (task, 0, NULL);
+              if (ftp->use_tls)
+                g_vfs_ftp_task_enable_tls (task, reconnect_certificate_cb, ftp);
               g_vfs_ftp_task_login (task, ftp->user, ftp->password);
               g_vfs_ftp_task_setup_connection (task);
               if (G_LIKELY (!g_vfs_ftp_task_is_in_error (task)))
@@ -1144,8 +1168,8 @@ g_vfs_ftp_task_setup_data_connection (GVfsFtpTask *task)
  * g_vfs_ftp_task_open_data_connection:
  * @task: a task
  *
- * Tries to open a data connection to the ftp server. If the operation fails,
- * @task will be set into an error state.
+ * Tries to open a data connection to the ftp server and secures it if
+ * necessary. If the operation fails, @task will be set into an error state.
  **/
 void
 g_vfs_ftp_task_open_data_connection (GVfsFtpTask *task)
@@ -1160,5 +1184,52 @@ g_vfs_ftp_task_open_data_connection (GVfsFtpTask *task)
     g_vfs_ftp_connection_accept_data_connection (task->conn, 
                                                  task->cancellable,
                                                  &task->error);
+
+  if (g_vfs_ftp_task_is_in_error (task))
+    return;
+
+  if (task->backend->use_tls)
+    g_vfs_ftp_connection_data_connection_enable_tls (task->conn,
+                                                     task->backend->server_identity,
+                                                     reconnect_certificate_cb,
+                                                     task->backend,
+                                                     task->cancellable,
+                                                     &task->error);
 }
 
+/**
+ * g_vfs_ftp_task_enable_tls:
+ * @task: a task
+ * @cb: callback called if there's a verification error
+ * @user_data: user data passed to @cb
+ *
+ * Tries to enable tls on the control connection to an ftp server.
+ * If the operation fails, @task will be set into an error state.
+ **/
+gboolean
+g_vfs_ftp_task_enable_tls (GVfsFtpTask *task,
+                           CertificateCallback cb,
+                           gpointer user_data)
+{
+  if (g_vfs_ftp_task_is_in_error (task))
+    return FALSE;
+
+  if (!g_vfs_ftp_task_send (task, 0, "AUTH TLS"))
+    return FALSE;
+
+  if (!g_vfs_ftp_connection_enable_tls (task->conn,
+                                        task->backend->server_identity,
+                                        cb,
+                                        user_data,
+                                        task->cancellable,
+                                        &task->error))
+    return FALSE;
+
+  if (!g_vfs_ftp_task_send (task, 0, "PBSZ 0"))
+    return FALSE;
+
+  if (!g_vfs_ftp_task_send (task, 0, "PROT P"))
+    return FALSE;
+
+  return TRUE;
+}
diff --git a/daemon/gvfsftptask.h b/daemon/gvfsftptask.h
index 442b467..f2626ba 100644
--- a/daemon/gvfsftptask.h
+++ b/daemon/gvfsftptask.h
@@ -93,6 +93,10 @@ gboolean                g_vfs_ftp_task_login                    (GVfsFtpTask *
                                                                  const char *           password);
 void                    g_vfs_ftp_task_setup_connection         (GVfsFtpTask *          task);
 
+gboolean                g_vfs_ftp_task_enable_tls               (GVfsFtpTask *          task,
+                                                                 CertificateCallback    cb,
+                                                                 gpointer               user_data);
+
 
 G_END_DECLS
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]