gvfs/daemon/gvfsftptask.c

1271 lines
41 KiB
C

/* GIO - GLib Input, Output and Streaming Library
*
* Copyright (C) 2009 Benjamin Otte <otte@gnome.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General
* Public License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
* Author: Benjamin Otte <otte@gnome.org>
*/
#include <config.h>
#include <stdio.h> /* for sscanf() */
#include <stdlib.h> /* for exit() */
#include <glib/gi18n.h>
#include "gvfsftptask.h"
/*** DOCS ***/
/**
* GVfsFtpResponseFlags:
* @G_VFS_FTP_PASS_100: Don't treat 1XX responses, but return them
* @G_VFS_FTP_PASS_300: Don't treat 3XX responses, but return them
* @G_VFS_FTP_PASS_400: Don't treat 4XX responses, but return them
* @G_VFS_FTP_PASS_500: Don't treat 5XX responses, but return them
* @G_VFS_FTP_PASS_550: Don't treat 550 responses, but return them
* @G_VFS_FTP_FAIL_200: Fail on a 2XX response
*
* These flags can be passed to gvfs_ftp_task_receive() (and in
* turn gvfs_ftp_task_send()) to influence the behavior of the functions.
*/
/**
* G_VFS_FTP_G_VFS_FTP_GROUP:
* @response: a valid ftp response
*
* Determines the group the given @response belongs to. The group is the first
* digit of the reply.
*
* Returns: The group the response code belonged to from 1-5
*/
/**
* G_VFS_FTP_TASK_INIT:
* @backend: the backend used by this task
* @job: the job that initiated the task or %NULL if none
*
* Initializes a new task structure for the given backend and job.
*/
/**
* GVfsFtpErrorFunc:
* @task: task to handle
* @data: data argument provided to g_vfs_ftp_task_send_and_check()
*
* Function prototype for error checking functions used by
* g_vfs_ftp_task_send_and_check(). When called, these functions are supposed
* to check a specific error condition and if met, set an error on the passed
* @task.
*/
/*** CODE ***/
gboolean
g_vfs_ftp_task_login (GVfsFtpTask *task,
const char * username,
const char * password)
{
guint status;
g_return_val_if_fail (task != NULL, FALSE);
g_return_val_if_fail (username != NULL, FALSE);
g_return_val_if_fail (password != NULL, FALSE);
if (g_vfs_ftp_task_is_in_error (task))
return FALSE;
status = g_vfs_ftp_task_send (task, G_VFS_FTP_PASS_300,
"USER %s", username);
if (G_VFS_FTP_RESPONSE_GROUP (status) == 3)
{
/* rationale for choosing the default password:
* - some ftp servers expect something that looks like an email address
* - we don't want to send the user's name or address, as that would be
* a privacy problem
* - we want to give ftp server administrators a chance to notify us of
* problems with our client.
* - we don't want to drown in spam.
*/
if (password == NULL || password[0] == 0)
password = "gvfsd-ftp-" VERSION "@example.com";
status = g_vfs_ftp_task_send (task, 0,
"PASS %s", password);
}
return status;
}
/**
* g_vfs_ftp_task_setup_connection:
* @task: the task
*
* Sends all commands necessary to put the connection into a usable state,
* like setting the transfer mode to binary. Note that passive mode will
* will be set on a case-by-case basis when opening a data connection.
**/
void
g_vfs_ftp_task_setup_connection (GVfsFtpTask *task)
{
g_return_if_fail (task != NULL);
/* only binary transfers please */
g_vfs_ftp_task_send (task, 0, "TYPE I");
if (g_vfs_ftp_task_is_in_error (task))
return;
#if 0
/* RFC 2428 suggests to send this to make NAT routers happy */
/* XXX: Disabled for the following reasons:
* - most ftp clients don't use it
* - lots of broken ftp servers can't see the difference between
* "EPSV" and "EPSV ALL"
* - impossible to dynamically fall back to regular PASV in case
* EPSV doesn't work for some reason.
* If this makes your ftp connection fail, please file a bug and we will
* try to invent a way to make this all work. Until then, we'll just
* ignore the RFC.
*/
if (g_vfs_backend_ftp_has_feature (task->backend, g_VFS_FTP_FEATURE_EPSV))
g_vfs_ftp_task_send (task, 0, "EPSV ALL");
g_vfs_ftp_task_clear_error (task);
#endif
/* instruct server that we'll give and assume we get utf8 */
if (g_vfs_backend_ftp_has_feature (task->backend, G_VFS_FTP_FEATURE_UTF8))
{
if (!g_vfs_ftp_task_send (task, 0, "OPTS UTF8 ON"))
g_vfs_ftp_task_clear_error (task);
}
}
static void
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
*
* Acquires a new connection for use by this @task. This uses the connection
* pool of @task's backend, so it reuses previously opened connections and
* does not reopen new connections unnecessarily. If all connections are busy,
* it waits %G_VFS_FTP_TIMEOUT_IN_SECONDS seconds for a new connection to
* become available. Keep in mind that a newly acquired connection might have
* timed out and therefore closed by the FTP server. You must account for
* this when sending the first command to the server.
*
* Returns: %TRUE if a connection could be acquired, %FALSE if an error
* occured
**/
static gboolean
g_vfs_ftp_task_acquire_connection (GVfsFtpTask *task)
{
GVfsBackendFtp *ftp;
gint64 end_time;
gulong id;
g_return_val_if_fail (task != NULL, FALSE);
g_return_val_if_fail (task->conn == NULL, FALSE);
if (g_vfs_ftp_task_is_in_error (task))
return FALSE;
ftp = task->backend;
g_mutex_lock (&ftp->mutex);
id = g_cancellable_connect (task->cancellable,
G_CALLBACK (do_broadcast),
&ftp->cond, NULL);
while (task->conn == NULL && ftp->queue != NULL)
{
if (g_cancellable_is_cancelled (task->cancellable))
{
task->error = g_error_new_literal (G_IO_ERROR, G_IO_ERROR_CANCELLED,
_("Operation was cancelled"));
break;
}
task->conn = g_queue_pop_head (ftp->queue);
if (task->conn != NULL)
{
if (g_vfs_ftp_connection_is_usable (task->conn))
break;
ftp->connections--;
g_vfs_ftp_connection_free (task->conn);
task->conn = NULL;
}
if (ftp->connections < ftp->max_connections)
{
static GThread *last_thread = NULL;
/* Save current number of connections here, so we can limit maximum
* connections later.
* This is necessary for threading reasons (connections can be
* opened or closed while we are still in the opening process. */
guint maybe_max_connections = ftp->connections;
ftp->connections++;
last_thread = g_thread_self ();
g_mutex_unlock (&ftp->mutex);
task->conn = g_vfs_ftp_connection_new (ftp->addr, task->cancellable, &task->error);
if (G_LIKELY (task->conn != NULL))
{
g_vfs_ftp_task_initial_handshake (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)))
goto out_unlocked;
g_vfs_ftp_connection_free (task->conn);
task->conn = NULL;
}
g_mutex_lock (&ftp->mutex);
ftp->connections--;
/* If this value is still equal to our thread it means there were no races
* trying to open connections and the maybe_max_connections value is
* reliable. */
if (last_thread == g_thread_self () &&
!g_vfs_ftp_task_error_matches (task, G_IO_ERROR, G_IO_ERROR_CANCELLED))
{
g_print ("maybe: %u, max %u (due to %s)\n", maybe_max_connections, ftp->max_connections, task->error->message);
ftp->max_connections = MIN (ftp->max_connections, maybe_max_connections);
if (ftp->max_connections == 0)
{
g_debug ("no more connections left, exiting...\n");
/* FIXME: shut down properly */
exit (0);
}
}
g_vfs_ftp_task_clear_error (task);
continue;
}
end_time = g_get_monotonic_time () + G_VFS_FTP_TIMEOUT_IN_SECONDS * G_TIME_SPAN_SECOND;
if (ftp->busy_connections >= ftp->connections ||
!g_cond_wait_until (&ftp->cond, &ftp->mutex, end_time))
{
task->error = g_error_new_literal (G_IO_ERROR, G_IO_ERROR_BUSY,
_("The FTP server is busy. Try again later"));
break;
}
}
if (!ftp->queue)
task->error = g_error_new_literal (G_IO_ERROR, G_IO_ERROR_NOT_MOUNTED,
_("Backend currently unmounting"));
g_mutex_unlock (&ftp->mutex);
out_unlocked:
g_cancellable_disconnect (task->cancellable, id);
return task->conn != NULL;
}
/**
* g_vfs_ftp_task_release_connection:
* @task: a task
*
* Releases the connection in use by @task to the backend's connection pool,
* or frees it if it is in an error state. You must use this function to free
* a @task's connection, never use g_vfs_ftp_connection_free() directly. If
* the task does not have a current connection, this function just returns.
*
* This function also closes all potentially open data connections.
**/
static void
g_vfs_ftp_task_release_connection (GVfsFtpTask *task)
{
g_return_if_fail (task != NULL);
/* we allow task->conn == NULL to ease error cases */
if (task->conn == NULL)
return;
g_vfs_ftp_task_close_data_connection (task);
g_mutex_lock (&task->backend->mutex);
if (task->backend->queue && g_vfs_ftp_connection_is_usable (task->conn))
{
g_queue_push_tail (task->backend->queue, task->conn);
g_cond_signal (&task->backend->cond);
}
else
{
task->backend->connections--;
g_vfs_ftp_connection_free (task->conn);
}
g_mutex_unlock (&task->backend->mutex);
task->conn = NULL;
}
/**
* g_vfs_ftp_task_done:
* @task: the task to finalize
*
* Finalizes the given task and clears all memory in use. It also marks the
* associated job as success or failure depending on the error state of the
* task.
**/
void
g_vfs_ftp_task_done (GVfsFtpTask *task)
{
g_return_if_fail (task != NULL);
g_vfs_ftp_task_release_connection (task);
if (task->job)
{
if (g_vfs_ftp_task_is_in_error (task))
g_vfs_job_failed_from_error (task->job, task->error);
else
g_vfs_job_succeeded (task->job);
}
g_vfs_ftp_task_clear_error (task);
}
/**
* g_vfs_ftp_task_set_error_from_response:
* @task: the task
* @response: the response code
*
* Sets the @task into an error state. The exact error is determined from the
* @response code.
**/
void
g_vfs_ftp_task_set_error_from_response (GVfsFtpTask *task, guint response)
{
const char *msg;
int code;
g_return_if_fail (task != NULL);
g_return_if_fail (task->error == NULL);
/* Please keep this list ordered by response code,
* but group responses with the same message. */
switch (response)
{
case 332: /* Need account for login. */
case 532: /* Need account for storing files. */
/* FIXME: implement a sane way to handle accounts. */
code = G_IO_ERROR_NOT_SUPPORTED;
msg = _("Accounts are unsupported");
break;
case 421: /* Service not available, closing control connection. */
code = G_IO_ERROR_FAILED;
msg = _("Host closed connection");
break;
case 425: /* Can't open data connection. */
code = G_IO_ERROR_CLOSED;
msg = _("Cannot open data connection. Maybe your firewall prevents this?");
break;
case 426: /* Connection closed; transfer aborted. */
code = G_IO_ERROR_CLOSED;
msg = _("Data connection closed");
break;
case 450: /* Requested file action not taken. File unavailable (e.g., file busy). */
case 550: /* Requested action not taken. File unavailable (e.g., file not found, no access). */
/* FIXME: This is a lot of different errors. So we have to pretend to
* be smart here. */
code = G_IO_ERROR_FAILED;
msg = _("Operation failed");
break;
case 451: /* Requested action aborted: local error in processing. */
code = G_IO_ERROR_FAILED;
msg = _("Operation failed");
break;
case 452: /* Requested action not taken. Insufficient storage space in system. */
case 552:
code = G_IO_ERROR_NO_SPACE;
msg = _("No space left on server");
break;
case 500: /* Syntax error, command unrecognized. */
case 501: /* Syntax error in parameters or arguments. */
case 502: /* Command not implemented. */
case 503: /* Bad sequence of commands. */
case 504: /* Command not implemented for that parameter. */
code = G_IO_ERROR_NOT_SUPPORTED;
msg = _("Operation not supported");
break;
case 522: /* EPRT: unsupported network protocol */
code = G_IO_ERROR_NOT_SUPPORTED;
msg = _("Unsupported network protocol");
break;
case 530: /* Not logged in. */
code = G_IO_ERROR_PERMISSION_DENIED;
msg = _("Permission denied");
break;
case 551: /* Requested action aborted: page type unknown. */
code = G_IO_ERROR_FAILED;
msg = _("Page type unknown");
break;
case 553: /* Requested action not taken. File name not allowed. */
code = G_IO_ERROR_INVALID_FILENAME;
msg = _("Invalid filename");
break;
default:
code = G_IO_ERROR_FAILED;
msg = _("Invalid reply");
break;
}
g_set_error_literal (&task->error, G_IO_ERROR, code, msg);
}
/**
* g_vfs_ftp_task_give_connection:
* @task: the task
* @conn: the connection that the @task should use
*
* Forces a given @task to do I/O using the given connection. The @task must
* not have a connection associated with itself. The @task will take
* ownership of @conn.
**/
void
g_vfs_ftp_task_give_connection (GVfsFtpTask * task,
GVfsFtpConnection *conn)
{
g_return_if_fail (task != NULL);
g_return_if_fail (task->conn == NULL);
task->conn = conn;
/* this connection is not busy anymore */
g_mutex_lock (&task->backend->mutex);
g_assert (task->backend->busy_connections > 0);
task->backend->busy_connections--;
g_mutex_unlock (&task->backend->mutex);
}
/**
* g_vfs_ftp_task_take_connection:
* @task: the task
*
* Acquires the connection in use by the @task, so it can later be used with
* g_vfs_ftp_task_give_connection(). This or any other task will not use the
* connection anymore. The @task must have a connection in use.
*
* Returns: The connection that @task was using. You acquire ownership of
* the connection.
**/
GVfsFtpConnection *
g_vfs_ftp_task_take_connection (GVfsFtpTask *task)
{
GVfsFtpConnection *conn;
GVfsBackendFtp *ftp;
g_return_val_if_fail (task != NULL, NULL);
g_return_val_if_fail (task->conn != NULL, NULL);
conn = task->conn;
task->conn = NULL;
ftp = task->backend;
/* mark this connection as busy */
g_mutex_lock (&ftp->mutex);
ftp->busy_connections++;
/* if all connections are busy, signal all waiting threads,
* so they stop waiting and return BUSY earlier */
if (ftp->busy_connections >= ftp->connections)
g_cond_broadcast (&ftp->cond);
g_mutex_unlock (&ftp->mutex);
return conn;
}
/**
* g_vfs_ftp_task_send:
* @task: the sending task
* @flags: response flags to use when sending
* @format: format string to construct command from
* (without trailing \r\n)
* @...: arguments to format string
*
* Shortcut to calling g_vfs_ftp_task_send_and_check() with the reply, funcs
* and data arguments set to %NULL. See that function for details.
*
* Returns: 0 on error or the received FTP code otherwise.
**/
guint
g_vfs_ftp_task_send (GVfsFtpTask * task,
GVfsFtpResponseFlags flags,
const char * format,
...)
{
va_list varargs;
guint response;
g_return_val_if_fail (task != NULL, 0);
g_return_val_if_fail (format != NULL, 0);
va_start (varargs, format);
response = g_vfs_ftp_task_sendv (task,
flags,
NULL,
format,
varargs);
va_end (varargs);
return response;
}
/**
* g_vfs_ftp_task_send_and_check:
* @task: the sending task
* @flags: response flags to use when sending
* @funcs: %NULL or %NULL-terminated array of functions used to determine the
* exact failure case upon a "550 Operation Failed" reply. This is
* often necessary
* @data: data to pass to @funcs.
* @reply: %NULL or pointer to take a char array containing the full reply of
* the ftp server upon successful reply. Use g_strfreev() to free
* after use.
* @format: format string to construct command from
* (without trailing \r\n)
* @...: arguments to format string
*
* Takes an ftp command in printf-style @format, potentially acquires a
* connection automatically, sends the command and waits for an answer from
* the ftp server. Without any @flags, FTP response codes other than 2xx cause
* an error. If @reply is not %NULL, the full reply will be put into a
* %NULL-terminated string array that must be freed with g_strfreev() after
* use.
* If @funcs is set, the 550 response code will cause all of these functions to
* be called in order passing them the @task and @data arguments given to this
* function until one of them sets an error on @task. This error will then be
* returned from this function. If none of those functions sets an error, the
* generic error for the 550 response will be used.
* If an error has been set on @task previously, this function will do nothing.
*
* Returns: 0 on error or the received FTP code otherwise.
**/
guint
g_vfs_ftp_task_send_and_check (GVfsFtpTask * task,
GVfsFtpResponseFlags flags,
const GVfsFtpErrorFunc *funcs,
gpointer data,
char *** reply,
const char * format,
...)
{
va_list varargs;
guint response;
g_return_val_if_fail (task != NULL, 0);
g_return_val_if_fail (format != NULL, 0);
g_return_val_if_fail (funcs == NULL || funcs[0] != NULL, 0);
if (funcs)
{
g_return_val_if_fail ((flags & G_VFS_FTP_PASS_550) == 0, 0);
flags |= G_VFS_FTP_PASS_550;
}
va_start (varargs, format);
response = g_vfs_ftp_task_sendv (task,
flags,
reply,
format,
varargs);
va_end (varargs);
if (response == 550 && funcs)
{
/* close a potentially open data connection, the error handlers
* might try to open new ones and that would cause assertions */
g_vfs_ftp_task_close_data_connection (task);
while (*funcs && !g_vfs_ftp_task_is_in_error (task))
{
(*funcs) (task, data);
funcs++;
}
if (!g_vfs_ftp_task_is_in_error (task))
g_vfs_ftp_task_set_error_from_response (task, response);
response = 0;
}
return response;
}
/**
* g_vfs_ftp_task_sendv:
* @task: the sending task
* @flags: response flags to use when receiving the reply
* @reply: %NULL or pointer to char array that takes the full reply from the
* server
* @format: format string to construct command from
* (without trailing \r\n)
* @varargs: arguments to format string
*
* This is the varargs version of g_vfs_ftp_task_send(). See that function
* for details.
*
* Returns: the received FTP code or 0 on error.
**/
guint
g_vfs_ftp_task_sendv (GVfsFtpTask * task,
GVfsFtpResponseFlags flags,
char *** reply,
const char * format,
va_list varargs)
{
GString *command;
gboolean retry_on_timeout = FALSE;
guint response;
if (g_vfs_ftp_task_is_in_error (task))
return 0;
command = g_string_new ("");
g_string_append_vprintf (command, format, varargs);
g_string_append (command, "\r\n");
retry:
if (task->conn == NULL)
{
if (!g_vfs_ftp_task_acquire_connection (task))
{
g_string_free (command, TRUE);
return 0;
}
retry_on_timeout = TRUE;
}
g_vfs_ftp_connection_send (task->conn,
command->str,
command->len,
task->cancellable,
&task->error);
response = g_vfs_ftp_task_receive (task, flags, reply);
/* NB: requires adaption if we allow passing 4xx responses */
if (retry_on_timeout &&
g_vfs_ftp_task_is_in_error (task) &&
!g_vfs_ftp_connection_is_usable (task->conn))
{
g_vfs_ftp_task_clear_error (task);
g_vfs_ftp_task_release_connection (task);
goto retry;
}
g_string_free (command, TRUE);
return response;
}
/**
* g_vfs_ftp_task_receive:
* @task: the receiving task
* @flags: response flags to use
* @reply: %NULL or pointer to char array that takes the full reply from the
* server
*
* Unless @task is in an error state, this function receives a reply from
* the @task's connection. The @task must have a connection set, which will
* happen when either g_vfs_ftp_task_send() or
* g_vfs_ftp_task_give_connection() have been called on the @task before.
* Unless @flags are given, all reply codes not in the 200s cause an error.
* If @task is in an error state when calling this function, nothing will
* happen and the function will just return.
*
* Returns: the received FTP code or 0 on error.
**/
guint
g_vfs_ftp_task_receive (GVfsFtpTask * task,
GVfsFtpResponseFlags flags,
char *** reply)
{
guint response;
g_return_val_if_fail (task != NULL, 0);
if (g_vfs_ftp_task_is_in_error (task))
return 0;
g_return_val_if_fail (task->conn != NULL, 0);
response = g_vfs_ftp_connection_receive (task->conn,
reply,
task->cancellable,
&task->error);
switch (G_VFS_FTP_RESPONSE_GROUP (response))
{
case 0:
return 0;
case 1:
if (flags & G_VFS_FTP_PASS_100)
break;
g_vfs_ftp_task_set_error_from_response (task, response);
break;
case 2:
if (flags & G_VFS_FTP_FAIL_200)
g_vfs_ftp_task_set_error_from_response (task, response);
break;
case 3:
if (flags & G_VFS_FTP_PASS_300)
break;
g_vfs_ftp_task_set_error_from_response (task, response);
break;
case 4:
g_vfs_ftp_task_set_error_from_response (task, response);
break;
case 5:
if ((flags & G_VFS_FTP_PASS_500) ||
(response == 550 && (flags & G_VFS_FTP_PASS_550)))
break;
g_vfs_ftp_task_set_error_from_response (task, response);
break;
default:
g_assert_not_reached ();
break;
}
if (g_vfs_ftp_task_is_in_error (task))
{
if (response != 0 && reply)
{
g_strfreev (*reply);
*reply = NULL;
}
response = 0;
}
return response;
}
/**
* g_vfs_ftp_task_close_data_connection:
* @task: a task potentially having an open data connection
*
* Closes any data connection @task might have opened.
*/
void
g_vfs_ftp_task_close_data_connection (GVfsFtpTask *task)
{
g_return_if_fail (task != NULL);
if (task->conn == NULL)
return;
g_vfs_ftp_connection_close_data_connection (task->conn);
}
static GSocketAddress *
g_vfs_ftp_task_create_remote_address (GVfsFtpTask *task, guint port)
{
GSocketAddress *old, *new;
old = g_vfs_ftp_connection_get_address (task->conn, &task->error);
if (old == NULL)
return NULL;
g_assert (G_IS_INET_SOCKET_ADDRESS (old));
new = g_inet_socket_address_new (g_inet_socket_address_get_address (G_INET_SOCKET_ADDRESS (old)), port);
return new;
}
static GVfsFtpMethod
g_vfs_ftp_task_setup_data_connection_epsv (GVfsFtpTask *task, GVfsFtpMethod method)
{
const char *s;
char **reply;
guint port;
GSocketAddress *addr;
guint status;
gboolean success;
g_assert (task->error == NULL);
status = g_vfs_ftp_task_send_and_check (task, G_VFS_FTP_PASS_500, NULL, NULL, &reply, "EPSV");
if (G_VFS_FTP_RESPONSE_GROUP (status) != 2)
return G_VFS_FTP_METHOD_ANY;
/* FIXME: parse multiple lines? */
s = strrchr (reply[0], '(');
if (!s)
goto fail;
s += 4;
port = strtoul (s, NULL, 10);
if (port == 0)
goto fail;
g_strfreev (reply);
addr = g_vfs_ftp_task_create_remote_address (task, port);
if (addr == NULL)
return G_VFS_FTP_METHOD_ANY;
success = g_vfs_ftp_connection_open_data_connection (task->conn,
addr,
task->cancellable,
&task->error);
g_object_unref (addr);
return success ? G_VFS_FTP_METHOD_EPSV : G_VFS_FTP_METHOD_ANY;
fail:
g_strfreev (reply);
return G_VFS_FTP_METHOD_ANY;
}
static GVfsFtpMethod
g_vfs_ftp_task_setup_data_connection_pasv (GVfsFtpTask *task, GVfsFtpMethod method)
{
guint ip1, ip2, ip3, ip4, port1, port2;
char **reply;
const char *s;
GSocketAddress *addr;
guint status;
gboolean success;
status = g_vfs_ftp_task_send_and_check (task, 0, NULL, NULL, &reply, "PASV");
if (status == 0)
return G_VFS_FTP_METHOD_ANY;
/* parse response and try to find the address to connect to.
* This code does the same as curl.
*/
for (s = reply[0]; *s; s++)
{
if (sscanf (s, "%u,%u,%u,%u,%u,%u",
&ip1, &ip2, &ip3, &ip4,
&port1, &port2) == 6)
break;
}
if (*s == 0)
{
g_strfreev (reply);
g_set_error_literal (&task->error, G_IO_ERROR, G_IO_ERROR_FAILED,
_("Invalid reply"));
return G_VFS_FTP_METHOD_ANY;
}
g_strfreev (reply);
if (method == G_VFS_FTP_METHOD_PASV || method == G_VFS_FTP_METHOD_ANY)
{
guint8 ip[4];
GInetAddress *inet_addr;
ip[0] = ip1;
ip[1] = ip2;
ip[2] = ip3;
ip[3] = ip4;
inet_addr = g_inet_address_new_from_bytes (ip, G_SOCKET_FAMILY_IPV4);
addr = g_inet_socket_address_new (inet_addr, port1 << 8 | port2);
g_object_unref (inet_addr);
success = g_vfs_ftp_connection_open_data_connection (task->conn,
addr,
task->cancellable,
&task->error);
g_object_unref (addr);
if (success)
return G_VFS_FTP_METHOD_PASV;
if (g_vfs_ftp_task_is_in_error (task) && method != G_VFS_FTP_METHOD_ANY)
return G_VFS_FTP_METHOD_ANY;
g_vfs_ftp_task_clear_error (task);
}
if (method == G_VFS_FTP_METHOD_PASV_ADDR || method == G_VFS_FTP_METHOD_ANY)
{
/* Workaround code:
* Various ftp servers aren't setup correctly when behind a NAT. They report
* their own IP address (like 10.0.0.4) and not the address in front of the
* NAT. But this is likely the same address that we connected to with our
* command connetion. So if the address given by PASV fails, we fall back
* to the address of the command stream.
*/
addr = g_vfs_ftp_task_create_remote_address (task, port1 << 8 | port2);
if (addr == NULL)
return G_VFS_FTP_METHOD_ANY;
success = g_vfs_ftp_connection_open_data_connection (task->conn,
addr,
task->cancellable,
&task->error);
g_object_unref (addr);
if (success)
return G_VFS_FTP_METHOD_PASV_ADDR;
}
return G_VFS_FTP_METHOD_ANY;
}
static GVfsFtpMethod
g_vfs_ftp_task_setup_data_connection_eprt (GVfsFtpTask *task, GVfsFtpMethod unused)
{
GSocketAddress *addr;
guint status, port, family;
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;
switch (g_socket_address_get_family (addr))
{
case G_SOCKET_FAMILY_IPV4:
family = 1;
break;
case G_SOCKET_FAMILY_IPV6:
family = 2;
break;
default:
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)));
/* if this ever happens (and it must not for IP4 and IP6 addresses),
* we need to add support for using a different separator */
g_assert (strchr (ip_string, '|') == NULL);
port = g_inet_socket_address_get_port (G_INET_SOCKET_ADDRESS (addr));
/* we could handle the 522 response here, (unsupported network family),
* but I don't think that will buy us anything */
status = g_vfs_ftp_task_send (task, 0, "EPRT |%u|%s|%u|", family, ip_string, port);
g_free (ip_string);
g_object_unref (addr);
if (status == 0)
return G_VFS_FTP_METHOD_ANY;
return G_VFS_FTP_METHOD_EPRT;
}
static GVfsFtpMethod
g_vfs_ftp_task_setup_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);
typedef struct _GVfsFtpOpenDataConnectionMethod GVfsFtpOpenDataConnectionMethod;
struct _GVfsFtpOpenDataConnectionMethod {
GVfsFtpFeature required_feature;
GSocketFamily required_family;
GVfsFtpOpenDataConnectionFunc func;
};
static gboolean
g_vfs_ftp_task_open_data_connection_method_is_supported (const GVfsFtpOpenDataConnectionMethod *method,
GVfsFtpTask * task,
GSocketFamily family)
{
if (method->required_feature &&
!g_vfs_backend_ftp_has_feature (task->backend, method->required_feature))
return FALSE;
if (method->required_family != G_SOCKET_FAMILY_INVALID &&
method->required_family != family)
return FALSE;
return TRUE;
}
static GSocketFamily
g_vfs_ftp_task_get_socket_family (GVfsFtpTask *task)
{
GSocketAddress *addr;
GSocketFamily family;
/* workaround for the task not having a connection yet */
if (task->conn == NULL &&
g_vfs_ftp_task_send (task, 0, "NOOP") == 0)
{
g_vfs_ftp_task_clear_error (task);
return G_SOCKET_FAMILY_INVALID;
}
addr = g_vfs_ftp_connection_get_address (task->conn, NULL);
if (addr == NULL)
return G_SOCKET_FAMILY_INVALID;
family = g_socket_address_get_family (addr);
g_object_unref (addr);
return family;
}
static GVfsFtpMethod
g_vfs_ftp_task_setup_data_connection_any (GVfsFtpTask *task, GVfsFtpMethod unused)
{
static const GVfsFtpOpenDataConnectionMethod funcs_ordered[] = {
{ 0, G_SOCKET_FAMILY_IPV4, g_vfs_ftp_task_setup_data_connection_pasv },
{ G_VFS_FTP_FEATURE_EPSV, G_SOCKET_FAMILY_INVALID, g_vfs_ftp_task_setup_data_connection_epsv },
{ 0, G_SOCKET_FAMILY_IPV4, g_vfs_ftp_task_setup_data_connection_port },
{ G_VFS_FTP_FEATURE_EPRT, G_SOCKET_FAMILY_INVALID, g_vfs_ftp_task_setup_data_connection_eprt }
};
GVfsFtpMethod method;
GSocketFamily family;
guint i;
family = g_vfs_ftp_task_get_socket_family (task);
/* first try all advertised features */
for (i = 0; i < G_N_ELEMENTS (funcs_ordered); i++)
{
if (!g_vfs_ftp_task_open_data_connection_method_is_supported (&funcs_ordered[i], task, family))
continue;
method = funcs_ordered[i].func (task, G_VFS_FTP_METHOD_ANY);
if (method != G_VFS_FTP_METHOD_ANY)
return method;
g_vfs_ftp_task_clear_error (task);
}
/* then try if the non-advertised features work */
for (i = 0; i < G_N_ELEMENTS (funcs_ordered); i++)
{
if (g_vfs_ftp_task_open_data_connection_method_is_supported (&funcs_ordered[i], task, family))
continue;
method = funcs_ordered[i].func (task, G_VFS_FTP_METHOD_ANY);
if (method != G_VFS_FTP_METHOD_ANY)
return method;
g_vfs_ftp_task_clear_error (task);
}
/* finally, just give up */
return G_VFS_FTP_METHOD_ANY;
}
/**
* g_vfs_ftp_task_setup_data_connection:
* @task: a task not having an open data connection
*
* Sets up a data connection to the ftp server with using the best method for
* this task. If the operation fails, @task will be set into an error state.
* You must call g_vfs_ftp_task_open_data_connection() to finish setup and
* ensure the data connection actually gets opened. Usually, this requires
* sending an FTP command down the stream.
**/
void
g_vfs_ftp_task_setup_data_connection (GVfsFtpTask *task)
{
static const GVfsFtpOpenDataConnectionFunc connect_funcs[] = {
[G_VFS_FTP_METHOD_ANY] = g_vfs_ftp_task_setup_data_connection_any,
[G_VFS_FTP_METHOD_EPSV] = g_vfs_ftp_task_setup_data_connection_epsv,
[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] = g_vfs_ftp_task_setup_data_connection_eprt,
[G_VFS_FTP_METHOD_PORT] = g_vfs_ftp_task_setup_data_connection_port
};
GVfsFtpMethod method, result;
g_return_if_fail (task != NULL);
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))
return;
result = connect_funcs[method] (task, method);
/* be sure to try all possibilities if one failed */
if (result == G_VFS_FTP_METHOD_ANY &&
method != G_VFS_FTP_METHOD_ANY &&
!g_vfs_ftp_task_is_in_error (task))
result = g_vfs_ftp_task_setup_data_connection_any (task, G_VFS_FTP_METHOD_ANY);
g_assert (result < G_N_ELEMENTS (connect_funcs) && connect_funcs[result]);
if (result != method)
{
static const char *methods[] = {
[G_VFS_FTP_METHOD_ANY] = "any",
[G_VFS_FTP_METHOD_EPSV] = "EPSV",
[G_VFS_FTP_METHOD_PASV] = "PASV",
[G_VFS_FTP_METHOD_PASV_ADDR] = "PASV with workaround",
[G_VFS_FTP_METHOD_EPRT] = "EPRT",
[G_VFS_FTP_METHOD_PORT] = "PORT"
};
g_atomic_int_set (&task->backend->method, result);
g_debug ("# set default data connection method from %s to %s\n",
methods[method], methods[result]);
}
task->method = result;
}
/**
* g_vfs_ftp_task_open_data_connection:
* @task: a task
*
* 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)
{
g_return_if_fail (task != NULL);
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);
if (g_vfs_ftp_task_is_in_error (task))
return;
if (task->backend->tls_mode != G_VFS_FTP_TLS_MODE_NONE)
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_initial_handshake:
* @task: a task
* @cb: callback called if there's a verification error
* @user_data: user data passed to @cb
*
* Performs the initial handshake with an FTP server, including reading the
* greeting and activating TLS. If the operation fails, @task will be set into
* an error state.
**/
gboolean
g_vfs_ftp_task_initial_handshake (GVfsFtpTask *task,
CertificateCallback cb,
gpointer user_data)
{
if (g_vfs_ftp_task_is_in_error (task))
return FALSE;
/* In implicit mode, we do the TLS handshake first, then receive the FTP
* greeting. Explicit mode is practically the opposite.
*/
if (task->backend->tls_mode == G_VFS_FTP_TLS_MODE_IMPLICIT)
{
if (!g_vfs_ftp_connection_enable_tls (task->conn,
task->backend->server_identity,
TRUE,
cb,
user_data,
task->cancellable,
&task->error))
return FALSE;
if (!g_vfs_ftp_task_receive (task, 0, NULL))
return FALSE;
}
else if (task->backend->tls_mode == G_VFS_FTP_TLS_MODE_EXPLICIT)
{
if (!g_vfs_ftp_task_receive (task, 0, NULL))
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,
FALSE,
cb,
user_data,
task->cancellable,
&task->error))
return FALSE;
}
else
{
if (!g_vfs_ftp_task_receive (task, 0, NULL))
return FALSE;
}
/* Request TLS also for the data channels. */
if (task->backend->tls_mode != G_VFS_FTP_TLS_MODE_NONE)
{
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;
}