/* GIO - GLib Input, Output and Streaming Library * * Copyright (C) 2009 Benjamin Otte * * 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 */ #include #include /* for sscanf() */ #include /* for exit() */ #include #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; 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; }