/*
  Copyright (C) 2000-2022 Free Software Foundation, Inc.

  This file is part of GNU Inetutils.

  GNU Inetutils 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 3 of the License, or (at
  your option) any later version.

  GNU Inetutils 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/'. */

#include <config.h>

#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <string.h>
#include <syslog.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>

#ifdef HAVE_TCPD_H
# include <tcpd.h>
#endif

#include <libinetutils.h>
#include "attribute.h"

int usefamily = AF_UNSPEC;	/* Address family for daemon.  */

static void reapchild (int);

#ifndef DEFPORT
# ifdef IPPORT_FTP
#  define DEFPORT IPPORT_FTP
# else /* !IPPORT_FTP */
#  define DEFPORT 21
# endif
#endif /* !DEFPORT */

#ifdef WITH_WRAP

# if !HAVE_DECL_HOSTS_CTL
extern int hosts_ctl (char *, char *, char *, char *);
# endif

#ifdef __CYGWIN__
/* provided by library (e.g. extern, here) on cygwin */
extern int allow_severity; /* set value in main() */
extern int deny_severity;  /* set value in main() */
#else
int allow_severity = LOG_INFO;
int deny_severity = LOG_NOTICE;
#endif

static int
check_host (struct sockaddr *sa, socklen_t len)
{
  int err;
  char addr[INET6_ADDRSTRLEN];
  char name[NI_MAXHOST];

  if (sa->sa_family != AF_INET
      && sa->sa_family != AF_INET6)
    return 1;

  (void) getnameinfo(sa, len, addr, sizeof (addr), NULL, 0, NI_NUMERICHOST);
  err = getnameinfo(sa, len, name, sizeof (name), NULL, 0, NI_NAMEREQD);
  if (!err)
    {
      if (!hosts_ctl ("ftpd", name, addr, STRING_UNKNOWN))
	{
	  syslog (deny_severity, "tcpwrappers rejected: %s [%s]",
		  name, addr);
	  return 0;
	}
    }
  else
    {
      if (!hosts_ctl ("ftpd", STRING_UNKNOWN, addr, STRING_UNKNOWN))
	{
	  syslog (deny_severity, "tcpwrappers rejected: [%s]", addr);
	  return 0;
	}
    }
  return (1);
}
#endif /* WITH_WRAP */

static void
reapchild (int signo MAYBE_UNUSED)
{
  int save_errno = errno;

  while (waitpid (-1, NULL, WNOHANG) > 0)
    ;
  errno = save_errno;
}

/* The parameter '*phis_addrlen' must be initiated
   with the space available at calling time.
   The size of used space will then be returned.
 */
int
server_mode (const char *pidfile, struct sockaddr *phis_addr,
	     socklen_t *phis_addrlen, char *argv[])
{
  int ctl_sock, fd;
  struct servent *sv;
  int port, err;
  char portstr[8];
  socklen_t saved_addrlen = *phis_addrlen;
  struct addrinfo hints, *res, *ai;

  /* Become a daemon.  */
  if (daemon (1, 1) < 0)
    {
      syslog (LOG_ERR, "failed to become a daemon");
      return -1;
    }
  signal (SIGCHLD, reapchild);

  /* Get port for ftp/tcp.  */
  sv = getservbyname ("ftp", "tcp");
  port = (sv == NULL) ? DEFPORT : ntohs (sv->s_port);
  snprintf (portstr, sizeof (portstr), "%u", port);

  memset (&hints, 0, sizeof (hints));
  hints.ai_socktype = SOCK_STREAM;
  hints.ai_flags = AI_PASSIVE;

  /* If an undetermined address family is passed,
   * we build a dual stacked listener from an AF_INET6
   * socket by unsetting the sockopt IPV6_V6ONLY.
   * Otherwise the resolver often prefers AF_INET.
   */
  hints.ai_family = (usefamily != AF_UNSPEC) ? usefamily : AF_INET6;

  err = getaddrinfo (NULL, portstr, &hints, &res);
  if (err)
    {
      syslog (LOG_ERR, "control socket: %s", gai_strerror (err));
      return -1;
    }
  /* Attempt to open socket, bind and start listen.  */
  for (ai = res; ai; ai = ai->ai_next)
    {
      ctl_sock = socket (ai->ai_family, ai->ai_socktype, ai->ai_protocol);
      if (ctl_sock < 0)
	continue;

      /* Enable local address reuse.  */
      {
	int on = 1;
	if (setsockopt (ctl_sock, SOL_SOCKET, SO_REUSEADDR,
			(char *) &on, sizeof (on)) < 0)
	  syslog (LOG_ERR, "control setsockopt: %m");
      }

      /* Upgrade to dual stacked socket.  */
      if (usefamily == AF_UNSPEC && ai->ai_family == AF_INET6)
	{
	  int off = 0;
	  if (setsockopt (ctl_sock, IPPROTO_IPV6, IPV6_V6ONLY,
			  (char *) &off, sizeof (off)) < 0)
	    syslog (LOG_DEBUG, "setsockopt bindv6only: %m");
	}

      if (bind (ctl_sock, ai->ai_addr, ai->ai_addrlen))
	{
	  close (ctl_sock);
	  ctl_sock = -1;
	  continue;
	}

      if (listen (ctl_sock, 32) < 0)
	{
	  close (ctl_sock);
	  ctl_sock = -1;
	  continue;
	}

      /* Accept the first choice!  */
      break;
    }

  if (res)
    freeaddrinfo (res);

  if (ai == NULL)
    {
      syslog (LOG_ERR, "control socket: %m");
      return -1;
    }

  /* Stash pid in pidfile.  */
  {
    FILE *pid_fp = fopen (pidfile, "w");
    if (pid_fp == NULL)
      syslog (LOG_ERR, "can't open %s: %m", PATH_FTPDPID);
    else
      {
	fprintf (pid_fp, "%d\n", (int) getpid ());
	fchmod (fileno (pid_fp), S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
	fclose (pid_fp);
      }
  }

  /* Loop forever accepting connection requests and forking off
     children to handle them.  */
  while (1)
    {
      *phis_addrlen = saved_addrlen;
      fd = accept (ctl_sock, phis_addr, phis_addrlen);
      if (fork () == 0)		/* child */
	{
	  dup2 (fd, 0);
	  dup2 (fd, 1);
	  close (ctl_sock);
	  break;
	}
      close (fd);
    }

#ifdef WITH_WRAP
  /* In the child.  */
  if (!check_host (phis_addr, *phis_addrlen))
    return -1;
#endif

#ifndef HAVE_FORK
  _exit (execvp (argv[0], argv));
#else
  (void) argv;		/* Silence warnings.  */
#endif

  return fd;
}
