/* 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 "gvfsftpconnection.h" #include #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 */ GSocketConnection * connection; /* original connection */ GDataInputStream * commands_in; /* wrapper around in stream to allow line-wise reading */ gboolean waiting_for_reply; /* TRUE if a command was sent but no reply received yet */ 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 */ }; static void enable_keepalive (GSocketConnection *conn) { /* We enable keepalive on the socket, because data connections can be * idle for a long time while data is transferred using the data * connection. And there are still buggy routers in existance that purge * idle connections from time to time. * To work around this problem, we set the keep alive flag here. It's the * user's responsibility to configure his kernel properly so that the * keepalive packets are sent before the buggy router disconnects the * TCP connection. If a user asks, a howto is at * http://tldp.org/HOWTO/html_single/TCP-Keepalive-HOWTO/ */ g_socket_set_keepalive (g_socket_connection_get_socket (conn), TRUE); } /* Set TCP_NODELAY on the connection to avoid a bad interaction between Nagle's * algorithm and delayed acks when doing a write-write-read. */ static void enable_nodelay (GSocketConnection *conn) { GError *error = NULL; GSocket *socket = g_socket_connection_get_socket (conn); if (!g_socket_set_option (socket, IPPROTO_TCP, TCP_NODELAY, TRUE, &error)) { g_warning ("Could not set TCP_NODELAY: %s\n", error->message); g_error_free (error); } } static void create_input_stream (GVfsFtpConnection *conn) { if (conn->commands_in) { g_filter_input_stream_set_close_base_stream (G_FILTER_INPUT_STREAM (conn->commands_in), FALSE); g_object_unref (conn->commands_in); } conn->commands_in = G_DATA_INPUT_STREAM (g_data_input_stream_new (g_io_stream_get_input_stream (conn->commands))); g_data_input_stream_set_newline_type (conn->commands_in, G_DATA_STREAM_NEWLINE_TYPE_CR_LF); } GVfsFtpConnection * g_vfs_ftp_connection_new (GSocketConnectable *addr, GCancellable * cancellable, 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_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->connection = G_SOCKET_CONNECTION (conn->commands); enable_nodelay (conn->connection); enable_keepalive (conn->connection); create_input_stream (conn); /* The first thing that needs to happen is receiving the welcome message */ conn->waiting_for_reply = TRUE; 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); 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 (!conn->waiting_for_reply, 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); conn->waiting_for_reply = TRUE; 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, 0); g_return_val_if_fail (conn->waiting_for_reply, 0); 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); } /* 1xx commands are intermediate commands and require a further * message from the server to complete */ if (response >= 200) conn->waiting_for_reply = 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 (conn->connection, error); } /** * g_vfs_ftp_connection_data_connection_enable_tls: * @conn: a connection with an active control connection * @server_identity: address of the server used to verify the certificate * @cb: callback called if there's a verification error * @user_data: user data passed to @cb * @cancellable: cancellable to interrupt wait * @error: %NULL or location to take a potential error * * Tries to enable TLS on the given @connection's data connection. If setting * up TLS fails, %FALSE will be returned and @error will be set. * * Returns: %TRUE on success, %FALSE otherwise. **/ gboolean g_vfs_ftp_connection_data_connection_enable_tls (GVfsFtpConnection *conn, GSocketConnectable *server_identity, CertificateCallback cb, gpointer user_data, GCancellable * cancellable, GError ** error) { GIOStream *secure; g_return_val_if_fail (conn != NULL, FALSE); g_return_val_if_fail (conn->commands != NULL, FALSE); secure = g_tls_client_connection_new (conn->data, server_identity, error); if (secure == NULL) return FALSE; g_object_unref (conn->data); conn->data = secure; g_tls_client_connection_copy_session_state (G_TLS_CLIENT_CONNECTION (secure), G_TLS_CLIENT_CONNECTION (conn->commands)); g_signal_connect (secure, "accept-certificate", G_CALLBACK (cb), user_data); if (!g_tls_connection_handshake (G_TLS_CONNECTION (secure), cancellable, error)) { /* Close here to be sure it won't get used anymore */ g_io_stream_close (secure, cancellable, NULL); return FALSE; } return TRUE; } gboolean 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)); if (conn->data) enable_nodelay (G_SOCKET_CONNECTION (conn->data)); 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, NULL); g_vfs_ftp_connection_stop_listening (conn); local = g_socket_connection_get_local_address (conn->connection, 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, cancellable, error); if (accepted == NULL) return FALSE; conn->data = G_IO_STREAM (g_socket_connection_factory_create_connection (accepted)); g_object_unref (accepted); enable_nodelay (G_SOCKET_CONNECTION (conn->data)); 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 be used to send new commands. For * example, if the connection is in the process of a command sequence or * 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); if (conn->waiting_for_reply) return FALSE; cond = G_IO_ERR | G_IO_HUP | G_IO_IN; cond = g_socket_condition_check (g_socket_connection_get_socket (conn->connection), cond); if (cond) { g_debug ("##%2d ## connection unusuable: %s%s%s\r\n", conn->debug_id, cond & G_IO_IN ? "IN " : "", cond & G_IO_HUP ? "HUP " : "", cond & G_IO_ERR ? "ERR " : ""); return FALSE; } return TRUE; } /** * g_vfs_ftp_connection_enable_tls: * @conn: a connection without an active data connection * @server_identity: address of the server used to verify the certificate * @cb: callback called if there's a verification error * @user_data: user data passed to @cb * @cancellable: cancellable to interrupt wait * @error: %NULL or location to take a potential error * * Tries to enable TLS on the given @connection. If setting up TLS fails, * %FALSE will be returned and @error will be set. When this function fails, * you need to check if the connection is still usable. It might have been * closed. * * Returns: %TRUE on success, %FALSE otherwise. **/ gboolean g_vfs_ftp_connection_enable_tls (GVfsFtpConnection * conn, GSocketConnectable *server_identity, gboolean implicit_tls, CertificateCallback cb, gpointer user_data, GCancellable * cancellable, GError ** error) { GIOStream *secure; g_return_val_if_fail (conn != NULL, FALSE); g_return_val_if_fail (conn->data == NULL, FALSE); g_return_val_if_fail (implicit_tls || !conn->waiting_for_reply, FALSE); g_return_val_if_fail (g_buffered_input_stream_get_available (G_BUFFERED_INPUT_STREAM (conn->commands_in)) == 0, FALSE); secure = g_tls_client_connection_new (conn->commands, server_identity, error); if (secure == NULL) return FALSE; g_object_unref (conn->commands); conn->commands = secure; create_input_stream (conn); g_signal_connect (secure, "accept-certificate", G_CALLBACK (cb), user_data); if (!g_tls_connection_handshake (G_TLS_CONNECTION (secure), cancellable, error)) { /* Close here to be sure it won't get used anymore */ g_io_stream_close (secure, cancellable, NULL); return FALSE; } return TRUE; }