/*
** Copyright (c) 1996, 1997 Thorsten Kukuk 
**
** This file is part of the NYS YP Server.
**
** The NYS YP Server 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.
**
** The NYS YP Server 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 the NYS YP Server; see the file COPYING.  If
** not, write to the Free Software Foundation, Inc., 675 Mass Ave,
** Cambridge, MA 02139, USA.
**
** Author: Thorsten Kukuk <kukuk@uni-paderborn.de>
*/

#ifndef LINT
static const char rcsid[] = "$Id: ypxfr.c,v 1.12 1997/01/29 19:16:31 kukuk Exp $";
#endif

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <time.h> /* Ultrix needs this */
#include "system.h"

#include <rpc/rpc.h>
#ifdef HAVE_RPC_CLNT_SOC_H
#include <rpc/clnt_soc.h>
#endif
#include <rpc/pmap_clnt.h>
#include "yp.h"
#include "ypxfrd.h"
#include <rpcsvc/ypclnt.h>
#include <netdb.h>
#include <fcntl.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <syslog.h>
#ifndef LOG_DAEMON
#include <sys/syslog.h> /* for ULTRIX */
#endif
#include <stdlib.h>
#include <stdarg.h>
#include <stdio.h>
#include <memory.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <gdbm.h>
#include <unistd.h>
#if defined(HAVE_GETOPT_H) && defined(HAVE_GETOPT_LONG)
#include <getopt.h>
#else
#include <compat/getopt.h>
#endif

#include "version.h"
#include "yp_msg.h"
#include "ypxfr.h"

#ifndef HAVE_STRDUP
#include <compat/strdup.c>
#endif

#ifndef HAVE_GETOPT_LONG
#include <compat/getopt.c>
#include <compat/getopt1.c>
#endif

#ifndef HAVE_STRERROR
#include <compat/strerror.c>
#endif

#ifndef YPMAPDIR
#define YPMAPDIR "/var/yp"
#endif

static char *path_ypdb = YPMAPDIR;

static char SourceHost[MAXHOSTNAMELEN], *TargetDomain=NULL, *SourceDomain=NULL;
static struct in_addr IpAddress;
static int Force=0, NoClear=0, TaskId=0, ProgramNumber=0, PortNumber=0;
static GDBM_FILE dbm;

int debug_flag=0;

static char *ypxfr_err_string(enum ypxfrstat error) 
{
  switch(error) 
    {
    case YPXFR_SUCC:	return "Success";
    case YPXFR_AGE:	return "Master's version not newer";
    case YPXFR_NOMAP:	return "Can't find server for map";
    case YPXFR_NODOM:	return "Domain not supported";
    case YPXFR_RSRC:	return "Local resource alloc failure";
    case YPXFR_RPC:	return "RPC failure talking to server";
    case YPXFR_MADDR:	return "Can't get master address";
    case YPXFR_YPERR:	return "YP server/map db error";
    case YPXFR_BADARGS: return "Request arguments bad";
    case YPXFR_DBM:	return "Local dbm operation failed";
    case YPXFR_FILE:	return "Local file I/O operation failed";
    case YPXFR_SKEW:	return "Map version skew during transfer";
    case YPXFR_CLEAR:	return "Can't send \"Clear\" req to local ypserv";
    case YPXFR_FORCE:	return "No local order number in map  use -f flag.";
    case YPXFR_XFRERR:  return "ypxfr error";
    case YPXFR_REFUSED: return "Transfer request refused by ypserv";
    }
  return "Unknown Error, should not happen";
}

/* NetBSD has a different prototype in struct ypall_callback */
#if defined(__NetBSD__)
static int ypxfr_foreach(u_long status, char *key, int keylen, 
			 char *val, int vallen, void *data)
#else
static int ypxfr_foreach(int status, char *key, int keylen, 
			 char *val, int vallen, int *data)
#endif
{
  datum outKey, outData;

  if(debug_flag>1)
    {
      if (keylen != 0 && vallen != 0)
	yp_msg("ypxfr_foreach: key=%.*s, val=%.*s\n",keylen, key, vallen, val);
      else
	yp_msg("ypxfr_foreach: empty key/value pair!\n");
    }
  
  if (status==YP_NOMORE)
    return 0;

  if (status!=YP_TRUE) 
    {
      int s=ypprot_err(status);
      yp_msg("%s\n", yperr_string(s));
      return 1;
    }
	
  if (keylen != 0)
    {
      outKey.dptr=keylen?key:""; outKey.dsize=keylen;
      outData.dptr=vallen?val:""; outData.dsize=vallen;
      if(gdbm_store(dbm,outKey, outData, GDBM_REPLACE) != GDBM_NO_ERROR)
	return 1;
    }

  return 0;
}

/*
** Collects all the answers for our query for hosts running ypserv
*/
static char *ypserver = NULL;
static bool_t eachresult(bool_t *resp, struct sockaddr_in *addr)
{
  if (*resp)
    {
      struct hostent *h;
      h = gethostbyaddr((char *) &addr->sin_addr.s_addr,
			sizeof(addr->sin_addr.s_addr), AF_INET);

      if (ypserver == NULL) 
	ypserver=strdup(h->h_name);

      if (debug_flag)
        {
          yp_msg("Find running ypserv on %s\n", h->h_name);
        }
      return 1;
    }
  else
    {
      return 0;
    }
}

/*
** At first, use the yp lib routines. If we are not bound to a server,
** do a broadcast and search one.
*/
static int __yp_master( char *domain, char *map, char **master)
{
  static ypresp_master res;
  ypreq_nokey req;
  int status;

  if((status=yp_master(domain, map, master))!=0)
    {
      bool_t out;
  
      status = clnt_broadcast(YPPROG, YPVERS, YPPROC_DOMAIN_NONACK,
                          (xdrproc_t) xdr_domainname, (void *)&domain,
                          (xdrproc_t) xdr_bool, (void *)&out,
                          (resultproc_t) eachresult);
      if (status != RPC_SUCCESS)
	{
	  yp_msg("broadcast: %s.", clnt_sperrno(status));
	}
      else
	{
	  req.domain = domain;
	  req.map = map;
	  if ((status = callrpc(ypserver,YPPROG,YPVERS,YPPROC_MASTER,
				(xdrproc_t)xdr_ypreq_nokey, (void*) &req,
				(xdrproc_t)xdr_ypresp_master, (void*) &res)) 
	      != RPC_SUCCESS) 
	    {
	      yp_msg("failed to send 'ypproc_master' to %s's ypserv: %s",
		     ypserver, clnt_sperrno((enum clnt_stat) status));
       
	    }
	  else
	    {
	      if((res.peer != NULL) && (strlen(res.peer) > 0))
		*master=strdup(res.peer);
	      else
		return 1;
	    }
	}
    }
  return status;
}

static enum ypxfrstat ypxfr(char *mapName) 
{
  int localOrderNum=0;
  int masterOrderNum=0;
  char *masterName = NULL;
  struct ypall_callback callback;
  char dbName[MAXPATHLEN+1];
  char dbName2[MAXPATHLEN+1];
  CLIENT *clnt_tcp, *clnt_udp;
  struct sockaddr_in sockaddr, sockaddr_udp;
  struct ypreq_nokey req_nokey;
  struct ypreq_key req_key;
  struct ypresp_order *resp_order;
  struct ypresp_master *resp_master;
  struct ypresp_val *resp_val;
  struct ypresp_all *resp_all;
  extern struct ypall_callback *xdr_ypall_callback;
  datum outKey, outData;
  char orderNum[12];
  int result;
  int sock;
  struct timeval TIMEOUT={25,0};

  /* 
  ** Get the Master hostname for the map, if no explicit 
  ** sourcename is given.
  */
  if (SourceHost[0] == '\0') 
    {
      if (__yp_master(SourceDomain, mapName, &masterName))
	return YPXFR_MADDR;
      strcpy(SourceHost,masterName);
    }

  /*
  ** Build a connection to the host with the master map, not the
  ** the nearest ypserv, because this map could be out of date.
  */
  memset(&sockaddr, '\0', sizeof(sockaddr));
  sockaddr.sin_family=AF_INET;
  {
    struct hostent *h=gethostbyname(SourceHost);
    if (!h) 
      return YPXFR_RSRC;

    memcpy(&sockaddr.sin_addr, h->h_addr, sizeof sockaddr.sin_addr);
  }
  memcpy(&sockaddr_udp, &sockaddr, sizeof(sockaddr));

  sock=RPC_ANYSOCK;
  clnt_tcp=clnttcp_create(&sockaddr, YPPROG, YPVERS, &sock, 0, 0);
  if (clnt_tcp==NULL) 
    {
      clnt_pcreateerror("YPXFR");
      return YPXFR_RPC;
    }

  sock=RPC_ANYSOCK;
  clnt_udp=clntudp_create(&sockaddr_udp, YPPROG, YPVERS, TIMEOUT, &sock);
  if (clnt_udp==NULL) 
    {
      clnt_pcreateerror("YPXFR");
      return YPXFR_RPC;
    }

  req_nokey.domain=SourceDomain;
  req_nokey.map=mapName;
  if ((resp_order=ypproc_order_2(&req_nokey, clnt_udp))==NULL)
    {
      clnt_perror(clnt_udp, "masterOrderNum");
      masterOrderNum=0x7fffffff;
    } 
  else 
    {
      masterOrderNum=resp_order->ordernum;
      xdr_free((xdrproc_t) xdr_ypresp_order, (char *)resp_order);
    }

  if (masterName == NULL)
    {
      req_nokey.domain=SourceDomain;
      req_nokey.map=mapName;
      if ((resp_master=ypproc_master_2(&req_nokey, clnt_udp))==NULL) 
	{
	  clnt_perror(clnt_udp, "ypmaster");
	}
      else
	{
	  masterName = strdup(resp_master->peer);
	  xdr_free((xdrproc_t) xdr_ypresp_master, (char *)resp_master);
	}
    }
  /*
  ** If we doesn't force the map, look, if the new map is really newer
  */
  if (!Force) 
    {
      datum inKey, inVal;

      if(strlen(path_ypdb)+strlen(TargetDomain)+strlen(mapName)+3 < MAXPATHLEN)
	sprintf(dbName, "%s/%s/%s", path_ypdb, TargetDomain, mapName);
      else
	{
	  yp_msg("ERROR: Path to long: %s/%s/%s\n",path_ypdb, TargetDomain,mapName);
	  return YPXFR_RSRC;
	}
      if ((dbm=gdbm_open(dbName,0,GDBM_READER, 0600, NULL))==NULL)
	{
	  yp_msg("Cannot open old %s - ignored.\n", dbName);
	  localOrderNum=0;
	} 
      else 
	{
	  inKey.dptr="YP_LAST_MODIFIED"; inKey.dsize=strlen(inKey.dptr);
	  inVal=gdbm_fetch(dbm,inKey);
	  if (inVal.dptr) 
	    {
	      int i;
	      char *d=inVal.dptr;
	      for (i=0; i<inVal.dsize; i++, d++) 
		{
		  if (!isdigit(*d)) 
		    {
		      gdbm_close(dbm);
		      return YPXFR_SKEW;
		    }
		}
	      localOrderNum=atoi(inVal.dptr);
	    }
	  gdbm_close(dbm);
	}
      if(debug_flag>1)
	yp_msg("masterOrderNum=%d, localOrderNum=%d\n",
	       masterOrderNum, localOrderNum);
      if (localOrderNum>=masterOrderNum) return YPXFR_AGE;
    }
  
  if(strlen(path_ypdb)+strlen(TargetDomain)+strlen(mapName)+4 < MAXPATHLEN)
    sprintf(dbName, "%s/%s/%s~", path_ypdb, TargetDomain, mapName);
  else
    {
      yp_msg("ERROR: Path to long: %s/%s/%s~\n",path_ypdb, TargetDomain,mapName);
      return YPXFR_RSRC;
    }

  /*
  ** Try to use ypxfrd for getting the new map. If it fails, use the old
  ** method.
  */
  if ((result=ypxfrd_transfer(masterName, mapName, TargetDomain, dbName))!=0)
    {
      if ((dbm=gdbm_open(dbName,0,GDBM_NEWDB,0600,NULL))==NULL) 
	{
	  yp_msg("Cannot open %s\n", dbName);
	  return YPXFR_DBM;
	}
  
      outKey.dptr="YP_MASTER_NAME"; outKey.dsize=strlen(outKey.dptr);
      outData.dptr=masterName; outData.dsize=strlen(outData.dptr);
      if(gdbm_store(dbm,outKey, outData, GDBM_REPLACE) != GDBM_NO_ERROR)
	{
	  gdbm_close(dbm);
	  unlink(dbName);
	  return YPXFR_DBM;
	}
      sprintf(orderNum, "%d", masterOrderNum);
      outKey.dptr="YP_LAST_MODIFIED"; outKey.dsize=strlen(outKey.dptr);
      outData.dptr=orderNum; outData.dsize=strlen(outData.dptr);
      if(gdbm_store(dbm, outKey, outData, GDBM_REPLACE) != GDBM_NO_ERROR)
	{
	  gdbm_close(dbm);
	  unlink(dbName);
	  return YPXFR_DBM;
	}
      
      /* 
      ** Get the YP_INTERDOMAIN field. This is needed from the SunOS ypserv.
      ** We ignore this, since we have the "dns" option. But a Sun could be
      ** a NIS slave server and request the map from us.
      */
      req_key.domain=SourceDomain;
      req_key.map=mapName;
      req_key.key.keydat_val="YP_INTERDOMAIN";
      req_key.key.keydat_len=strlen("YP_INTERDOMAIN");
      if ((resp_val=ypproc_match_2(&req_key, clnt_udp))==NULL) 
	{
	  clnt_perror(clnt_udp, "yproc_match");
	}
      else
	{
	  if (resp_val->stat == YP_TRUE)
	    {
	      outKey.dptr="YP_INTERDOMAIN"; 
	      outKey.dsize=strlen(outKey.dptr);
	      outData.dptr=""; 
	      outData.dsize=0;
	      if (gdbm_store(dbm, outKey, outData, GDBM_REPLACE) != GDBM_NO_ERROR)
		{
		  gdbm_close(dbm);
		  unlink(dbName);
		  return YPXFR_DBM;
		}
	    }
	  xdr_free((xdrproc_t) xdr_ypresp_val, (char *)resp_val);
	}
      
      /* 
      ** Get the YP_SECURE field. This is needed by the SunOS ypserv.
      ** We ignore this in the moment, since we have the /etc/ypserv.conf for this.
      */
      req_key.domain=SourceDomain;
      req_key.map=mapName;
      req_key.key.keydat_val="YP_SECURE";
      req_key.key.keydat_len=strlen("YP_SECURE");
      if ((resp_val=ypproc_match_2(&req_key, clnt_udp))==NULL) 
	{
	  clnt_perror(clnt_udp, "yproc_match");
	}
      else
	{
	  if (resp_val->stat == YP_TRUE)
	    {
	      outKey.dptr="YP_SECURE"; 
	      outKey.dsize=strlen(outKey.dptr);
	      outData.dptr=""; 
	      outData.dsize=0;
	      if (gdbm_store(dbm, outKey, outData, GDBM_REPLACE) != GDBM_NO_ERROR)
		{
		  gdbm_close(dbm);
		  unlink(dbName);
		  return YPXFR_DBM;
		}
	    }
	  xdr_free((xdrproc_t) xdr_ypresp_val, (char *)resp_val);
	}

      callback.foreach=ypxfr_foreach;
      callback.data=NULL;
      {  
	
	req_nokey.domain=SourceDomain;
	req_nokey.map=mapName;
	xdr_ypall_callback=&callback;
	if ((resp_all=ypproc_all_2(&req_nokey, clnt_tcp))==NULL) 
	  {
	    clnt_perror(clnt_tcp, "ypall");
	  }
	else
	  {
	  switch (resp_all->ypresp_all_u.val.stat) 
	    {
	    case YP_TRUE:
	    case YP_NOMORE:
	      result=0;
	      break;
	    default:
	      result=ypprot_err(resp_all->ypresp_all_u.val.stat);
	    }
	  clnt_freeres(clnt_tcp, (xdrproc_t) ypxfr_xdr_ypresp_all, (void *)resp_all);
	  }
      }

      clnt_destroy(clnt_tcp);

      gdbm_close(dbm);
    }
  
  if(strlen(path_ypdb)+strlen(TargetDomain)+strlen(mapName)+3 < MAXPATHLEN)
    sprintf(dbName2, "%s/%s/%s", path_ypdb, TargetDomain, mapName);
  else
    {
      yp_msg("ERROR: Path to long: %s/%s/%s\n",path_ypdb,TargetDomain,mapName);
      return YPXFR_RSRC;
    }
  unlink(dbName2);
  rename(dbName, dbName2);
  
  if (!NoClear) 
    {
      char in = 0;
      char *out = NULL;
      int stat;

      if ((stat = callrpc("localhost",YPPROG,YPVERS,YPPROC_CLEAR,
			  (xdrproc_t)xdr_void, &in,
			  (xdrproc_t)xdr_void, out)) != RPC_SUCCESS) 
	{
	  yp_msg("failed to send 'clear' to local ypserv: %s",
		  clnt_sperrno((enum clnt_stat) stat));
	  return YPXFR_CLEAR;
	}
    }
  return result==0?YPXFR_SUCC:YPXFR_YPERR;
}

void Usage(int exit_code)
{
  fprintf(stderr,"usage: ypxfr [-f] [-c] [-d target domain] [-h source host] [-s source domain]\n");
  fprintf(stderr,"             [-C taskid program-number ipaddr port] [-p yp_path] mapname ...\n");
  fprintf(stderr,"       ypxfr --version\n");
  fprintf(stderr,"\n");
  fprintf(stderr,"where\n");
  fprintf(stderr,"\t-f forces transfer even if the master's copy is not newer.\n");
  fprintf(stderr,"\thost may be either a name or an internet\n");
  fprintf(stderr,"\t     address of form ww.xx.yy.zz\n");
  fprintf(stderr,"\t-c inhibits sending a \"Clear map\" message to the local ypserv.\n");
  fprintf(stderr,"\t-C is used by ypserv to pass callback information.\n");
  exit(exit_code);
}

int main (int argc, char **argv)
{
  static char res;

  if (argc < 2)
    Usage(1);

  if(!isatty(fileno(stderr)))
    openlog("ypxfr", LOG_PID, LOG_DAEMON);
  else
    debug_flag=1;

  memset(SourceHost, '\0', sizeof(SourceHost));

  while(1) 
    {
      int c;
      int option_index = 0;
      static struct option long_options[] =
      {
        {"version", no_argument, NULL, '\255'},
	{"debug", no_argument, NULL, '\254'},
        {"help", no_argument, NULL, 'u'},
        {"usage", no_argument, NULL, 'u'},
	{"path", required_argument, NULL, 'p'},
        {NULL, 0, NULL, '\0'}
      };

      c=getopt_long(argc, argv, "ufcd:h:p:s:C:S",long_options, &option_index);
      if (c==EOF) break;
      switch (c) 
	{
	case 'p':
	  path_ypdb = optarg;
          if (debug_flag)
            yp_msg("Using database directory: %s\n", path_ypdb);
          break;
	case 'f':
	  Force++;
	  break;
	case 'c':
	  NoClear++;
	  break;
	case 'd':
	  TargetDomain=optarg;
	  break;
	case 'h':
	  strcpy(SourceHost,optarg);
	  break;
	case 's':
	  SourceDomain=optarg;
	  break;
	case 'C':
	  if(optind+3 > argc) 
	    {
	      Usage(1);
	      break;
	    }
	  TaskId=atoi(optarg);
	  ProgramNumber=atoi(argv[optind++]);
	  IpAddress.s_addr=inet_addr(argv[optind++]);
	  PortNumber=atoi(argv[optind++]);
	  break;
	case 'u':
	  Usage(0);
	  break;
	case '\254':
	  debug_flag=2;
	  break;
	case '\255':
	  fprintf(stderr,"ypxfr - NYS YP Server version %s\n",version);
          exit(0);
	default:
	  Usage(1);
	  break;
	}
    }
  argc-=optind;
  argv+=optind;

  if (!TargetDomain) 
    yp_get_default_domain(&TargetDomain);
    
  if (!SourceDomain) 
    SourceDomain=TargetDomain;
    

  for (; *argv; argv++) 
    {
      enum ypxfrstat y;

      /*
      ** Start the map Transfer
      */
      if ((y=ypxfr(*argv))!=YPXFR_SUCC) 
	{
	  yp_msg("ypxfr: %s\n", ypxfr_err_string(y));
	}

      /*
      ** Now send the status to the yppush program, so it can display a 
      ** message for the sysop and do not timeout.
      */
      if (TaskId) 
	{
	  struct sockaddr_in addr;
	  struct timeval wait;
	  CLIENT *clnt;
	  int s;
	  ypresp_xfr resp;
	  static struct timeval tv={10,0};
	  
	  memset(&addr, '\0', sizeof addr);
	  addr.sin_addr=IpAddress;
	  addr.sin_port=htons(PortNumber);
	  addr.sin_family=AF_INET;
	  wait.tv_sec=25; wait.tv_usec=0;
	  s=RPC_ANYSOCK;

	  clnt=clntudp_create(&addr, ProgramNumber, 1, wait, &s);
	  if (!clnt) 
	    {
	      clnt_pcreateerror("ypxfr_callback");
	      continue;
	    }

	  resp.transid=TaskId;
	  resp.xfrstat=y;

	  if(clnt_call(clnt, 1, (xdrproc_t) xdr_ypresp_xfr, (void *) &resp,
		       (xdrproc_t) xdr_void, &res, tv) != RPC_SUCCESS) 
	    clnt_perror(clnt, "ypxfr_callback");

	  clnt_destroy(clnt);
	}
    }
  return 0;
}
