summaryrefslogtreecommitdiff
path: root/daemon/gvfsftptask.c
diff options
context:
space:
mode:
authorBenjamin Otte <otte@gnome.org>2009-06-02 13:19:51 +0200
committerBenjamin Otte <otte@gnome.org>2009-06-11 10:05:40 +0200
commite62142b001e218d268a85f2f90993cdf42148fae (patch)
tree4948320f6778cb04118a1c6541291eac338fed73 /daemon/gvfsftptask.c
parent2f5c4dcfd579f9b9cceb8eb08b71d4318e9c03b6 (diff)
downloadgvfs-e62142b001e218d268a85f2f90993cdf42148fae.tar.gz
[FTP] introduce GVfsFtpTask
split out the old FtpConnection struct into a separate GVfsFtpTask structure that acts as a one-stop solution to vfuncs. It keeps track of all important structures: - the backend - the current job - the potential connection to the server - the error state during the lifetime of a backend vfunc and supplies convenience functions to ease implementing these vfuncs. The API of gvfsftptask.h is documented.
Diffstat (limited to 'daemon/gvfsftptask.c')
-rw-r--r--daemon/gvfsftptask.c888
1 files changed, 888 insertions, 0 deletions
diff --git a/daemon/gvfsftptask.c b/daemon/gvfsftptask.c
new file mode 100644
index 00000000..af889fe0
--- /dev/null
+++ b/daemon/gvfsftptask.c
@@ -0,0 +1,888 @@
+/* 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., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, 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);
+}
+
+/**
+ * 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;
+ GTimeVal now;
+ 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)
+ break;
+
+ if (ftp->connections < ftp->max_connections)
+ {
+ /* 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++;
+ 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_receive (task, 0, NULL);
+ 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)))
+ break;
+ }
+
+ g_vfs_ftp_task_clear_error (task);
+ g_vfs_ftp_connection_free (task->conn);
+ task->conn = NULL;
+ g_mutex_lock (ftp->mutex);
+ ftp->connections--;
+ ftp->max_connections = MIN (ftp->max_connections, maybe_max_connections);
+ if (ftp->max_connections == 0)
+ {
+ DEBUG ("no more connections left, exiting...");
+ /* FIXME: shut down properly */
+ exit (0);
+ }
+
+ continue;
+ }
+
+ g_get_current_time (&now);
+ g_time_val_add (&now, G_VFS_FTP_TIMEOUT_IN_SECONDS * 1000 * 1000);
+ if (!g_cond_timed_wait (ftp->cond, ftp->mutex, &now))
+ {
+ task->error = g_error_new_literal (G_IO_ERROR, G_IO_ERROR_BUSY,
+ _("The FTP server is busy. Try again later"));
+ break;
+ }
+ }
+ g_cancellable_disconnect (task->cancellable, id);
+ g_mutex_unlock (ftp->mutex);
+
+ 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.
+ **/
+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_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
+ 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);
+
+ if (task->conn)
+ 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 unsupported");
+ 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;
+}
+
+/**
+ * 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;
+
+ g_return_val_if_fail (task != NULL, NULL);
+ g_return_val_if_fail (task->conn != NULL, NULL);
+
+ conn = task->conn;
+ task->conn = NULL;
+
+ 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)
+ {
+ 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;
+ }
+
+#ifdef PRINT_DEBUG
+ if (g_str_has_prefix (command->str, "PASS"))
+ DEBUG ("--> PASS ***\n");
+ else
+ {
+ command->str[command->len - 2] = 0;
+ DEBUG ("--> %s\n", command->str);
+ command->str[command->len - 2] = '\r';
+ }
+#endif
+ 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);
+ return 0;
+ case 2:
+ if (flags & G_VFS_FTP_FAIL_200)
+ {
+ g_vfs_ftp_task_set_error_from_response (task, response);
+ return 0;
+ }
+ break;
+ case 3:
+ if (flags & G_VFS_FTP_PASS_300)
+ break;
+ g_vfs_ftp_task_set_error_from_response (task, response);
+ return 0;
+ case 4:
+ g_vfs_ftp_task_set_error_from_response (task, response);
+ return 0;
+ 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);
+ return 0;
+ default:
+ g_assert_not_reached ();
+ break;
+ }
+
+ return response;
+}
+
+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 gboolean
+g_vfs_ftp_task_open_data_connection_epsv (GVfsFtpTask *task)
+{
+ const char *s;
+ char **reply;
+ guint port;
+ GSocketAddress *addr;
+ guint status;
+
+ g_assert (task->error == NULL);
+
+ if (!g_vfs_backend_ftp_has_feature (task->backend, G_VFS_FTP_FEATURE_EPSV) ||
+ g_vfs_backend_ftp_uses_workaround (task->backend, G_VFS_FTP_WORKAROUND_BROKEN_EPSV))
+ return FALSE;
+
+ 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)
+ goto fail;
+
+ /* 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 FALSE;
+
+ if (!g_vfs_ftp_connection_open_data_connection (task->conn,
+ addr,
+ task->cancellable,
+ &task->error))
+ {
+ g_object_unref (addr);
+ DEBUG ("Successful EPSV response code, but data connection failed. Enabling FTP_WORKAROUND_BROKEN_EPSV.\n");
+ g_vfs_backend_ftp_use_workaround (task->backend, G_VFS_FTP_WORKAROUND_BROKEN_EPSV);
+ g_vfs_ftp_task_clear_error (task);
+ return FALSE;
+ }
+
+ g_object_unref (addr);
+ return TRUE;
+
+fail:
+ g_strfreev (reply);
+ return FALSE;
+}
+
+static gboolean
+g_vfs_ftp_task_open_data_connection_pasv (GVfsFtpTask *task)
+{
+ guint ip1, ip2, ip3, ip4, port1, port2;
+ char **reply;
+ const char *s;
+ GSocketAddress *addr;
+ guint status;
+
+ /* only binary transfers please */
+ status = g_vfs_ftp_task_send_and_check (task, 0, NULL, NULL, &reply, "PASV");
+ if (status == 0)
+ return FALSE;
+
+ /* 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;
+ }
+ g_strfreev (reply);
+ if (*s == 0)
+ {
+ g_set_error_literal (&task->error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ _("Invalid reply"));
+ return FALSE;
+ }
+
+ if (!g_vfs_backend_ftp_uses_workaround (task->backend, G_VFS_FTP_WORKAROUND_PASV_ADDR))
+ {
+ 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);
+
+ if (g_vfs_ftp_connection_open_data_connection (task->conn,
+ addr,
+ task->cancellable,
+ &task->error))
+ {
+ g_object_unref (addr);
+ return TRUE;
+ }
+
+ g_object_unref (addr);
+ /* set workaround flag (see below), so we don't try this again */
+ DEBUG ("Successfull PASV response but data connection failed. Enabling FTP_WORKAROUND_PASV_ADDR.\n");
+ g_vfs_backend_ftp_use_workaround (task->backend, G_VFS_FTP_WORKAROUND_PASV_ADDR);
+ g_vfs_ftp_task_clear_error (task);
+ }
+
+ /* 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 FALSE;
+ if (!g_vfs_ftp_connection_open_data_connection (task->conn,
+ addr,
+ task->cancellable,
+ &task->error))
+ {
+ g_object_unref (addr);
+ return FALSE;
+ }
+
+ g_object_unref (addr);
+ return TRUE;
+}
+
+/**
+ * g_vfs_ftp_task_close_data_connection:
+ * @task: a task potentially having an open data connection
+ *
+ * Closes any data connection @task might have opened.
+ */
+/**
+ * g_vfs_ftp_task_open_data_connection:
+ * @task: a task not having an open data connection
+ *
+ * Tries to open a data connection to the ftp server. 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;
+
+ /* only check this here, erroneous connection might have failed to acquire a connection. */
+ g_return_if_fail (task->conn != NULL);
+
+ if (g_vfs_ftp_task_open_data_connection_epsv (task))
+ return;
+
+ if (g_vfs_ftp_task_is_in_error (task))
+ return;
+
+ g_vfs_ftp_task_open_data_connection_pasv (task);
+}
+