e22d84acf8cae4f752d3ee5ba114c73e50d39518
[spider.git] / src / client.c
1 /*
2  * C Client for the DX Spider cluster program
3  *
4  * Eventually this program will be a complete replacement
5  * for the perl version.
6  *
7  * This program provides the glue necessary to talk between
8  * an input (eg from telnet or ax25) and the perl DXSpider
9  * node.
10  *
11  * Currently, this program connects STDIN/STDOUT to the
12  * message system used by cluster.pl
13  *
14  * Copyright (c) 2000 Dirk Koopman G1TLH
15  *
16  * $Id$
17  */
18
19 #include <stdio.h>
20 #include <sys/time.h>
21 #include <sys/types.h>
22 #include <unistd.h>
23 #include <ctype.h>
24 #include <stdlib.h>
25 #include <stdarg.h>
26 #include <netdb.h>
27 #include <sys/socket.h>
28 #include <netinet/in.h>
29 #include <errno.h>
30 #include <signal.h>
31 #include <string.h>
32 #include <termios.h>
33 #include <regex.h>
34
35 #include "sel.h"
36 #include "cmsg.h"
37 #include "debug.h"
38
39 #define TEXT 1
40 #define MSG 2
41 #define MAXBUFL 1024
42
43 #ifndef MAXPATHLEN 
44 #define MAXPATHLEN 256
45 #endif
46
47 #define DEFPACLEN 128
48 #define MAXPACLEN 236
49
50 #define DBUF 1
51 #define DMSG 2
52
53 typedef struct 
54 {
55         int cnum;                                       /* the connection number */
56         int sort;                                       /* the type of connection either text or msg */
57         cmsg_t *in;                                     /* current input message being built up */
58         cmsg_t *out;                            /* current output message being sent */
59         cmsg_t *obuf;                           /* current output being buffered */
60         reft *inq;                                      /* input queue */
61         reft *outq;                                     /* output queue */
62         sel_t *sp;                                      /* my select fcb address */
63         struct termios t;                       /* any termios associated with this cnum */
64         char echo;                                      /* echo characters back to this cnum */
65         char t_set;                                     /* the termios structure is valid */
66         char buffer_it;                         /* buffer outgoing packets for paclen */
67 } fcb_t;
68
69 typedef struct 
70 {
71         char *in;
72         regex_t *regex;
73 } myregex_t;
74
75
76 char *node_addr = "localhost";  /* the node tcp address, can be overridden by DXSPIDER_HOST */
77 int node_port = 27754;                  /* the tcp port of the node at the above address can be overidden by DXSPIDER_PORT*/
78 char *call;                                             /* the caller's callsign */
79 char *connsort;                                 /* the type of connection */
80 fcb_t *in;                                              /* the fcb of 'stdin' that I shall use */
81 fcb_t *node;                                    /* the fcb of the msg system */
82 char nl = '\n';                                 /* line end character */
83 char ending = 0;                                /* set this to end the program */
84 char send_Z = 1;                                /* set a Z record to the node on termination */
85 char echo = 1;                                  /* echo characters on stdout from stdin */
86 char int_tabs = 0;                              /* interpret tabs -> spaces */
87 char *root = "/spider";         /* root of data tree, can be overridden by DXSPIDER_ROOT  */
88 int timeout = 60;                               /* default timeout for logins and things */
89 int paclen = DEFPACLEN;                 /* default buffer size for outgoing packets */
90 int tabsize = 8;                                /* default tabsize for text messages */
91
92 myregex_t iscallreg[] = {               /* regexes to determine whether this is a reasonable callsign */
93         {
94                 "^[A-Z]+[0-9]+[A-Z]+", 0
95         },
96         {
97                 "^[0-9]+[A-Z]+[0-9]+[A-Z]+", 0
98         },
99         {
100                 0, 0
101         }
102 };
103
104 void terminate(int);
105
106 /*
107  * utility routines - various
108  */
109
110 void die(char *s, ...)
111 {
112         char buf[2000];
113         
114         va_list ap;
115         va_start(ap, s);
116         vsnprintf(buf, sizeof(buf)-1, s, ap);
117         va_end(ap);
118         fprintf(stderr,"%s\n", buf);
119         terminate(-1);
120 }
121
122 char *strupper(char *s)
123 {
124         char *d = malloc(strlen(s)+1);
125         char *p = d;
126         
127         if (!d)
128                 die("out of room in strupper");
129         while (*p++ = toupper(*s++)) ;
130         return d;
131 }
132
133 char *strlower(char *s)
134 {
135         char *d = malloc(strlen(s)+1);
136         char *p = d;
137         
138         if (!d)
139                 die("out of room in strlower");
140         while (*p++ = tolower(*s++)) ;
141         return d;
142 }
143
144 int eq(char *a, char *b)
145 {
146         return (strcmp(a, b) == 0);
147 }
148
149 int xopen(char *dir, char *name, int mode)
150 {
151         char fn[MAXPATHLEN+1];
152         snprintf(fn, MAXPATHLEN, "%s/%s/%s", root, dir, name);
153         return open(fn, mode);
154 }
155
156 int iscallsign(char *s)
157 {
158         myregex_t *rp;
159         for (rp = iscallreg; rp->in; ++rp) {
160                 if (regexec(rp->regex, s, 0, 0, 0) == 0)
161                         return 1;
162         }
163         return 0;
164 }
165
166 /*
167  * higher level send and receive routines
168  */
169
170 fcb_t *fcb_new(int cnum, int sort)
171 {
172         fcb_t *f = malloc(sizeof(fcb_t));
173         if (!f)
174                 die("no room in fcb_new");
175         memset (f, 0, sizeof(fcb_t));
176         f->cnum = cnum;
177         f->sort = sort;
178         f->inq = chain_new();
179         f->outq = chain_new();
180         return f;
181 }
182
183 void flush_text(fcb_t *f)
184 {
185         if (f->obuf) {
186                 cmsg_send(f->outq, f->obuf, 0);
187                 f->sp->flags |= SEL_OUTPUT;
188                 f->obuf = 0;
189         }
190 }
191
192 void send_text(fcb_t *f, char *s, int l)
193 {
194         cmsg_t *mp;
195         char *p;
196         
197         if (f->buffer_it && f->obuf) {
198                 mp = f->obuf;
199         } else {
200                 f->obuf = mp = cmsg_new(paclen+1, f->sort, f);
201         }
202
203         for (p = s; p < s+l; ) {
204                 if (mp->inp >= mp->data + paclen) {
205                         flush_text(f);
206                         f->obuf = mp = cmsg_new(paclen+1, f->sort, f);
207                 }
208                 *mp->inp++ = *p++;
209         }
210         if (mp->inp >= mp->data + paclen) {
211                 flush_text(f);
212                 f->obuf = mp = cmsg_new(paclen+1, f->sort, f);
213         }
214         *mp->inp++ = nl;
215         if (!f->buffer_it)
216                 flush_text(f);
217 }
218
219 void send_msg(fcb_t *f, char let, char *s, int l)
220 {
221         cmsg_t *mp;
222         int ln;
223         int myl = strlen(call)+2+l;
224
225         mp = cmsg_new(myl+4+1, f->sort, f);
226         ln = htonl(myl);
227         memcpy(mp->inp, &ln, 4);
228         mp->inp += 4;
229         *mp->inp++ = let;
230         strcpy(mp->inp, call);
231         mp->inp += strlen(call);
232         *mp->inp++ = '|';
233         if (l > 0) {
234                 memcpy(mp->inp, s, l);
235                 mp->inp += l;
236         }
237         *mp->inp = 0;
238         cmsg_send(f->outq, mp, 0);
239         f->sp->flags |= SEL_OUTPUT;
240 }
241
242 /*
243  * the callback (called by sel_run) that handles all the inputs and outputs
244  */
245
246 int fcb_handler(sel_t *sp, int in, int out, int err)
247 {
248         fcb_t *f = sp->fcb;
249         cmsg_t *mp, *omp;
250         
251         /* input modes */
252         if (in) {
253                 char *p, buf[MAXBUFL];
254                 int r;
255
256                 /* read what we have into a buffer */
257                 r = read(f->cnum, buf, MAXBUFL);
258                 if (r < 0) {
259                         switch (errno) {
260                         case EINTR:
261                         case EINPROGRESS:
262                         case EAGAIN:
263                                 goto lout;
264                         default:
265                                 if (f->sort == MSG)
266                                         send_Z = 0;
267                                 ending++;
268                                 return 0;
269                         }
270                 } else if (r == 0) {
271                         if (f->sort == MSG)
272                                 send_Z = 0;
273                         ending++;
274                         return 0;
275                 }
276
277                 dbgdump(DBUF, "in ->", buf, r);
278                 
279                 /* create a new message buffer if required */
280                 if (!f->in)
281                         f->in = cmsg_new(MAXBUFL+1, f->sort, f);
282                 mp = f->in;
283
284                 switch (f->sort) {
285                 case TEXT:
286                         p = buf;
287                         if (f->echo)
288                                 omp = cmsg_new(3*r+1, f->sort, f);
289                         while (r > 0 && p < &buf[r]) {
290
291                                 /* echo processing */
292                                 if (f->echo) {
293                                         switch (*p) {
294                                         case '\b':
295                                         case 0x7f:
296                                                 strcpy(omp->inp, "\b \b");
297                                                 omp->inp += strlen(omp->inp);
298                                                 break;
299                                         default:
300                                                 *omp->inp++ = *p;
301                                         }
302                                 }
303                                 
304                                 /* character processing */
305                                 switch (*p) {
306                                 case '\t':
307                                         if (int_tabs) {
308                                                 memset(mp->inp, ' ', tabsize);
309                                                 mp->inp += tabsize;
310                                                 ++p;
311                                         } else {
312                                                 *mp->inp++ = *p++;
313                                         }
314                                         break;
315                                 case '\b':
316                                 case 0x7f:
317                                         if (mp->inp > mp->data)
318                                                 mp->inp--;
319                                         ++p;
320                                         break;
321                                 default:
322                                         if (nl == '\n' && *p == '\r') {   /* ignore \r in telnet mode (ugh) */
323                                                 p++;
324                                         } else if (*p == nl) {
325                                                 if (mp->inp == mp->data)
326                                                         *mp->inp++ = ' ';
327                                                 *mp->inp = 0;              /* zero terminate it, but don't include it in the length */
328                                                 dbgdump(DMSG, "QUEUE TEXT", mp->data, mp->inp-mp->data);
329                                                 cmsg_send(f->inq, mp, 0);
330                                                 f->in = mp = cmsg_new(MAXBUFL+1, f->sort, f);
331                                                 ++p;
332                                         } else {
333                                                 if (mp->inp < &mp->data[MAXBUFL-8])
334                                                         *mp->inp++ = *p++;
335                                                 else {
336                                                         mp->inp = mp->data;
337                                                 }
338                                         }
339                                 }
340                         }
341                         
342                         /* queue any echo text */
343                         if (f->echo) {
344                                 dbgdump(DMSG, "QUEUE ECHO TEXT", omp->data, omp->inp - omp->data);
345                                 cmsg_send(f->outq, omp, 0);
346                                 f->sp->flags |= SEL_OUTPUT;
347                         }
348                         
349                         break;
350
351                 case MSG:
352                         p = buf;
353                         while (r > 0 && p < &buf[r]) {
354
355                                 /* build up the size into the likely message length (yes I know it's a short) */
356                                 switch (mp->state) {
357                                 case 0:
358                                 case 1:
359                                         mp->state++;
360                                         break;
361                                 case 2:
362                                 case 3:
363                                         mp->size = (mp->size << 8) | (*p++ & 0xff);
364                                         if (mp->size > MAXBUFL)
365                                                 die("Message size too big from node (%d > %d)", mp->size, MAXBUFL);
366                                         mp->state++;
367                                         break;
368                                 default:
369                                         if (mp->inp - mp->data < mp->size) {
370                                                 *mp->inp++ = *p++;
371                                         } 
372                                         if (mp->inp - mp->data >= mp->size) {
373                                                 /* kick it upstairs */
374                                                 dbgdump(DMSG, "QUEUE MSG", mp->data, mp->inp - mp->data);
375                                                 cmsg_send(f->inq, mp, 0);
376                                                 mp = f->in = cmsg_new(MAXBUFL+1, f->sort, f);
377                                         }
378                                 }
379                         }
380                         break;
381                         
382                 default:
383                         die("invalid sort (%d) in input handler", f->sort);
384                 }
385         }
386         
387         /* output modes */
388 lout:;
389         if (out) {
390                 int l, r;
391                 
392                 if (!f->out) {
393                         mp = f->out = cmsg_next(f->outq);
394                         if (!mp) {
395                                 sp->flags &= ~SEL_OUTPUT;
396                                 return 0;
397                         }
398                         mp->inp = mp->data;
399                 }
400                 l = mp->size - (mp->inp - mp->data);
401                 if (l > 0) {
402                         
403                         dbgdump(DBUF, "<-out", mp->inp, l);
404                         
405                         r = write(f->cnum, mp->inp, l);
406                         if (r < 0) {
407                                 switch (errno) {
408                                 case EINTR:
409                                 case EINPROGRESS:
410                                 case EAGAIN:
411                                         goto lend;
412                                 default:
413                                         if (f->sort == MSG)
414                                                 send_Z = 0;
415                                         ending++;
416                                         return;
417                                 }
418                         } else if (r > 0) {
419                                 mp->inp += r;
420                         }
421                 } else if (l < 0) 
422                         die("got negative length in handler on node");
423                 if (mp->inp - mp->data >= mp->size) {
424                         cmsg_callback(mp, 0);
425                         f->out = 0;
426                 }
427         }
428 lend:;
429         return 0;
430 }
431
432 /*
433  * things to do with initialisation
434  */
435
436 void initargs(int argc, char *argv[])
437 {
438         int i, c, err = 0;
439
440         while ((c = getopt(argc, argv, "h:p:x:")) > 0) {
441                 switch (c) {
442                 case 'h':
443                         node_addr = optarg;
444                         break;
445                 case 'l':
446                         paclen = atoi(optarg);
447                         if (paclen < 80)
448                                 paclen = 80;
449                         if (paclen > MAXPACLEN)
450                                 paclen = MAXPACLEN;
451                         break;
452                 case 'p':
453                         node_port = atoi(optarg);
454                         break;
455                 case 'x':
456                         dbginit("client");
457                         dbgset(atoi(optarg));
458                         break;
459                 default:
460                         ++err;
461                         goto lerr;
462                 }
463         }
464
465 lerr:
466         if (err) {
467                 die("usage: client [-x n|-h<host>|-p<port>|-l<paclen>] <call>|login [local|telnet|ax25]");
468         }
469         
470         if (optind < argc) {
471                 call = strupper(argv[optind]);
472                 ++optind;
473         }
474         if (!call)
475                 die("Must have at least a callsign (for now)");
476
477         if (optind < argc) {
478                 connsort = strlower(argv[optind]);
479                 if (eq(connsort, "telnet") || eq(connsort, "local")) {
480                         nl = '\n';
481                         echo = 1;
482                 } else if (eq(connsort, "ax25")) {
483                         nl = '\r';
484                         echo = 0;
485                 } else {
486                         die("2nd argument must be \"telnet\" or \"ax25\" or \"local\"");
487                 }
488         } else {
489                 connsort = "local";
490                 nl = '\n';
491                 echo = 1;
492         }
493
494         /* this is kludgy, but hey so is the rest of this! */
495         if (!eq(connsort, "ax25") && paclen == DEFPACLEN) {
496                 paclen = MAXPACLEN;
497         }
498 }
499
500 void connect_to_node()
501 {
502         struct hostent *hp, *gethostbyname();
503         struct sockaddr_in server;
504         int nodef;
505         sel_t *sp;
506                                 
507         if ((hp = gethostbyname(node_addr)) == 0) 
508                 die("Unknown host tcp host %s for printer", node_addr);
509
510         memset(&server, 0, sizeof server);
511         server.sin_family = AF_INET;
512         memcpy(&server.sin_addr, hp->h_addr, hp->h_length);
513         server.sin_port = htons(node_port);
514                                                 
515         nodef = socket(AF_INET, SOCK_STREAM, 0);
516         if (nodef < 0) 
517                 die("Can't open socket to %s port %d (%d)", node_addr, node_port, errno);
518
519         if (connect(nodef, (struct sockaddr *) &server, sizeof server) < 0) {
520                 die("Error on connect to %s port %d (%d)", node_addr, node_port, errno);
521         }
522         node = fcb_new(nodef, MSG);
523         node->sp = sel_open(nodef, node, "Msg System", fcb_handler, MSG, SEL_INPUT);
524         
525 }
526
527 /*
528  * things to do with going away
529  */
530
531 void term_timeout(int i)
532 {
533         /* none of this is going to be reused so don't bother cleaning up properly */
534         if (in && in->t_set)
535                 tcsetattr(0, TCSANOW, &in->t);
536         if (node) {
537                 close(node->cnum);
538         }
539         exit(i);
540 }
541
542 void terminate(int i)
543 {
544         if (node && send_Z && call) {
545                 send_msg(node, 'Z', "", 0);
546         }
547         
548         signal(SIGALRM, term_timeout);
549         alarm(10);
550         
551         while ((in && !is_chain_empty(in->outq)) ||
552                    (node && !is_chain_empty(node->outq))) {
553                 sel_run();
554         }
555         if (in && in->t_set)
556                 tcsetattr(0, TCSADRAIN, &in->t);
557         if (node) 
558                 close(node->cnum);
559         exit(i);
560 }
561
562 void login_timeout(int i)
563 {
564         write(0, "Timed Out", 10);
565         write(0, &nl, 1);
566         sel_run();                                      /* force a coordination */
567         if (in && in->t_set)
568                 tcsetattr(0, TCSANOW, &in->t);
569         exit(i);
570 }
571
572 /*
573  * things to do with ongoing processing of inputs
574  */
575
576 void process_stdin()
577 {
578         cmsg_t *mp = cmsg_next(in->inq);
579         if (mp) {
580                 dbg(DMSG, "MSG size: %d", mp->size);
581         
582                 if (mp->size > 0 && mp->inp > mp->data) {
583                         send_msg(node, 'I', mp->data, mp->size);
584                 }
585                 cmsg_callback(mp, 0);
586         }
587 }
588
589 void process_node()
590 {
591         cmsg_t *mp = cmsg_next(node->inq);
592         if (mp) {
593                 dbg(DMSG, "MSG size: %d", mp->size);
594         
595                 if (mp->size > 0 && mp->inp > mp->data) {
596                         char *p = strchr(mp->data, '|');
597                         if (p)
598                                 p++;
599                         switch (mp->data[0]) {
600                         case 'Z':
601                                 send_Z = 0;
602                                 ending++;
603                                 return;
604                         case 'E':
605                                 if (isdigit(*p))
606                                         in->echo = *p - '0';
607                                 break;
608                         case 'B':
609                                 if (isdigit(*p))
610                                         in->buffer_it = *p - '0';
611                                 break;
612                         case 'D':
613                                 if (p) {
614                                         int l = mp->inp - (unsigned char *) p;
615                                         send_text(in, p, l);
616                                 }
617                                 break;
618                         default:
619                                 break;
620                         }
621                 }
622                 cmsg_callback(mp, 0);
623         } else {
624                 flush_text(in);
625         }
626 }
627
628 /*
629  * the program itself....
630  */
631
632 main(int argc, char *argv[])
633 {
634         /* set up environment */
635         {
636                 char *p = getenv("DXSPIDER_ROOT");
637                 if (p)
638                         root = p;
639                 p = getenv("DXSPIDER_HOST");
640                 if (p)
641                         node_addr = p;
642                 p = getenv("DXSPIDER_PORT");
643                 if (p)
644                         node_port = atoi(p);
645                 p = getenv("DXSPIDER_PACLEN");
646                 if (p) {
647                         paclen = atoi(p);
648                         if (paclen < 80)
649                                 paclen = 80;
650                         if (paclen > MAXPACLEN)
651                                 paclen = MAXPACLEN;
652                 }
653         }
654         
655         /* get program arguments, initialise stuff */
656         initargs(argc, argv);
657         sel_init(10, 0, 10000);
658
659         /* trap signals */
660         signal(SIGHUP, SIG_IGN);
661         signal(SIGINT, terminate);
662         signal(SIGQUIT, terminate);
663         signal(SIGTERM, terminate);
664 #ifdef SIGPWR
665         signal(SIGPWR, terminate);
666 #endif
667
668         /* compile regexes for iscallsign */
669         {
670                 myregex_t *rp;
671                 for (rp = iscallreg; rp->in; ++rp) {
672                         regex_t reg;
673                         int r = regcomp(&reg, rp->in, REG_EXTENDED|REG_ICASE|REG_NOSUB);
674                         if (r)
675                                 die("regcomp returned %d for '%s'", r, rp->in);
676                         rp->regex = malloc(sizeof(regex_t));
677                         if (!rp->regex)
678                                 die("out of room - compiling regexes");
679                         *rp->regex = reg;
680                 }
681         }
682         
683         /* is this a login? */
684         if (eq(call, "LOGIN")) {
685                 char buf[MAXPACLEN+1];
686                 int r;
687                 int f = xopen("data", "issue", 0);
688                 if (f > 0) {
689                         while ((r = read(f, buf, paclen)) > 0) {
690                                 if (nl != '\n') {
691                                         char *p;
692                                         for (p = buf; p < &buf[r]; ++p) {
693                                                 if (*p == '\n')
694                                                         *p = nl;
695                                         }
696                                 }
697                                 write(0, buf, r);
698                         }
699                         close(f);
700                 }
701                 signal(SIGALRM, login_timeout);
702                 alarm(timeout);
703                 write(0, "login: ", 7);
704                 r = read(0, buf, 20);
705                 if (r <= 0)
706                         die("No login or error (%d)", errno);
707                 signal(SIGALRM, SIG_IGN);
708                 alarm(0);
709                 while (r > 0) {
710                         if (buf[r-1] == ' ' || buf[r-1] == '\r' || buf[r-1] == '\n')
711                                 --r;
712                         else
713                                 break;
714                 }
715                 buf[r] = 0;
716                 call = strupper(buf);
717                 if (!iscallsign(call)) {
718                         die("Sorry, %s isn't a valid callsign", buf);
719                 }
720         }
721         
722         /* connect up stdin */
723         in = fcb_new(0, TEXT);
724         in->sp = sel_open(0, in, "STDIN", fcb_handler, TEXT, SEL_INPUT);
725         if (tcgetattr(0, &in->t) < 0) {
726                 echo = 0;
727                 in->t_set = 0;
728         } else {
729                 struct termios t = in->t;
730                 t.c_lflag &= ~(ECHO|ECHONL|ICANON);
731                 if (tcsetattr(0, TCSANOW, &t) < 0) 
732                         die("tcsetattr (%d)", errno);
733                 in->echo = echo;
734                 in->t_set = 1;
735         }
736         in->buffer_it = 1;
737
738         /* connect up node */
739         connect_to_node();
740
741         /* tell the cluster who I am */
742         send_msg(node, 'A', connsort, strlen(connsort));
743         
744         /* main processing loop */
745         while (!ending) {
746                 sel_run();
747                 if (!ending) {
748                         process_stdin();
749                         process_node();
750                 }
751         }
752         terminate(0);
753 }
754
755
756
757
758
759