/* realmd -- Realm configuration service
*
* Copyright 2013 Red Hat Inc
*
* This program 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 licence or (at
* your option) any later version.
*
* See the included COPYING file for more information.
*
* Author: Stef Walter <stefw@redhat.com>
*/
#include "config.h"
#include "realm-ldap.h"
#include <glib/gi18n.h>
#include <glib-unix.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <lber.h>
/*
* So the reason that we don't use GSocket is because its fd's are always
* non-blocking internally. We can't just go and hand these non-blocking
* fds to openldap, which then fiddles with blocking state.
*/
GQuark
realm_ldap_error_get_quark (void)
{
static GQuark quark = 0;
if (!quark)
quark = g_quark_from_static_string ("realm-ldap-error");
return quark;
}
typedef struct {
GSource source;
int sock;
LDAP *ldap;
GPollFD pollfd;
GIOCondition condition;
GCancellable *cancellable;
GPollFD cancel_pollfd;
gboolean connect_done;
/* An LDAP failure we should always return if non-zero */
int force_fail;
} LdapSource;
static gboolean
ldap_source_prepare (GSource *source,
gint *timeout)
{
LdapSource *ls = (LdapSource *)source;
if (ls->force_fail != 0)
return TRUE;
if (g_cancellable_is_cancelled (ls->cancellable))
return TRUE;
*timeout = -1;
if ((ls->condition & ls->pollfd.revents) != 0)
return TRUE;
ls->pollfd.events = ls->condition;
return FALSE;
}
static gboolean
ldap_source_check (GSource *source)
{
int timeout;
return ldap_source_prepare (source, &timeout);
}
static void
ldap_set_result_code (LDAP *ldap,
int rc)
{
int check;
if (ldap_get_option (ldap, LDAP_OPT_RESULT_CODE, &check) < 0 || check != rc) {
ldap_set_option (ldap, LDAP_OPT_RESULT_CODE, &rc);
ldap_set_option (ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, NULL);
}
}
static gboolean
ldap_source_dispatch (GSource *source,
GSourceFunc callback,
gpointer user_data)
{
RealmLdapCallback func = (RealmLdapCallback)callback;
LdapSource *ls = (LdapSource *)source;
GIOCondition cond;
socklen_t slen;
int error;
cond = ls->pollfd.revents & ls->condition;
/*
* We report cancels as an error. The callback can check if it
* was cancelled by looking for LDAP_CANCELLED result code.
*/
if (g_cancellable_is_cancelled (ls->cancellable)) {
ls->force_fail = LDAP_CANCELLED;
} else if (cond & (G_IO_HUP | G_IO_ERR)) {
g_debug ("socket closed or error");
ls->force_fail = LDAP_SERVER_DOWN;
} else if (cond & G_IO_OUT) {
/*
* Read out the result of the asynchronous non-blocking connect()
* call. If it fails we propagate that error up.
*/
if (!ls->connect_done) {
ls->connect_done = TRUE;
slen = sizeof (int);
if (getsockopt (ls->sock, SOL_SOCKET, SO_ERROR, &error, &slen) != 0) {
g_warning ("getsockopt() for SO_ERROR failed");
ls->force_fail = LDAP_SERVER_DOWN;
} else if (error != 0) {
g_debug ("Cannot connect: %s", g_strerror (error));
ls->force_fail = LDAP_SERVER_DOWN;
}
}
}
if (ls->force_fail != 0) {
ldap_set_result_code (ls->ldap, ls->force_fail);
cond |= G_IO_ERR;
}
if (func != NULL && cond != 0) {
cond = (* func) (ls->ldap, cond, user_data);
if ((cond & G_IO_NVAL) == G_IO_NVAL)
return FALSE;
cond |= G_IO_HUP | G_IO_ERR;
ls->condition = cond;
}
return TRUE;
}
static void
ldap_source_finalize (GSource *source)
{
LdapSource *ls = (LdapSource *)source;
/* Yeah, this is pretty rough, but we don't want blocking here */
close (ls->sock);
ldap_destroy (ls->ldap);
if (ls->cancellable) {
g_cancellable_release_fd (ls->cancellable);
g_object_unref (ls->cancellable);
}
}
static GSourceFuncs socket_source_funcs = {
ldap_source_prepare,
ldap_source_check,
ldap_source_dispatch,
ldap_source_finalize,
};
/* Not included in ldap.h but documented */
int ldap_init_fd (ber_socket_t fd, int proto, LDAP_CONST char *url, struct ldap **ldp);
GSource *
realm_ldap_connect_anonymous (GSocketAddress *address,
GSocketProtocol protocol,
GCancellable *cancellable)
{
GSource *source;
LdapSource *ls;
gchar *addrname;
GInetSocketAddress *inet;
struct berval cred;
Sockbuf *sb = NULL;
gsize native_len;
gpointer native;
int version;
gint port;
gchar *url;
int rc;
g_return_val_if_fail (G_IS_INET_SOCKET_ADDRESS (address), NULL);
inet = G_INET_SOCKET_ADDRESS (address);
addrname = g_inet_address_to_string (g_inet_socket_address_get_address (inet));
port = g_inet_socket_address_get_port (inet);
if (port == 0)
port = 389;
source = g_source_new (&socket_source_funcs, sizeof (LdapSource));
g_source_set_name (source, "LdapSource");
ls = (LdapSource *)source;
switch (protocol) {
case G_SOCKET_PROTOCOL_TCP:
ls->sock = socket (g_socket_address_get_family (address),
SOCK_STREAM, IPPROTO_TCP);
/* Not an expected failure */
if (ls->sock < 0) {
g_critical ("couldn't open socket to: %s: %s", addrname, strerror (errno));
return NULL;
}
if (!g_unix_set_fd_nonblocking (ls->sock, TRUE, NULL))
g_warning ("couldn't set to non-blocking");
native_len = g_socket_address_get_native_size (address);
native = g_malloc (native_len);
if (!g_socket_address_to_native (address, native, native_len, NULL))
g_return_val_if_reached (NULL);
if (connect (ls->sock, native, native_len) < 0 &&
errno != EINPROGRESS) {
g_debug ("Cannot connect: %s", g_strerror (errno));
ls->force_fail = LDAP_SERVER_DOWN;
}
if (!g_unix_set_fd_nonblocking (ls->sock, FALSE, NULL))
g_warning ("couldn't set to blocking");
rc = ldap_init_fd (ls->sock, 1, NULL, &ls->ldap);
g_free (native);
/* Not an expected failure */
if (rc != LDAP_SUCCESS) {
g_warning ("ldap_init_fd() failed: %s", ldap_err2string (rc));
return NULL;
}
break;
case G_SOCKET_PROTOCOL_UDP:
url = g_strdup_printf ("cldap://%s:%d", addrname, port);
/*
* HACK: ldap_init_fd() does not work for UDP, otherwise we
* could use the same code path as above, but it doesn't
* block while connecting anyway, so just use ldap_initialize()
*/
rc = ldap_initialize (&ls->ldap, url);
g_free (url);
/* Not an expected failure */
if (rc != LDAP_SUCCESS) {
g_warning ("ldap_initialize() failed: %s", ldap_err2string (rc));
return NULL;
}
/*
* An anonymous bind is used to actually connect the connection
* so we can get at the socket. For UDP with openldap an anonymous
* bind is treated as a no-op.
*/
cred.bv_val = "";
cred.bv_len = 0;
rc = ldap_sasl_bind_s (ls->ldap, NULL, LDAP_SASL_SIMPLE, &cred, NULL, NULL, NULL);
/* Not an expected failure */
if (rc != LDAP_SUCCESS) {
g_warning ("ldap_sasl_bind_s() failed: %s", ldap_err2string (rc));
return NULL;
}
if (ldap_get_option (ls->ldap, LDAP_OPT_SOCKBUF, &sb) < 0)
g_return_val_if_reached (NULL);
g_return_val_if_fail (sb != NULL, NULL);
if (ber_sockbuf_ctrl (sb, LBER_SB_OPT_GET_FD, &ls->sock) != 1)
g_return_val_if_reached (NULL);
ls->connect_done = TRUE;
break;
default:
g_return_val_if_reached (NULL);
break;
}
g_free (addrname);
version = LDAP_VERSION3;
if (ldap_set_option (ls->ldap, LDAP_OPT_PROTOCOL_VERSION, &version) != 0)
g_return_val_if_reached (NULL);
ldap_set_option (ls->ldap, LDAP_OPT_REFERRALS , LDAP_OPT_OFF);
ls->condition = G_IO_IN | G_IO_OUT | G_IO_HUP | G_IO_ERR;
ls->pollfd.fd = ls->sock;
ls->pollfd.events = ls->condition;
ls->pollfd.revents = 0;
g_source_add_poll (source, &ls->pollfd);
if (g_cancellable_make_pollfd (cancellable,
&ls->cancel_pollfd)) {
ls->cancellable = g_object_ref (cancellable);
g_source_add_poll (source, &ls->cancel_pollfd);
}
return source;
}
void
realm_ldap_set_condition (GSource *source,
GIOCondition cond)
{
LdapSource *ls = (LdapSource *)source;
GMainContext *context;
ls->condition = cond | G_IO_HUP | G_IO_ERR;
context = g_source_get_context (source);
if (context != NULL)
g_main_context_wakeup (context);
}
void
realm_ldap_set_error (GError **error,
LDAP *ldap,
int code)
{
char *info = NULL;
if (code <= 0) {
if (ldap_get_option (ldap, LDAP_OPT_RESULT_CODE, &code) != 0)
g_return_if_reached ();
}
if (code == LDAP_CANCELLED) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED,
_("The operation was cancelled"));
return;
}
if (ldap != NULL) {
if (ldap_get_option (ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, (void*)&info) != 0)
info = NULL;
}
g_set_error_literal (error, REALM_LDAP_ERROR, code,
info ? info : ldap_err2string (code));
}