/*
 *   mx_client_verify() -- subroutine for ZMailer smtpserver
 *
 *   By Matti Aarnio <mea@nic.funet.fi> 1997-1999,2002-2003
 */

#include "smtpserver.h"
#include "zresolv.h"

extern int use_ipv6;

static int dnsmxlookup __((const char*, int, int, int));

extern int debug;
static char * txt_buf = NULL;

static int
dnsmxlookup(host, depth, mxmode, qtype)
	const char *host;
	int depth;
	int mxmode;
	int qtype;
{
	HEADER *hp;
	msgdata *eom, *cp;
	int qlen, n, i, qdcount, ancount, nscount, arcount, maxpref, class;
	u_short type;
	int saw_cname = 0, had_mx_record = 0;
	int ttl;
	struct addrinfo req, *ai;
#define MAXMX 128
	char *mx[MAXMX];
	int   mxtype[MAXMX];
	int mxcount;
	querybuf qbuf, answer;
	msgdata buf[8192], realname[8192];

	if (depth == 0)
	  h_errno = 0;

	if (depth > 3)
	  return -EX_NOHOST;

	if (debug) {
	  printf("000- dnsmxlookup('%s', depth=%d mxmode=%d qtype=%s)\n",
		 host, depth, mxmode,
		 ((qtype == T_TXT) ? "T_TXT" :
		  ((qtype == T_MX) ? "T_MX" : "other")));
	}

	qlen = res_mkquery(QUERY, host, C_IN, qtype, NULL, 0, NULL,
			   (void*)&qbuf, sizeof qbuf);
	if (qlen < 0) {
	  if (debug)
	    printf("000- res_mkquery failed\n");
	  return -EX_SOFTWARE;
	}
	n = res_send((void*)&qbuf, qlen, (void*)&answer, sizeof answer);
	if (n < 0) {
	  return -EX_TEMPFAIL;
	}

	eom = (msgdata *)&answer + n;
	/*
	 * find first satisfactory answer
	 */
	hp = (HEADER *) &answer;
	qdcount = ntohs(hp->qdcount);
	ancount = ntohs(hp->ancount);
	nscount = ntohs(hp->nscount);
	arcount = ntohs(hp->arcount);

	if (debug)
	  printf("000-  len=%d rcode=%d qdcount=%d ancount=%d nscount=%d arcount=%d TC=%d\n",
		 n, hp->rcode, qdcount, ancount, nscount, arcount, hp->tc);

	if (hp->rcode != NOERROR || ancount == 0) {
	  switch (hp->rcode) {
	  case NXDOMAIN:
	    /* Non-authoritative iff response from cache.
	     * Old BINDs used to return non-auth NXDOMAINs
	     * due to a bug; if that is the case by you,
	     * change to return EX_TEMPFAIL iff hp->aa == 0.
	     */
	    return -EX_NOHOST;
	  case SERVFAIL:
	    return -EX_TEMPFAIL;
	  case NOERROR:
	    goto perhaps_address_record;

	  case FORMERR:
	  case NOTIMP:
	  case REFUSED:
	    return -EX_NOPERM;
	  }
	  return -EX_UNAVAILABLE;
	}

	cp = (msgdata *)&answer + sizeof(HEADER);
	for (; qdcount > 0; --qdcount) {
#if	defined(BIND_VER) && (BIND_VER >= 473)
	  cp += dn_skipname(cp, eom) + QFIXEDSZ;
#else	/* !defined(BIND_VER) || (BIND_VER < 473) */
	  cp += dn_skip(cp) + QFIXEDSZ;
#endif	/* defined(BIND_VER) && (BIND_VER >= 473) */
	}
	realname[0] = '\0';
	maxpref = 70000;
	mxcount = 0;
	while (--ancount >= 0 && cp < eom) {
	  n = dn_expand((msgdata *)&answer, eom, cp, (void*)buf, sizeof buf);
	  if (n < 0)
	    break;
	  cp += n;

	  NS_GET16(type,  cp); /* type  */
	  NS_GET16(class, cp); /* class */
	  NS_GET32(ttl,   cp); /* ttl   */
	  NS_GET16(n,     cp); /* dlen  */

	  if (cp + n > eom) {
	    /* BAD data.. */
	    break;
	  }

	  if (type == T_CNAME) {
	    cp += dn_expand((msgdata *)&answer, eom, cp,
			    (void*)realname, sizeof realname);
	    saw_cname = 1;
	    continue;
	  }

	  if (type != qtype)  {
	    /* Not looked for .. */
	    cp += n;
	    continue;
	  }

	  if (type == T_MX) {
	    cp += 2; /* MX preference value */
	    n = dn_expand((msgdata *)&answer, eom, cp, (void*)buf, sizeof buf);
	    if (n < 0)
	      break;
	    cp += n;

	    if (debug)
	      printf("000  MX[%d] = '%s'\n", mxcount, buf);

	    if (mxcount < MAXMX) {
	      mx[mxcount] = strdup((const char *)buf);
	      if (!mx[mxcount]) break; /* Out of memory ?? */
	      mxtype[mxcount] = 0;
	      ++mxcount;
	    }
	    /* If too many MXes, just skip the rest.. */

	    had_mx_record = 1;
	    continue;
	  } /* ===== END OF MX DATA PROCESING ========= */

	  if (type == T_TXT) {
	    int len = (*cp) & 0xFF; /* 0..255 chars */

	    /* Mal-formed inputs are possible overflowing the buffer.. */
	    if (len > (eom - cp))
	      len = (eom - cp);
	    if (len > n - 1)
	      len = n - 1;

	    if (txt_buf != NULL)
	      free(txt_buf);
	    txt_buf = emalloc(len+1);
	    ++cp;
	    memcpy(txt_buf, cp, len);
	    txt_buf[len] = '\0';
	    for (i = 0; i < mxcount; ++i) {
	      if (mx[i]) free(mx[i]);
	      mx[i] = NULL;
	    }
	    return 1; /* OK! */
	  }

	  /* If reached here, skip the data tail */
	  /* In theory could be an abort even..  */
	  cp += n;
	} /* ===== END OF DNS ANSWER PROCESSING ======= */


	if (ancount > 0) {
	  /* Sigh, waste of time :-( */
	  for (i = 0; i < mxcount; ++i) if (mx[i]) free(mx[i]);
	  return -EX_SOFTWARE;
	}


	if (qtype == T_MX && !mxmode && had_mx_record) {
	  /* Accept if found ANYTHING! */
	  if (debug) printf("000-  ... accepted!\n");
	  for (i = 0; i < mxcount; ++i) if (mx[i]) free(mx[i]);
	  return 1;
	}


	/* Now skip the AUTHORITY SECTION data */

	while (nscount > 0 && cp < eom) {
#if	defined(BIND_VER) && (BIND_VER >= 473)
	  n = dn_skipname(cp, eom);
#else	/* !defined(BIND_VER) || (BIND_VER < 473) */
	  n = dn_skip(cp);
#endif	/* defined(BIND_VER) && (BIND_VER >= 473) */
	  if (n < 0)
	    break;
	  cp += n;
	  if (cp+10 > eom) { cp = eom; break; }

	  NS_GET16(type,  cp); /* type  - short */
	  NS_GET16(class, cp); /* class - short */
	  cp += NS_INT32SZ;    /* ttl   - long  */
	  NS_GET16(n, cp);     /* dlen  - short */

	  cp += n; /* We simply skip this data.. */
	  if (cp <= eom)
	    --nscount;
	}

	if (debug)
	  printf("000-  nscount=%d (== 0 ?)  arcount=%d cp<eom ? (%d)\n",
		 nscount, arcount, cp < eom);

	/* Ok, can continue to pick the ADDITIONAL SECTION data */

	/* To be sure that all ADDITIONAL SECTION data is valid, we
	   look for the 'AA' bit.  If it isn't set, we don't use this
	   data, but do explicite lookups below. */

	while (hp->aa && nscount == 0 && arcount > 0 && cp < eom) {
	  n = dn_expand((msgdata *)&answer, eom, cp, (void*)buf, sizeof buf);
	  if (n < 0) { cp = eom; break; }
	  cp += n;
	  if (cp+10 > eom) { cp = eom; break; }

	  NS_GET16(type,  cp); /* type  - short */
	  NS_GET16(class, cp); /* class - short */
	  cp += NS_INT32SZ;    /* ttl   - long  */
	  NS_GET16(n, cp);     /* dlen  - short */

	  if (cp + n > eom) { cp = eom; break; }

	  if (class != C_IN) {
	    cp += n; --nscount;
	    continue;
	  }

	  /* Ok, we have Type IN data in the ADDITIONAL SECTION */


	  /* A and AAAA are known here! */

	  if (type == T_A
#if defined(AF_INET6) && defined(INET6)
	      || (type == T_AAAA)
#endif
	      ) {

	    Usockaddr usa;

	    /* Pick the address data */
	    for (i = 0; i < mxcount; ++i) {
	      /* Is this known (wanted) name ?? */
	      if (strcasecmp((const char *)buf, mx[i]) == 0) {
		/* YES! */

		mxtype[i] |= (type == T_A) ? 1 : 2 ; /* bitflag: 1 or 2 */

		/* We do have a wanted name! */

		/* build addrinfo block, pick addresses */

		/* WARNING! WARNING! WARNING! WARNING! WARNING! WARNING!
		   This assumes intimate knowledge of the system
		   implementation of the  ``struct addrinfo'' !  */

		memset(&usa, 0, sizeof(usa));

		switch (type) {
#if defined(AF_INET6) && defined(INET6)
		case T_AAAA:
		  usa.v6.sin6_family = PF_INET6;
		  memcpy(&usa.v6.sin6_addr, cp, 16);
		  break;
#endif
		case T_A:
		  usa.v4.sin_family = PF_INET;
		  memcpy(&usa.v4.sin_addr, cp, 4);
		  break;
		default:
		  break;
		}

#if 1
		if (debug) {
		  if (usa.v4.sin_family == AF_INET) {
		    inet_ntop(AF_INET, (void*) & usa.v4.sin_addr, (char *)buf, sizeof(buf));
		    printf("000-  matching %s AR address IPv4:[%s]\n", mx[i], buf);
		  }
#if defined(AF_INET6) && defined(INET6)
		  else if (usa.v6.sin6_family == AF_INET6) {
		    inet_ntop(AF_INET6, (void*) & usa.v6.sin6_addr, buf, sizeof(buf));
		    printf("000-  matching %s AR address IPv6:[%s]\n", mx[i], buf);
		  }
#endif
		  else
		    printf("000- matching unknown %s AR address family address; AF=%d\n",
			   mx[i], usa.v4.sin_family);
		}
#endif

		i = matchmyaddress( &usa );
		if (i == 1) {
		  if (debug)
		    printf("000-   AR ADDRESS MATCH!\n");
		  for (i = 0; i < mxcount; ++i) if (mx[i]) free(mx[i]);
		  return 1; /* Found a match! */
		} else
		  if (debug)
		    printf("000-   AR matchmyaddress() yields: %d\n", i);

		break; /* Name matched, no need to spin more here.. */
	      } /* Matched name! */
	    } /* Name matching loop */
	  } /* type = T_A or T_AAAA */

	  cp += n;
	  --arcount;
	} /* Additional data collected! */

	/* Now scan thru all MXes, if there are cases WITHOUT A or AAAA
	   records, look them up here. */

	for (n = 0; n < mxcount; ++n) {

	  struct addrinfo *ai2;
	  int k = 0, rc;

	  memset(&req, 0, sizeof(req));

	  switch(mxtype[n]) {
	  case 0: /* no addresses seen! */
	    req.ai_family   = 0; /* Both OK (IPv4/IPv6) */
	    /* Definitely ask for it! */
	    break;
	  case 1: /* T_A only seen */
#if defined(AF_INET6) && defined(INET6)
	    if (use_ipv6)
	      req.ai_family = AF_INET6;
	    else
	      continue; /* AF_INET address already seen, skip it.. */
#else
	    continue; /* Skip it! */
#endif
	    break;
	  case 2: /* T_AAAA only seen */
	    req.ai_family = AF_INET;
	    break;
	  case 3: /* T_A and T_AAAA seen */
	  default: /* BUG! */
	    continue; /* No need for any lookup, if this is not 0..2 */
	    break;
	  }

	  req.ai_socktype = SOCK_STREAM;
	  req.ai_protocol = IPPROTO_TCP;
	  req.ai_flags    = AI_CANONNAME;
	  /*  ai_family  set above. */
	  ai = NULL;
	  /* This resolves CNAME, it should not happen in case
	     of MX server, though..    */
#ifdef HAVE__GETADDRINFO_
	  i = _getaddrinfo_(mx[n], "0", &req, &ai,
			    debug ? stdout : NULL);
#else
	  i = getaddrinfo(mx[n], "0", &req, &ai);
#endif
	  if (debug)
	    printf("000-  getaddrinfo('%s','0') -> r=%d, ai=%p\n",mx[n],i,ai);
	    
	  if (i != 0)
	    continue;		/* Well well.. spurious! */

	  if (!mxmode) /* Accept if found ANYTHING! */ {
	    if (debug) printf("000-  ... accepted!\n");
	    freeaddrinfo(ai);
	    for (i = 0; i < mxcount; ++i) if (mx[i]) free(mx[i]);

	    return 1;
	  }
	  
	    
	  for ( ai2 = ai ; ai2 != NULL; ai2 = ai2->ai_next) {
	    ++k;
#if 1
	    if (debug) {
	      Usockaddr * usa = (Usockaddr *) ai2->ai_addr;
	      char buf[60];

	      if (usa->v4.sin_family == AF_INET) {
		inet_ntop(AF_INET, (void*) & usa->v4.sin_addr, buf, sizeof(buf));
		printf("000-  matching %s address IPv4:[%s]\n", mx[n], buf);
	      }
#if defined(AF_INET6) && defined(INET6)
	      else if (usa->v6.sin6_family == AF_INET6) {
		inet_ntop(AF_INET6, (void*) & usa->v6.sin6_addr, buf, sizeof(buf));
		printf("000-  matching %s address IPv6:[%s]\n", mx[n], buf);
	      }
#endif
	      else
		printf("000- matching %s unknown address family address; AF=%d\n",
		       mx[n], usa->v4.sin_family);
	    }
#endif
	    rc = matchmyaddress((Usockaddr *)ai2->ai_addr);
	    if (rc == 1) {
	      if (debug)
		printf("000-   ADDRESS MATCH!\n");
	      freeaddrinfo(ai);
	      for (i = 0; i < mxcount; ++i) if (mx[i]) free(mx[i]);
	      return 1; /* Found a match! */
	    } else
	      if (debug)
		printf("000-   matchmyaddress() yields: %d\n", rc);
	  }
	  if (debug)
	    printf("000-   No address match among %d address!\n", k);

	  freeaddrinfo(ai);
	}

	for (i = 0; i < mxcount; ++i) if (mx[i]) free(mx[i]);


	/* Didn't find any, but saw CNAME ? Recurse with the real name */
	if (saw_cname)
	  return dnsmxlookup((void *)realname, depth+1, mxmode, qtype);

	if (had_mx_record && mxmode)
	    return 2; /* We have SOME date, but no match on ourselves! */

perhaps_address_record:
	if (qtype == T_MX) {
	  /* No MX, perhaps A ? */
	  memset(&req, 0, sizeof(req));
	  req.ai_socktype = SOCK_STREAM;
	  req.ai_protocol = IPPROTO_TCP;
	  req.ai_flags    = AI_CANONNAME;
	  req.ai_family   = PF_INET;
	  ai = NULL;


	  /* This resolves CNAME, it should not happen in case
	     of MX server, though..    */
#ifdef HAVE__GETADDRINFO_
	  if (debug)
	    printf("000-  perhaps A?\n");
	  i = _getaddrinfo_((const char*)host, "0", &req, &ai, debug ? stdout : NULL);
#else
	  i = getaddrinfo((const char*)host, "0", &req, &ai);
#endif
	  if (debug)
	    printf("000-   getaddrinfo('%s','0') (PF_INET) -> r=%d (%s), ai=%p\n",host,i,gai_strerror(i),ai);

#if defined(AF_INET6) && defined(INET6)
	  if (use_ipv6) {

	    /* Want, but not have AAAA, ask for it. */

	    int n2;
	    struct addrinfo *ai2 = NULL;

	    memset(&req, 0, sizeof(req));
	    req.ai_socktype = SOCK_STREAM;
	    req.ai_protocol = IPPROTO_TCP;
	    req.ai_flags    = AI_CANONNAME;
	    req.ai_family   = PF_INET6;

	  /* This resolves CNAME, it should not happen in case
	     of MX server, though..    */
#ifdef HAVE__GETADDRINFO_
	    n2 = _getaddrinfo_((const char *)host, "0", &req, &ai2,
			       debug ? stdout : NULL);
#else
	    n2 = getaddrinfo((const char *)host, "0", &req, &ai2);
#endif
	    if (debug)
	      printf("000-   getaddrinfo('%s','0') (PF_INET6) -> r=%d (%s), ai=%p\n",host,n2,gai_strerror(n2),ai2);


	    if (i != 0 && n2 == 0) {
	      /* IPv6 address, no IPv4 (or error..) */
	      i = n2;
	      ai = ai2; ai2 = NULL;
	    }
	    if (ai2 && ai) {
	      /* BOTH ?!  Catenate them! */
	      struct addrinfo **aip;
	      aip = &ai->ai_next;
	      while (*aip) aip = &((*aip)->ai_next);
	      *aip = ai2;
	    }
	  }
#endif

	  if (i)
	    return i;


	  i = matchmyaddresses(ai);
#if 1
	  /* With this we can refuse to accept any message with
	     source domain pointing back to loopback ! */
	  if (i == 2) {
	    /* Loopback ! */
	    freeaddrinfo(ai);
	    return 0;
	  }
#endif
	  if (i == 0 && mxmode) {
	    freeaddrinfo(ai);
	    return 2; /* Didn't find our local address in client-MX-mode */
	  }

	  freeaddrinfo(ai);
	  return 1; /* Found any address, or in client-MX-mode,
		       a local address! */
	}

	if (mxmode)
	  return 2; /* Not found, had no MX data either */
	
	return 0; /* Not found! */
}


/* For SOFT errors, return -102, for hard errors, -2.
   For 'we are MX', return 0.
   For (retmode == '+'), and without MX, return 1.
 */

int mx_client_verify(retmode, domain, alen)
     int retmode, alen;
     const char *domain;
{
	char hbuf[2000];
	int rc;

	if (alen >= sizeof(hbuf)-2)
	  alen = sizeof(hbuf)-2;

	strncpy(hbuf, domain, alen);
	hbuf[alen] = 0; /* Chop off the trailers from the name */

	rc = dnsmxlookup(hbuf, 0, 1, T_MX);

	if (rc == 1) return 0; /* Found! */

	if (rc == -EX_TEMPFAIL) {
	  return -104;
	}
	if (retmode == '+') {
	  if (rc == -EX_NOHOST ||
	      rc == -EX_UNAVAILABLE)
	    return -2; /* Definitely hard errors */
	  if (rc == 2)
	    return -103;
	  return -102; /* Soft error */
	}

	if (rc == 2)
	  return -3;
	return -2;     /* Reject */
}

int sender_dns_verify(retmode, domain, alen)
     int retmode, alen;
     const char *domain;
{
	char hbuf[500];
	int rc;

	if (alen >= sizeof(hbuf)-2)
	  alen = sizeof(hbuf)-2;

	strncpy(hbuf, domain, alen);
	hbuf[alen] = 0; /* Chop off the trailers from the name */

	rc = dnsmxlookup(hbuf, 0, 0, T_MX);

	if (debug)
	  printf("000- dnsmxlookup() did yield: %d, retmode='%c'\n",
		 rc,retmode);

	if (rc == 1) return 0; /* Found! */

	if (rc == -EX_TEMPFAIL) {
	  return -104;
	}
	if (retmode == '+') {
	  if (rc == -EX_NOHOST      ||
	      rc == -EX_UNAVAILABLE ||
	      rc == EAI_NONAME)
	    return -2; /* Definitely hard errors */
	  if (rc == 2)
	    return -103;
	  return -102; /* Soft error */
	}

	if (rc == 2)
	  return -3;
	return -2;     /* Reject */
}

int client_dns_verify(retmode, domain, alen)
     int retmode, alen;
     const char *domain;
{
	return sender_dns_verify(retmode, domain, alen);
}

int rbl_dns_test(ipaf, ipaddr, rbldomain, msgp)
     const int ipaf;
     const u_char *ipaddr;
     char *rbldomain;
     char **msgp;
{
	char hbuf[2000], *s, *suf;
	int hspc;
	struct hostent *hp;
	int has_ok = 0;

	if (ipaf == P_K_IPv4) {
	  sprintf(hbuf, "%d.%d.%d.%d.",
		  ipaddr[3], ipaddr[2], ipaddr[1], ipaddr[0]);

	} else { /* Ok, the other variant is IPv6 ... */

	  int i;
	  for (i = 15; i >= 0; --i) {
	    sprintf(hbuf + ((15-i) << 2),
		    "%x.%x.", ipaddr[i] & 0x0F, (ipaddr[i] >> 4) & 0x0F);
	  }
	  strcpy(hbuf+64,"ip6."); /* Fixed length of hex nybbles */
	}

	suf = hbuf + strlen(hbuf);
	hspc = sizeof(hbuf) - strlen(hbuf) - 2;

	while (*rbldomain) {
	  /* "rbldomain" is possibly a COLON-demarked set of
	     domain names:  rbl.maps.vix.com:dul.maps.vix.com
	     which isn't so easy to read, but ... */
	  /* The 2000 char buffer should be way oversized
	     for this routine's needs..  And it is managerial
	     input at the policy database, which has the "unpredictable"
	     size...  */
	  s = strchr(rbldomain, ':');
	  if (s) *s = 0;
	  if (strcmp(rbldomain,"+") == 0)
	    strncpy (suf, "rbl.maps.vix.com", hspc);
	  else
	    strncpy (suf, rbldomain, hspc);
	  suf[hspc] = '\0';

	  if (s) {
	    *s = ':';
	    rbldomain = s+1;
	  } else {
	    rbldomain += strlen(rbldomain);
	  }

	  /* Add explicite DOT into the tail of the lookup object.
	     That way the lookup should never use resolver's  SEARCH
	     suffix set. */

	  s = suf + strlen(suf) - 1;
	  if (*s != '.') {
	    *(++s) = '.';
	    *(++s) = 0;
	  }


	  if (debug)
	    printf("000- looking up DNS A object: %s\n", hbuf);


	  hp = gethostbyname(hbuf);
	  if (hp != NULL) {
	    /* XX: Should verify that the named object has A record: 127.0.0.2
	       D'uh.. alternate dataset has A record: 127.0.0.3 */
	    char abuf[30];

	    inet_ntop(AF_INET, (void*) hp->h_addr, abuf, sizeof(abuf));

	    type(NULL,0,NULL, "Looked up DNS A object: %s -> %s", hbuf, abuf);

	    if (strncmp("127.0.0.",abuf,8) != 0) {
	      has_ok = 1;
	      continue; /* Isn't  127.0.0.* */
	    }
#if 0
	    if (strcmp("127.0.0.4",abuf) == 0) {
	      /* ORBS NETBLOCK */
	      if (has_ok) continue;
	    }
#endif
	    /* Ok, then lookup for the TXT entry too! */
	    if (debug)
	      printf("000- looking up DNS TXT object: %s\n", hbuf);

	    if (dnsmxlookup(hbuf, 0, 0, T_TXT) == 1) {
	      if (*msgp != NULL)
		free(*msgp);
	      *msgp = strdup(txt_buf);
	      s = *msgp;
	      if (s) {
		for ( ;*s; ++s) {
		  int c = ((*s) & 0xFF);
		  /* Characters not printable in ISO-8859-*
		     are masked with space. */
		  if (c < ' ' || (c >= 127 && c < 128+32) || c == 255)
		    *s = ' ';
		}
	      }
	      type(NULL,0,NULL,"Found DNS TXT object: %s\n", *msgp ? *msgp : "<nil>");
	    }
	    return -1;
	  }
	  /* Didn't find A record */
	  type(NULL,0,NULL, "Didn't find DNS A object: %s", hbuf);
	}

	return 0;
}


syntax highlighted by Code2HTML, v. 0.9.1