#include "server_config.h" #include #include #include #include #include #include #include #include #include #include "gam_error.h" #include "gam_connection.h" #include "gam_channel.h" #include "gam_protocol.h" /* #define CHANNEL_VERBOSE_DEBUGGING */ /************************************************************************ * * * Connection socket handling * * * ************************************************************************/ /** * gam_client_conn_send_cred: * * The write read on the connection send a NUL byte to allow the client * to check the server credentials early on. */ static gboolean gam_client_conn_send_cred(int fd) { char data[2] = { 0, 0 }; int written; #if defined(HAVE_CMSGCRED) && (!defined(LOCAL_CREDS) || defined(__FreeBSD__)) union { struct cmsghdr hdr; char cred[CMSG_SPACE (sizeof (struct cmsgcred))]; } cmsg; struct iovec iov; struct msghdr msg; iov.iov_base = &data[0]; iov.iov_len = 1; memset (&msg, 0, sizeof (msg)); msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = (caddr_t) &cmsg; msg.msg_controllen = CMSG_SPACE (sizeof (struct cmsgcred)); memset (&cmsg, 0, sizeof (cmsg)); cmsg.hdr.cmsg_len = CMSG_LEN (sizeof (struct cmsgcred)); cmsg.hdr.cmsg_level = SOL_SOCKET; cmsg.hdr.cmsg_type = SCM_CREDS; #endif retry: #if defined(HAVE_CMSGCRED) && (!defined(LOCAL_CREDS) || defined(__FreeBSD__)) written = sendmsg(fd, &msg, 0); #else written = write(fd, &data[0], 1); #endif if (written < 0) { if (errno == EINTR) goto retry; gam_error(DEBUG_INFO, "Failed to write credential bytes to socket %d\n", fd); return (-1); } if (written != 1) { gam_error(DEBUG_INFO, "Wrote %d credential bytes to socket %d\n", written, fd); return (-1); } #ifdef CHANNEL_VERBOSE_DEBUGGING GAM_DEBUG(DEBUG_INFO, "Wrote credential bytes to socket %d\n", fd); #endif return (written); } /** * gam_client_conn_check_cred: * * The first read on the connection gathers credentials from the client * and checks them. Parts directly borrowed from DBus code. */ static gboolean gam_client_conn_check_cred(GIOChannel * source, int fd, GamConnDataPtr conn) { struct msghdr msg; struct iovec iov; char buf; pid_t c_pid; uid_t c_uid, s_uid; gid_t c_gid; #ifdef HAVE_CMSGCRED struct cmsgcred *cred; union { struct cmsghdr hdr; char cred[CMSG_SPACE (sizeof (struct cmsgcred))]; } cmsg; #endif s_uid = getuid(); #if defined(LOCAL_CREDS) && defined(HAVE_CMSGCRED) && !defined(__FreeBSD__) /* Set the socket to receive credentials on the next message */ { int on = 1; if (setsockopt(fd, 0, LOCAL_CREDS, &on, sizeof(on)) < 0) { gam_error(DEBUG_INFO, "Unable to set LOCAL_CREDS socket option\n"); return FALSE; } } #endif iov.iov_base = &buf; iov.iov_len = 1; memset(&msg, 0, sizeof(msg)); msg.msg_iov = &iov; msg.msg_iovlen = 1; #ifdef HAVE_CMSGCRED memset(&cmsg, 0, sizeof(cmsg)); msg.msg_control = (caddr_t) &cmsg; msg.msg_controllen = CMSG_SPACE (sizeof (struct cmsgcred)); #endif retry: if (recvmsg(fd, &msg, 0) < 0) { if (errno == EINTR) goto retry; GAM_DEBUG(DEBUG_INFO, "Failed to read credentials byte on %d\n", fd); goto failed; } if (buf != '\0') { GAM_DEBUG(DEBUG_INFO, "Credentials byte was not nul on %d\n", fd); goto failed; } #ifdef HAVE_CMSGCRED if (cmsg.hdr.cmsg_len < CMSG_LEN (sizeof (struct cmsgcred)) || cmsg.hdr.cmsg_type != SCM_CREDS) { GAM_DEBUG(DEBUG_INFO, "Message from recvmsg() was not SCM_CREDS\n"); goto failed; } #endif GAM_DEBUG(DEBUG_INFO, "read credentials byte\n"); { #ifdef SO_PEERCRED struct ucred cr; socklen_t cr_len = sizeof(cr); if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cr, &cr_len) == 0 && cr_len == sizeof(cr)) { c_pid = cr.pid; c_uid = cr.uid; c_gid = cr.gid; } else { GAM_DEBUG(DEBUG_INFO, "Failed to getsockopt() credentials on %d, returned len %d/%d\n", fd, cr_len, (int) sizeof(cr)); goto failed; } #elif defined(HAVE_CMSGCRED) cred = (struct cmsgcred *) CMSG_DATA (&cmsg); c_pid = cred->cmcred_pid; c_uid = cred->cmcred_euid; c_gid = cred->cmcred_groups[0]; #else /* !SO_PEERCRED && !HAVE_CMSGCRED */ GAM_DEBUG(DEBUG_INFO, "Socket credentials not supported on this OS\n"); goto failed; #endif } if (s_uid != c_uid) { GAM_DEBUG(DEBUG_INFO, "Credentials check failed: s_uid %d, c_uid %d\n", (int) s_uid, (int) c_uid); goto failed; } GAM_DEBUG(DEBUG_INFO, "Credentials: s_uid %d, c_uid %d, c_gid %d, c_pid %d\n", (int) s_uid, (int) c_uid, (int) c_gid, (int) c_pid); if (gam_connection_set_pid(conn, c_pid) < 0) { GAM_DEBUG(DEBUG_INFO, "Failed to save PID\n"); goto failed; } if (!gam_client_conn_send_cred(fd)) { GAM_DEBUG(DEBUG_INFO, "Failed to send credential byte to client\n"); goto failed; } return TRUE; failed: gam_client_conn_shutdown(source, conn); return (FALSE); } /** * gam_client_conn_read: * * Incoming data on the socket. */ static gboolean gam_client_conn_read(GIOChannel * source, GIOCondition condition, gpointer info) { char *data; /* actually a GAMPacketPtr */ int size; int fd; int ret; GamConnDataPtr conn = (GamConnDataPtr) info; if ((condition == G_IO_HUP) || (condition == G_IO_NVAL) || (condition == G_IO_ERR)) { return (gam_conn_error(source, condition, info)); } if (conn == NULL) { GAM_DEBUG(DEBUG_INFO, "lost informations\n"); return (FALSE); } GAM_DEBUG(DEBUG_INFO, "gam_client_conn_read called\n"); fd = gam_connection_get_fd(conn); if (fd < 0) { GAM_DEBUG(DEBUG_INFO, "failed to get file descriptor\n"); return (FALSE); } switch (gam_connection_get_state(conn)) { case GAM_STATE_AUTH: if (!gam_client_conn_check_cred(source, fd, conn)) return (FALSE); return (TRUE); case GAM_STATE_ERROR: GAM_DEBUG(DEBUG_INFO, "connection in error state\n"); return (FALSE); case GAM_STATE_CLOSED: GAM_DEBUG(DEBUG_INFO, "connection is closed\n"); return (FALSE); case GAM_STATE_OKAY: break; } if (gam_connection_get_data(conn, &data, &size) < 0) { GAM_DEBUG(DEBUG_INFO, "connection data error, disconnecting\n"); gam_client_conn_shutdown(source, conn); return (FALSE); } retry: ret = read(fd, data, size); if (ret < 0) { if (errno == EINTR) goto retry; GAM_DEBUG(DEBUG_INFO, "failed to read() from client connection\n"); gam_client_conn_shutdown(source, conn); return (FALSE); } if (ret == 0) { GAM_DEBUG(DEBUG_INFO, "end from client connection\n"); gam_client_conn_shutdown(source, conn); return (FALSE); } #ifdef CHANNEL_VERBOSE_DEBUGGING GAM_DEBUG(DEBUG_INFO, "read %d bytes from client\n", ret); #endif /* * there is no garantee of alignment, that the request is complete * we may also get multiple requests in a single packet, so make no * assumption on data contant at this point, though in most case it * will be a complete, fully aligned request. */ if (gam_connection_data(conn, ret) < 0) { GAM_DEBUG(DEBUG_INFO, "error in client data, closing client connection\n"); gam_client_conn_shutdown(source, conn); return (FALSE); } return (TRUE); } /** * gam_incoming_conn_read: * * Incoming data on the socket. */ gboolean gam_incoming_conn_read(GIOChannel * source, GIOCondition condition, gpointer data) { GMainLoop *loop; GIOChannel *socket = NULL; GamConnDataPtr conn; GAM_DEBUG(DEBUG_INFO, "gam_incoming_conn_read called\n"); loop = (GMainLoop *) data; socket = gam_client_create(source); if (socket == NULL) goto failed; conn = gam_connection_new(loop, socket); if (conn == NULL) goto failed; g_io_add_watch(socket, G_IO_IN | G_IO_HUP | G_IO_NVAL | G_IO_ERR, gam_client_conn_read, conn); return (TRUE); failed: if (socket != NULL) g_io_channel_unref(socket); return (FALSE); } // #define GAM_CHANNEL_VERBOSE /************************************************************************ * * * Path for the socket connection * * * ************************************************************************/ /** * gam_get_socket_path: * @session: the session name or NULL * * Get the file path to the socket to connect the FAM server. * The fam server interface is available though a socket whose * id is available though an environment variable GAM_CLIENT_ID * or passed as the @session argument though the command line. * * Returns a new string or NULL in case of error. */ static gchar * gam_get_socket_path(const char *session) { const char *gam_client_id; const gchar *user; gchar *ret; if (session == NULL) { gam_client_id = g_getenv("GAM_CLIENT_ID"); if (gam_client_id == NULL) { GAM_DEBUG(DEBUG_INFO, "Error getting GAM_CLIENT_ID\n"); gam_client_id = ""; } } else { gam_client_id = session; } user = g_get_user_name(); if (user == NULL) { GAM_DEBUG(DEBUG_INFO, "Error getting user informations\n"); return (NULL); } #ifdef HAVE_ABSTRACT_SOCKETS ret = g_strconcat("/tmp/fam-", user, "-", gam_client_id, NULL); #else ret = g_strconcat("/tmp/fam-", user, "/fam-", gam_client_id, NULL); #endif return(ret); } #ifndef HAVE_ABSTRACT_SOCKETS /** * gam_get_socket_dir: * * Get the directory path to the socket to connect the FAM server. * * Returns a new string or NULL in case of error. */ static gchar * gam_get_socket_dir(void) { const gchar *user; gchar *ret; user = g_get_user_name(); if (user == NULL) { GAM_DEBUG(DEBUG_INFO, "Error getting user informations\n"); return (NULL); } ret = g_strconcat("/tmp/fam-", user, NULL); return(ret); } /************************************************************************ * * * Security for OSes without abstract sockets * * * ************************************************************************/ /** * gam_check_secure_dir: * * Tries to create or ensure that the directory used to hold the socket used * for communicating with is a safe directory to avoid possible attacks. * * Returns TRUE if safe and FALSE otherwise. */ static gboolean gam_check_secure_dir(void) { gchar *dir; struct stat st; int ret; int tries = 0; dir = gam_get_socket_dir(); if (dir == NULL) { gam_error(DEBUG_INFO, "Failed to get path to socket directory\n"); return(FALSE); } create: ret = mkdir(dir, 0700); if (ret >= 0) { GAM_DEBUG(DEBUG_INFO, "Created socket directory %s\n", dir); g_free(dir); return(TRUE); } switch (errno) { case EEXIST: ret = stat(dir, &st); if (ret < 0) { gam_error(DEBUG_INFO, "Failed to stat socket directory %s\n", dir); g_free(dir); return(FALSE); } if (st.st_uid != getuid()) { gam_error(DEBUG_INFO, "Socket directory %s has different owner\n", dir); break; } if (!S_ISDIR (st.st_mode)) { gam_error(DEBUG_INFO, "Socket path %s is not a directory\n", dir); break; } if (st.st_mode & (S_IRWXG|S_IRWXO)) { gam_error(DEBUG_INFO, "Socket directory %s has wrong permissions\n", dir); break; } if (((st.st_mode & (S_IRWXU)) != S_IRWXU)) { gam_error(DEBUG_INFO, "Socket directory %s has wrong permissions\n", dir); break; } /* * all checks on existing dir seems okay */ GAM_DEBUG(DEBUG_INFO, "Reusing socket directory %s\n", dir); g_free(dir); return(TRUE); case EACCES: gam_error(DEBUG_INFO, "No permission to create socket directory %s\n", dir); g_free(dir); return(FALSE); case ENAMETOOLONG: gam_error(DEBUG_INFO, "Socket directory %s name too long\n", dir); g_free(dir); return(FALSE); default: gam_error(DEBUG_INFO, "Failed to create socket directory %s\n", dir); g_free(dir); return(FALSE); } /* * The path to the directory is considered unsafe * try to remove the given path to rebuild the directory. */ ret = rmdir(dir); if (ret < 0) { ret = unlink(dir); if (ret < 0) { gam_error(DEBUG_INFO, "Failed to remove unsafe path %s\n", dir); g_free(dir); return(FALSE); } } #ifdef GAM_CHANNEL_VERBOSE GAM_DEBUG(DEBUG_INFO, "Removed %s\n", dir); #endif tries++; if (tries < 5) goto create; g_free(dir); return(FALSE); } /** * gam_check_secure_path: * @path: path to the (possibly abstract) socket * * Tries to create or ensure that the socket used for communicating with * the clients are in a safe directory to avoid possible attacks. * * Returns the socket file descriptor or -1 in case of error. */ static gboolean gam_check_secure_path(const char *path) { struct stat st; int ret; if (!gam_check_secure_dir()) return(FALSE); /* * Check the existing socket if any */ ret = stat(path, &st); if (ret < 0) return(TRUE); if (st.st_uid != getuid()) { gam_error(DEBUG_INFO, "Socket %s has different owner\n", path); goto cleanup; } #ifdef S_ISSOCK if (!S_ISSOCK (st.st_mode)) { gam_error(DEBUG_INFO, "Socket path %s is not a socket\n", path); goto cleanup; } #endif if (st.st_mode & (S_IRWXG|S_IRWXO)) { gam_error(DEBUG_INFO, "Socket %s has wrong permissions\n", path); goto cleanup; } /* * Looks good though binding may fail due to an existing server */ return(TRUE); cleanup: /* * the existing file at the socket location seems strange, try to remove it */ ret = unlink(path); if (ret < 0) { gam_error(DEBUG_INFO, "Failed to remove %s\n", path); return(FALSE); } return(TRUE); } #endif /* ! HAVE_ABSTRACT_SOCKETS */ /************************************************************************ * * * Connection socket shutdown * * * ************************************************************************/ /** * gam_conn_shutdown: * @session: the session name or NULL * * Shutdown the connection socket if any */ void gam_conn_shutdown(const char *session) { #ifndef HAVE_ABSTRACT_SOCKETS gchar *path; int ret; path = gam_get_socket_path(session); ret = unlink(path); if (ret < 0) { gam_error(DEBUG_INFO, "Failed to remove %s\n", path); } g_free(path); #endif } /************************************************************************ * * * Connection socket handling * * * ************************************************************************/ /** * gam_listen_unix_socket: * @path: path to the (possibly abstract) socket * Returns the socket file descriptor or -1 in case of error. */ static int gam_listen_unix_socket(const char *path) { int fd; struct sockaddr_un addr; struct stat st; fd = socket(PF_UNIX, SOCK_STREAM, 0); if (fd < 0) { GAM_DEBUG(DEBUG_INFO, "Failed to create unix socket"); return (-1); } memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; #ifdef HAVE_ABSTRACT_SOCKETS /* * Abstract socket do not hit the filesystem */ addr.sun_path[0] = '\0'; strncpy(&addr.sun_path[1], path, (sizeof(addr) - 4) - 2); #else /* * if the socket is exposed at the filesystem level we need to take * some extra protection checks. Also make sure the socket is created * with restricted mode */ if (!gam_check_secure_dir()) { close(fd); return (-1); } if (stat(path, &st) == 0) { /* bind() will fail if the socket already exists */ if (unlink(path) < 0) { GAM_DEBUG(DEBUG_INFO, "Failed to remove %s\n", path); close(fd); return (-1); } } strncpy(&addr.sun_path[0], path, (sizeof(addr) - 4) - 1); umask(0077); #endif if (bind(fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) { GAM_DEBUG(DEBUG_INFO, "Failed to bind to socket %s\n", path); close(fd); return (-1); } if (listen(fd, 30 /* backlog */ ) < 0) { GAM_DEBUG(DEBUG_INFO, "Failed to listen to socket %s\n", path); close(fd); return (-1); } GAM_DEBUG(DEBUG_INFO, "Ready listening to socket %s : %d\n", path, fd); return (fd); } /************************************************************************ * * * General channel interface * * * ************************************************************************/ /** * gam_client_conn_shutdown: * * shutdown a Glib I/O channel initiated by the server */ void gam_client_conn_shutdown(GIOChannel * source, GamConnDataPtr conn) { GError *error = NULL; if (conn != NULL) { if (gam_connection_exists(conn)) { GAM_DEBUG(DEBUG_INFO, "Shutting down client socket %d\n", g_io_channel_unix_get_fd(source)); g_io_channel_shutdown(source, FALSE, &error); gam_connection_close(conn); } else { GAM_DEBUG(DEBUG_INFO, "could not found connection on socket %d\n", g_io_channel_unix_get_fd(source)); } } else { GAM_DEBUG(DEBUG_INFO, "Shutting down server socket %d\n", g_io_channel_unix_get_fd(source)); g_io_channel_shutdown(source, FALSE, &error); g_io_channel_unref(source); } } /** * gam_incoming_conn_error: * * shutdown a Glib I/O channel initiated by an error on the socket */ gboolean gam_conn_error(GIOChannel * source, GIOCondition condition, gpointer data) { GError *error = NULL; GamConnDataPtr conn = (GamConnDataPtr) data; if (conn != NULL) { if (gam_connection_exists(conn)) { GAM_DEBUG(DEBUG_INFO, "Error condition raised on client socket %d\n", g_io_channel_unix_get_fd(source)); g_io_channel_shutdown(source, FALSE, &error); gam_connection_close(conn); } else { GAM_DEBUG(DEBUG_INFO, "could not found connection on socket %d\n", g_io_channel_unix_get_fd(source)); } } else { GAM_DEBUG(DEBUG_INFO, "Error condition raised on server socket\n"); g_io_channel_shutdown(source, FALSE, &error); g_io_channel_unref(source); } return (FALSE); } /** * gam_channel_create: * @session: the session name or NULL * * Creation of a channel on which the server waits for clients * * Returns the new GIOChannel or NULL in case of error. */ GIOChannel * gam_server_create(const char *session) { GIOChannel *socket; gchar *path = NULL; int fd = -1; path = gam_get_socket_path(session); if (path == NULL) return (NULL); fd = gam_listen_unix_socket(path); g_free(path); if (fd == -1) return (NULL); socket = g_io_channel_unix_new(fd); if (socket == NULL) close(fd); else g_io_channel_set_close_on_unref(socket, TRUE); return (socket); } /** * gam_client_create: * * Creation of a channel on which the server connects to the client * * Returns the new GIOChannel or NULL in case of error. */ GIOChannel * gam_client_create(GIOChannel * server) { GIOChannel *socket = NULL; int sock; int client = -1; socklen_t client_addrlen; struct sockaddr client_addr; sock = g_io_channel_unix_get_fd(server); if (sock < 0) { GAM_DEBUG(DEBUG_INFO, "failed to get incoming socket\n"); return (NULL); } retry: client_addrlen = sizeof(client_addr); client = accept(sock, &client_addr, &client_addrlen); if (client < 0) { if (errno == EINTR) goto retry; GAM_DEBUG(DEBUG_INFO, "failed to accept() incoming connection\n"); return (NULL); } socket = g_io_channel_unix_new(client); if (socket == NULL) { if (client != -1) close(client); return (NULL); } g_io_channel_set_close_on_unref(socket, TRUE); GAM_DEBUG(DEBUG_INFO, "accepted incoming connection: %d\n", client); return (socket); } /** * gam_client_conn_write: * * Incoming data on the socket. */ gboolean gam_client_conn_write(GIOChannel * source, int fd, gpointer data, size_t len) { int written; int remaining; /** * Todo: check if write will block, or use non-blocking options */ if (fd < 0) { fd = g_io_channel_unix_get_fd(source); } if (fd < 0) return (FALSE); remaining = len; do { written = write(fd, data, remaining); if (written < 0) { if (errno == EINTR) continue; GAM_DEBUG(DEBUG_INFO, "%s: Failed to write bytes to socket %d: %s\n", __FUNCTION__, fd, strerror (errno)); return (FALSE); } data += written; remaining -= written; } while (remaining > 0); #ifdef CHANNEL_VERBOSE_DEBUGGING GAM_DEBUG(DEBUG_INFO, "Wrote %d bytes to socket %d\n", len, fd); #endif return (TRUE); }