/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 *
 * Copyright 2025 GNOME Foundation, Inc.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *  - Philip Withnall <pwithnall@gnome.org>
 */

#include <arpa/inet.h>
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <err.h>
#include <fcntl.h>
#include <limits.h>
#include <nss.h>
#include <netdb.h>
#include <pwd.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>


/**
 * NSS hardcoded test module
 *
 * This is an NSS module with some hardcoded `gethostbyname()` results, which we
 * can use as a fallback behind the `malcontent` NSS module to test whether it
 * works (and whether it’s recursing lookups and passing through lookups
 * correctly).
 *
 * It is *not* meant to be used in production.
 *
 * NSS documentation:
 *  - https://www.gnu.org/software/libc/manual/html_node/NSS-Modules-Interface.html
 *  - https://www.gnu.org/software/libc/manual/html_node/NSS-Module-Function-Internals.html
 *  - https://elixir.bootlin.com/glibc/glibc-2.41/source/nss/getaddrinfo.c
 */

/* Exported module API: */
enum nss_status _nss_hardcoded_gethostbyname3_r (const char      *name,
                                                 int              af,
                                                 struct hostent  *result,
                                                 char            *buffer,
                                                 size_t           buffer_len,
                                                 int             *errnop,
                                                 int             *h_errnop,
                                                 int32_t         *ttlp,
                                                 char           **canonp);
enum nss_status _nss_hardcoded_gethostbyname2_r (const char     *name,
                                                 int             af,
                                                 struct hostent *result,
                                                 char           *buffer,
                                                 size_t          buffer_len,
                                                 int            *errnop,
                                                 int            *h_errnop);

static inline size_t
align_as_pointer (size_t in)
{
  const size_t ptr_alignment = __alignof__ (void *);
  return (in + (ptr_alignment - 1)) & ~(ptr_alignment - 1);
}

/* As per https://elixir.bootlin.com/glibc/glibc-2.41/source/nss/getaddrinfo.c,
 * glibc only calls gethostbyname4_r and gethostbyname3_r conditionally. If we
 * want to support the most possible queries (and versions of glibc), provide
 * gethostbyname2_r. glibc will handle the fallbacks for other API versions.
 *
 * We do need to provide a gethostbyname3_r() function, though, as that’s
 * explicitly called when AI_CANONNAME is set in the request flags. */
enum nss_status
_nss_hardcoded_gethostbyname3_r (const char      *name,
                                 int              af,
                                 struct hostent  *result,
                                 char            *buffer,
                                 size_t           buffer_len,
                                 int             *errnop,
                                 int             *h_errnop,
                                 int32_t         *ttlp,
                                 char           **canonp)
{
  const char *result_name = NULL;
  const char *result_addr_str = NULL;
  const char *result_addr6_str = NULL;
  struct in_addr result_addr = { .s_addr = 0 };
  struct in6_addr result_addr6 = { .s6_addr = { 0, } };

  /* Is the app querying for a protocol which we support? */
  if (af != AF_INET && af != AF_INET6)
    {
      *errnop = EAFNOSUPPORT;
      *h_errnop = HOST_NOT_FOUND;
      return NSS_STATUS_UNAVAIL;
    }

  /* Define certain well-known domains with well-known resolutions, which we can
   * then use in the unit tests to check results. The actual IP addresses are
   * arbitrary and meaningless, they just need to be distinct between domains.
   */
  if (strcmp (name, "always-resolves.com") == 0)
    {
      result_name = name;
      result_addr_str = "1.2.3.4";
      result_addr6_str = "2001:0DB8:AC10:FE01::";
    }
  else if (strcmp (name, "never-resolves.com") == 0)
    {
      result_name = name;
      result_addr_str = NULL;
      result_addr6_str = NULL;
    }
  else if (strcmp (name, "redirects.com") == 0)
    {
      result_name = "safe.redirects.com";
      result_addr_str = "1.2.3.5";
      result_addr6_str = "2001:0DB8:AC10:FE02::";
    }

  if (result_name != NULL && result_addr_str != NULL && result_addr6_str != NULL)
    {
      size_t buffer_offset = 0;
      size_t h_length = (af == AF_INET6) ? sizeof (struct in6_addr) : sizeof (struct in_addr);

      /* Check the buffer size first. */
      if (buffer_len < align_as_pointer (strlen (result_name) + 1) + align_as_pointer (sizeof (void *)) + align_as_pointer (sizeof (void *) * 2) + align_as_pointer (h_length))
        {
          *errnop = ERANGE;
          *h_errnop = NO_RECOVERY;
          return NSS_STATUS_TRYAGAIN;
        }

      /* Build the result. Even though we never set any h_aliases, tools like
       * `getent` expect a non-NULL (though potentially empty) array. */
      if (inet_pton (AF_INET, result_addr_str, &result_addr) != 1 ||
          inet_pton (AF_INET6, result_addr6_str, &result_addr6) != 1)
        assert (0);

      strcpy (buffer, result_name);
      result->h_name = buffer;
      buffer_offset = align_as_pointer (strlen (result_name) + 1);

      result->h_aliases = (char **) (buffer + buffer_offset);
      buffer_offset += align_as_pointer (sizeof (void *));
      result->h_aliases[0] = NULL;

      result->h_addrtype = af;
      result->h_length = h_length;

      result->h_addr_list = (char **) (buffer + buffer_offset);
      buffer_offset += align_as_pointer (sizeof (void *) * 2);
      memcpy (buffer + buffer_offset, (af == AF_INET6) ? (char *) &result_addr6 : (char *) &result_addr, result->h_length);
      result->h_addr_list[0] = buffer + buffer_offset;
      buffer_offset += align_as_pointer (result->h_length);
      result->h_addr_list[1] = NULL;

      assert (buffer_offset <= buffer_len);

      if (ttlp != NULL)
        *ttlp = 0;
      if (canonp != NULL)
        *canonp = result->h_name;

      *errnop = 0;
      *h_errnop = 0;
      return NSS_STATUS_SUCCESS;
    }
  else
    {
      /* Not found in the filter list, so let another module actually resolve it. */
      *errnop = EINVAL;
      *h_errnop = NO_ADDRESS;
      return NSS_STATUS_NOTFOUND;
    }
}

enum nss_status
_nss_hardcoded_gethostbyname2_r (const char     *name,
                                 int             af,
                                 struct hostent *result,
                                 char           *buffer,
                                 size_t          buffer_len,
                                 int            *errnop,
                                 int            *h_errnop)
{
  return _nss_hardcoded_gethostbyname3_r (name, af, result, buffer, buffer_len,
                                          errnop, h_errnop, NULL, NULL);
}
