/*
   sslh-fork: forking server

# Copyright (C) 2007-2025  Yves Rutschle
# 
# 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.
# 
# The full text for the General Public License is here:
# http://www.gnu.org/licenses/gpl.html

*/

#include "common.h"
#include "probe.h"
#include "sslh-conf.h"
#include "tcp-probe.h"
#include "log.h"

#if HAVE_LIBBSD
#include <bsd/unistd.h>
#endif

const char* server_type = "sslh-fork";

/* shovels data from one fd to the other and vice-versa 
   returns after one socket closed
 */
int shovel(struct connection *cnx)
{
   fd_set fds;
   int res, i;
   int max_fd = MAX(cnx->q[0].fd, cnx->q[1].fd) + 1;

   FD_ZERO(&fds);
   while (1) {
      FD_SET(cnx->q[0].fd, &fds);
      FD_SET(cnx->q[1].fd, &fds);

      res = select(
                   max_fd,
                   &fds,
                   NULL,
                   NULL,
                   NULL
                  );
      CHECK_RES_DIE(res, "select");

      for (i = 0; i < 2; i++) {
          if (FD_ISSET(cnx->q[i].fd, &fds)) {
              res = fd2fd(&cnx->q[1-i], &cnx->q[i]);
              if (res == FD_CNXCLOSED) {
                  print_message(msg_fd, "%s %s", i ? "client" : "server", "socket closed\n");
                  return res;
              }
          }
      }
   }
}

/* Child process that finds out what to connect to and proxies 
 */
void start_shoveler(int in_socket)
{
   fd_set fds;
   struct timeval tv;
   int res = PROBE_AGAIN;
   struct connection cnx;
   struct connection_desc desc;

   init_cnx(&cnx);
   cnx.q[0].fd = in_socket;

   FD_ZERO(&fds);
   FD_SET(in_socket, &fds);
   memset(&tv, 0, sizeof(tv));
   tv.tv_sec = cfg.timeout;

   while (res == PROBE_AGAIN) {
       /* POSIX does not guarantee that tv will be updated, but the client can
        * only postpone the inevitable for so long */
       res = select(in_socket + 1, &fds, NULL, NULL, &tv);
       if (res == -1)
           perror("select");

       if (FD_ISSET(in_socket, &fds)) {
           /* Received data: figure out what protocol it is */
           res = probe_client_protocol(&cnx);
       } else {
           /* Timed out: it's necessarily SSH */
           cnx.proto = timeout_protocol();
           print_message(msg_fd, "timed out, connect to %s\n", cnx.proto->name);
           break;
       }
   }

   if (cnx.proto->service &&
       check_access_rights(in_socket, cnx.proto->service)) {
       exit(0);
   }

   /* Connect the target socket */
   connect_addr(&cnx, in_socket, BLOCKING);
   CHECK_RES_DIE(cnx.q[1].fd, "connect");

   set_capabilities(0);

   get_connection_desc(&desc, &cnx);
   log_connection(&desc, &cnx);
   set_proctitle_shovel(&desc, &cnx);

   flush_deferred(&cnx.q[1]);

   shovel(&cnx);

   close(in_socket);
   close(cnx.q[1].fd);
   
   print_message(msg_fd, "connection closed down\n");

   exit(0);
}

static pid_t *listener_pid;
static int listener_pid_number = 0;

void stop_listeners(int sig)
{
    int i;

    for (i = 0; i < listener_pid_number; i++) {
        kill(listener_pid[i], sig);
    }
}

void set_listen_procname(struct listen_endpoint *listen_socket)
{
#if HAVE_LIBBSD
    int res;
    struct addrinfo addr;
    struct sockaddr_storage ss;
    char listen_addr[NI_MAXHOST+1+NI_MAXSERV+1];

    addr.ai_addr = (struct sockaddr*)&ss;
    addr.ai_addrlen = sizeof(ss);
    res = getsockname(listen_socket->socketfd, addr.ai_addr, &addr.ai_addrlen);
    if (res != -1) {
        sprintaddr(listen_addr, sizeof(listen_addr), &addr);
        setproctitle("listener %s", listen_addr);
    }
#endif
}

static void mask_sigchld(void)
{
    struct sigaction action;

    memset(&action, 0, sizeof(action));
    action.sa_handler = NULL;
    action.sa_flags = SA_NOCLDWAIT;
    int res = sigaction(SIGCHLD, &action, NULL);
    CHECK_RES_DIE(res, "sigaction");
}


extern volatile sig_atomic_t received_sigchld;
/* EINTR, so we probably received a signal; check if it's SIGCHLD */
static void process_signals(struct listen_endpoint* endpoint)
{
    int chld;
    if (received_sigchld) {
        received_sigchld = 0;
        do {
            chld = waitpid(-1, NULL, WNOHANG);
            CHECK_RES_RETURN(chld, "waitpid", );
            if (chld) {
                endpoint->num_connections--;
                print_message(msg_fd, "child died, %d concurrent connections remaining\n", endpoint->num_connections);
            }
        } while (chld);
    }
}

/* At least MacOS does not know these two options, so define them to something
 * equivalent for our use case */
#ifndef ENONET
#define ENONET EWOULDBLOCK
#endif
/* /MacOS kludge */

/* TCP listener: connections, fork a child for each new connection 
 * IN: 
 *      endpoint: array of listening endpoint objects
 *      num_endpoints: size of endpoint array
 *      active_endpoint: which endpoint is this listener working on
 * Does not return
 * */
void tcp_listener(struct listen_endpoint* endpoint, int num_endpoints, int active_endpoint)
{
    int i, in_socket;

    while (1) {
        in_socket = accept(endpoint[active_endpoint].socketfd, 0, 0);
        if (in_socket == -1) {
            print_message(msg_system_error, "%s:%d:%s:%d:%s\n",
                          __FILE__, __LINE__, "accept", errno, strerror(errno));
            switch(errno) {
            case ENETDOWN:  /* accept(2) cites all these errnos as "you should retry" */
            case EPROTO:
            case ENOPROTOOPT:
            case EHOSTDOWN:
            case ENONET:
            case EHOSTUNREACH:
            case EOPNOTSUPP:
            case ENETUNREACH:
            case ECONNABORTED:
                continue;

            case EINTR:
                process_signals(endpoint);
                continue;

            default:  /* Otherwise, it's something wrong in our parameters, we fail */
                return;
            }
        }
        endpoint->num_connections++;
        print_message(msg_fd, "accepted fd %d (%d concurrent connections)\n", in_socket, endpoint->num_connections);
        if ((endpoint->endpoint_cfg->max_connections_is_present) 
            && (endpoint->num_connections > endpoint->endpoint_cfg->max_connections)) 
        {
            print_message(msg_fd, "too many connection, reclosing fd %d\n", in_socket);
            endpoint->num_connections--;
            close(in_socket);
            continue;
        }

        switch(fork()) {
        case -1: print_message(msg_system_error, "fork failed: err %d: %s\n", errno, strerror(errno));
                 break;

        case 0: /* In child process */
                 /* Shoveler processes don't need to hog file descriptors */
                 for (i = 0; i < num_endpoints; i++)
                     close(endpoint[i].socketfd);
                 start_shoveler(in_socket);
                 exit(0);

        default: /* In parent process */
                 break;
        }
        close(in_socket);
    }
}


void main_loop(struct listen_endpoint listen_sockets[], int num_addr_listen)
{
    int i, res;
    struct sigaction action;

    listener_pid_number = num_addr_listen;
    listener_pid = malloc(listener_pid_number * sizeof(listener_pid[0]));
    CHECK_ALLOC(listener_pid, "malloc");

    tcp_init();

    /* Start one process for each listening address */
    for (i = 0; i < num_addr_listen; i++) {
        listener_pid[i] = fork();
        switch(listener_pid[i]) {
        /* Log if fork() fails for some reason */
        case -1: print_message(msg_system_error, "fork failed: err %d: %s\n", errno, strerror(errno));
                 break;
        /* We're in the child, we have work to do  */
        case 0:
            set_listen_procname(&listen_sockets[i]);
            if (listen_sockets[i].type == SOCK_DGRAM)
                print_message(msg_config_error, "UDP not supported in sslh-fork\n");
            else
                tcp_listener(listen_sockets, num_addr_listen, i);

            exit(0);
	    break;

	/* We're in the parent, which does nothing but wait to be killed by
         * SIGTERM, which it will send to its children */
	default:
            mask_sigchld();
	    break;
        }
    }

    /* Set SIGTERM to "stop_listeners" which further kills all listener
     * processes. Note this won't kill processes that listeners forked, which
     * means active connections remain active. */
    memset(&action, 0, sizeof(action));
    action.sa_handler = stop_listeners;
    res = sigaction(SIGTERM, &action, NULL);
    CHECK_RES_DIE(res, "sigaction");


    wait(NULL);
}

/* The actual main() is in sslh_main.c: it's the same for all versions of
 * the server
 */

