Commit dccf4aee authored by Ross Lagerwall's avatar Ross Lagerwall

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
parent 7cf5d5ff
......@@ -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)
......
......@@ -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)
......
[Mount]
Type=ftps
Exec=@libexecdir@/gvfsd-ftp
AutoMount=false
Scheme=ftps
DefaultPort=21
HostnameIsInetAddress=true
......@@ -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;
}
......
......@@ -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 */
......
......@@ -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;
}
......@@ -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
......
......@@ -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;
}
......@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment