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