/* 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., 59 Temple Place, Suite 330, * Boston, MA 02111-1307, USA. * * Author: Benjamin Otte */ #include #include "gvfsftpconnection.h" #include #include #include "gvfsbackendftp.h" /* used for identifying the connection during debugging */ static volatile int debug_id = 0; struct _GVfsFtpConnection { GSocketClient * client; /* socket client used for opening connections */ GIOStream * commands; /* ftp command stream */ GDataInputStream * commands_in; /* wrapper around in stream to allow line-wise reading */ GSocket * listen_socket; /* socket we are listening on for active FTP connections */ GIOStream * data; /* ftp data stream or NULL if not in use */ int debug_id; /* unique id for debugging purposes */ }; GVfsFtpConnection * g_vfs_ftp_connection_new (GSocketConnectable *addr, GCancellable * cancellable, GError ** error) { GVfsFtpConnection *conn; g_return_val_if_fail (G_IS_SOCKET_CONNECTABLE (addr), NULL); conn = g_slice_new0 (GVfsFtpConnection); conn->client = g_socket_client_new (); conn->debug_id = g_atomic_int_exchange_and_add (&debug_id, 1); conn->commands = G_IO_STREAM (g_socket_client_connect (conn->client, addr, cancellable, error)); if (conn->commands == NULL) { g_object_unref (conn->client); g_slice_free (GVfsFtpConnection, conn); return NULL; } conn->commands_in = G_DATA_INPUT_STREAM (g_data_input_stream_new (g_io_stream_get_input_stream (conn->commands))); g_data_input_stream_set_newline_type (conn->commands_in, G_DATA_STREAM_NEWLINE_TYPE_CR_LF); return conn; } static void g_vfs_ftp_connection_stop_listening (GVfsFtpConnection *conn) { if (conn->listen_socket) { g_object_unref (conn->listen_socket); conn->listen_socket = NULL; } } void g_vfs_ftp_connection_free (GVfsFtpConnection *conn) { g_return_if_fail (conn != NULL); g_vfs_ftp_connection_stop_listening (conn); if (conn->data) g_vfs_ftp_connection_close_data_connection (conn); g_object_unref (conn->commands_in); g_object_unref (conn->commands); g_object_unref (conn->client); g_slice_free (GVfsFtpConnection, conn); } gboolean g_vfs_ftp_connection_send (GVfsFtpConnection *conn, const char * command, int len, GCancellable * cancellable, GError ** error) { g_return_val_if_fail (conn != NULL, FALSE); g_return_val_if_fail (command != NULL, FALSE); g_return_val_if_fail (len >= -1, FALSE); if (len < 0) len = strlen (command); g_return_val_if_fail (command[len-2] == '\r' && command[len-1] == '\n', FALSE); if (g_str_has_prefix (command, "PASS")) g_debug ("--%2d -> PASS ***\r\n", conn->debug_id); else g_debug ("--%2d -> %s", conn->debug_id, command); return g_output_stream_write_all (g_io_stream_get_output_stream (conn->commands), command, len, NULL, cancellable, error); } guint g_vfs_ftp_connection_receive (GVfsFtpConnection *conn, char *** reply, GCancellable * cancellable, GError ** error) { char *line; enum { FIRST_LINE, MULTILINE, DONE } reply_state = FIRST_LINE; GPtrArray *lines; guint response = 0; g_return_val_if_fail (conn != NULL, FALSE); if (reply) lines = g_ptr_array_new_with_free_func (g_free); else lines = NULL; while (reply_state != DONE) { line = g_data_input_stream_read_line (conn->commands_in, NULL, cancellable, error); if (line == NULL) { if (error && *error == NULL) g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_CLOSED, _("Unexpected end of stream")); goto fail; } g_debug ("<-%2d -- %s\r\n", conn->debug_id, line); if (lines) g_ptr_array_add (lines, line); if (reply_state == FIRST_LINE) { if (line[0] <= '0' || line[0] > '5' || line[1] < '0' || line[1] > '9' || line[2] < '0' || line[2] > '9') { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, _("Invalid reply")); goto fail; } response = 100 * (line[0] - '0') + 10 * (line[1] - '0') + (line[2] - '0'); if (line[3] == ' ') reply_state = DONE; else if (line[3] == '-') reply_state = MULTILINE; else { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, _("Invalid reply")); goto fail; } } else { if (line[0] - '0' == response / 100 && line[1] - '0' == (response / 10) % 10 && line[2] - '0' == response % 10 && line[3] == ' ') reply_state = DONE; } if (!lines) g_free (line); } if (lines) { g_ptr_array_add (lines, NULL); *reply = (char **) g_ptr_array_free (lines, FALSE); } return response; fail: if (lines) g_ptr_array_free (lines, TRUE); else g_free (line); return 0; } GSocketAddress * g_vfs_ftp_connection_get_address (GVfsFtpConnection *conn, GError **error) { g_return_val_if_fail (conn != NULL, NULL); return g_socket_connection_get_remote_address (G_SOCKET_CONNECTION (conn->commands), error); } gboolean g_vfs_ftp_connection_open_data_connection (GVfsFtpConnection *conn, GSocketAddress * addr, GCancellable * cancellable, GError ** error) { g_return_val_if_fail (conn != NULL, FALSE); g_return_val_if_fail (conn->data == NULL, FALSE); g_vfs_ftp_connection_stop_listening (conn); conn->data = G_IO_STREAM (g_socket_client_connect (conn->client, G_SOCKET_CONNECTABLE (addr), cancellable, error)); return conn->data != NULL; } /** * g_vfs_ftp_connection_listen_data_connection: * @conn: a connection * @error: %NULL or location to take potential errors * * Initiates a listening socket that the FTP server can connect to. To accept * connections and initialize data transfers, use * g_vfs_ftp_connection_accept_data_connection(). * This function supports what is known as "active FTP", while * g_vfs_ftp_connection_open_data_connection() is to be used for "passive FTP". * * Returns: the actual address the socket is listening on or %NULL on error **/ GSocketAddress * g_vfs_ftp_connection_listen_data_connection (GVfsFtpConnection *conn, GError ** error) { GSocketAddress *local, *addr; g_return_val_if_fail (conn != NULL, NULL); g_return_val_if_fail (conn->data == NULL, FALSE); g_vfs_ftp_connection_stop_listening (conn); local = g_socket_connection_get_local_address (G_SOCKET_CONNECTION (conn->commands), error); if (local == NULL) return NULL; conn->listen_socket = g_socket_new (g_socket_address_get_family (local), G_SOCKET_TYPE_STREAM, G_SOCKET_PROTOCOL_TCP, error); if (conn->listen_socket == NULL) return NULL; g_assert (G_IS_INET_SOCKET_ADDRESS (local)); addr = g_inet_socket_address_new (g_inet_socket_address_get_address (G_INET_SOCKET_ADDRESS (local)), 0); g_object_unref (local); if (!g_socket_bind (conn->listen_socket, addr, TRUE, error) || !g_socket_listen (conn->listen_socket, error) || !(local = g_socket_get_local_address (conn->listen_socket, error))) { g_object_unref (addr); g_vfs_ftp_connection_stop_listening (conn); return NULL; } g_object_unref (addr); return local; } static void cancel_timer_cb (GCancellable *orig, GCancellable *to_cancel) { g_cancellable_cancel (to_cancel); } static gboolean cancel_cancellable (gpointer cancellable) { g_cancellable_cancel (cancellable); return FALSE; } /** * g_vfs_ftp_connection_accept_data_connection: * @conn: a listening connection * @cancellable: cancellable to interrupt wait * @error: %NULL or location to take a potential error * * Opens a data connection for @conn by accepting an incoming connection on the * address it is listening on via g_vfs_ftp_connection_listen_data_connection(), * which must have been called prior to this function. * If this function succeeds, a data connection will have been opened, and calls * to g_vfs_ftp_connection_get_data_stream() will work. * * Returns: %TRUE if a connection was successfully acquired **/ gboolean g_vfs_ftp_connection_accept_data_connection (GVfsFtpConnection *conn, GCancellable * cancellable, GError ** error) { GSocket *accepted; GCancellable *timer; gulong cancel_cb_id; GIOCondition condition; g_return_val_if_fail (conn != NULL, FALSE); g_return_val_if_fail (conn->data == NULL, FALSE); g_return_val_if_fail (G_IS_SOCKET (conn->listen_socket), FALSE); timer = g_cancellable_new (); cancel_cb_id = g_cancellable_connect (cancellable, G_CALLBACK (cancel_timer_cb), timer, NULL); g_object_ref (timer); g_timeout_add_seconds_full (G_PRIORITY_DEFAULT, G_VFS_FTP_TIMEOUT_IN_SECONDS, cancel_cancellable, timer, g_object_unref); condition = g_socket_condition_wait (conn->listen_socket, G_IO_IN, timer, error); g_cancellable_disconnect (cancellable, cancel_cb_id); g_object_unref (timer); if ((condition & G_IO_IN) == 0) { if (g_cancellable_is_cancelled (timer) && !g_cancellable_is_cancelled (cancellable)) { g_clear_error (error); g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_HOST_NOT_FOUND, _("Failed to create active FTP connection. " "Maybe your router does not support this?")); } else if (error && *error == NULL) { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_HOST_NOT_FOUND, _("Failed to create active FTP connection.")); } return FALSE; } accepted = g_socket_accept (conn->listen_socket, error); if (accepted == NULL) return FALSE; conn->data = G_IO_STREAM (g_socket_connection_factory_create_connection (accepted)); g_object_unref (accepted); return TRUE; } void g_vfs_ftp_connection_close_data_connection (GVfsFtpConnection *conn) { g_return_if_fail (conn != NULL); if (conn->data == NULL) return; g_object_unref (conn->data); conn->data = NULL; } /** * g_vfs_ftp_connection_get_debug_id: * @conn: the connection * * Gets an ID that uniquely identifies this connection. Intended for use in * debug print statements. * * Returns: the ID to use for referring to this connection in debug messages **/ guint g_vfs_ftp_connection_get_debug_id (GVfsFtpConnection *conn) { g_return_val_if_fail (conn != NULL, 0); return conn->debug_id; } /** * g_vfs_ftp_connection_get_data_stream: * @conn: a connection * * Gets the data stream in use by @conn. It is an error to call this function * when no data stream exists. Be sure to check the return value of * g_vfs_ftp_connection_open_data_connection(). * * Returns: the data stream of @conn **/ GIOStream * g_vfs_ftp_connection_get_data_stream (GVfsFtpConnection *conn) { g_return_val_if_fail (conn != NULL, NULL); g_return_val_if_fail (conn->data != NULL, NULL); return conn->data; } /** * g_vfs_ftp_connection_is_usable: * @conn: a connection * * Checks if this connection can still be used to send new commands. For * example, if the connection was closed, this is not possible and this * function will return %FALSE. * * Returns: %TRUE if the connection is still usable **/ gboolean g_vfs_ftp_connection_is_usable (GVfsFtpConnection *conn) { GIOCondition cond; g_return_val_if_fail (conn != NULL, FALSE); /* FIXME: return FALSE here if a send or receive failed irrecoverably */ cond = G_IO_ERR | G_IO_HUP; cond = g_socket_condition_check (g_socket_connection_get_socket (G_SOCKET_CONNECTION (conn->commands)), cond); if (cond) return FALSE; return TRUE; }