/* $Id: avif.C,v 1.28 2006/02/16 03:15:34 dm Exp $ */

/*
 *
 * Copyright (C) 2003 David Mazieres (dm@uun.org)
 *
 * 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, 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 *
 */


#include "asmtpd.h"
#include "rawnet.h"

ihash<const uid_t, avcount, &avcount::uid, &avcount::link> avctab;

avcount::avcount (uid_t u)
  : uid (u), num (0), release_lock (false)
{
  avctab.insert (this);
}

avcount::~avcount ()
{
  avctab.remove (this);
}

void
avcount::release ()
{
  assert (num > 0);
  num--;
  if (release_lock)
    return;
  release_lock = true;

  while (num < int (opt->avenger_max_per_user) && !waiters.empty ())
    (*waiters.pop_front ()) ();

  if (num)
    release_lock = false;
  else
    delete this;
}

bool
avcount::acquire ()
{
  if (num >= int (opt->avenger_max_per_user))
    return false;
  num++;
  return true;
}

avcount *
avcount::get (uid_t u)
{
  if (avcount *avc = avctab[u])
    return avc;
  return New avcount (u);
}

avif::avif (const smtpd *s, str n, avcount *avc, pid_t p, int fd, cb_t c)
  : cb (c), smtp (s), pid (p), aio (aios::alloc (fd)), name (n), avc (avc)
{
  if (opt->debug_avenger)
    aio->setdebug (strbuf ("%s (a%d)", name.cstr (), fd));
}

void
avif::init ()
{
  chldcb (pid, wrap (this, &avif::reap));
  aio->readline (wrap (this, &avif::input));
}

avif::~avif ()
{
  aio->readcancel ();
  aio->abort ();
  if (pid > 0) {
    chldcb (pid, NULL);
    if (killpg (pid, SIGKILL) < 0)
      kill (pid, SIGKILL);
  }
  if (avc)
    avc->release ();
  while (result *rp = reslist.first)
    delres (rp);
}

void
avif::input (str line, int err)
{
  if (!line || strlen (line) != line.len ()) {
    (*cb) (NEXT, NULL);		// By default accept mail
    delete this;
    return;
  }

  static rxx spfrx ("^spf([01])?\\s+(\\w+)\\s+(.*)$");
  static rxx dnsarx ("^dns-a\\s+(\\w+)\\s+(\\S+)$");
  static rxx dnsptrrx ("^dns-ptr\\s+(\\w+)\\s+(\\S+)$");
  static rxx dnsmxrx ("^dns-mx\\s+(\\w+)\\s+(\\S+)$");
  static rxx dnstxtrx ("^dns-txt\\s+(\\w+)\\s+(\\S+)$");
  static rxx netpathrx ("^netpath\\s+(\\w+)\\s+(\\S+)(\\s+(-?\\d+))?$");
  static rxx retrx ("^return\\s+(([245]\\d\\d)([ -]).*)$");
  static rxx retcontrx ("^(([245]\\d\\d)([ -]).*)$");
  static rxx redirrx ("^redirect\\s+(\\S+)$");
  static rxx bodytestrx ("^bodytest\\s+(\\S.*)$");

  if (retcode) {
    if (!retcontrx.match (line) || retcode != retcontrx[2]) {
      badinput (line);
      return;
    }
    retbuf << retcontrx[1] << "\r\n";
    if (retcontrx[3] != "-") {
      (*cb) (DONE, retbuf);
      delete this;
      return;
    }
  }
  else if (!line.len ())
    ;
  else if (line[0] == '.')
    newres ()->res = line << "\n";
  else if (spfrx.match (line)) {
    str from = smtp->get_from ();
    if (!from || !from.len ())
      from = smtp->get_helo ();
    spf_t *spf = New spf_t (smtp->get_addr (), from);
    spf->spfrec = spfrx[3];
    spf->helo = smtp->get_helo ();
    spf->ptr_cache = smtp->ptr_cache;
    result *rp = newres ();
    spf->cb = wrap (this, &avif::spf_cb, spfrx[2], rp,
		    spfrx[1] && spfrx[1][0] == '1');
    rp->abortcb = wrap (spf_cancel, spf);
    spf->init ();
  }
  else if (dnsarx.match (line)) {
    result *rp = newres ();
    if (dnsreq *rqp
	= dns_hostbyname (dnsarx[2],
			  wrap (this, &avif::dns_a_cb, dnsarx[1], rp),
			  false, false))
      rp->abortcb = wrap (dnsreq_cancel, rqp);
  }
  else if (dnsptrrx.match (line)) {
    str var = dnsptrrx[1];
    str name = dnsptrrx[2];
    result *rp = newres ();
    in_addr a;
    int r = inet_aton (name, &a);
    if (r == 0)
      rp->res = var << "=\n";
    else if (r < 0)
      rp->res = "";
    else if (dnsreq *rqp
	     = dns_hostbyaddr (a, wrap (this, &avif::dns_ptr_cb, var, rp)))
      rp->abortcb = wrap (dnsreq_cancel, rqp);
  }
  else if (dnsmxrx.match (line)) {
    result *rp = newres ();
    if (dnsreq *rqp
	= dns_mxbyname (dnsmxrx[2],
			wrap (this, &avif::dns_mx_cb, dnsmxrx[1], rp),
			false))
      rp->abortcb = wrap (dnsreq_cancel, rqp);
  }
  else if (dnstxtrx.match (line)) {
    result *rp = newres ();
    if (dnsreq *rqp
	= dns_txtbyname (dnstxtrx[2],
			wrap (this, &avif::dns_txt_cb, dnstxtrx[1], rp),
			false))
      rp->abortcb = wrap (dnsreq_cancel, rqp);
  }
  else if (netpathrx.match (line)) {
    result *rp = newres ();
    int hops = 0;
    if (str h = netpathrx[4])
      convertint (h, &hops);
    if (dnsreq *rqp = dns_hostbyname (netpathrx[2],
				     wrap (this, &avif::netpath_cb1,
					   netpathrx[1], hops, rp),
				     true, true))
      rp->abortcb = wrap (dnsreq_cancel, rqp);
  }
  else if (retrx.match (line)) {
    str code (retrx[2]);
    if (retcode && retcode != code) {
      badinput (line);
      return;
    }
    retcode = code;
    retbuf << retrx[1] << "\r\n";
    if (retrx[3] != "-") {
      (*cb) (DONE, retbuf);
      delete this;
      return;
    }
  }
  else if (redirrx.match (line)) {
    (*cb) (REDIR, redirrx[1]);
    delete this;
    return;
  }
  else if (bodytestrx.match (line)) {
    (*cb) (BODY, bodytestrx[1]);
    delete this;
    return;
  }
  else {
    badinput (line);
    return;
  }

  aio->readline (wrap (this, &avif::input));
  maybe_reply ();
}

str
safestring (str msg)
{
  /* Who knows what a bad user might accomplish by sending weird
   * control characters... */
  strbuf sb;
  for (u_int i = 0; i < msg.len (); i++) {
    u_char c = msg[i];
    if (c == 0x7f)
      sb.tosuio ()->copy ("^?", 2);
    else if (c >= ' ')
      sb.tosuio ()->copy (&c, 1);
    else {
      sb.tosuio ()->copy ("^", 1);
      c = c + '@';
      sb.tosuio ()->copy (&c, 1);
    }
  }
  return sb;
}
void
avif::badinput (str line)
{
  warn ("bad input from %s's %s: ", name.cstr (), AVENGER)
    << safestring (line) << "\n";
  (*cb) (NEXT, NULL);
  delete this;
  return;
}

void
avif::spf_cb (str var, result *rp, bool one, spf_t *spf)
{
  if (one)
    rp->res = var << "=" << spf1_print (spf->result) << "\n";
  else
    rp->res = var << "=" << spf_print (spf->result) << "\n";
  maybe_reply ();
}

void
avif::dns_a_cb (str var, result *rp, ptr<hostent> h, int err)
{
  strbuf sb;
  if (h) {
    char **ap = h->h_addr_list;
    sb << var << "=" << str (inet_ntoa (*(in_addr *) *ap));
    while (*++ap)
      sb << " " << str (inet_ntoa (*(in_addr *) *ap));
    sb << "\n";
  }
  else if (!dns_tmperr (err))
    sb << var << "=\n";

  rp->res = sb;
  maybe_reply ();
}

void
avif::dns_ptr_cb (str var, result *rp, ptr<hostent> h, int err)
{
  strbuf sb;
  if (h) {
    sb << var << "=" << h->h_name;
    for (char **np = h->h_aliases; *np; np++)
      sb << " " << *np;
    sb << "\n";
  }
  else if (!dns_tmperr (err))
    sb << var << "=\n";

  rp->res = sb;
  maybe_reply ();
}

void
avif::dns_mx_cb (str var, result *rp, ptr<mxlist> mxl, int err)
{
  strbuf sb;
  if (mxl) {
    sb << var << "=";
    sb << int (mxl->m_mxes[0].pref) << ":" << mxl->m_mxes[0].name;
    for (u_int i = 1; i < mxl->m_nmx; i++)
      sb << " " << int (mxl->m_mxes[i].pref) << ":" << mxl->m_mxes[i].name;
    sb << "\n";
  }
  else if (!dns_tmperr (err))
    sb << var << "=\n";

  rp->res = sb;
  maybe_reply ();
}

void
avif::dns_txt_cb (str var, result *rp, ptr<txtlist> t, int err)
{
  strbuf sb;
  if (t) {
    /* XXX - This is bad if there are multiple TXT records.  moreover,
     * if a TXT record contains a newline, this will cause maybe_reply
     * to fail, and thus the variable will not get set. */
    sb << var << "=" << t->t_txts[0] << "\n";
  }
  else if (!dns_tmperr (err))
    sb << var << "=\n";
  rp->res = sb;
  maybe_reply ();
}

void
avif::netpath_cb1 (str var, int hops, result *rp, ptr<hostent> h, int err)
{
  rp->abortcb = NULL;		// not needed
  if (h) {
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons (0);
    sin.sin_addr = *(in_addr *) h->h_addr;
    if (traceroute *trp = netpath (&sin, hops,
				   wrap (this, &avif::netpath_cb2, var, rp)))
      rp->abortcb = wrap (netpath_cancel, trp);
  }
  else {
    rp->res = "";
    maybe_reply ();
  }
}
void
avif::netpath_cb2 (str var, result *rp, int nhops, in_addr *av, int an)
{
  strbuf sb;
  if (an > 0) {
    sb << var << "=" << nhops;
    for (int i = 0; i < an; i++)
      sb << " " << inet_ntoa (av[i]);
    sb << "\n";
  }
  rp->res = sb;
  maybe_reply ();
}

void
avif::maybe_reply ()
{
  result *rp;
  while ((rp = reslist.first) && rp->res) {
    /* Sheer paranoia--what if some weird caracters come back in DNS
     * requests (or something) and somehow don't get filtered by the
     * resolver. */
    if (strchr (rp->res, '\n') == rp->res.cstr () + rp->res.len () - 1
	&& !memchr (rp->res, '\0', rp->res.len ()))
      aio << rp->res;
    else if (rp->res.len ())
      warn << "user " << name << " newline should be at end of variable\n";
    delres (rp);
  }
}

void
avif::chldinit (struct passwd *pw, int fd, bool sys, str ext)
{
  if (opt->avenger_timeout)
    alarm (opt->avenger_timeout);

  bool root = getuid () <= 0;
  str avdir;
  if (sys)
    avdir = pw->pw_dir;
  else
    avdir = strbuf () << pw->pw_dir << "/.avenger";

#ifdef HAVE_SETEUID
  if (!sys) {
    /* quick optimization because setgroups is expensive */
    GETGROUPS_T gid = pw->pw_gid;
    setgid (gid);
    if (root)
      seteuid (pw->pw_uid);
    struct stat sb;
    if (!sys && lstat (avdir, &sb)) {
      if (smtpd::tmperr (errno)) {
	aout << "return 451 " << avdir << ": " << strerror (errno) << "\n";
	aout->flush ();
      }
      _exit (0);
    }
    if (root)
      seteuid (getuid ());
    if (!S_ISDIR (sb.st_mode) || (sb.st_uid && sb.st_uid != pw->pw_uid)) {
      warn << avdir << " should be directory owned by " << pw->pw_name << "\n";
      _exit (0);
    }
  }
#endif /* HAVE_SETEUID */

  if (sys)
    setgroups (opt->av_groups.size (), opt->av_groups.base ());
  become_user (pw, !sys);

  if (chdir (avdir) < 0) {
    maybe_warn (strbuf ("%s: %m\n", avdir.cstr ()));
    if (smtpd::tmperr (errno)) {
      aout << "return 451 " << avdir << ": " << strerror (errno) << "\n";
      aout->flush ();
      _exit (0);
    }
    if (!sys)
      _exit (0);
  }
}

void
avif::alloc (struct passwd *pw, const smtpd *s, str recip, char mode,
	     avcount *avc, str ext, str avuser, cb_t cb, str extraenv)
{
  if (mode == 's') {
    str path = strbuf () << opt->etcdir << "/" << ext;
    if (access (path, 0) < 0 && errno == ENOENT) {
      (*cb) (NEXT, NULL);
      return;
    }
  }

  int fds[2];
  if (socketpair (AF_UNIX, SOCK_STREAM, 0, fds) < 0) {
    (*cb) (DONE, strbuf ("451 %m\r\n"));
    return;
  }

  close_on_exec (fds[0]);
  if (fds[1] > 1)
    close_on_exec (fds[1]);

  const char *av[] = { path_avenger, NULL, NULL, NULL };
  str modestr;
  if (mode) {
    av[1] = modestr = strbuf ("-%c", mode);
    av[2] = ext;
  }
  else
    av[1] = ext;

  vec<str> senv;
  s->envinit (&senv, pw);

  if (!strncmp (senv[0], "PWD=", 4))
    senv.pop_front ();
  if (mode == 's')
    senv.push_back (strbuf ("PWD=%s", pw->pw_dir));
  else
    senv.push_back (strbuf ("PWD=%s/.avenger", pw->pw_dir));
  senv.push_back (strbuf ("RECIPIENT=") << recip);
  senv.push_back (strbuf ("RECIPIENT_HOST=")
		  << mytolower (extract_domain (recip)));
  senv.push_back (strbuf ("RECIPIENT_LOCAL=")
		  << mytolower (extract_local (recip)));
  if (ext && mode != 's')
    senv.push_back (strbuf ("EXT=") << ext);
  if (extraenv)
    senv.push_back (extraenv);
  if (avuser)
    senv.push_back (strbuf () << "AVUSER=" << avuser);

  vec<const char *> env;
  env.reserve (senv.size () + 1);
  for (const str *sp = senv.base (); sp < senv.lim (); sp++)
    env.push_back (sp->cstr ());
  env.push_back (NULL);

  pid_t pid = aspawn (path_avenger, av, fds[1], fds[1], errfd,
		      wrap (&chldinit, pw, fds[1], mode == 's', ext),
		      const_cast<char **> (env.base ()));
  if (pid <= 0) {
    (*cb) (DONE, strbuf ("451 %m\r\n"));
    return;
  }

  str name;
  if (mode == 's' && ext)
    name = strbuf ("%c", opt->separator ? opt->separator : ' ') << ext;
  else if (ext)
    name = strbuf ("%s%c", pw->pw_name, opt->separator) << ext;
  else
    name = pw->pw_name;

  close (fds[1]);
  (New avif (s, name, avc, pid, fds[0], cb))->init ();
}

void
avif::reap (int status)
{
  pid = -1;
  if (status)
    warn << AVENGER " for " << name << " exited with "
	 << exitstr (status) << "\n";
}


syntax highlighted by Code2HTML, v. 0.9.1