dc: fix "dc does_not_exist" SEGVing
[oweals/busybox.git] / miscutils / chat.c
1 /* vi: set sw=4 ts=4: */
2 /*
3  * bare bones chat utility
4  * inspired by ppp's chat
5  *
6  * Copyright (C) 2008 by Vladimir Dronnikov <dronnikov@gmail.com>
7  *
8  * Licensed under GPLv2, see file LICENSE in this source tree.
9  */
10 //config:config CHAT
11 //config:       bool "chat (6.6 kb)"
12 //config:       default y
13 //config:       help
14 //config:       Simple chat utility.
15 //config:
16 //config:config FEATURE_CHAT_NOFAIL
17 //config:       bool "Enable NOFAIL expect strings"
18 //config:       depends on CHAT
19 //config:       default y
20 //config:       help
21 //config:       When enabled expect strings which are started with a dash trigger
22 //config:       no-fail mode. That is when expectation is not met within timeout
23 //config:       the script is not terminated but sends next SEND string and waits
24 //config:       for next EXPECT string. This allows to compose far more flexible
25 //config:       scripts.
26 //config:
27 //config:config FEATURE_CHAT_TTY_HIFI
28 //config:       bool "Force STDIN to be a TTY"
29 //config:       depends on CHAT
30 //config:       default n
31 //config:       help
32 //config:       Original chat always treats STDIN as a TTY device and sets for it
33 //config:       so-called raw mode. This option turns on such behaviour.
34 //config:
35 //config:config FEATURE_CHAT_IMPLICIT_CR
36 //config:       bool "Enable implicit Carriage Return"
37 //config:       depends on CHAT
38 //config:       default y
39 //config:       help
40 //config:       When enabled make chat to terminate all SEND strings with a "\r"
41 //config:       unless "\c" is met anywhere in the string.
42 //config:
43 //config:config FEATURE_CHAT_SWALLOW_OPTS
44 //config:       bool "Swallow options"
45 //config:       depends on CHAT
46 //config:       default y
47 //config:       help
48 //config:       Busybox chat require no options. To make it not fail when used
49 //config:       in place of original chat (which has a bunch of options) turn
50 //config:       this on.
51 //config:
52 //config:config FEATURE_CHAT_SEND_ESCAPES
53 //config:       bool "Support weird SEND escapes"
54 //config:       depends on CHAT
55 //config:       default y
56 //config:       help
57 //config:       Original chat uses some escape sequences in SEND arguments which
58 //config:       are not sent to device but rather performs special actions.
59 //config:       E.g. "\K" means to send a break sequence to device.
60 //config:       "\d" delays execution for a second, "\p" -- for a 1/100 of second.
61 //config:       Before turning this option on think twice: do you really need them?
62 //config:
63 //config:config FEATURE_CHAT_VAR_ABORT_LEN
64 //config:       bool "Support variable-length ABORT conditions"
65 //config:       depends on CHAT
66 //config:       default y
67 //config:       help
68 //config:       Original chat uses fixed 50-bytes length ABORT conditions. Say N here.
69 //config:
70 //config:config FEATURE_CHAT_CLR_ABORT
71 //config:       bool "Support revoking of ABORT conditions"
72 //config:       depends on CHAT
73 //config:       default y
74 //config:       help
75 //config:       Support CLR_ABORT directive.
76
77 //applet:IF_CHAT(APPLET(chat, BB_DIR_USR_SBIN, BB_SUID_DROP))
78
79 //kbuild:lib-$(CONFIG_CHAT) += chat.o
80
81 //usage:#define chat_trivial_usage
82 //usage:       "EXPECT [SEND [EXPECT [SEND...]]]"
83 //usage:#define chat_full_usage "\n\n"
84 //usage:       "Useful for interacting with a modem connected to stdin/stdout.\n"
85 //usage:       "A script consists of \"expect-send\" argument pairs.\n"
86 //usage:       "Example:\n"
87 //usage:       "chat '' ATZ OK ATD123456 CONNECT '' ogin: pppuser word: ppppass '~'"
88
89 #include "libbb.h"
90 #include "common_bufsiz.h"
91
92 // default timeout: 45 sec
93 #define DEFAULT_CHAT_TIMEOUT 45*1000
94 // max length of "abort string",
95 // i.e. device reply which causes termination
96 #define MAX_ABORT_LEN 50
97
98 // possible exit codes
99 enum {
100         ERR_OK = 0,     // all's well
101         ERR_MEM,        // read too much while expecting
102         ERR_IO,         // signalled or I/O error
103         ERR_TIMEOUT,    // timed out while expecting
104         ERR_ABORT,      // first abort condition was met
105 //      ERR_ABORT2,     // second abort condition was met
106 //      ...
107 };
108
109 // exit code
110 #define exitcode bb_got_signal
111
112 // trap for critical signals
113 static void signal_handler(UNUSED_PARAM int signo)
114 {
115         // report I/O error condition
116         exitcode = ERR_IO;
117 }
118
119 #if !ENABLE_FEATURE_CHAT_IMPLICIT_CR
120 #define unescape(s, nocr) unescape(s)
121 #endif
122 static size_t unescape(char *s, int *nocr)
123 {
124         char *start = s;
125         char *p = s;
126
127         while (*s) {
128                 char c = *s;
129                 // do we need special processing?
130                 // standard escapes + \s for space and \N for \0
131                 // \c inhibits terminating \r for commands and is noop for expects
132                 if ('\\' == c) {
133                         c = *++s;
134                         if (c) {
135 #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
136                                 if ('c' == c) {
137                                         *nocr = 1;
138                                         goto next;
139                                 }
140 #endif
141                                 if ('N' == c) {
142                                         c = '\0';
143                                 } else if ('s' == c) {
144                                         c = ' ';
145 #if ENABLE_FEATURE_CHAT_NOFAIL
146                                 // unescape leading dash only
147                                 // TODO: and only for expect, not command string
148                                 } else if ('-' == c && (start + 1 == s)) {
149                                         //c = '-';
150 #endif
151                                 } else {
152                                         c = bb_process_escape_sequence((const char **)&s);
153                                         s--;
154                                 }
155                         }
156                 // ^A becomes \001, ^B -- \002 and so on...
157                 } else if ('^' == c) {
158                         c = *++s-'@';
159                 }
160                 // put unescaped char
161                 *p++ = c;
162 #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
163  next:
164 #endif
165                 // next char
166                 s++;
167         }
168         *p = '\0';
169
170         return p - start;
171 }
172
173 int chat_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
174 int chat_main(int argc UNUSED_PARAM, char **argv)
175 {
176         int record_fd = -1;
177         bool echo = 0;
178         // collection of device replies which cause unconditional termination
179         llist_t *aborts = NULL;
180         // inactivity period
181         int timeout = DEFAULT_CHAT_TIMEOUT;
182         // maximum length of abort string
183 #if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
184         size_t max_abort_len = 0;
185 #else
186 #define max_abort_len MAX_ABORT_LEN
187 #endif
188 #if ENABLE_FEATURE_CHAT_TTY_HIFI
189         struct termios tio0, tio;
190 #endif
191         // directive names
192         enum {
193                 DIR_HANGUP = 0,
194                 DIR_ABORT,
195 #if ENABLE_FEATURE_CHAT_CLR_ABORT
196                 DIR_CLR_ABORT,
197 #endif
198                 DIR_TIMEOUT,
199                 DIR_ECHO,
200                 DIR_SAY,
201                 DIR_RECORD,
202         };
203
204         // make x* functions fail with correct exitcode
205         xfunc_error_retval = ERR_IO;
206
207         // trap vanilla signals to prevent process from being killed suddenly
208         bb_signals(0
209                 + (1 << SIGHUP)
210                 + (1 << SIGINT)
211                 + (1 << SIGTERM)
212                 + (1 << SIGPIPE)
213                 , signal_handler);
214
215 #if ENABLE_FEATURE_CHAT_TTY_HIFI
216 //TODO: use set_termios_to_raw()
217         tcgetattr(STDIN_FILENO, &tio);
218         tio0 = tio;
219         cfmakeraw(&tio);
220         tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio);
221 #endif
222
223 #if ENABLE_FEATURE_CHAT_SWALLOW_OPTS
224         getopt32(argv, "vVsSE");
225         argv += optind;
226 #else
227         argv++; // goto first arg
228 #endif
229         // handle chat expect-send pairs
230         while (*argv) {
231                 // directive given? process it
232                 int key = index_in_strings(
233                         "HANGUP\0" "ABORT\0"
234 #if ENABLE_FEATURE_CHAT_CLR_ABORT
235                         "CLR_ABORT\0"
236 #endif
237                         "TIMEOUT\0" "ECHO\0" "SAY\0" "RECORD\0"
238                         , *argv
239                 );
240                 if (key >= 0) {
241                         bool onoff;
242                         // cache directive value
243                         char *arg = *++argv;
244
245                         if (!arg) {
246 #if ENABLE_FEATURE_CHAT_TTY_HIFI
247                                 tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
248 #endif
249                                 bb_show_usage();
250                         }
251                         // OFF -> 0, anything else -> 1
252                         onoff = (0 != strcmp("OFF", arg));
253                         // process directive
254                         if (DIR_HANGUP == key) {
255                                 // turn SIGHUP on/off
256                                 signal(SIGHUP, onoff ? signal_handler : SIG_IGN);
257                         } else if (DIR_ABORT == key) {
258                                 // append the string to abort conditions
259 #if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
260                                 size_t len = strlen(arg);
261                                 if (len > max_abort_len)
262                                         max_abort_len = len;
263 #endif
264                                 llist_add_to_end(&aborts, arg);
265 #if ENABLE_FEATURE_CHAT_CLR_ABORT
266                         } else if (DIR_CLR_ABORT == key) {
267                                 llist_t *l;
268                                 // remove the string from abort conditions
269                                 // N.B. gotta refresh maximum length too...
270 # if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
271                                 max_abort_len = 0;
272 # endif
273                                 for (l = aborts; l; l = l->link) {
274 # if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
275                                         size_t len = strlen(l->data);
276 # endif
277                                         if (strcmp(arg, l->data) == 0) {
278                                                 llist_unlink(&aborts, l);
279                                                 continue;
280                                         }
281 # if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
282                                         if (len > max_abort_len)
283                                                 max_abort_len = len;
284 # endif
285                                 }
286 #endif
287                         } else if (DIR_TIMEOUT == key) {
288                                 // set new timeout
289                                 // -1 means OFF
290                                 timeout = atoi(arg) * 1000;
291                                 // 0 means default
292                                 // >0 means value in msecs
293                                 if (!timeout)
294                                         timeout = DEFAULT_CHAT_TIMEOUT;
295                         } else if (DIR_ECHO == key) {
296                                 // turn echo on/off
297                                 // N.B. echo means dumping device input/output to stderr
298                                 echo = onoff;
299                         } else if (DIR_RECORD == key) {
300                                 // turn record on/off
301                                 // N.B. record means dumping device input to a file
302                                         // close previous record_fd
303                                 if (record_fd > 0)
304                                         close(record_fd);
305                                 // N.B. do we have to die here on open error?
306                                 record_fd = (onoff) ? xopen(arg, O_WRONLY|O_CREAT|O_TRUNC) : -1;
307                         } else if (DIR_SAY == key) {
308                                 // just print argument verbatim
309                                 // TODO: should we use full_write() to avoid unistd/stdio conflict?
310                                 bb_error_msg("%s", arg);
311                         }
312                         // next, please!
313                         argv++;
314                 // ordinary expect-send pair!
315                 } else {
316                         //-----------------------
317                         // do expect
318                         //-----------------------
319                         int expect_len;
320                         size_t buf_len = 0;
321                         size_t max_len = max_abort_len;
322
323                         struct pollfd pfd;
324 #if ENABLE_FEATURE_CHAT_NOFAIL
325                         int nofail = 0;
326 #endif
327                         char *expect = *argv++;
328
329                         // sanity check: shall we really expect something?
330                         if (!expect)
331                                 goto expect_done;
332
333 #if ENABLE_FEATURE_CHAT_NOFAIL
334                         // if expect starts with -
335                         if ('-' == *expect) {
336                                 // swallow -
337                                 expect++;
338                                 // and enter nofail mode
339                                 nofail++;
340                         }
341 #endif
342
343 #ifdef ___TEST___BUF___ // test behaviour with a small buffer
344 #       undef COMMON_BUFSIZE
345 #       define COMMON_BUFSIZE 6
346 #endif
347                         // expand escape sequences in expect
348                         expect_len = unescape(expect, &expect_len /*dummy*/);
349                         if (expect_len > max_len)
350                                 max_len = expect_len;
351                         // sanity check:
352                         // we should expect more than nothing but not more than input buffer
353                         // TODO: later we'll get rid of fixed-size buffer
354                         if (!expect_len)
355                                 goto expect_done;
356                         if (max_len >= COMMON_BUFSIZE) {
357                                 exitcode = ERR_MEM;
358                                 goto expect_done;
359                         }
360
361                         // get reply
362                         pfd.fd = STDIN_FILENO;
363                         pfd.events = POLLIN;
364                         while (!exitcode
365                             && poll(&pfd, 1, timeout) > 0
366                             && (pfd.revents & POLLIN)
367                         ) {
368                                 llist_t *l;
369                                 ssize_t delta;
370 #define buf bb_common_bufsiz1
371                                 setup_common_bufsiz();
372
373                                 // read next char from device
374                                 if (safe_read(STDIN_FILENO, buf+buf_len, 1) > 0) {
375                                         // dump device input if RECORD fname
376                                         if (record_fd > 0) {
377                                                 full_write(record_fd, buf+buf_len, 1);
378                                         }
379                                         // dump device input if ECHO ON
380                                         if (echo) {
381 //                                              if (buf[buf_len] < ' ') {
382 //                                                      full_write(STDERR_FILENO, "^", 1);
383 //                                                      buf[buf_len] += '@';
384 //                                              }
385                                                 full_write(STDERR_FILENO, buf+buf_len, 1);
386                                         }
387                                         buf_len++;
388                                         // move input frame if we've reached higher bound
389                                         if (buf_len > COMMON_BUFSIZE) {
390                                                 memmove(buf, buf+buf_len-max_len, max_len);
391                                                 buf_len = max_len;
392                                         }
393                                 }
394                                 // N.B. rule of thumb: values being looked for can
395                                 // be found only at the end of input buffer
396                                 // this allows to get rid of strstr() and memmem()
397
398                                 // TODO: make expect and abort strings processed uniformly
399                                 // abort condition is met? -> bail out
400                                 for (l = aborts, exitcode = ERR_ABORT; l; l = l->link, ++exitcode) {
401                                         size_t len = strlen(l->data);
402                                         delta = buf_len-len;
403                                         if (delta >= 0 && !memcmp(buf+delta, l->data, len))
404                                                 goto expect_done;
405                                 }
406                                 exitcode = ERR_OK;
407
408                                 // expected reply received? -> goto next command
409                                 delta = buf_len - expect_len;
410                                 if (delta >= 0 && !memcmp(buf+delta, expect, expect_len))
411                                         goto expect_done;
412 #undef buf
413                         } /* while (have data) */
414
415                         // device timed out or unexpected reply received
416                         exitcode = ERR_TIMEOUT;
417  expect_done:
418 #if ENABLE_FEATURE_CHAT_NOFAIL
419                         // on success and when in nofail mode
420                         // we should skip following subsend-subexpect pairs
421                         if (nofail) {
422                                 if (!exitcode) {
423                                         // find last send before non-dashed expect
424                                         while (*argv && argv[1] && '-' == argv[1][0])
425                                                 argv += 2;
426                                         // skip the pair
427                                         // N.B. do we really need this?!
428                                         if (!*argv++ || !*argv++)
429                                                 break;
430                                 }
431                                 // nofail mode also clears all but IO errors (or signals)
432                                 if (ERR_IO != exitcode)
433                                         exitcode = ERR_OK;
434                         }
435 #endif
436                         // bail out unless we expected successfully
437                         if (exitcode)
438                                 break;
439
440                         //-----------------------
441                         // do send
442                         //-----------------------
443                         if (*argv) {
444 #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
445                                 int nocr = 0; // inhibit terminating command with \r
446 #endif
447                                 char *loaded = NULL; // loaded command
448                                 size_t len;
449                                 char *buf = *argv++;
450
451                                 // if command starts with @
452                                 // load "real" command from file named after @
453                                 if ('@' == *buf) {
454                                         // skip the @ and any following white-space
455                                         trim(++buf);
456                                         buf = loaded = xmalloc_xopen_read_close(buf, NULL);
457                                 }
458                                 // expand escape sequences in command
459                                 len = unescape(buf, &nocr);
460
461                                 // send command
462                                 alarm(timeout);
463                                 pfd.fd = STDOUT_FILENO;
464                                 pfd.events = POLLOUT;
465                                 while (len && !exitcode
466                                     && poll(&pfd, 1, -1) > 0
467                                     && (pfd.revents & POLLOUT)
468                                 ) {
469 #if ENABLE_FEATURE_CHAT_SEND_ESCAPES
470                                         // "\\d" means 1 sec delay, "\\p" means 0.01 sec delay
471                                         // "\\K" means send BREAK
472                                         char c = *buf;
473                                         if ('\\' == c) {
474                                                 c = *++buf;
475                                                 if ('d' == c) {
476                                                         sleep(1);
477                                                         len--;
478                                                         continue;
479                                                 }
480                                                 if ('p' == c) {
481                                                         usleep(10000);
482                                                         len--;
483                                                         continue;
484                                                 }
485                                                 if ('K' == c) {
486                                                         tcsendbreak(STDOUT_FILENO, 0);
487                                                         len--;
488                                                         continue;
489                                                 }
490                                                 buf--;
491                                         }
492                                         if (safe_write(STDOUT_FILENO, buf, 1) != 1)
493                                                 break;
494                                         len--;
495                                         buf++;
496 #else
497                                         len -= full_write(STDOUT_FILENO, buf, len);
498 #endif
499                                 } /* while (can write) */
500                                 alarm(0);
501
502                                 // report I/O error if there still exists at least one non-sent char
503                                 if (len)
504                                         exitcode = ERR_IO;
505
506                                 // free loaded command (if any)
507                                 if (loaded)
508                                         free(loaded);
509 #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
510                                 // or terminate command with \r (if not inhibited)
511                                 else if (!nocr)
512                                         xwrite(STDOUT_FILENO, "\r", 1);
513 #endif
514                                 // bail out unless we sent command successfully
515                                 if (exitcode)
516                                         break;
517                         } /* if (*argv) */
518                 }
519         } /* while (*argv) */
520
521 #if ENABLE_FEATURE_CHAT_TTY_HIFI
522         tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
523 #endif
524
525         return exitcode;
526 }