chiark / gitweb /
New state diagrams whitespace changes
[innduct.git] / backends / innxbatch.c
1 /*  $Id: innxbatch.c 6351 2003-05-19 02:00:06Z rra $
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 "portable/time.h"
35 #include <ctype.h>
36 #include <errno.h>
37 #include <fcntl.h>
38 #include <setjmp.h>
39 #include <signal.h>
40 #include <syslog.h>
41 #include <sys/stat.h>
42
43 /* Needed on AIX 4.1 to get fd_set and friends. */
44 #ifdef HAVE_SYS_SELECT_H
45 # include <sys/select.h>
46 #endif
47
48 #include "inn/innconf.h"
49 #include "inn/messages.h"
50 #include "inn/timer.h"
51 #include "libinn.h"
52 #include "nntp.h"
53
54 /*
55 ** Syslog formats - collected together so they remain consistent
56 */
57 static char     STAT1[] =
58         "%s stats offered %lu accepted %lu refused %lu rejected %lu";
59 static char     STAT2[] = "%s times user %.3f system %.3f elapsed %.3f";
60 static char     CANT_CONNECT[] = "%s connect failed %s";
61 static char     CANT_AUTHENTICATE[] = "%s authenticate failed %s";
62 static char     XBATCH_FAIL[] = "%s xbatch failed %s";
63 static char     UNKNOWN_REPLY[] = "Unknown reply after sending batch -- %s";
64 static char     CANNOT_UNLINK[] = "cannot unlink %s: %m";
65 /*
66 **  Global variables.
67 */
68 static bool             Debug = 0;
69 static bool             STATprint;
70 static char             *REMhost;
71 static double           STATbegin;
72 static double           STATend;
73 static char             *XBATCHname;
74 static int              FromServer;
75 static int              ToServer;
76 static sig_atomic_t     GotAlarm;
77 static sig_atomic_t     GotInterrupt;
78 static sig_atomic_t     JMPyes;
79 static jmp_buf          JMPwhere;
80 static unsigned long    STATaccepted;
81 static unsigned long    STAToffered;
82 static unsigned long    STATrefused;
83 static unsigned long    STATrejected;
84
85 /*
86 **  Send a line to the server. \r\n will be appended
87 */
88 static bool
89 REMwrite(int fd, char *p)
90 {
91   int           i;
92   int           err;
93   char          *dest;
94   static char           buff[NNTP_STRLEN];
95
96   for (dest = buff, i = 0; p[i]; ) *dest++ = p[i++];
97   *dest++ = '\r';
98   *dest++ = '\n';
99   *dest++ = '\0';
100
101   for (dest = buff, i+=2; i; dest += err, i -= err) {
102     err = write(fd, dest, i);
103     if (err < 0) {
104       syswarn("cannot write %s to %s", dest, REMhost);
105       return false;
106     }
107   }
108   if (Debug)
109     fprintf(stderr, "> %s\n", p);
110
111   return true;
112 }
113
114 /*
115 **  Print transfer statistics, clean up, and exit.
116 */
117 static void
118 ExitWithStats(int x)
119 {
120   static char           QUIT[] = "quit";
121   double                usertime;
122   double                systime;
123
124   REMwrite(ToServer, QUIT);
125
126   STATend = TMRnow_double();
127   if (GetResourceUsage(&usertime, &systime) < 0) {
128     usertime = 0;
129     systime = 0;
130   }
131
132   if (STATprint) {
133     printf(STAT1,
134         REMhost, STAToffered, STATaccepted, STATrefused, STATrejected);
135     printf("\n");
136     printf(STAT2, REMhost, usertime, systime, STATend - STATbegin);
137     printf("\n");
138   }
139
140   syslog(L_NOTICE, STAT1,
141          REMhost, STAToffered, STATaccepted, STATrefused, STATrejected);
142   syslog(L_NOTICE, STAT2, REMhost, usertime, systime, STATend - STATbegin);
143
144   exit(x);
145   /* NOTREACHED */
146 }
147
148
149 /*
150 **  Clean up the NNTP escapes from a line.
151 */
152 static char *
153 REMclean(char *buff)
154 {
155     char        *p;
156
157     if ((p = strchr(buff, '\r')) != NULL)
158         *p = '\0';
159     if ((p = strchr(buff, '\n')) != NULL)
160         *p = '\0';
161
162     /* The dot-escape is only in text, not command responses. */
163     return buff;
164 }
165
166
167 /*
168 **  Read a line of input, with timeout. We expect only answer lines, so
169 **  we ignore \r\n-->\n mapping and the dot escape.
170 **  Return true if okay, *or we got interrupted.*
171 */
172 static bool
173 REMread(char *start, int size)
174 {
175   char *p, *h;
176   struct timeval t;
177   fd_set rmask;
178   int i;
179
180   for (p = start; size; ) {
181     FD_ZERO(&rmask);
182     FD_SET(FromServer, &rmask);
183     t.tv_sec = 10 * 60;
184     t.tv_usec = 0;
185     i = select(FromServer + 1, &rmask, NULL, NULL, &t);
186     if (GotInterrupt) return true;
187     if (i < 0) {
188       if (errno == EINTR) continue;
189       else return false;
190     }
191     if (i == 0 || !FD_ISSET(FromServer, &rmask)) return false;
192     i = read(FromServer, p, size-1);
193     if (GotInterrupt) return true;
194     if (i <= 0) return false;
195     h = p;
196     p += i;
197     size -= i;
198     for ( ; h < p; h++) {
199       if (h > start && '\n' == *h && '\r' == h[-1]) {
200         *h = h[-1] = '\0';
201         size = 0;
202       }
203     }
204   }
205
206   if (Debug)
207     fprintf(stderr, "< %s\n", start);
208
209   return true;
210 }
211
212
213 /*
214 **  Handle the interrupt.
215 */
216 static void
217 Interrupted(void)
218 {
219   warn("interrupted");
220   ExitWithStats(1);
221 }
222
223
224 /*
225 **  Send a whole xbatch to the server. Uses the global variables
226 **  REMbuffer & friends
227 */
228 static bool
229 REMsendxbatch(int fd, char *buf, int size)
230 {
231   char  *p;
232   int           i;
233   int           err;
234
235   for (i = size, p = buf; i; p += err, i -= err) {
236     err = write(fd, p, i);
237     if (err < 0) {
238       syswarn("cannot write xbatch to %s", REMhost);
239       return false;
240     }
241   }
242   if (GotInterrupt) Interrupted();
243   if (Debug)
244     fprintf(stderr, "> [%d bytes of xbatch]\n", size);
245
246   /* What did the remote site say? */
247   if (!REMread(buf, size)) {
248     syswarn("no reply after sending xbatch");
249     return false;
250   }
251   if (GotInterrupt) Interrupted();
252   
253   /* Parse the reply. */
254   switch (atoi(buf)) {
255   default:
256     warn("unknown reply after sending batch -- %s", buf);
257     syslog(L_ERROR, UNKNOWN_REPLY, buf);
258     return false;
259     /* NOTREACHED */
260     break;
261   case NNTP_RESENDIT_VAL:
262   case NNTP_GOODBYE_VAL:
263     syslog(L_NOTICE, XBATCH_FAIL, REMhost, buf);
264     STATrejected++;
265     return false;
266     /* NOTREACHED */
267     break;
268   case NNTP_OK_XBATCHED_VAL:
269     STATaccepted++;
270     if (Debug) fprintf(stderr, "will unlink(%s)\n", XBATCHname);
271     if (unlink(XBATCHname)) {
272       /* probably another incarantion was faster, so avoid further duplicate
273        * work
274        */
275       syswarn("cannot unlink %s", XBATCHname);
276       syslog(L_NOTICE, CANNOT_UNLINK, 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 RETSIGTYPE
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 /* ARGSUSED0 */
303 static RETSIGTYPE
304 CATCHalarm(int s UNUSED)
305 {
306     GotAlarm = true;
307     if (JMPyes)
308         longjmp(JMPwhere, 1);
309 }
310
311
312 /*
313 **  Print a usage message and exit.
314 */
315 static void
316 Usage(void)
317 {
318     warn("Usage: innxbatch [-Dv] [-t#] [-T#] host file ...");
319 #ifdef FROMSTDIN
320     warn("       innxbatch [-Dv] [-t#] [-T#] -i host");
321 #endif
322     exit(1);
323 }
324
325
326 int
327 main(int ac, char *av[])
328 {
329   int                   i;
330   char                  *p;
331   FILE                  *From;
332   FILE                  *To;
333   char                  buff[NNTP_STRLEN];
334   RETSIGTYPE            (*old)(int) = NULL;
335   unsigned int          ConnectTimeout;
336   unsigned int          TotalTimeout;
337   struct stat           statbuf;
338   int                   fd;
339   int                   err;
340   char                  *XBATCHbuffer = NULL;
341   int                   XBATCHbuffersize = 0;
342   int                   XBATCHsize;
343
344   openlog("innxbatch", L_OPENLOG_FLAGS | LOG_PID, LOG_INN_PROG);
345   message_program_name = "innxbatch";
346
347   /* Set defaults. */
348   if (!innconf_read(NULL))
349       exit(1);
350   ConnectTimeout = 0;
351   TotalTimeout = 0;
352   umask(NEWSUMASK);
353
354   /* Parse JCL. */
355   while ((i = getopt(ac, av, "Dit:T:v")) != EOF)
356     switch (i) {
357     default:
358       Usage();
359       /* NOTREACHED */
360       break;
361     case 'D':
362       Debug++;
363       break;
364 #ifdef FROMSTDIN
365     case 'i':
366       FromStdin = true;
367       break;
368 #endif
369     case 't':
370       ConnectTimeout = atoi(optarg);
371       break;
372     case 'T':
373       TotalTimeout = atoi(optarg);
374       break;
375     case 'v':
376       STATprint = true;
377       break;
378     }
379   ac -= optind;
380   av += optind;
381
382   /* Parse arguments; host and filename. */
383   if (ac < 2)
384     Usage();
385   REMhost = av[0];
386   ac--;
387   av++;
388
389   /* Open a connection to the remote server. */
390   if (ConnectTimeout) {
391     GotAlarm = false;
392     old = xsignal(SIGALRM, CATCHalarm);
393     JMPyes = true;
394     if (setjmp(JMPwhere))
395       die("cannot connect to %s: timed out", REMhost);
396     alarm(ConnectTimeout);
397   }
398   if (NNTPconnect(REMhost, NNTP_PORT, &From, &To, buff) < 0 || GotAlarm) {
399     i = errno;
400     warn("cannot connect to %s: %s", REMhost,
401          buff[0] ? REMclean(buff): strerror(errno));
402     if (GotAlarm)
403       syslog(L_NOTICE, CANT_CONNECT, REMhost, "timeout");
404     else
405       syslog(L_NOTICE, CANT_CONNECT, REMhost,
406              buff[0] ? REMclean(buff) : strerror(i));
407     exit(1);
408   }
409
410   if (Debug)
411     fprintf(stderr, "< %s\n", REMclean(buff));
412   if (NNTPsendpassword(REMhost, From, To) < 0 || GotAlarm) {
413     i = errno;
414     syswarn("cannot authenticate with %s", REMhost);
415     syslog(L_ERROR, CANT_AUTHENTICATE,
416            REMhost, GotAlarm ? "timeout" : strerror(i));
417     /* Don't send quit; we want the remote to print a message. */
418     exit(1);
419   }
420   if (ConnectTimeout) {
421     alarm(0);
422     xsignal(SIGALRM, old);
423     JMPyes = false;
424   }
425   
426   /* We no longer need standard I/O. */
427   FromServer = fileno(From);
428   ToServer = fileno(To);
429
430 #if     defined(SOL_SOCKET) && defined(SO_SNDBUF) && defined(SO_RCVBUF)
431   i = 24 * 1024;
432   if (setsockopt(ToServer, SOL_SOCKET, SO_SNDBUF, (char *)&i, sizeof i) < 0)
433     perror("cant setsockopt(SNDBUF)");
434   if (setsockopt(FromServer, SOL_SOCKET, SO_RCVBUF, (char *)&i, sizeof i) < 0)
435     perror("cant setsockopt(RCVBUF)");
436 #endif  /* defined(SOL_SOCKET) && defined(SO_SNDBUF) && defined(SO_RCVBUF) */
437
438   GotInterrupt = false;
439   GotAlarm = false;
440
441   /* Set up signal handlers. */
442   xsignal(SIGHUP, CATCHinterrupt);
443   xsignal(SIGINT, CATCHinterrupt);
444   xsignal(SIGTERM, CATCHinterrupt);
445   xsignal(SIGPIPE, SIG_IGN);
446   if (TotalTimeout) {
447     xsignal(SIGALRM, CATCHalarm);
448     alarm(TotalTimeout);
449   }
450
451   /* Start timing. */
452   STATbegin = TMRnow_double();
453
454   /* main loop over all specified files */
455   for (XBATCHname = *av; ac && (XBATCHname = *av); av++, ac--) {
456
457     if (Debug) fprintf(stderr, "will work on %s\n", XBATCHname);
458
459     if (GotAlarm) {
460       warn("timed out");
461       ExitWithStats(1);
462     }
463     if (GotInterrupt) Interrupted();
464
465     if ((fd = open(XBATCHname, O_RDONLY, 0)) < 0) {
466       syswarn("cannot open %s, skipping", XBATCHname);
467       continue;
468     }
469
470     if (fstat(fd, &statbuf)) {
471       syswarn("cannot stat %s, skipping", XBATCHname);
472       close(i);
473       continue;
474     }
475
476     XBATCHsize = statbuf.st_size;
477     if (XBATCHsize == 0) {
478       warn("batch file %s is zero length, skipping", XBATCHname);
479       close(i);
480       unlink(XBATCHname);
481       continue;
482     } else if (XBATCHsize > XBATCHbuffersize) {
483       XBATCHbuffersize = XBATCHsize;
484       if (XBATCHbuffer) free(XBATCHbuffer);
485       XBATCHbuffer = xmalloc(XBATCHsize);
486     }
487
488     err = 0; /* stupid compiler */
489     for (i = XBATCHsize, p = XBATCHbuffer; i; i -= err, p+= err) {
490       err = read(fd, p, i);
491       if (err < 0) {
492         syswarn("error reading %s, skipping", XBATCHname);
493         break;
494       } else if (0 == err) {
495         syswarn("unexpected EOF reading %s, truncated", XBATCHname);
496         XBATCHsize = p - XBATCHbuffer;
497         break;
498       }
499     }
500     close(fd);
501     if (err < 0)
502       continue;
503
504     if (GotInterrupt) Interrupted();
505
506     /* Offer the xbatch. */
507     snprintf(buff, sizeof(buff), "xbatch %d", XBATCHsize);
508     if (!REMwrite(ToServer, buff)) {
509       syswarn("cannot offer xbatch to %s", REMhost);
510       ExitWithStats(1);
511     }
512     STAToffered++;
513     if (GotInterrupt) Interrupted();
514
515     /* Does he want it? */
516     if (!REMread(buff, (int)sizeof buff)) {
517       syswarn("no reply to XBATCH %d from %s", XBATCHsize, REMhost);
518       ExitWithStats(1);
519     }
520     if (GotInterrupt) Interrupted();
521
522     /* Parse the reply. */
523     switch (atoi(buff)) {
524     default:
525       warn("unknown reply to %s -- %s", XBATCHname, buff);
526       ExitWithStats(1);
527       /* NOTREACHED */
528       break;
529     case NNTP_RESENDIT_VAL:
530     case NNTP_GOODBYE_VAL:
531       /* Most likely out of space -- no point in continuing. */
532       syslog(L_NOTICE, XBATCH_FAIL, REMhost, buff);
533       ExitWithStats(1);
534       /* NOTREACHED */
535     case NNTP_CONT_XBATCH_VAL:
536       if (!REMsendxbatch(ToServer, XBATCHbuffer, XBATCHsize))
537         ExitWithStats(1);
538       /* NOTREACHED */
539       break;
540     case NNTP_SYNTAX_VAL:
541     case NNTP_BAD_COMMAND_VAL:
542       warn("server %s seems not to understand XBATCH: %s", REMhost, buff);
543       syslog(L_FATAL, XBATCH_FAIL, REMhost, buff);
544       break;
545     }
546   }
547   ExitWithStats(0);
548   /* NOTREACHED */
549   return 0;
550 }