/*************************************************
*     Exim - an Internet mail transport agent    *
*************************************************/

/* Copyright (c) The Exim Maintainers 2021 - 2022 */
/* Copyright (c) Jeremy Harris 2020 */
/* See the file NOTICE for conditions of use and distribution. */
/* SPDX-License-Identifier: GPL-2.0-or-later */

#include "../exim.h"
#include "lf_functions.h"


static int
internal_readsock_open(client_conn_ctx * cctx, const uschar * sspec,
  int timeout, uschar * do_tls, uschar ** errmsg)
{
const uschar * server_name;
host_item host;

if (Ustrncmp(sspec, "inet:", 5) == 0)
  {
  int port;
  uschar * port_name;

  DEBUG(D_lookup)
    debug_printf_indent("  new inet socket needed for readsocket\n");

  server_name = sspec + 5;
  port_name = Ustrrchr(server_name, ':');

  /* Sort out the port */

  if (!port_name)
    {
    /* expand_string_message results in an EXPAND_FAIL, from our
    only caller.  Lack of it gets a SOCK_FAIL; we feed back via errmsg
    for that, which gets copied to search_error_message. */

    expand_string_message =
      string_sprintf("missing port for readsocket %s", sspec);
    return FAIL;
    }
  *port_name++ = 0;           /* Terminate server name */

  if (isdigit(*port_name))
    {
    uschar *end;
    port = Ustrtol(port_name, &end, 0);
    if (end != port_name + Ustrlen(port_name))
      {
      expand_string_message =
	string_sprintf("invalid port number %s", port_name);
      return FAIL;
      }
    }
  else
    {
    struct servent *service_info = getservbyname(CS port_name, "tcp");
    if (!service_info)
      {
      expand_string_message = string_sprintf("unknown port \"%s\"",
	port_name);
      return FAIL;
      }
    port = ntohs(service_info->s_port);
    }

  /* Not having the request-string here in the open routine means
  that we cannot do TFO; a pity */

  cctx->sock = ip_connectedsocket(SOCK_STREAM, server_name, port, port,
	  timeout, &host, errmsg, NULL);
  callout_address = NULL;
  if (cctx->sock < 0)
    return FAIL;
  }

else
  {
  struct sockaddr_un sockun;         /* don't call this "sun" ! */
  int rc;

  DEBUG(D_lookup)
    debug_printf_indent("  new unix socket needed for readsocket\n");

  if ((cctx->sock = socket(PF_UNIX, SOCK_STREAM, 0)) == -1)
    {
    *errmsg = string_sprintf("failed to create socket: %s", strerror(errno));
    return FAIL;
    }

  sockun.sun_family = AF_UNIX;
  sprintf(sockun.sun_path, "%.*s", (int)(sizeof(sockun.sun_path)-1),
    sspec);
  server_name = US sockun.sun_path;

  sigalrm_seen = FALSE;
  ALARM(timeout);
  rc = connect(cctx->sock, (struct sockaddr *) &sockun, sizeof(sockun));
  ALARM_CLR(0);
  if (sigalrm_seen)
    {
    *errmsg = US "socket connect timed out";
    goto bad;
    }
  if (rc < 0)
    {
    *errmsg = string_sprintf("failed to connect to socket "
      "%s: %s", sspec, strerror(errno));
    goto bad;
    }
  host.name = server_name;
  host.address = US"";
  }

#ifndef DISABLE_TLS
if (do_tls)
  if (!tls_client_adjunct_start(&host, cctx, do_tls, errmsg))
    goto bad;
#endif

DEBUG(D_expand|D_lookup) debug_printf_indent("  connected to socket %s\n", sspec);
return OK;

bad:
  close(cctx->sock);
  return FAIL;
}

/* All use of allocations will be done against the POOL_SEARCH memory,
which is freed once by search_tidyup(). */

/*************************************************
*              Open entry point                  *
*************************************************/

/* See local README for interface description */
/* We just create a placeholder record with a closed socket, so
that connection cacheing at the framework layer works. */

static void *
readsock_open(const uschar * filename, uschar ** errmsg)
{
client_conn_ctx * cctx = store_get(sizeof(*cctx), GET_UNTAINTED);
cctx->sock = -1;
cctx->tls_ctx = NULL;
DEBUG(D_lookup) debug_printf_indent("readsock: allocated context\n");
return cctx;
}





/*************************************************
*         Find entry point for lsearch           *
*************************************************/

/* See local README for interface description */

static int
readsock_find(void * handle, const uschar * filename, const uschar * keystring,
  int length, uschar ** result, uschar ** errmsg, uint * do_cache,
  const uschar * opts)
{
client_conn_ctx * cctx = handle;
int sep = ',';
struct {
	BOOL do_shutdown:1;
	BOOL cache:1;
	uschar * do_tls;	/* NULL, empty-string, or SNI */
} lf = {.do_shutdown = TRUE};
uschar * eol = NULL;
int timeout = 5;
gstring * yield;
int ret = DEFER;

DEBUG(D_lookup)
  debug_printf_indent("readsock: file=\"%s\" key=\"%s\" len=%d opts=\"%s\"\n",
    filename, keystring, length, opts);

/* Parse options */

if (opts) for (uschar * s; s = string_nextinlist(&opts, &sep, NULL, 0); )
  if (Ustrncmp(s, "timeout=", 8) == 0)
    timeout = readconf_readtime(s + 8, 0, FALSE);
  else if (Ustrncmp(s, "shutdown=", 9) == 0)
    lf.do_shutdown = Ustrcmp(s + 9, "no") != 0;
#ifndef DISABLE_TLS
  else if (Ustrncmp(s, "tls=", 4) == 0 && Ustrcmp(s + 4, US"no") != 0 && !lf.do_tls)
    lf.do_tls = US"";
  else if (Ustrncmp(s, "sni=", 4) == 0)
    lf.do_tls = s + 4;
#endif
  else if (Ustrncmp(s, "eol=", 4) == 0)
    eol = string_unprinting(s + 4);
  else if (Ustrcmp(s, "cache=yes") == 0)
    lf.cache = TRUE;
  else if (Ustrcmp(s, "send=no") == 0)
    length = 0;

if (!filename) return FAIL;	/* Server spec is required */

/* Open the socket, if not cached */

if (cctx->sock == -1)
  if (internal_readsock_open(cctx, filename, timeout, lf.do_tls, errmsg) != OK)
    return ret;

testharness_pause_ms(100);	/* Allow sequencing of test actions */

/* Write the request string, if not empty or already done */

if (length)
  {
  if ((
#ifndef DISABLE_TLS
      cctx->tls_ctx ? tls_write(cctx->tls_ctx, keystring, length, FALSE) :
#endif
		      write(cctx->sock, keystring, length)) != length)
    {
    *errmsg = string_sprintf("request write to socket "
      "failed: %s", strerror(errno));
    goto out;
    }
  }

/* Shut down the sending side of the socket. This helps some servers to
recognise that it is their turn to do some work. Just in case some
system doesn't have this function, make it conditional. */

#ifdef SHUT_WR
if (!cctx->tls_ctx && lf.do_shutdown)
  shutdown(cctx->sock, SHUT_WR);
#endif

testharness_pause_ms(100);

/* Now we need to read from the socket, under a timeout. The function
that reads a file can be used.  If we're using a stdio buffered read,
and might need later write ops on the socket, the stdio must be in
writable mode or the underlying socket goes non-writable. */

sigalrm_seen = FALSE;
#ifdef DISABLE_TLS
if (TRUE)
#else
if (!cctx->tls_ctx)
#endif
  {
  FILE * fp = fdopen(cctx->sock, "rb");
  if (!fp)
    {
    log_write(0, LOG_MAIN|LOG_PANIC, "readsock fdopen: %s\n", strerror(errno));
    goto out;
    }
  ALARM(timeout);
  yield = cat_file(fp, NULL, eol);
  }
else
  {
  ALARM(timeout);
  yield = cat_file_tls(cctx->tls_ctx, NULL, eol);
  }

ALARM_CLR(0);

if (sigalrm_seen)
  { *errmsg = US "socket read timed out"; goto out; }

*result = yield ? string_from_gstring(yield) : US"";
ret = OK;
if (!lf.cache) *do_cache = 0;

out:

#ifndef DISABLE_TLS
if (cctx->tls_ctx) tls_close(cctx->tls_ctx, TLS_SHUTDOWN_NOWAIT);
#endif

(void) close(cctx->sock);
cctx->sock = -1;
return ret;
}



/*************************************************
*              Close entry point                 *
*************************************************/

/* See local README for interface description */

static void
readsock_close(void * handle)
{
client_conn_ctx * cctx = handle;
if (cctx->sock < 0) return;
#ifndef DISABLE_TLS
if (cctx->tls_ctx) tls_close(cctx->tls_ctx, TLS_SHUTDOWN_NOWAIT);
#endif
close(cctx->sock);
cctx->sock = -1;
}



static lookup_info readsock_lookup_info = {
  .name = US"readsock",			/* lookup name */
  .type = lookup_querystyle,
  .open = readsock_open,		/* open function */
  .check = NULL,
  .find = readsock_find,		/* find function */
  .close = readsock_close,
  .tidy = NULL,
  .quote = NULL,			/* no quoting function */
  .version_report = NULL
};


#ifdef DYNLOOKUP
#define readsock_lookup_module_info _lookup_module_info
#endif

static lookup_info *_lookup_list[] = { &readsock_lookup_info };
lookup_module_info readsock_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };

/* End of lookups/readsock.c */
