root/crypto/heimdal/kdc/announce.c
/*
 * Copyright (c) 2008 Apple Inc.  All Rights Reserved.
 *
 * Export of this software from the United States of America may require
 * a specific license from the United States Government.  It is the
 * responsibility of any person or organization contemplating export to
 * obtain such a license before exporting.
 *
 * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
 * distribute this software and its documentation for any purpose and
 * without fee is hereby granted, provided that the above copyright
 * notice appear in all copies and that both that copyright notice and
 * this permission notice appear in supporting documentation, and that
 * the name of Apple Inc. not be used in advertising or publicity pertaining
 * to distribution of the software without specific, written prior
 * permission.  Apple Inc. makes no representations about the suitability of
 * this software for any purpose.  It is provided "as is" without express
 * or implied warranty.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
 * WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 */

#include "kdc_locl.h"

#if defined(__APPLE__) && defined(HAVE_GCD)

#include <CoreFoundation/CoreFoundation.h>
#include <SystemConfiguration/SCDynamicStore.h>
#include <SystemConfiguration/SCDynamicStoreCopySpecific.h>
#include <SystemConfiguration/SCDynamicStoreKey.h>

#include <dispatch/dispatch.h>

#include <asl.h>
#include <resolv.h>

#include <dns_sd.h>
#include <err.h>

static krb5_kdc_configuration *announce_config;
static krb5_context announce_context;

struct entry {
    DNSRecordRef recordRef;
    char *domain;
    char *realm;
#define F_EXISTS 1
#define F_PUSH 2
    int flags;
    struct entry *next;
};

/* #define REGISTER_SRV_RR */

static struct entry *g_entries = NULL;
static CFStringRef g_hostname = NULL;
static DNSServiceRef g_dnsRef = NULL;
static SCDynamicStoreRef g_store = NULL;
static dispatch_queue_t g_queue = NULL;

#define LOG(...) asl_log(NULL, NULL, ASL_LEVEL_INFO, __VA_ARGS__)

static void create_dns_sd(void);
static void destroy_dns_sd(void);
static void update_all(SCDynamicStoreRef, CFArrayRef, void *);


/* parameters */
static CFStringRef NetworkChangedKey_BackToMyMac = CFSTR("Setup:/Network/BackToMyMac");


static char *
CFString2utf8(CFStringRef string)
{
    size_t size;
    char *str;

    size = 1 + CFStringGetMaximumSizeForEncoding(CFStringGetLength(string), kCFStringEncodingUTF8);
    str = malloc(size);
    if (str == NULL)
        return NULL;

    if (CFStringGetCString(string, str, size, kCFStringEncodingUTF8) == false) {
        free(str);
        return NULL;
    }
    return str;
}

/*
 *
 */

static void
retry_timer(void)
{
    dispatch_source_t s;
    dispatch_time_t t;

    s = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
                               0, 0, g_queue);
    t = dispatch_time(DISPATCH_TIME_NOW, 5ull * NSEC_PER_SEC);
    dispatch_source_set_timer(s, t, 0, NSEC_PER_SEC);
    dispatch_source_set_event_handler(s, ^{
            create_dns_sd();
            dispatch_release(s);
        });
    dispatch_resume(s);
}

/*
 *
 */

static void
create_dns_sd(void)
{
    DNSServiceErrorType error;
    dispatch_source_t s;

    error = DNSServiceCreateConnection(&g_dnsRef);
    if (error) {
        retry_timer();
        return;
    }

    dispatch_suspend(g_queue);

    s = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
                               DNSServiceRefSockFD(g_dnsRef),
                               0, g_queue);

    dispatch_source_set_event_handler(s, ^{
            DNSServiceErrorType ret = DNSServiceProcessResult(g_dnsRef);
            /* on error tear down and set timer to recreate */
            if (ret != kDNSServiceErr_NoError && ret != kDNSServiceErr_Transient) {
                dispatch_source_cancel(s);
            }
        });

    dispatch_source_set_cancel_handler(s, ^{
            destroy_dns_sd();
            retry_timer();
            dispatch_release(s);
        });

    dispatch_resume(s);

    /* Do the first update ourself */
    update_all(g_store, NULL, NULL);
    dispatch_resume(g_queue);
}

static void
domain_add(const char *domain, const char *realm, int flag)
{
    struct entry *e;

    for (e = g_entries; e != NULL; e = e->next) {
        if (strcmp(domain, e->domain) == 0 && strcmp(realm, e->realm) == 0) {
            e->flags |= flag;
            return;
        }
    }

    LOG("Adding realm %s to domain %s", realm, domain);

    e = calloc(1, sizeof(*e));
    if (e == NULL)
        return;
    e->domain = strdup(domain);
    e->realm = strdup(realm);
    if (e->domain == NULL || e->realm == NULL) {
        free(e->domain);
        free(e->realm);
        free(e);
        return;
    }
    e->flags = flag | F_PUSH; /* if we allocate, we push */
    e->next = g_entries;
    g_entries = e;
}

struct addctx {
    int flags;
    const char *realm;
};

static void
domains_add(const void *key, const void *value, void *context)
{
    char *str = CFString2utf8((CFStringRef)value);
    struct addctx *ctx = context;

    if (str == NULL)
        return;
    if (str[0] != '\0')
        domain_add(str, ctx->realm, F_EXISTS | ctx->flags);
    free(str);
}


static void
dnsCallback(DNSServiceRef sdRef __attribute__((unused)),
            DNSRecordRef RecordRef __attribute__((unused)),
            DNSServiceFlags flags __attribute__((unused)),
            DNSServiceErrorType errorCode __attribute__((unused)),
            void *context __attribute__((unused)))
{
}

#ifdef REGISTER_SRV_RR

/*
 * Register DNS SRV rr for the realm.
 */

static const char *register_names[2] = {
    "_kerberos._tcp",
    "_kerberos._udp"
};

static struct {
    DNSRecordRef *val;
    size_t len;
} srvRefs = { NULL, 0 };

static void
register_srv(const char *realm, const char *hostname, int port)
{
    unsigned char target[1024];
    int i;
    int size;

    /* skip registering LKDC realms */
    if (strncmp(realm, "LKDC:", 5) == 0)
        return;

    /* encode SRV-RR */
    target[0] = 0; /* priority */
    target[1] = 0; /* priority */
    target[2] = 0; /* weight */
    target[3] = 0; /* weigth */
    target[4] = (port >> 8) & 0xff; /* port */
    target[5] = (port >> 0) & 0xff; /* port */

    size = dn_comp(hostname, target + 6, sizeof(target) - 6, NULL, NULL);
    if (size < 0)
        return;

    size += 6;

    LOG("register SRV rr for realm %s hostname %s:%d", realm, hostname, port);

    for (i = 0; i < sizeof(register_names)/sizeof(register_names[0]); i++) {
        char name[kDNSServiceMaxDomainName];
        DNSServiceErrorType error;
        void *ptr;

        ptr = realloc(srvRefs.val, sizeof(srvRefs.val[0]) * (srvRefs.len + 1));
        if (ptr == NULL)
            errx(1, "malloc: out of memory");
        srvRefs.val = ptr;

        DNSServiceConstructFullName(name, NULL, register_names[i], realm);

        error = DNSServiceRegisterRecord(g_dnsRef,
                                         &srvRefs.val[srvRefs.len],
                                         kDNSServiceFlagsUnique | kDNSServiceFlagsShareConnection,
                                         0,
                                         name,
                                         kDNSServiceType_SRV,
                                         kDNSServiceClass_IN,
                                         size,
                                         target,
                                         0,
                                         dnsCallback,
                                         NULL);
        if (error) {
            LOG("Failed to register SRV rr for realm %s: %d", realm, error);
        } else
            srvRefs.len++;
    }
}

static void
unregister_srv_realms(void)
{
    if (g_dnsRef) {
        for (i = 0; i < srvRefs.len; i++)
            DNSServiceRemoveRecord(g_dnsRef, srvRefs.val[i], 0);
    }
    free(srvRefs.val);
    srvRefs.len = 0;
    srvRefs.val = NULL;
}

static void
register_srv_realms(CFStringRef host)
{
    krb5_error_code ret;
    char *hostname;
    size_t i;

    /* first unregister old names */

    hostname = CFString2utf8(host);
    if (hostname == NULL)
        return;

    for(i = 0; i < announce_config->num_db; i++) {
        char **realms, **r;

        if (announce_config->db[i]->hdb_get_realms == NULL)
            continue;

        ret = (announce_config->db[i]->hdb_get_realms)(announce_context, &realms);
        if (ret == 0) {
            for (r = realms; r && *r; r++)
                register_srv(*r, hostname, 88);
            krb5_free_host_realm(announce_context, realms);
        }
    }

    free(hostname);
}
#endif /* REGISTER_SRV_RR */

static void
update_dns(void)
{
    DNSServiceErrorType error;
    struct entry **e = &g_entries;
    char *hostname;

    hostname = CFString2utf8(g_hostname);
    if (hostname == NULL)
        return;

    while (*e != NULL) {
        /* remove if this wasn't updated */
        if (((*e)->flags & F_EXISTS) == 0) {
            struct entry *drop = *e;
            *e = (*e)->next;

            LOG("Deleting realm %s from domain %s",
                drop->realm, drop->domain);

            if (drop->recordRef && g_dnsRef)
                DNSServiceRemoveRecord(g_dnsRef, drop->recordRef, 0);
            free(drop->domain);
            free(drop->realm);
            free(drop);
            continue;
        }
        if ((*e)->flags & F_PUSH) {
            struct entry *update = *e;
            char *dnsdata, *name;
            size_t len;

            len = strlen(update->realm);
            asprintf(&dnsdata, "%c%s", (int)len, update->realm);
            if (dnsdata == NULL)
                errx(1, "malloc");

            asprintf(&name, "_kerberos.%s.%s", hostname, update->domain);
            if (name == NULL)
                errx(1, "malloc");

            if (update->recordRef)
                DNSServiceRemoveRecord(g_dnsRef, update->recordRef, 0);

            error = DNSServiceRegisterRecord(g_dnsRef,
                                             &update->recordRef,
                                             kDNSServiceFlagsShared | kDNSServiceFlagsAllowRemoteQuery,
                                             0,
                                             name,
                                             kDNSServiceType_TXT,
                                             kDNSServiceClass_IN,
                                             len+1,
                                             dnsdata,
                                             0,
                                             dnsCallback,
                                             NULL);
            free(name);
            free(dnsdata);
            if (error)
                errx(1, "failure to update entry for %s/%s",
                     update->domain, update->realm);
        }
        e = &(*e)->next;
    }
    free(hostname);
}

static void
update_entries(SCDynamicStoreRef store, const char *realm, int flags)
{
    CFDictionaryRef btmm;

    /* we always announce in the local domain */
    domain_add("local", realm, F_EXISTS | flags);

    /* announce btmm */
    btmm = SCDynamicStoreCopyValue(store, NetworkChangedKey_BackToMyMac);
    if (btmm) {
        struct addctx addctx;

        addctx.flags = flags;
        addctx.realm = realm;

        CFDictionaryApplyFunction(btmm, domains_add, &addctx);
        CFRelease(btmm);
    }
}

static void
update_all(SCDynamicStoreRef store, CFArrayRef changedKeys, void *info)
{
    struct entry *e;
    CFStringRef host;
    int i, flags = 0;

    LOG("something changed, running update");

    host = SCDynamicStoreCopyLocalHostName(store);
    if (host == NULL)
        return;

    if (g_hostname == NULL || CFStringCompare(host, g_hostname, 0) != kCFCompareEqualTo) {
        if (g_hostname)
            CFRelease(g_hostname);
        g_hostname = CFRetain(host);
        flags = F_PUSH; /* if hostname has changed, force push */

#ifdef REGISTER_SRV_RR
        register_srv_realms(g_hostname);
#endif
    }

    for (e = g_entries; e != NULL; e = e->next)
        e->flags &= ~(F_EXISTS|F_PUSH);

    for(i = 0; i < announce_config->num_db; i++) {
        krb5_error_code ret;
        char **realms, **r;

        if (announce_config->db[i]->hdb_get_realms == NULL)
            continue;

        ret = (announce_config->db[i]->hdb_get_realms)(announce_context, announce_config->db[i], &realms);
        if (ret == 0) {
            for (r = realms; r && *r; r++)
                update_entries(store, *r, flags);
            krb5_free_host_realm(announce_context, realms);
        }
    }

    update_dns();

    CFRelease(host);
}

static void
delete_all(void)
{
    struct entry *e;

    for (e = g_entries; e != NULL; e = e->next)
        e->flags &= ~(F_EXISTS|F_PUSH);

    update_dns();
    if (g_entries != NULL)
        errx(1, "Failed to remove all bonjour entries");
}

static void
destroy_dns_sd(void)
{
    if (g_dnsRef == NULL)
        return;

    delete_all();
#ifdef REGISTER_SRV_RR
    unregister_srv_realms();
#endif

    DNSServiceRefDeallocate(g_dnsRef);
    g_dnsRef = NULL;
}


static SCDynamicStoreRef
register_notification(void)
{
    SCDynamicStoreRef store;
    CFStringRef computerNameKey;
    CFMutableArrayRef keys;

    computerNameKey = SCDynamicStoreKeyCreateHostNames(kCFAllocatorDefault);

    store = SCDynamicStoreCreate(kCFAllocatorDefault, CFSTR("Network watcher"),
                                 update_all, NULL);
    if (store == NULL)
        errx(1, "SCDynamicStoreCreate");

    keys = CFArrayCreateMutable(kCFAllocatorDefault, 2, &kCFTypeArrayCallBacks);
    if (keys == NULL)
        errx(1, "CFArrayCreateMutable");

    CFArrayAppendValue(keys, computerNameKey);
    CFArrayAppendValue(keys, NetworkChangedKey_BackToMyMac);

    if (SCDynamicStoreSetNotificationKeys(store, keys, NULL) == false)
        errx(1, "SCDynamicStoreSetNotificationKeys");

    CFRelease(computerNameKey);
    CFRelease(keys);

    if (!SCDynamicStoreSetDispatchQueue(store, g_queue))
        errx(1, "SCDynamicStoreSetDispatchQueue");

    return store;
}
#endif

void
bonjour_announce(krb5_context context, krb5_kdc_configuration *config)
{
#if defined(__APPLE__) && defined(HAVE_GCD)
    g_queue = dispatch_queue_create("com.apple.kdc_announce", NULL);
    if (!g_queue)
        errx(1, "dispatch_queue_create");

    g_store = register_notification();
    announce_config = config;
    announce_context = context;

    create_dns_sd();
#endif
}