/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley

   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; version 2 dated June, 1991, or
   (at your option) version 3 dated 29 June, 2007.
 
   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/>.
*/

#include "dnsmasq.h"
#include "../log.h"

#ifdef HAVE_SCRIPT

/* This file has code to fork a helper process which receives data via a pipe 
   shared with the main process and which is responsible for calling a script when
   DHCP leases change.

   The helper process is forked before the main process drops root, so it retains root 
   privs to pass on to the script. For this reason it tries to be paranoid about 
   data received from the main process, in case that has been compromised. We don't
   want the helper to give an attacker root. In particular, the script to be run is
   not settable via the pipe, once the fork has taken place it is not alterable by the 
   main process.
*/

static void my_setenv(const char *name, const char *value, int *error);
static unsigned char *grab_extradata(unsigned char *buf, unsigned char *end,  char *env, int *err);

#ifdef HAVE_LUASCRIPT
#define LUA_COMPAT_ALL
#include <lua.h>  
#include <lualib.h>  
#include <lauxlib.h>  

#ifndef lua_open
#define lua_open()     luaL_newstate()
#endif

lua_State *lua;

static unsigned char *grab_extradata_lua(unsigned char *buf, unsigned char *end, char *field);
#endif


struct script_data
{
  int flags;
  int action, hwaddr_len, hwaddr_type;
  int clid_len, hostname_len, ed_len;
  struct in_addr addr, giaddr;
  unsigned int remaining_time;
#ifdef HAVE_BROKEN_RTC
  unsigned int length;
#else
  time_t expires;
#endif
#ifdef HAVE_TFTP
  off_t file_len;
#endif
  struct in6_addr addr6;
#ifdef HAVE_DHCP6
  int vendorclass_count;
  unsigned int iaid;
#endif
  unsigned char hwaddr[DHCP_CHADDR_MAX];
  char interface[IF_NAMESIZE];
};

static struct script_data *buf = NULL;
static size_t bytes_in_buf = 0, buf_size = 0;

int create_helper(int event_fd, int err_fd, uid_t uid, gid_t gid, long max_fd)
{
  pid_t pid;
  int i, pipefd[2];
  struct sigaction sigact;
  unsigned char *alloc_buff = NULL;
  
  /* create the pipe through which the main program sends us commands,
     then fork our process. */
  if (pipe(pipefd) == -1 || !fix_fd(pipefd[1]) || (pid = fork()) == -1)
    {
      send_event(err_fd, EVENT_PIPE_ERR, errno, NULL);
      _exit(0);
    }

  if (pid != 0)
    {
      close(pipefd[0]); /* close reader side */
      return pipefd[1];
    }

  /**** Pi-hole modification ****/
  logg("Started script helper");
  /******************************/

  /* ignore SIGTERM and SIGINT, so that we can clean up when the main process gets hit
     and SIGALRM so that we can use sleep() */
  sigact.sa_handler = SIG_IGN;
  sigact.sa_flags = 0;
  sigemptyset(&sigact.sa_mask);
  sigaction(SIGTERM, &sigact, NULL);
  sigaction(SIGALRM, &sigact, NULL);
  sigaction(SIGINT, &sigact, NULL);

  if (!option_bool(OPT_DEBUG) && uid != 0)
    {
      gid_t dummy;
      if (setgroups(0, &dummy) == -1 || 
	  setgid(gid) == -1 || 
	  setuid(uid) == -1)
	{
	  if (option_bool(OPT_NO_FORK))
	    /* send error to daemon process if no-fork */
	    send_event(event_fd, EVENT_USER_ERR, errno, daemon->scriptuser);
	  else
	    {
	      /* kill daemon */
	      send_event(event_fd, EVENT_DIE, 0, NULL);
	      /* return error */
	      send_event(err_fd, EVENT_USER_ERR, errno, daemon->scriptuser);
	    }
	  _exit(0);
	}
    }

  /* close all the sockets etc, we don't need them here. 
     Don't close err_fd, in case the lua-init fails.
     Note that we have to do this before lua init
     so we don't close any lua fds. */
  close_fds(max_fd, pipefd[0], event_fd, err_fd);
  
#ifdef HAVE_LUASCRIPT
  if (daemon->luascript)
    {
      const char *lua_err = NULL;
      lua = lua_open();
      luaL_openlibs(lua);
      
      /* get Lua to load our script file */
      if (luaL_dofile(lua, daemon->luascript) != 0)
	lua_err = lua_tostring(lua, -1);
      else
	{
	  lua_getglobal(lua, "lease");
	  if (lua_type(lua, -1) != LUA_TFUNCTION) 
	    lua_err = _("lease() function missing in Lua script");
	}
      
      if (lua_err)
	{
	  if (option_bool(OPT_NO_FORK) || option_bool(OPT_DEBUG))
	    /* send error to daemon process if no-fork */
	    send_event(event_fd, EVENT_LUA_ERR, 0, (char *)lua_err);
	  else
	    {
	      /* kill daemon */
	      send_event(event_fd, EVENT_DIE, 0, NULL);
	      /* return error */
	      send_event(err_fd, EVENT_LUA_ERR, 0, (char *)lua_err);
	    }
	  _exit(0);
	}
      
      lua_pop(lua, 1);  /* remove nil from stack */
      lua_getglobal(lua, "init");
      if (lua_type(lua, -1) == LUA_TFUNCTION)
	lua_call(lua, 0, 0);
      else
	lua_pop(lua, 1);  /* remove nil from stack */	
    }
#endif

  /* All init done, close our copy of the error pipe, so that main process can return */
  if (err_fd != -1)
    close(err_fd);
    
  /* loop here */
  while(1)
    {
      struct script_data data;
      char *p, *action_str, *hostname = NULL, *domain = NULL;
      unsigned char *buf = (unsigned char *)daemon->namebuff;
      unsigned char *end, *extradata;
      int is6, err = 0;
      int pipeout[2];

      /* Free rarely-allocated memory from previous iteration. */
      if (alloc_buff)
	{
	  free(alloc_buff);
	  alloc_buff = NULL;
	}
      
      /* we read zero bytes when pipe closed: this is our signal to exit */ 
      if (!read_write(pipefd[0], (unsigned char *)&data, sizeof(data), 1))
	{
#ifdef HAVE_LUASCRIPT
	  if (daemon->luascript)
	    {
	      lua_getglobal(lua, "shutdown");
	      if (lua_type(lua, -1) == LUA_TFUNCTION)
		lua_call(lua, 0, 0);
	    }
#endif
	  _exit(0);
	}
 
      is6 = !!(data.flags & (LEASE_TA | LEASE_NA));
      
      if (data.action == ACTION_DEL)
	action_str = "del";
      else if (data.action == ACTION_ADD)
	action_str = "add";
      else if (data.action == ACTION_OLD || data.action == ACTION_OLD_HOSTNAME)
	action_str = "old";
      else if (data.action == ACTION_TFTP)
	{
	  action_str = "tftp";
	  is6 = (data.flags != AF_INET);
	}
      else if (data.action == ACTION_ARP)
	{
	  action_str = "arp-add";
	  is6 = (data.flags != AF_INET);
	}
       else if (data.action == ACTION_ARP_DEL)
	{
	  action_str = "arp-del";
	  is6 = (data.flags != AF_INET);
	  data.action = ACTION_ARP;
	}
       else if (data.action == ACTION_RELAY_SNOOP)
	 {
	   is6 = 1;
	   action_str = "relay-snoop";
	 }
       else
	 continue;

      /************************** Pi-hole modification **************************/
      FTL_log_helper(1, action_str);
      /**************************************************************************/
      	
      /* stringify MAC into dhcp_buff */
      p = daemon->dhcp_buff;
      if (data.hwaddr_type != ARPHRD_ETHER || data.hwaddr_len == 0) 
	p += sprintf(p, "%.2x-", data.hwaddr_type);
      for (i = 0; (i < data.hwaddr_len) && (i < DHCP_CHADDR_MAX); i++)
	{
	  p += sprintf(p, "%.2x", data.hwaddr[i]);
	  if (i != data.hwaddr_len - 1)
	    p += sprintf(p, ":");
	}
      
      /* supplied data may just exceed normal buffer (unlikely) */
      if ((data.hostname_len + data.ed_len + data.clid_len) > MAXDNAME && 
	  !(alloc_buff = buf = malloc(data.hostname_len + data.ed_len + data.clid_len)))
	continue;
      
      if (!read_write(pipefd[0], buf, 
		      data.hostname_len + data.ed_len + data.clid_len, 1))
	continue;

      /* CLID into packet */
      for (p = daemon->packet, i = 0; i < data.clid_len; i++)
	{
	  p += sprintf(p, "%.2x", buf[i]);
	  if (i != data.clid_len - 1) 
	      p += sprintf(p, ":");
	}

#ifdef HAVE_DHCP6
      if (is6)
	{
	  /* or IAID and server DUID for IPv6 */
	  sprintf(daemon->dhcp_buff3, "%s%u", data.flags & LEASE_TA ? "T" : "", data.iaid);	
	  for (p = daemon->dhcp_packet.iov_base, i = 0; i < daemon->duid_len; i++)
	    {
	      p += sprintf(p, "%.2x", daemon->duid[i]);
	      if (i != daemon->duid_len - 1) 
		p += sprintf(p, ":");
	    }

	}
#endif

      buf += data.clid_len;

      if (data.hostname_len != 0)
	{
	  char *dot;
	  hostname = (char *)buf;
	  hostname[data.hostname_len - 1] = 0;
	  if (data.action != ACTION_TFTP && data.action != ACTION_RELAY_SNOOP)
	    {
	      if (!legal_hostname(hostname))
		hostname = NULL;
	      else if ((dot = strchr(hostname, '.')))
		{
		  domain = dot+1;
		  *dot = 0;
		} 
	    }
	}
    
      extradata = buf + data.hostname_len;
    
      if (!is6)
	inet_ntop(AF_INET, &data.addr, daemon->addrbuff, ADDRSTRLEN);
      else
	inet_ntop(AF_INET6, &data.addr6, daemon->addrbuff, ADDRSTRLEN);

#ifdef HAVE_TFTP
      /* file length */
      if (data.action == ACTION_TFTP)
	sprintf(is6 ? daemon->packet : daemon->dhcp_buff, "%lu", (unsigned long)data.file_len);
#endif

#ifdef HAVE_LUASCRIPT
      if (daemon->luascript)
	{
	  if (data.action == ACTION_TFTP)
	    {
	      lua_getglobal(lua, "tftp"); 
	      if (lua_type(lua, -1) != LUA_TFUNCTION)
		lua_pop(lua, 1); /* tftp function optional */
	      else
		{
		  lua_pushstring(lua, action_str); /* arg1 - action */
		  lua_newtable(lua);               /* arg2 - data table */
		  lua_pushstring(lua, daemon->addrbuff);
		  lua_setfield(lua, -2, "destination_address");
		  lua_pushstring(lua, hostname);
		  lua_setfield(lua, -2, "file_name"); 
		  lua_pushstring(lua, is6 ? daemon->packet : daemon->dhcp_buff);
		  lua_setfield(lua, -2, "file_size");
		  lua_call(lua, 2, 0);	/* pass 2 values, expect 0 */
		}
	    }
	  else if (data.action == ACTION_RELAY_SNOOP)
	    {
	      lua_getglobal(lua, "snoop"); 
	      if (lua_type(lua, -1) != LUA_TFUNCTION)
		lua_pop(lua, 1); /* tftp function optional */
	      else
		{
		  lua_pushstring(lua, action_str); /* arg1 - action */
		  lua_newtable(lua);               /* arg2 - data table */
		  lua_pushstring(lua, daemon->addrbuff);
		  lua_setfield(lua, -2, "client_address");
		  lua_pushstring(lua, hostname);
		  lua_setfield(lua, -2, "prefix"); 
		  lua_pushstring(lua, data.interface);
		  lua_setfield(lua, -2, "client_interface");
		  lua_call(lua, 2, 0);	/* pass 2 values, expect 0 */
		}
	    }
	  else if (data.action == ACTION_ARP)
	    {
	      lua_getglobal(lua, "arp"); 
	      if (lua_type(lua, -1) != LUA_TFUNCTION)
		lua_pop(lua, 1); /* arp function optional */
	      else
		{
		  lua_pushstring(lua, action_str); /* arg1 - action */
		  lua_newtable(lua);               /* arg2 - data table */
		  lua_pushstring(lua, daemon->addrbuff);
		  lua_setfield(lua, -2, "client_address");
		  lua_pushstring(lua, daemon->dhcp_buff);
		  lua_setfield(lua, -2, "mac_address");
		  lua_call(lua, 2, 0);	/* pass 2 values, expect 0 */
		}
	    }
	  else
	    {
	      lua_getglobal(lua, "lease");     /* function to call */
	      lua_pushstring(lua, action_str); /* arg1 - action */
	      lua_newtable(lua);               /* arg2 - data table */
	      
	      if (is6)
		{
		  lua_pushstring(lua, daemon->packet);
		  lua_setfield(lua, -2, "client_duid");
		  lua_pushstring(lua, daemon->dhcp_packet.iov_base);
		  lua_setfield(lua, -2, "server_duid");
		  lua_pushstring(lua, daemon->dhcp_buff3);
		  lua_setfield(lua, -2, "iaid");
		}
	      
	      if (!is6 && data.clid_len != 0)
		{
		  lua_pushstring(lua, daemon->packet);
		  lua_setfield(lua, -2, "client_id");
		}
	      
	      if (strlen(data.interface) != 0)
		{
		  lua_pushstring(lua, data.interface);
		  lua_setfield(lua, -2, "interface");
		}
	      
#ifdef HAVE_BROKEN_RTC	
	      lua_pushnumber(lua, data.length);
	      lua_setfield(lua, -2, "lease_length");
#else
	      lua_pushnumber(lua, data.expires);
	      lua_setfield(lua, -2, "lease_expires");
#endif
	      
	      if (hostname)
		{
		  lua_pushstring(lua, hostname);
		  lua_setfield(lua, -2, "hostname");
		}
	      
	      if (domain)
		{
		  lua_pushstring(lua, domain);
		  lua_setfield(lua, -2, "domain");
		}
	      
	      end = extradata + data.ed_len;
	      buf = extradata;

	      lua_pushnumber(lua, data.ed_len == 0 ? 1 : 0);
	      lua_setfield(lua, -2, "data_missing");
	      
	      if (!is6)
		buf = grab_extradata_lua(buf, end, "vendor_class");
#ifdef HAVE_DHCP6
	      else  if (data.vendorclass_count != 0)
		{
		  sprintf(daemon->dhcp_buff2, "vendor_class_id");
		  buf = grab_extradata_lua(buf, end, daemon->dhcp_buff2);
		  for (i = 0; i < data.vendorclass_count - 1; i++)
		    {
		      sprintf(daemon->dhcp_buff2, "vendor_class%i", i);
		      buf = grab_extradata_lua(buf, end, daemon->dhcp_buff2);
		    }
		}
#endif
	      
	      buf = grab_extradata_lua(buf, end, "supplied_hostname");
	      
	      if (!is6)
		{
		  buf = grab_extradata_lua(buf, end, "cpewan_oui");
		  buf = grab_extradata_lua(buf, end, "cpewan_serial");   
		  buf = grab_extradata_lua(buf, end, "cpewan_class");
		  buf = grab_extradata_lua(buf, end, "circuit_id");
		  buf = grab_extradata_lua(buf, end, "subscriber_id");
		  buf = grab_extradata_lua(buf, end, "remote_id");
		}

	      buf = grab_extradata_lua(buf, end, "requested_options");
	      buf = grab_extradata_lua(buf, end, "mud_url");
	      buf = grab_extradata_lua(buf, end, "tags");
	      
	      if (is6)
		buf = grab_extradata_lua(buf, end, "relay_address");
	      else if (data.giaddr.s_addr != 0)
		{
		  inet_ntop(AF_INET, &data.giaddr, daemon->dhcp_buff2, ADDRSTRLEN);
		  lua_pushstring(lua, daemon->dhcp_buff2);
		  lua_setfield(lua, -2, "relay_address");
		}
	      
	      for (i = 0; buf; i++)
		{
		  sprintf(daemon->dhcp_buff2, "user_class%i", i);
		  buf = grab_extradata_lua(buf, end, daemon->dhcp_buff2);
		}
	      
	      if (data.action != ACTION_DEL && data.remaining_time != 0)
		{
		  lua_pushnumber(lua, data.remaining_time);
		  lua_setfield(lua, -2, "time_remaining");
		}
	      
	      if (data.action == ACTION_OLD_HOSTNAME && hostname)
		{
		  lua_pushstring(lua, hostname);
		  lua_setfield(lua, -2, "old_hostname");
		}
	      
	      if (!is6 || data.hwaddr_len != 0)
		{
		  lua_pushstring(lua, daemon->dhcp_buff);
		  lua_setfield(lua, -2, "mac_address");
		}
	      
	      lua_pushstring(lua, daemon->addrbuff);
	      lua_setfield(lua, -2, "ip_address");
	    
	      lua_call(lua, 2, 0);	/* pass 2 values, expect 0 */
	    }
	}
#endif

      /* no script, just lua */
      if (!daemon->lease_change_command)
	continue;

      /* Pipe to capture stdout and stderr from script */
      if (!option_bool(OPT_DEBUG) && pipe(pipeout) == -1)
	continue;
      
      /* possible fork errors are all temporary resource problems */
      while ((pid = fork()) == -1 && (errno == EAGAIN || errno == ENOMEM))
	sleep(2);

      if (pid == -1)
        {
	  if (!option_bool(OPT_DEBUG))
	    {
	      close(pipeout[0]);
	      close(pipeout[1]);
	    }
	  continue;
        }
      
      /* wait for child to complete */
      if (pid != 0)
	{
	  if (!option_bool(OPT_DEBUG))
	    {
	      FILE *fp;
	  
	      close(pipeout[1]);
	      
	      /* Read lines sent to stdout/err by the script and pass them back to be logged */
	      if (!(fp = fdopen(pipeout[0], "r")))
		close(pipeout[0]);
	      else
		{
		  while (fgets(daemon->packet, daemon->packet_buff_sz, fp))
		    {
		      /* do not include new lines, log will append them */
		      size_t len = strlen(daemon->packet);
		      if (len > 0)
			{
			  --len;
			  if (daemon->packet[len] == '\n')
			    daemon->packet[len] = 0;
			}
		      send_event(event_fd, EVENT_SCRIPT_LOG, 0, daemon->packet);
		    }
		  fclose(fp);
		}
	    }
	  
	  /* reap our children's children, if necessary */
	  while (1)
	    {
	      int status;
	      pid_t rc = wait(&status);
	      
	      if (rc == pid)
		{
		  /* On error send event back to main process for logging */
		  if (WIFSIGNALED(status))
		    send_event(event_fd, EVENT_KILLED, WTERMSIG(status), NULL);
		  else if (WIFEXITED(status) && WEXITSTATUS(status) != 0)
		    send_event(event_fd, EVENT_EXITED, WEXITSTATUS(status), NULL);
		  break;
		}
	      
	      if (rc == -1 && errno != EINTR)
		break;
	    }
	  
	  continue;
	}

      if (!option_bool(OPT_DEBUG))
	{
	  /* map stdout/stderr of script to pipeout */
	  close(pipeout[0]);
	  dup2(pipeout[1], STDOUT_FILENO);
	  dup2(pipeout[1], STDERR_FILENO);
	  close(pipeout[1]);
	}
      
      if (data.action != ACTION_TFTP && data.action != ACTION_ARP && data.action != ACTION_RELAY_SNOOP)
	{
#ifdef HAVE_DHCP6
	  my_setenv("DNSMASQ_IAID", is6 ? daemon->dhcp_buff3 : NULL, &err);
	  my_setenv("DNSMASQ_SERVER_DUID", is6 ? daemon->dhcp_packet.iov_base : NULL, &err); 
	  my_setenv("DNSMASQ_MAC", is6 && data.hwaddr_len != 0 ? daemon->dhcp_buff : NULL, &err);
#endif
	  
	  my_setenv("DNSMASQ_CLIENT_ID", !is6 && data.clid_len != 0 ? daemon->packet : NULL, &err);
	  my_setenv("DNSMASQ_INTERFACE", strlen(data.interface) != 0 ? data.interface : NULL, &err);
	  
#ifdef HAVE_BROKEN_RTC
	  sprintf(daemon->dhcp_buff2, "%u", data.length);
	  my_setenv("DNSMASQ_LEASE_LENGTH", daemon->dhcp_buff2, &err);
#else
	  sprintf(daemon->dhcp_buff2, "%lu", (unsigned long)data.expires);
	  my_setenv("DNSMASQ_LEASE_EXPIRES", daemon->dhcp_buff2, &err); 
#endif
	  
	  my_setenv("DNSMASQ_DOMAIN", domain, &err);
	  
	  end = extradata + data.ed_len;
	  buf = extradata;

	  if (data.ed_len == 0)
	    my_setenv("DNSMASQ_DATA_MISSING", "1", &err);
	  
	  if (!is6)
	    buf = grab_extradata(buf, end, "DNSMASQ_VENDOR_CLASS", &err);
#ifdef HAVE_DHCP6
	  else
	    {
	      if (data.vendorclass_count != 0)
		{
		  buf = grab_extradata(buf, end, "DNSMASQ_VENDOR_CLASS_ID", &err);
		  for (i = 0; i < data.vendorclass_count - 1; i++)
		    {
		      sprintf(daemon->dhcp_buff2, "DNSMASQ_VENDOR_CLASS%i", i);
		      buf = grab_extradata(buf, end, daemon->dhcp_buff2, &err);
		    }
		}
	    }
#endif
	  
	  buf = grab_extradata(buf, end, "DNSMASQ_SUPPLIED_HOSTNAME", &err);
	  
	  if (!is6)
	    {
	      buf = grab_extradata(buf, end, "DNSMASQ_CPEWAN_OUI", &err);
	      buf = grab_extradata(buf, end, "DNSMASQ_CPEWAN_SERIAL", &err);   
	      buf = grab_extradata(buf, end, "DNSMASQ_CPEWAN_CLASS", &err);
	      buf = grab_extradata(buf, end, "DNSMASQ_CIRCUIT_ID", &err);
	      buf = grab_extradata(buf, end, "DNSMASQ_SUBSCRIBER_ID", &err);
	      buf = grab_extradata(buf, end, "DNSMASQ_REMOTE_ID", &err);
	    }
	  
	  buf = grab_extradata(buf, end, "DNSMASQ_REQUESTED_OPTIONS", &err);
	  buf = grab_extradata(buf, end, "DNSMASQ_MUD_URL", &err);
	  buf = grab_extradata(buf, end, "DNSMASQ_TAGS", &err);
	  	  
	  if (is6)
	    buf = grab_extradata(buf, end, "DNSMASQ_RELAY_ADDRESS", &err);
	  else
	    {
	      const char *giaddr = NULL;
	      if (data.giaddr.s_addr != 0)
		  giaddr = inet_ntop(AF_INET, &data.giaddr, daemon->dhcp_buff2, ADDRSTRLEN);
	      my_setenv("DNSMASQ_RELAY_ADDRESS", giaddr, &err);
	    }
	  
	  for (i = 0; buf; i++)
	    {
	      sprintf(daemon->dhcp_buff2, "DNSMASQ_USER_CLASS%i", i);
	      buf = grab_extradata(buf, end, daemon->dhcp_buff2, &err);
	    }
	  
	  sprintf(daemon->dhcp_buff2, "%u", data.remaining_time);
	  my_setenv("DNSMASQ_TIME_REMAINING", data.action != ACTION_DEL && data.remaining_time != 0 ? daemon->dhcp_buff2 : NULL, &err);
	  
	  my_setenv("DNSMASQ_OLD_HOSTNAME", data.action == ACTION_OLD_HOSTNAME ? hostname : NULL, &err);
	  if (data.action == ACTION_OLD_HOSTNAME)
	    hostname = NULL;
	  
	  my_setenv("DNSMASQ_LOG_DHCP", option_bool(OPT_LOG_OPTS) ? "1" : NULL, &err);
	}
      
      /* we need to have the event_fd around if exec fails */
      if ((i = fcntl(event_fd, F_GETFD)) != -1)
	fcntl(event_fd, F_SETFD, i | FD_CLOEXEC);
      close(pipefd[0]);

      if (data.action == ACTION_RELAY_SNOOP)
	strcpy(daemon->packet, data.interface);
      
      /**************************** Pi-hole modification ****************************/
      FTL_log_helper(5, daemon->lease_change_command, action_str,
		     (is6 && data.action != ACTION_ARP) ? daemon->packet : daemon->dhcp_buff,
		     daemon->addrbuff, hostname);
      /******************************************************************************/

      p =  strrchr(daemon->lease_change_command, '/');
      if (err == 0)
	{
	  execl(daemon->lease_change_command, 
		p ? p+1 : daemon->lease_change_command, action_str, 
		(is6 && data.action != ACTION_ARP) ? daemon->packet : daemon->dhcp_buff, 
		daemon->addrbuff, hostname, (char*)NULL);
	  err = errno;
	}
      /* failed, send event so the main process logs the problem */
      /**************************** Pi-hole modification ****************************/
      FTL_log_helper(2, daemon->lease_change_command, strerror(err));
      /******************************************************************************/
      send_event(event_fd, EVENT_EXEC_ERR, err, NULL);
      _exit(0); 
    }
}

static void my_setenv(const char *name, const char *value, int *error)
{
  if (*error == 0)
    {
      if (!value)
	unsetenv(name);
      else if (setenv(name, value, 1) != 0)
	*error = errno;
    }
}
 
static unsigned char *grab_extradata(unsigned char *buf, unsigned char *end,  char *env, int *err)
{
  unsigned char *next = NULL;
  char *val = NULL;

  if (buf && (buf != end))
    {
      for (next = buf; ; next++)
	if (next == end)
	  {
	    next = NULL;
	    break;
	  }
	else if (*next == 0)
	  break;

      if (next && (next != buf))
	{
	  char *p;
	  /* No "=" in value */
	  if ((p = strchr((char *)buf, '=')))
	    *p = 0;
	  val = (char *)buf;
	}
    }
  
  my_setenv(env, val, err);
   
  return next ? next + 1 : NULL;
}

#ifdef HAVE_LUASCRIPT
static unsigned char *grab_extradata_lua(unsigned char *buf, unsigned char *end, char *field)
{
  unsigned char *next;

  if (!buf || (buf == end))
    return NULL;

  for (next = buf; *next != 0; next++)
    if (next == end)
      return NULL;
  
  if (next != buf)
    {
      lua_pushstring(lua,  (char *)buf);
      lua_setfield(lua, -2, field);
    }

  return next + 1;
}
#endif

static void buff_alloc(size_t size)
{
  if (size > buf_size)
    {
      struct script_data *new;
      
      /* start with reasonable size, will almost never need extending. */
      if (size < sizeof(struct script_data) + 200)
	size = sizeof(struct script_data) + 200;

      if (!(new = whine_malloc(size)))
	return;
      if (buf)
	free(buf);
      buf = new;
      buf_size = size;
    }
}

/* pack up lease data into a buffer */    
void queue_script(int action, struct dhcp_lease *lease, char *hostname, time_t now)
{
  unsigned char *p;
  unsigned int hostname_len = 0, clid_len = 0, ed_len = 0;
  int fd = daemon->dhcpfd;
#ifdef HAVE_DHCP6 
  if (!daemon->dhcp)
    fd = daemon->dhcp6fd;
#endif

  /* no script */
  if (daemon->helperfd == -1)
    return;

  if (lease->extradata)
    ed_len = lease->extradata_len;
  if (lease->clid)
    clid_len = lease->clid_len;
  if (hostname)
    hostname_len = strlen(hostname) + 1;

  buff_alloc(sizeof(struct script_data) +  clid_len + ed_len + hostname_len);

  buf->action = action;
  buf->flags = lease->flags;
#ifdef HAVE_DHCP6 
  buf->vendorclass_count = lease->vendorclass_count;
  buf->addr6 = lease->addr6;
  buf->iaid = lease->iaid;
#endif
  buf->hwaddr_len = lease->hwaddr_len;
  buf->hwaddr_type = lease->hwaddr_type;
  buf->clid_len = clid_len;
  buf->ed_len = ed_len;
  buf->hostname_len = hostname_len;
  buf->addr = lease->addr;
  buf->giaddr = lease->giaddr;
  memcpy(buf->hwaddr, lease->hwaddr, DHCP_CHADDR_MAX);
  if (!indextoname(fd, lease->last_interface, buf->interface))
    buf->interface[0] = 0;
  
#ifdef HAVE_BROKEN_RTC 
  buf->length = lease->length;
#else
  buf->expires = lease->expires;
#endif

  if (lease->expires != 0)
    buf->remaining_time = (unsigned int)difftime(lease->expires, now);
  else
    buf->remaining_time = 0;

  p = (unsigned char *)(buf+1);
  if (clid_len != 0)
    {
      memcpy(p, lease->clid, clid_len);
      p += clid_len;
    }
  if (hostname_len != 0)
    {
      memcpy(p, hostname, hostname_len);
      p += hostname_len;
    }
  if (ed_len != 0)
    {
      memcpy(p, lease->extradata, ed_len);
      p += ed_len;
    }
  bytes_in_buf = p - (unsigned char *)buf;
}

#ifdef HAVE_DHCP6
void queue_relay_snoop(struct in6_addr *client, int if_index, struct in6_addr *prefix, int prefix_len)
{
  /* no script */
  if (daemon->helperfd == -1)
    return;
  
  inet_ntop(AF_INET6, prefix, daemon->addrbuff, ADDRSTRLEN);

  /* 5 for /nnn and zero on the end of the prefix. */
  buff_alloc(sizeof(struct script_data) + ADDRSTRLEN + 5);
  memset(buf, 0, sizeof(struct script_data));

  buf->action = ACTION_RELAY_SNOOP;
  buf->addr6 = *client;
  buf->hostname_len = sprintf((char *)(buf+1), "%s/%u", daemon->addrbuff, prefix_len) + 1;
  
  indextoname(daemon->dhcp6fd, if_index, buf->interface);

  bytes_in_buf = sizeof(struct script_data) + buf->hostname_len;
}
#endif

#ifdef HAVE_TFTP
/* This nastily re-uses DHCP-fields for TFTP stuff */
void queue_tftp(off_t file_len, char *filename, union mysockaddr *peer)
{
  unsigned int filename_len;

  /* no script */
  if (daemon->helperfd == -1)
    return;
  
  filename_len = strlen(filename) + 1;
  buff_alloc(sizeof(struct script_data) +  filename_len);
  memset(buf, 0, sizeof(struct script_data));

  buf->action = ACTION_TFTP;
  buf->hostname_len = filename_len;
  buf->file_len = file_len;

  if ((buf->flags = peer->sa.sa_family) == AF_INET)
    buf->addr = peer->in.sin_addr;
  else
    buf->addr6 = peer->in6.sin6_addr;

  memcpy((unsigned char *)(buf+1), filename, filename_len);
  
  bytes_in_buf = sizeof(struct script_data) +  filename_len;
}
#endif

void queue_arp(int action, unsigned char *mac, int maclen, int family, union all_addr *addr)
{
  /* no script */
  if (daemon->helperfd == -1)
    return;
  
  buff_alloc(sizeof(struct script_data));
  memset(buf, 0, sizeof(struct script_data));

  buf->action = action;
  buf->hwaddr_len = maclen;
  buf->hwaddr_type =  ARPHRD_ETHER; 
  if ((buf->flags = family) == AF_INET)
    buf->addr = addr->addr4;
  else
    buf->addr6 = addr->addr6;
  
  memcpy(buf->hwaddr, mac, maclen);
  
  bytes_in_buf = sizeof(struct script_data);
}

int helper_buf_empty(void)
{
  return bytes_in_buf == 0;
}

void helper_write(void)
{
  ssize_t rc;

  if (bytes_in_buf == 0)
    return;
  
  if ((rc = write(daemon->helperfd, buf, bytes_in_buf)) != -1)
    {
      if (bytes_in_buf != (size_t)rc)
	memmove(buf, buf + rc, bytes_in_buf - rc); 
      bytes_in_buf -= rc;
    }
  else
    {
      if (errno == EAGAIN || errno == EINTR)
	return;
      bytes_in_buf = 0;
    }
}

#endif /* HAVE_SCRIPT */
