1 /*  $Id: innxbatch.c 10305 2018-12-02 14:21:56Z iulius $
2 **
3 **  Transmit batches to remote site, using the XBATCH command
4 **  Modelled after innxmit.c and nntpbatch.c
5 **
6 **  Invocation:
7 **	innxbatch [options] <serverhost> <file> ...
8 #ifdef FROMSTDIN
9 **	innxbatch -i <serverhost>
10 #endif FROMSTDIN
11 **  will connect to serverhost's nntp port, and transfer the named files,
12 **  with an xbatch command for every file. Files that have been sent
13 **  successfully are unlink()ed. In case of any error, innxbatch terminates
14 **  and leaves any remaining files untouched, for later transmission.
15 **  Options:
16 **	-D	increase debug level
17 **	-v	report statistics to stdout
18 #ifdef FROMSTDIN
19 **	-i	read batch file names from stdin instead from command line.
20 **		For each successfully transmitted batch, an OK is printed on
21 **		stdout, to indicate that another file name is expected.
22 #endif
23 **	-t	Timeout for connection attempt
24 **	-T	Timeout for batch transfers.
25 **  We do not use any file locking. At worst, a batch could be transmitted
26 **  twice in parallel by two independant invocations of innxbatch.
27 **  To prevent this, innxbatch should be invoked by a shell script that uses
28 **  shlock(1) to achieve mutual exclusion.
29 */
30 
31 #include "config.h"
32 #include "clibrary.h"
33 #include "portable/socket.h"
34 #include <ctype.h>
35 #include <errno.h>
36 #include <fcntl.h>
37 #include <setjmp.h>
38 #include <signal.h>
39 #include <syslog.h>
40 #include <sys/stat.h>
41 
42 #ifdef HAVE_SYS_TIME_H
43 # include <sys/time.h>
44 #endif
45 #include <time.h>
46 
47 /* Needed on AIX 4.1 to get fd_set and friends. */
48 #ifdef HAVE_SYS_SELECT_H
49 # include <sys/select.h>
50 #endif
51 
52 #include "inn/innconf.h"
53 #include "inn/messages.h"
54 #include "inn/timer.h"
55 #include "inn/libinn.h"
56 #include "inn/nntp.h"
57 
58 /*
59 **  Global variables.
60 */
61 static bool		Debug = false;
62 static bool		STATprint;
63 #ifdef FROMSTDIN
64 static bool             FromStdin = false;
65 #endif
66 static char		*REMhost;
67 static double		STATbegin;
68 static double		STATend;
69 static char		*XBATCHname;
70 static int		FromServer;
71 static int		ToServer;
72 static sig_atomic_t	GotAlarm;
73 static sig_atomic_t	GotInterrupt;
74 static sig_atomic_t	JMPyes;
75 static jmp_buf		JMPwhere;
76 static unsigned long	STATaccepted;
77 static unsigned long	STAToffered;
78 static unsigned long	STATrefused;
79 static unsigned long	STATrejected;
80 
81 /*
82 **  Prototypes.
83 */
84 static void             ExitWithStats(int) __attribute__ ((__noreturn__));
85 
86 /*
87 **  Send a line to the server. \r\n will be appended
88 */
89 static bool
REMwrite(int fd,char * p)90 REMwrite(int fd, char *p)
91 {
92   int		i;
93   int		err;
94   char		*dest;
95   static char		buff[NNTP_MAXLEN_COMMAND];
96 
97   for (dest = buff, i = 0; p[i]; ) *dest++ = p[i++];
98   *dest++ = '\r';
99   *dest++ = '\n';
100   *dest++ = '\0';
101 
102   for (dest = buff, i+=2; i; dest += err, i -= err) {
103     err = write(fd, dest, i);
104     if (err < 0) {
105       syswarn("cannot write %s to %s", dest, REMhost);
106       return false;
107     }
108   }
109   if (Debug)
110     fprintf(stderr, "> %s\n", p);
111 
112   return true;
113 }
114 
115 /*
116 **  Print transfer statistics, clean up, and exit.
117 */
118 static void
ExitWithStats(int x)119 ExitWithStats(int x)
120 {
121   static char		QUIT[] = "quit";
122   double		usertime;
123   double		systime;
124 
125   REMwrite(ToServer, QUIT);
126 
127   STATend = TMRnow_double();
128   if (GetResourceUsage(&usertime, &systime) < 0) {
129     usertime = 0;
130     systime = 0;
131   }
132 
133   if (STATprint) {
134       printf("%s stats offered %lu accepted %lu refused %lu rejected %lu\n",
135              REMhost, STAToffered, STATaccepted, STATrefused, STATrejected);
136       printf("%s times user %.3f system %.3f elapsed %.3f\n", REMhost,
137              usertime, systime, STATend - STATbegin);
138   }
139 
140   notice("%s stats offered %lu accepted %lu refused %lu rejected %lu",
141 	 REMhost, STAToffered, STATaccepted, STATrefused, STATrejected);
142   notice("%s times user %.3f system %.3f elapsed %.3f", REMhost, usertime,
143          systime, STATend - STATbegin);
144 
145   exit(x);
146   /* NOTREACHED */
147 }
148 
149 
150 /*
151 **  Clean up the NNTP escapes from a line.
152 */
153 static char *
REMclean(char * buff)154 REMclean(char *buff)
155 {
156     char	*p;
157 
158     if ((p = strchr(buff, '\r')) != NULL)
159 	*p = '\0';
160     if ((p = strchr(buff, '\n')) != NULL)
161 	*p = '\0';
162 
163     /* The dot-escape is only in text, not command responses. */
164     return buff;
165 }
166 
167 
168 /*
169 **  Read a line of input, with timeout. We expect only answer lines, so
170 **  we ignore \r\n-->\n mapping and the dot escape.
171 **  Return true if okay, *or we got interrupted.*
172 */
173 static bool
REMread(char * start,int size)174 REMread(char *start, int size)
175 {
176   char *p, *h;
177   struct timeval t;
178   fd_set rmask;
179   int i;
180 
181   for (p = start; size; ) {
182     FD_ZERO(&rmask);
183     FD_SET(FromServer, &rmask);
184     t.tv_sec = 10 * 60;
185     t.tv_usec = 0;
186     i = select(FromServer + 1, &rmask, NULL, NULL, &t);
187     if (GotInterrupt) return true;
188     if (i < 0) {
189       if (errno == EINTR) continue;
190       else return false;
191     }
192     if (i == 0 || !FD_ISSET(FromServer, &rmask)) return false;
193     i = read(FromServer, p, size-1);
194     if (GotInterrupt) return true;
195     if (i <= 0) return false;
196     h = p;
197     p += i;
198     size -= i;
199     for ( ; h < p; h++) {
200       if (h > start && '\n' == *h && '\r' == h[-1]) {
201 	*h = h[-1] = '\0';
202 	size = 0;
203       }
204     }
205   }
206 
207   if (Debug)
208     fprintf(stderr, "< %s\n", start);
209 
210   return true;
211 }
212 
213 
214 /*
215 **  Handle the interrupt.
216 */
217 static void
Interrupted(void)218 Interrupted(void)
219 {
220   warn("interrupted");
221   ExitWithStats(1);
222 }
223 
224 
225 /*
226 **  Send a whole xbatch to the server. Uses the global variables
227 **  REMbuffer & friends
228 */
229 static bool
REMsendxbatch(int fd,char * buf,int size)230 REMsendxbatch(int fd, char *buf, int size)
231 {
232   char	*p;
233   int		i;
234   int		err;
235 
236   for (i = size, p = buf; i; p += err, i -= err) {
237     err = write(fd, p, i);
238     if (err < 0) {
239       syswarn("cannot write xbatch to %s", REMhost);
240       return false;
241     }
242   }
243   if (GotInterrupt) Interrupted();
244   if (Debug)
245     fprintf(stderr, "> [%d bytes of xbatch]\n", size);
246 
247   /* What did the remote site say? */
248   if (!REMread(buf, size)) {
249     syswarn("no reply after sending xbatch");
250     return false;
251   }
252   if (GotInterrupt) Interrupted();
253 
254   /* Parse the reply. */
255   switch (atoi(buf)) {
256   default:
257     warn("unknown reply after sending batch -- %s", buf);
258     return false;
259     /* NOTREACHED */
260     break;
261   case NNTP_FAIL_XBATCH:
262   case NNTP_FAIL_TERMINATING:
263   case NNTP_FAIL_ACTION:
264     notice("%s xbatch failed %s", REMhost, buf);
265     STATrejected++;
266     return false;
267     /* NOTREACHED */
268     break;
269   case NNTP_OK_XBATCH:
270     STATaccepted++;
271     if (Debug) fprintf(stderr, "will unlink(%s)\n", XBATCHname);
272     if (unlink(XBATCHname)) {
273       /* probably another incarantion was faster, so avoid further duplicate
274        * work
275        */
276       syswarn("cannot unlink %s", XBATCHname);
277       return false;
278     }
279     break;
280   }
281 
282   /* Article sent */
283   return true;
284 }
285 
286 /*
287 **  Mark that we got interrupted.
288 */
289 static void
CATCHinterrupt(int s)290 CATCHinterrupt(int s)
291 {
292     GotInterrupt = true;
293 
294     /* Let two interrupts kill us. */
295     xsignal(s, SIG_DFL);
296 }
297 
298 
299 /*
300 **  Mark that the alarm went off.
301 */
302 static void
CATCHalarm(int s UNUSED)303 CATCHalarm(int s UNUSED)
304 {
305     GotAlarm = true;
306     if (JMPyes)
307 	longjmp(JMPwhere, 1);
308 }
309 
310 
311 /*
312 **  Print a usage message and exit.
313 */
314 static void
Usage(void)315 Usage(void)
316 {
317     warn("Usage: innxbatch [-Dv] [-t#] [-T#] host file ...");
318 #ifdef FROMSTDIN
319     warn("       innxbatch [-Dv] [-t#] [-T#] -i host");
320 #endif
321     exit(1);
322 }
323 
324 
325 int
main(int ac,char * av[])326 main(int ac, char *av[])
327 {
328   int			i;
329   char                  *p;
330   FILE			*From;
331   FILE			*To;
332   char			buff[NNTP_MAXLEN_COMMAND];
333   void	        	(*volatile old)(int) = NULL;
334   struct stat		statbuf;
335   int			fd;
336   int			err;
337   char *volatile        XBATCHbuffer = NULL;
338   char **volatile       argv;
339   volatile int		XBATCHbuffersize = 0;
340   volatile int		XBATCHsize, argc;
341   volatile unsigned int	ConnectTimeout;
342   volatile unsigned int	TotalTimeout;
343 
344   openlog("innxbatch", L_OPENLOG_FLAGS | LOG_PID, LOG_INN_PROG);
345   message_program_name = "innxbatch";
346   message_handlers_warn(1, message_log_syslog_err, message_log_stderr);
347   message_handlers_die(1, message_log_syslog_err, message_log_stderr);
348   message_handlers_notice(1, message_log_syslog_notice);
349 
350   /* Set defaults. */
351   if (!innconf_read(NULL))
352       exit(1);
353   ConnectTimeout = 0;
354   TotalTimeout = 0;
355   umask(NEWSUMASK);
356 
357   /* Parse JCL. */
358   while ((i = getopt(ac, av, "Dit:T:v")) != EOF)
359     switch (i) {
360     default:
361       Usage();
362       /* NOTREACHED */
363       break;
364     case 'D':
365       Debug = true;
366       break;
367 #ifdef FROMSTDIN
368     case 'i':
369       FromStdin = true;
370       break;
371 #endif
372     case 't':
373       ConnectTimeout = atoi(optarg);
374       break;
375     case 'T':
376       TotalTimeout = atoi(optarg);
377       break;
378     case 'v':
379       STATprint = true;
380       break;
381     }
382   ac -= optind;
383   av += optind;
384 
385   /* Parse arguments; host and filename. */
386   if (ac < 2)
387     Usage();
388   REMhost = av[0];
389   ac--;
390   av++;
391   argc = ac;
392   argv = av;
393 
394   /* Open a connection to the remote server. */
395   if (ConnectTimeout) {
396     GotAlarm = false;
397     old = xsignal(SIGALRM, CATCHalarm);
398     JMPyes = true;
399     if (setjmp(JMPwhere))
400       die("cannot connect to %s: timed out", REMhost);
401     alarm(ConnectTimeout);
402   }
403   if (NNTPconnect(REMhost, NNTP_PORT, &From, &To, buff, sizeof(buff)) < 0
404       || GotAlarm) {
405     i = errno;
406     if (GotAlarm)
407         warn("%s connect failed: timeout", REMhost);
408     else
409         syswarn("%s connect failed: %s", REMhost,
410                 buff[0] ? REMclean(buff) : strerror(i));
411     exit(1);
412   }
413 
414   if (Debug)
415     fprintf(stderr, "< %s\n", REMclean(buff));
416   if (NNTPsendpassword(REMhost, From, To) < 0 || GotAlarm) {
417     i = errno;
418     syswarn("%s authentication failed: %s", REMhost,
419             GotAlarm ? "timeout" : strerror(i));
420     /* Don't send quit; we want the remote to print a message. */
421     exit(1);
422   }
423   if (ConnectTimeout) {
424     alarm(0);
425     xsignal(SIGALRM, old);
426     JMPyes = false;
427   }
428 
429   /* We no longer need standard I/O. */
430   FromServer = fileno(From);
431   ToServer = fileno(To);
432 
433 #if	defined(SOL_SOCKET) && defined(SO_SNDBUF) && defined(SO_RCVBUF)
434   i = 24 * 1024;
435   if (setsockopt(ToServer, SOL_SOCKET, SO_SNDBUF, (char *)&i, sizeof i) < 0)
436     syswarn("cant setsockopt(SNDBUF)");
437   if (setsockopt(FromServer, SOL_SOCKET, SO_RCVBUF, (char *)&i, sizeof i) < 0)
438     syswarn("cant setsockopt(RCVBUF)");
439 #endif	/* defined(SOL_SOCKET) && defined(SO_SNDBUF) && defined(SO_RCVBUF) */
440 
441   GotInterrupt = false;
442   GotAlarm = false;
443 
444   /* Set up signal handlers. */
445   xsignal(SIGHUP, CATCHinterrupt);
446   xsignal(SIGINT, CATCHinterrupt);
447   xsignal(SIGTERM, CATCHinterrupt);
448   xsignal(SIGPIPE, SIG_IGN);
449   if (TotalTimeout) {
450     xsignal(SIGALRM, CATCHalarm);
451     alarm(TotalTimeout);
452   }
453 
454   /* Start timing. */
455   STATbegin = TMRnow_double();
456 
457   /* main loop over all specified files */
458   for (XBATCHname = *argv; argc && (XBATCHname = *argv); argv++, argc--) {
459 
460     if (Debug) fprintf(stderr, "will work on %s\n", XBATCHname);
461 
462     if (GotAlarm) {
463       warn("timed out");
464       ExitWithStats(1);
465     }
466     if (GotInterrupt) Interrupted();
467 
468     if ((fd = open(XBATCHname, O_RDONLY, 0)) < 0) {
469       syswarn("cannot open %s, skipping", XBATCHname);
470       continue;
471     }
472 
473     if (fstat(fd, &statbuf)) {
474       syswarn("cannot stat %s, skipping", XBATCHname);
475       close(i);
476       continue;
477     }
478 
479     XBATCHsize = statbuf.st_size;
480     if (XBATCHsize == 0) {
481       warn("batch file %s is zero length, skipping", XBATCHname);
482       close(i);
483       unlink(XBATCHname);
484       continue;
485     } else if (XBATCHsize > XBATCHbuffersize) {
486       XBATCHbuffersize = XBATCHsize;
487       if (XBATCHbuffer) free(XBATCHbuffer);
488       XBATCHbuffer = xmalloc(XBATCHsize);
489     }
490 
491     err = 0; /* stupid compiler */
492     for (i = XBATCHsize, p = XBATCHbuffer; i; i -= err, p+= err) {
493       err = read(fd, p, i);
494       if (err < 0) {
495         syswarn("error reading %s, skipping", XBATCHname);
496 	break;
497       } else if (0 == err) {
498         syswarn("unexpected EOF reading %s, truncated", XBATCHname);
499 	XBATCHsize = p - XBATCHbuffer;
500 	break;
501       }
502     }
503     close(fd);
504     if (err < 0)
505       continue;
506 
507     if (GotInterrupt) Interrupted();
508 
509     /* Offer the xbatch. */
510     snprintf(buff, sizeof(buff), "XBATCH %d", XBATCHsize);
511     if (!REMwrite(ToServer, buff)) {
512       syswarn("cannot offer xbatch to %s", REMhost);
513       ExitWithStats(1);
514     }
515     STAToffered++;
516     if (GotInterrupt) Interrupted();
517 
518     /* Does he want it? */
519     if (!REMread(buff, (int)sizeof buff)) {
520       syswarn("no reply to XBATCH %d from %s", XBATCHsize, REMhost);
521       ExitWithStats(1);
522     }
523     if (GotInterrupt) Interrupted();
524 
525     /* Parse the reply. */
526     switch (atoi(buff)) {
527     default:
528       warn("unknown reply to %s -- %s", XBATCHname, buff);
529       ExitWithStats(1);
530       /* NOTREACHED */
531       break;
532     case NNTP_FAIL_XBATCH:
533     case NNTP_FAIL_TERMINATING:
534     case NNTP_FAIL_ACTION:
535       /* Most likely out of space -- no point in continuing. */
536       notice("%s xbatch failed %s", REMhost, buff);
537       ExitWithStats(1);
538       /* NOTREACHED */
539     case NNTP_CONT_XBATCH:
540       if (!REMsendxbatch(ToServer, XBATCHbuffer, XBATCHsize))
541 	ExitWithStats(1);
542       /* NOTREACHED */
543       break;
544     case NNTP_ERR_SYNTAX:
545     case NNTP_ERR_COMMAND:
546       warn("%s xbatch failed %s", REMhost, buff);
547       break;
548     }
549   }
550   ExitWithStats(0);
551   /* NOTREACHED */
552   return 0;
553 }
554