/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include "alloc-util.h"
#include "dns-packet.h"
#include "dns-resolver-internal.h"
#include "errno-util.h"
#include "siphash24.h"
#include "socket-netlink.h"
#include "string-util.h"
#include "strv.h"
#include "unaligned.h"

void sd_dns_resolver_done(sd_dns_resolver *res) {
        assert(res);

        res->auth_name = mfree(res->auth_name);
        res->addrs = mfree(res->addrs);
        res->dohpath = mfree(res->dohpath);
}

sd_dns_resolver *sd_dns_resolver_unref(sd_dns_resolver *res) {
        if (!res)
                return NULL;

        sd_dns_resolver_done(res);
        return mfree(res);
}

DEFINE_ARRAY_FREE_FUNC(dns_resolver_free_array, sd_dns_resolver, sd_dns_resolver_done);

int dns_resolver_prio_compare(const sd_dns_resolver *a, const sd_dns_resolver *b) {
        return CMP(ASSERT_PTR(a)->priority, ASSERT_PTR(b)->priority);
}

int sd_dns_resolver_get_priority(sd_dns_resolver *res, uint16_t *ret_priority) {
        assert_return(res, -EINVAL);
        assert_return(ret_priority, -EINVAL);

        *ret_priority = res->priority;
        return 0;
}

int sd_dns_resolver_get_adn(sd_dns_resolver *res, const char **ret_adn) {
        assert_return(res, -EINVAL);
        assert_return(ret_adn, -EINVAL);

        /* Without adn only Do53 can be supported */
        if (!res->auth_name)
                return -ENODATA;

        *ret_adn = res->auth_name;
        return 0;
}

int sd_dns_resolver_get_inet_addresses(sd_dns_resolver *res, const struct in_addr **ret_addrs, size_t
                *ret_n_addrs) {
        assert_return(res, -EINVAL);
        assert_return(ret_addrs, -EINVAL);
        assert_return(ret_n_addrs, -EINVAL);
        assert_return(res->family == AF_INET, -EINVAL);

        /* ADN-only mode has no addrs */
        if (res->n_addrs == 0)
                return -ENODATA;

        struct in_addr *addrs = new(struct in_addr, res->n_addrs);
        if (!addrs)
                return -ENOMEM;

        for (size_t i = 0; i < res->n_addrs; i++)
                addrs[i] = res->addrs[i].in;
        *ret_addrs = addrs;
        *ret_n_addrs = res->n_addrs;

        return 0;
}

int sd_dns_resolver_get_inet6_addresses(sd_dns_resolver *res, const struct in6_addr **ret_addrs, size_t
                *ret_n_addrs) {
        assert_return(res, -EINVAL);
        assert_return(ret_addrs, -EINVAL);
        assert_return(ret_n_addrs, -EINVAL);
        assert_return(res->family == AF_INET6, -EINVAL);

        /* ADN-only mode has no addrs */
        if (res->n_addrs == 0)
                return -ENODATA;

        struct in6_addr *addrs = new(struct in6_addr, res->n_addrs);
        if (!addrs)
                return -ENOMEM;

        for (size_t i = 0; i < res->n_addrs; i++)
                addrs[i] = res->addrs[i].in6;
        *ret_addrs = addrs;
        *ret_n_addrs = res->n_addrs;

        return 0;
}

int sd_dns_resolver_get_alpn(sd_dns_resolver *res, sd_dns_alpn_flags *ret_alpn) {
        assert_return(res, -EINVAL);
        assert_return(ret_alpn, -EINVAL);

        /* ADN-only mode has no transports */
        if (!res->transports)
                return -ENODATA;

        *ret_alpn = res->transports;
        return 0;
}

int sd_dns_resolver_get_port(sd_dns_resolver *res, uint16_t *ret_port) {
        assert_return(res, -EINVAL);
        assert_return(ret_port, -EINVAL);

        /* port = 0 is the default port */
        *ret_port = res->port;
        return 0;
}

int sd_dns_resolver_get_dohpath(sd_dns_resolver *res, const char **ret_dohpath) {
        assert_return(res, -EINVAL);
        assert_return(ret_dohpath, -EINVAL);

        /* only present in DoH resolvers */
        if (!res->dohpath)
                return -ENODATA;

        *ret_dohpath = res->dohpath;
        return 0;
}

void siphash24_compress_resolver(const sd_dns_resolver *res, struct siphash *state) {
        assert(res);

        siphash24_compress_typesafe(res->priority, state);
        siphash24_compress_typesafe(res->transports, state);
        siphash24_compress_typesafe(res->port, state);

        siphash24_compress_string(res->auth_name, state);
        siphash24_compress_string(res->dohpath, state);

        siphash24_compress_typesafe(res->family, state);
        FOREACH_ARRAY(addr, res->addrs, res->n_addrs)
                siphash24_compress_typesafe(*addr, state);
}

int dns_resolver_transports_to_strv(sd_dns_alpn_flags transports, char ***ret) {
        _cleanup_strv_free_ char **ans = NULL;

        assert(ret);

        if (FLAGS_SET(transports, SD_DNS_ALPN_DO53)) {
                /* Do53 has no ALPN, this flag is only for our own usage. */
        }

        if (FLAGS_SET(transports, SD_DNS_ALPN_HTTP_2_TLS))
                if (strv_extend(&ans, "h2") < 0)
                        return -ENOMEM;
        if (FLAGS_SET(transports, SD_DNS_ALPN_HTTP_3))
                if (strv_extend(&ans, "h3") < 0)
                        return -ENOMEM;
        if (FLAGS_SET(transports, SD_DNS_ALPN_DOT))
                if (strv_extend(&ans, "dot") < 0)
                        return -ENOMEM;
        if (FLAGS_SET(transports, SD_DNS_ALPN_DOQ))
                if (strv_extend(&ans, "doq") < 0)
                        return -ENOMEM;

        *ret = TAKE_PTR(ans);
        return 0;
}

int dnr_parse_svc_params(const uint8_t *option, size_t len, sd_dns_resolver *resolver) {
        size_t offset = 0;
        int r;

        assert(option || len == 0);
        assert(resolver);

        sd_dns_alpn_flags transports = 0;
        uint16_t port = 0;
        _cleanup_free_ char *dohpath = NULL;
        bool alpn = false;

        uint16_t lastkey = 0;
        while (offset < len) {
                if (offset + 4 > len)
                        return -EBADMSG;

                uint16_t key = unaligned_read_be16(&option[offset]);
                offset += 2;

                /* RFC9460 § 2.2 SvcParam MUST appear in strictly increasing numeric order */
                if (lastkey >= key)
                        return -EBADMSG;
                lastkey = key;

                uint16_t plen = unaligned_read_be16(&option[offset]);
                offset += 2;
                if (offset + plen > len)
                        return -EBADMSG;

                switch (key) {
                /* Mandatory keys must be understood by the client, otherwise the record should be discarded.
                 * Automatic mandatory keys must not appear in the mandatory parameter, so these are all
                 * supplementary. We don't understand any supplementary keys, so if the mandatory parameter
                 * is present, we cannot use this record. */
                case DNS_SVC_PARAM_KEY_MANDATORY:
                        if (plen > 0)
                                return -EBADMSG;
                        break;

                case DNS_SVC_PARAM_KEY_ALPN:
                        if (plen == 0)
                                return 0;
                        alpn = true; /* alpn is required. Record that the requirement is met. */

                        size_t poff = offset;
                        size_t pend = offset + plen;
                        while (poff < pend) {
                                uint8_t alen = option[poff++];
                                if (poff + alen > len)
                                        return -EBADMSG;
                                if (memcmp_nn(&option[poff], alen, "dot", STRLEN("dot")) == 0)
                                        transports |= SD_DNS_ALPN_DOT;
                                if (memcmp_nn(&option[poff], alen, "h2", STRLEN("h2")) == 0)
                                        transports |= SD_DNS_ALPN_HTTP_2_TLS;
                                if (memcmp_nn(&option[poff], alen, "h3", STRLEN("h3")) == 0)
                                        transports |= SD_DNS_ALPN_HTTP_3;
                                if (memcmp_nn(&option[poff], alen, "doq", STRLEN("doq")) == 0)
                                        transports |= SD_DNS_ALPN_DOQ;
                                poff += alen;
                        }
                        if (poff != pend)
                                return -EBADMSG;
                        break;

                case DNS_SVC_PARAM_KEY_PORT:
                        if (plen != sizeof(uint16_t))
                                return -EBADMSG;
                        port = unaligned_read_be16(&option[offset]);
                        /* Server should indicate default port by omitting this param */
                        if (port == 0)
                                return -EBADMSG;
                        break;

                /* RFC9463 § 5.1 service params MUST NOT include ipv4hint/ipv6hint */
                case DNS_SVC_PARAM_KEY_IPV4HINT:
                case DNS_SVC_PARAM_KEY_IPV6HINT:
                        return -EBADMSG;

                case DNS_SVC_PARAM_KEY_DOHPATH:
                        r = make_cstring(&option[offset], plen,
                                         MAKE_CSTRING_REFUSE_TRAILING_NUL, &dohpath);
                        if (ERRNO_IS_NEG_RESOURCE(r))
                                return r;
                        if (r < 0)
                                return -EBADMSG;
                        /* dohpath is a RFC6750 URI template. We don't parse these, but at least check the
                         * charset is reasonable. */
                        if (!in_charset(dohpath, URI_VALID "{}"))
                                return -EBADMSG;
                        break;

                default:
                        ;
                }
                offset += plen;
        }
        if (offset != len)
                return -EBADMSG;

        /* DNR cannot be used without alpn */
        if (!alpn)
                return -EBADMSG;

        /* RFC9461 § 5: If the [SvcParam] indicates support for HTTP, "dohpath" MUST be present. */
        if (!dohpath && (FLAGS_SET(transports, SD_DNS_ALPN_HTTP_2_TLS) ||
                        FLAGS_SET(transports, SD_DNS_ALPN_HTTP_3)))
                return -EBADMSG;

        /* No useful transports */
        if (!transports)
                return 0;

        resolver->transports = transports;
        resolver->port = port;
        free_and_replace(resolver->dohpath, dohpath);
        return transports;
}

int dns_resolvers_to_dot_addrs(const sd_dns_resolver *resolvers, size_t n_resolvers,
                struct in_addr_full ***ret_addrs, size_t *ret_n_addrs) {
        assert(ret_addrs);
        assert(ret_n_addrs);
        assert(resolvers || n_resolvers == 0);

        struct in_addr_full **addrs = NULL;
        size_t n = 0;
        CLEANUP_ARRAY(addrs, n, in_addr_full_free_array);

        FOREACH_ARRAY(res, resolvers, n_resolvers) {
                if (!FLAGS_SET(res->transports, SD_DNS_ALPN_DOT))
                        continue;

                FOREACH_ARRAY(i, res->addrs, res->n_addrs) {
                        _cleanup_(in_addr_full_freep) struct in_addr_full *addr = NULL;
                        int r;

                        addr = new0(struct in_addr_full, 1);
                        if (!addr)
                                return -ENOMEM;
                        if (!GREEDY_REALLOC(addrs, n+1))
                                return -ENOMEM;

                        r = free_and_strdup(&addr->server_name, res->auth_name);
                        if (r < 0)
                                return r;
                        addr->family = res->family;
                        addr->port = res->port;
                        addr->address = *i;

                        addrs[n++] = TAKE_PTR(addr);
                }
        }

        *ret_addrs = TAKE_PTR(addrs);
        *ret_n_addrs = n;
        return n;
}

int dns_resolvers_to_dot_strv(const sd_dns_resolver *resolvers, size_t n_resolvers, char ***ret_names) {
        assert(ret_names);
        int r;

        _cleanup_strv_free_ char **names = NULL;
        size_t len = 0;

        struct in_addr_full **addrs = NULL;
        size_t n = 0;
        CLEANUP_ARRAY(addrs, n, in_addr_full_free_array);

        r = dns_resolvers_to_dot_addrs(resolvers, n_resolvers, &addrs, &n);
        if (r < 0)
                return r;

        FOREACH_ARRAY(addr, addrs, n) {
                const char *name = in_addr_full_to_string(*addr);
                if (!name)
                        return -ENOMEM;
                r = strv_extend_with_size(&names, &len, name);
                if (r < 0)
                        return r;
        }

        *ret_names = TAKE_PTR(names);
        return len;
}
