/****************************************************************//**
 *
 * @file mdns_responder.c
 *
 * @author   Logan Gunthorpe <logang@deltatee.com>
 *
 * @brief    mdns service discovery
 *           reqirues SO_REUSE=1 and LWIP_IGMP=1
 *
 * Copyright (c) Deltatee Enterprises Ltd. 2013
 * All rights reserved.
 *
 ********************************************************************/

/* 
 * Redistribution and use in source and binary forms, with or without
 * modification,are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Author: Logan Gunthorpe <logang@deltatee.com>
 *
 */

#include "mdns_responder.h"

#include "lwipopts.h"
#include <lwip/src/include/lwip/udp.h>
#include <lwip/src/include/ipv4/lwip/igmp.h>
#include <lwip/src/include/lwip/debug.h>
#include <lwip/src/include/lwip/mem.h>

#ifndef MDNS_DEBUG
#define MDNS_DEBUG LWIP_DBG_OFF
#endif

#ifndef MDNS_PORT
#define MDNS_PORT 5353
#endif

#include <stdio.h>
#include <string.h>
#include <stdlib.h>			// for strtol


#ifndef ip_set_option
#define ip_set_option(pcb, opt)   ((pcb)->so_options |= (opt))
#endif
extern struct netif gs_net_if;


#define FLAG_RESP   0x8400

const char *all_services = "\x09_services\x07_dns-sd\x04_udp\x05local";

#define QCLASS_IN   0x0001
#define CACHE_FLUSH 0x8000

#define QTYPE_A     0x0001
#define QTYPE_PTR   0x000C
#define QTYPE_TXT   0x0010
#define QTYPE_SRV   0x0021

#define DATA_POINTER 0xC000

#define TTL          10*60

static struct mdns_state mdns_state;

static const char dotlocal[] = "\x05local";
static const int dotlocal_len = sizeof(dotlocal);

struct mdns_header {
    uint16_t id;
    uint16_t flags;
    uint16_t questions;
    uint16_t answers;
    uint16_t authorities;
    uint16_t additionals;
};

struct record {
    uint16_t qtype;
    uint16_t qclass;
    uint32_t ttl;
    uint16_t data_length;
    char data[];
} __attribute__((__packed__));

struct srv_record {
    uint16_t priority;
    uint16_t weight;
    uint16_t port;
    char target[];
} __attribute__((__packed__));

struct mdns_state {
    char *hostnames[4];
    char *service_host;
    const struct mdns_service *services;
    int num_services;
    char *txt_records;
    struct udp_pcb *sendpcb;
    struct netif *netif;
};

static struct pbuf *populate_header(int answers, int authorities,
                                    int additionals)
{
    struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, sizeof(struct mdns_header),
                                PBUF_RAM);

    struct mdns_header *hdr = (struct mdns_header *) p->payload;
    hdr->id = 0;
    hdr->flags = htons(FLAG_RESP);
    hdr->questions = 0;
    hdr->answers = htons(answers);
    hdr->authorities = htons(authorities);
    hdr->additionals = htons(additionals);

    return p;
}

static int special_strlen(const char *name)
{
    int ret = 0;

    while (1) {
        ret++;
        int x = *name++;
        if (x == 0)
            return ret;
        if (x & 0xC0)
            return ret + 1;

        ret +=x;
        name += x;
    }
}

static int special_strcpy(char * dest, const char *name,
                          const struct pbuf *p)
{
    int ret = 0;
    int link_ret = -1;

    const char *end = ((char *) p->payload) + p->len;

    while (1) {
        int x = *name++;
        ret++;

        if (name > end) {
            *dest = 0;
            return -1;
        }

        if (x & 0xC0) {
            ret++;
            if (link_ret < 0) link_ret = ret;
            x = (x << 8) | *name++;
            name = &((char *)p->payload)[x & 0x3FFF];
            continue;
        }

        *dest++ = x;

        if (x == 0)
            return (link_ret >= 0) ? link_ret : ret;

        ret +=x;
        memcpy(dest, name, x);
        name += x;
        dest += x;
    }
}

static uint16_t populate_record(const char *name,
                                uint16_t qtype, uint16_t qclass,
                                uint32_t ttl, const void *data,
                                uint16_t datalen,
                                struct pbuf *dest)
{
    int title_len = special_strlen(name);
    int msglen = title_len  + sizeof(struct record) + datalen;
    struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, msglen, PBUF_RAM);

    memcpy(p->payload, name, title_len);
    char *end = ((char *)p->payload) + title_len;

    struct record *rec = (struct record *) end;
    rec->qtype = htons(qtype);
    rec->qclass = htons(qclass);
    rec->ttl = htonl(ttl);
    rec->data_length = htons(datalen);
    memcpy(rec->data, data, datalen);

    uint16_t ret = dest->tot_len + title_len + sizeof(*rec);

    pbuf_cat(dest, p);

    return htons(DATA_POINTER | ret);
}

static void send_a_record(struct mdns_state *ms, const char *name)
{
    struct pbuf *hdr = populate_header(1, 0, 0);

    populate_record(name, QTYPE_A, QCLASS_IN,
                    TTL, &ms->netif->ip_addr,
                    sizeof(ms->netif->ip_addr),
                    hdr);

    udp_send(ms->sendpcb, hdr);

    LWIP_DEBUGF(MDNS_DEBUG | LWIP_DBG_STATE,
                ("mdns: sending A response\n"));

    pbuf_free(hdr);
}

static void send_all_services(struct mdns_state *ms, const char *name)
{
    struct pbuf *hdr = populate_header(ms->num_services, 0, 0);

    uint16_t first_ptr = htons(DATA_POINTER | hdr->len);

    for (int i = 0; i < ms->num_services; i++) {
        if (i != 0)
            name = (char *) &first_ptr;
        populate_record(name, QTYPE_PTR, QCLASS_IN,
                        TTL, ms->services[i].name,
                        strlen(ms->services[i].name)+1,
                        hdr);
    }

    udp_send(ms->sendpcb, hdr);

    LWIP_DEBUGF(MDNS_DEBUG | LWIP_DBG_STATE,
                ("mdns: sending ALL response\n"));

    pbuf_free(hdr);

}

static void send_ptr_record(struct mdns_state *ms, const char *domain,
                            int service)
{
    struct pbuf *hdr = populate_header(1, 0, 3);

    uint16_t first_ptr = htons(DATA_POINTER | hdr->len);

    //PTR Record
    int service_len = strlen(ms->service_host);
    char service_name[service_len + 2];
    strcpy(service_name, ms->service_host);
    memcpy(&service_name[service_len], &first_ptr, sizeof(first_ptr));
    uint16_t srv_ptr = populate_record(domain, QTYPE_PTR, QCLASS_IN,
                                       TTL, service_name,
                                       sizeof(service_name),
                                       hdr);

    //SRV Record
    char buf[sizeof(struct srv_record) + service_len + dotlocal_len];
    struct srv_record *srv_rec = (struct srv_record *) buf;
    srv_rec->priority = htons(50);
    srv_rec->weight = htons(0);
    srv_rec->port = htons(ms->services[service].port);
    memcpy(srv_rec->target, ms->service_host, service_len);
    memcpy(srv_rec->target + service_len, dotlocal, dotlocal_len);
    uint16_t arec_ptr = populate_record((char *)&srv_ptr, QTYPE_SRV,
                                        QCLASS_IN, TTL,
                                        buf, sizeof(buf),
                                        hdr);

    arec_ptr = htons(ntohs(arec_ptr) + sizeof(struct srv_record));

    //TXT Record
    populate_record((char *) &srv_ptr, QTYPE_TXT,
                    QCLASS_IN, TTL,
                    ms->txt_records, strlen(ms->txt_records) + 1,
                    hdr);

    //A Record
    populate_record((char *) &srv_ptr, QTYPE_A,
                    QCLASS_IN, TTL,
                    &ms->netif->ip_addr,
                    sizeof(ms->netif->ip_addr),
                    hdr);

    udp_send(ms->sendpcb, hdr);

    LWIP_DEBUGF(MDNS_DEBUG | LWIP_DBG_STATE,
                ("mdns: sending PTR response\n"));

    pbuf_free(hdr);
}

static void send_srv_record(struct mdns_state *ms, const char *domain,
                            int service)
{
    struct pbuf *hdr = populate_header(1, 0, 2);

    uint16_t first_ptr = htons(DATA_POINTER | hdr->len);

    //SRV Record
    int service_len = strlen(ms->service_host);
    char buf[sizeof(struct srv_record) + service_len + dotlocal_len];
    struct srv_record *srv_rec = (struct srv_record *) buf;
    srv_rec->priority = htons(50);
    srv_rec->weight = htons(0);
    srv_rec->port = htons(ms->services[service].port);
    memcpy(srv_rec->target, ms->service_host, service_len);
    memcpy(srv_rec->target + service_len, dotlocal, dotlocal_len);
    uint16_t arec_ptr = populate_record(domain, QTYPE_SRV,
                                        QCLASS_IN, TTL,
                                        buf, sizeof(buf),
                                        hdr);

    arec_ptr = htons(ntohs(arec_ptr) + sizeof(struct srv_record));

    //TXT Record
    const char txt[1] = "";
    populate_record((char *) &first_ptr, QTYPE_TXT,
                    QCLASS_IN, TTL,
                    txt, sizeof(txt),
                    hdr);

    //A Record
    populate_record((char *) &arec_ptr, QTYPE_A,
                    QCLASS_IN, TTL,
                    &ms->netif->ip_addr,
                    sizeof(ms->netif->ip_addr),
                    hdr);

    udp_send(ms->sendpcb, hdr);

    LWIP_DEBUGF(MDNS_DEBUG | LWIP_DBG_STATE,
                ("mdns: sending SRV response\n"));

    pbuf_free(hdr);
}

static void send_rev_record(struct mdns_state *ms, const char *domain)
{
    struct pbuf *hdr = populate_header(1, 0, 0);

    int service_len = strlen(ms->service_host);
    char service_name[service_len + dotlocal_len];
    strcpy(service_name, ms->service_host);
    strcat(service_name, dotlocal);
    populate_record(domain, QTYPE_PTR, QCLASS_IN,
                    TTL, service_name,
                    sizeof(service_name),
                    hdr);

    udp_send(ms->sendpcb, hdr);

    LWIP_DEBUGF(MDNS_DEBUG | LWIP_DBG_STATE,
                ("mdns: sending REV PTR response\n"));

    pbuf_free(hdr);
}

static int query_hostname(struct mdns_state *ms, const char *domain)
{
    for (int i = 0; i < sizeof(ms->hostnames) / sizeof(*ms->hostnames); i++) {
        if (strcasecmp(domain, ms->hostnames[i]) == 0)
            return 1;
    }

    return 0;
}

static int query_ptr(struct mdns_state *ms, const char *domain)
{
    for (int i = 0; i < ms->num_services; i++) {
        if (strcasecmp(domain, ms->services[i].name) == 0)
            return i;
    }

    return -1;
}

static int query_service(struct mdns_state *ms, const char *domain)
{
    int hostlen = strlen(ms->service_host);

    if (!strncasecmp(domain, ms->service_host, hostlen))
        return -1;

    return query_ptr(ms, &domain[hostlen]);
}

static int compare_reverse_ptr(struct mdns_state *ms, char *buf)
{
    buf++;
    if (strtol(buf, &buf, 10) != ip4_addr4(&ms->netif->ip_addr))
        return 0;

    buf++;
    if (strtol(buf, &buf, 10) != ip4_addr3(&ms->netif->ip_addr))
        return 0;

    buf++;
    if (strtol(buf, &buf, 10) != ip4_addr2(&ms->netif->ip_addr))
        return 0;

    buf++;
    if (strtol(buf, &buf, 10) != ip4_addr1(&ms->netif->ip_addr))
        return 0;

    if(strcmp(buf, "\x07in-addr\04arpa") == 0)
        return 1;

    return 0;
}

static int parse_question(struct mdns_state *ms, struct udp_pcb *upcb,
                          char *data, int qlen, struct pbuf *p)
{
    int offset = 0;

    char buf[255];
    offset = special_strcpy(buf, data, p);

    if (offset < 0)
        return -1;

    int qtype = (data[offset] << 8) | data[offset+1];
    offset += 2;
    int qclass = (data[offset] << 8) | data[offset+1];
    (void)qclass;		// dc42 suppress 'unused' warning when debug is disabled

    offset += 2;

    LWIP_DEBUGF(MDNS_DEBUG | LWIP_DBG_STATE,
                ("mdns: question '%s' type %d class %d\n",
                    &buf[1], qtype, qclass));

    if (qtype == QTYPE_A) {
        if (query_hostname(ms, buf))
            send_a_record(ms, buf);
    } else if (qtype == QTYPE_PTR) {
        if (strcasecmp(all_services, buf) == 0) {
            send_all_services(ms, buf);
        } else if (compare_reverse_ptr(ms, buf)) {
            send_rev_record(ms, buf);
        } else {
            int service;
            if ((service = query_ptr(ms, buf)) >= 0)
                send_ptr_record(ms, buf, service);
        }
    } else if (qtype == QTYPE_SRV || qtype == QTYPE_TXT) {
        int service;
        if ((service = query_service(ms, buf)) >= 0)
            send_srv_record(ms, buf, service);
    }

    return offset;
}

static void recv(void *arg, struct udp_pcb *upcb, struct pbuf *p,
                 ip_addr_t *addr, u16_t port)
{
    struct mdns_state *ms = (struct mdns_state *) arg;
    struct mdns_header *h = (struct mdns_header *) p->payload;
    char *questions = (char *) &h[1];
    int qlen = p->len - sizeof(*h);

    h->flags = ntohs(h->flags);
    h->questions = ntohs(h->questions);

    LWIP_DEBUGF(MDNS_DEBUG | LWIP_DBG_STATE,
                ("mdns: packet from "));
    ip_addr_debug_print(MDNS_DEBUG | LWIP_DBG_STATE, addr);
    LWIP_DEBUGF(MDNS_DEBUG | LWIP_DBG_STATE,
                (" %04x %04x\n", h->flags, h->questions));

    if (h->id != 0 || h->questions == 0)
        goto free_and_return;

    for (int i = 0; i < h->questions; i++) {
        int offset = parse_question(ms, upcb, questions, qlen, p);

        if (offset < 0)
            goto free_and_return;

        questions += offset;
        qlen -= offset;
        if (qlen <= 0)
            break;
    }

free_and_return:
    pbuf_free(p);
}


static void free_hostnames(struct mdns_state *ms)
{
    for (int i = 0; i < sizeof(ms->hostnames) / sizeof(*ms->hostnames); i++) {
        if (ms->hostnames[i] != NULL) {
            mem_free(ms->hostnames[i]);
            ms->hostnames[i] = NULL;
        }
    }

    if (ms->service_host != NULL) {
        mem_free(ms->service_host);
        ms->service_host = NULL;
    }
}

static void setup_hostnames(struct mdns_state *ms, struct netif *netif)
{
    int hostlen = strlen(netif->hostname);

    ms->hostnames[0] = mem_malloc(hostlen + dotlocal_len + 1);
    sprintf(ms->hostnames[0], "%c%s%s", hostlen, netif->hostname,
            dotlocal);

    ms->hostnames[1] = mem_malloc(hostlen + 2 + dotlocal_len + 2);
    sprintf(ms->hostnames[1], "%c%s-%02X%s", hostlen+3, netif->hostname,
            netif->hwaddr[5], dotlocal);

    char macaddr[12];
    sprintf(macaddr, "%02X%02X%02X%02X%02X%02X",
            netif->hwaddr[0], netif->hwaddr[1], netif->hwaddr[2],
            netif->hwaddr[3], netif->hwaddr[4], netif->hwaddr[5]);
    int maclen = strlen(macaddr);

    ms->hostnames[2] = mem_malloc(hostlen + maclen + dotlocal_len + 2);
    sprintf(ms->hostnames[2], "%c%s-%s%s", maclen+hostlen+1, netif->hostname,
            macaddr, dotlocal);

    ms->hostnames[3] = mem_malloc(strlen(macaddr) + dotlocal_len + 1);
    sprintf(ms->hostnames[3], "%c%s%s", maclen, macaddr, dotlocal);

    for (int i = 0; i < sizeof(ms->hostnames) / sizeof(*ms->hostnames); i++) {
        LWIP_DEBUGF(MDNS_DEBUG | LWIP_DBG_STATE,
                    ("mdns: hostname registered: %s\n",
                     &ms->hostnames[i][1]));
    }

    ms->service_host = mem_malloc(hostlen + maclen + 3);
    sprintf(ms->service_host, "%c%s-%s", maclen+hostlen+1, netif->hostname,
            macaddr);

}

static void setup_txt_records(struct mdns_state *ms, const char *txt_records[])
{
    int totlen = 0;

    for (const char **rec = txt_records; *rec != NULL; rec++)
        totlen += 1 + strlen(*rec);

    ms->txt_records = mem_malloc(totlen+1);

    char *t = ms->txt_records;
    for (const char **rec = txt_records; *rec != NULL; rec++) {
        int l = strlen(*rec);
        *t++ = l;
        strcpy(t, *rec);
        t += l;
    }

}

err_t mdns_responder_init(const struct mdns_service *services,
                          int num_services,
                          const char *txt_records[])
{
    err_t ret;

    memset(&mdns_state, 0, sizeof(mdns_state));
    mdns_state.netif = &gs_net_if;

    setup_hostnames(&mdns_state, &gs_net_if);
    setup_txt_records(&mdns_state, txt_records);

    mdns_state.services = services;
    mdns_state.num_services = num_services;

    struct ip_addr ipgroup;
    IP4_ADDR(&ipgroup, 224, 0, 0, 251);

    mdns_state.sendpcb = udp_new();
    if (mdns_state.sendpcb == NULL)
        return ERR_MEM;

    struct udp_pcb *pcb = udp_new();
    if (pcb == NULL) {
        udp_remove(mdns_state.sendpcb);
        return ERR_MEM;
    }

    if ((ret = igmp_joingroup(IP_ADDR_ANY, &ipgroup)) != ERR_OK)
        return ret;

    ip_set_option(pcb, SOF_REUSEADDR);
    ip_set_option(mdns_state.sendpcb, SOF_REUSEADDR);

    if ((ret = udp_bind(pcb, IP_ADDR_ANY, MDNS_PORT)) != ERR_OK)
        goto error_exit;

    udp_recv(pcb, recv, &mdns_state);

    if ((ret = udp_bind(mdns_state.sendpcb, 0, MDNS_PORT)) != ERR_OK)
        goto error_exit;

    if ((ret = udp_connect(mdns_state.sendpcb, &ipgroup, MDNS_PORT)) != ERR_OK)
        goto error_exit;

    return ERR_OK;

error_exit:
    udp_remove(pcb);
    udp_remove(mdns_state.sendpcb);
    return ret;

}

void mdns_update_hostname()
{
    free_hostnames(&mdns_state);
    setup_hostnames(&mdns_state, &gs_net_if);
}

void mdns_announce()
{
    struct mdns_state *ms = &mdns_state;
    int count = sizeof(ms->hostnames) / sizeof(*ms->hostnames);
    struct pbuf *hdr = populate_header(count, 0, 0);

    for (int i = 0; i < count; i++) {
        populate_record(ms->hostnames[i], QTYPE_A, QCLASS_IN | CACHE_FLUSH,
                        TTL, &ms->netif->ip_addr,
                        sizeof(ms->netif->ip_addr),
                        hdr);
    }

    udp_send(ms->sendpcb, hdr);

    LWIP_DEBUGF(MDNS_DEBUG | LWIP_DBG_STATE,
                ("mdns: sending announcment\n"));

    pbuf_free(hdr);
}
