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