setserial: make it NOEXEC
[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                         // cache directive value
242                         char *arg = *++argv;
243                         // OFF -> 0, anything else -> 1
244                         bool onoff = (0 != strcmp("OFF", arg));
245                         // process directive
246                         if (DIR_HANGUP == key) {
247                                 // turn SIGHUP on/off
248                                 signal(SIGHUP, onoff ? signal_handler : SIG_IGN);
249                         } else if (DIR_ABORT == key) {
250                                 // append the string to abort conditions
251 #if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
252                                 size_t len = strlen(arg);
253                                 if (len > max_abort_len)
254                                         max_abort_len = len;
255 #endif
256                                 llist_add_to_end(&aborts, arg);
257 #if ENABLE_FEATURE_CHAT_CLR_ABORT
258                         } else if (DIR_CLR_ABORT == key) {
259                                 llist_t *l;
260                                 // remove the string from abort conditions
261                                 // N.B. gotta refresh maximum length too...
262 # if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
263                                 max_abort_len = 0;
264 # endif
265                                 for (l = aborts; l; l = l->link) {
266 # if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
267                                         size_t len = strlen(l->data);
268 # endif
269                                         if (strcmp(arg, l->data) == 0) {
270                                                 llist_unlink(&aborts, l);
271                                                 continue;
272                                         }
273 # if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
274                                         if (len > max_abort_len)
275                                                 max_abort_len = len;
276 # endif
277                                 }
278 #endif
279                         } else if (DIR_TIMEOUT == key) {
280                                 // set new timeout
281                                 // -1 means OFF
282                                 timeout = atoi(arg) * 1000;
283                                 // 0 means default
284                                 // >0 means value in msecs
285                                 if (!timeout)
286                                         timeout = DEFAULT_CHAT_TIMEOUT;
287                         } else if (DIR_ECHO == key) {
288                                 // turn echo on/off
289                                 // N.B. echo means dumping device input/output to stderr
290                                 echo = onoff;
291                         } else if (DIR_RECORD == key) {
292                                 // turn record on/off
293                                 // N.B. record means dumping device input to a file
294                                         // close previous record_fd
295                                 if (record_fd > 0)
296                                         close(record_fd);
297                                 // N.B. do we have to die here on open error?
298                                 record_fd = (onoff) ? xopen(arg, O_WRONLY|O_CREAT|O_TRUNC) : -1;
299                         } else if (DIR_SAY == key) {
300                                 // just print argument verbatim
301                                 // TODO: should we use full_write() to avoid unistd/stdio conflict?
302                                 bb_error_msg("%s", arg);
303                         }
304                         // next, please!
305                         argv++;
306                 // ordinary expect-send pair!
307                 } else {
308                         //-----------------------
309                         // do expect
310                         //-----------------------
311                         int expect_len;
312                         size_t buf_len = 0;
313                         size_t max_len = max_abort_len;
314
315                         struct pollfd pfd;
316 #if ENABLE_FEATURE_CHAT_NOFAIL
317                         int nofail = 0;
318 #endif
319                         char *expect = *argv++;
320
321                         // sanity check: shall we really expect something?
322                         if (!expect)
323                                 goto expect_done;
324
325 #if ENABLE_FEATURE_CHAT_NOFAIL
326                         // if expect starts with -
327                         if ('-' == *expect) {
328                                 // swallow -
329                                 expect++;
330                                 // and enter nofail mode
331                                 nofail++;
332                         }
333 #endif
334
335 #ifdef ___TEST___BUF___ // test behaviour with a small buffer
336 #       undef COMMON_BUFSIZE
337 #       define COMMON_BUFSIZE 6
338 #endif
339                         // expand escape sequences in expect
340                         expect_len = unescape(expect, &expect_len /*dummy*/);
341                         if (expect_len > max_len)
342                                 max_len = expect_len;
343                         // sanity check:
344                         // we should expect more than nothing but not more than input buffer
345                         // TODO: later we'll get rid of fixed-size buffer
346                         if (!expect_len)
347                                 goto expect_done;
348                         if (max_len >= COMMON_BUFSIZE) {
349                                 exitcode = ERR_MEM;
350                                 goto expect_done;
351                         }
352
353                         // get reply
354                         pfd.fd = STDIN_FILENO;
355                         pfd.events = POLLIN;
356                         while (!exitcode
357                             && poll(&pfd, 1, timeout) > 0
358                             && (pfd.revents & POLLIN)
359                         ) {
360                                 llist_t *l;
361                                 ssize_t delta;
362 #define buf bb_common_bufsiz1
363                                 setup_common_bufsiz();
364
365                                 // read next char from device
366                                 if (safe_read(STDIN_FILENO, buf+buf_len, 1) > 0) {
367                                         // dump device input if RECORD fname
368                                         if (record_fd > 0) {
369                                                 full_write(record_fd, buf+buf_len, 1);
370                                         }
371                                         // dump device input if ECHO ON
372                                         if (echo) {
373 //                                              if (buf[buf_len] < ' ') {
374 //                                                      full_write(STDERR_FILENO, "^", 1);
375 //                                                      buf[buf_len] += '@';
376 //                                              }
377                                                 full_write(STDERR_FILENO, buf+buf_len, 1);
378                                         }
379                                         buf_len++;
380                                         // move input frame if we've reached higher bound
381                                         if (buf_len > COMMON_BUFSIZE) {
382                                                 memmove(buf, buf+buf_len-max_len, max_len);
383                                                 buf_len = max_len;
384                                         }
385                                 }
386                                 // N.B. rule of thumb: values being looked for can
387                                 // be found only at the end of input buffer
388                                 // this allows to get rid of strstr() and memmem()
389
390                                 // TODO: make expect and abort strings processed uniformly
391                                 // abort condition is met? -> bail out
392                                 for (l = aborts, exitcode = ERR_ABORT; l; l = l->link, ++exitcode) {
393                                         size_t len = strlen(l->data);
394                                         delta = buf_len-len;
395                                         if (delta >= 0 && !memcmp(buf+delta, l->data, len))
396                                                 goto expect_done;
397                                 }
398                                 exitcode = ERR_OK;
399
400                                 // expected reply received? -> goto next command
401                                 delta = buf_len - expect_len;
402                                 if (delta >= 0 && !memcmp(buf+delta, expect, expect_len))
403                                         goto expect_done;
404 #undef buf
405                         } /* while (have data) */
406
407                         // device timed out or unexpected reply received
408                         exitcode = ERR_TIMEOUT;
409  expect_done:
410 #if ENABLE_FEATURE_CHAT_NOFAIL
411                         // on success and when in nofail mode
412                         // we should skip following subsend-subexpect pairs
413                         if (nofail) {
414                                 if (!exitcode) {
415                                         // find last send before non-dashed expect
416                                         while (*argv && argv[1] && '-' == argv[1][0])
417                                                 argv += 2;
418                                         // skip the pair
419                                         // N.B. do we really need this?!
420                                         if (!*argv++ || !*argv++)
421                                                 break;
422                                 }
423                                 // nofail mode also clears all but IO errors (or signals)
424                                 if (ERR_IO != exitcode)
425                                         exitcode = ERR_OK;
426                         }
427 #endif
428                         // bail out unless we expected successfully
429                         if (exitcode)
430                                 break;
431
432                         //-----------------------
433                         // do send
434                         //-----------------------
435                         if (*argv) {
436 #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
437                                 int nocr = 0; // inhibit terminating command with \r
438 #endif
439                                 char *loaded = NULL; // loaded command
440                                 size_t len;
441                                 char *buf = *argv++;
442
443                                 // if command starts with @
444                                 // load "real" command from file named after @
445                                 if ('@' == *buf) {
446                                         // skip the @ and any following white-space
447                                         trim(++buf);
448                                         buf = loaded = xmalloc_xopen_read_close(buf, NULL);
449                                 }
450                                 // expand escape sequences in command
451                                 len = unescape(buf, &nocr);
452
453                                 // send command
454                                 alarm(timeout);
455                                 pfd.fd = STDOUT_FILENO;
456                                 pfd.events = POLLOUT;
457                                 while (len && !exitcode
458                                     && poll(&pfd, 1, -1) > 0
459                                     && (pfd.revents & POLLOUT)
460                                 ) {
461 #if ENABLE_FEATURE_CHAT_SEND_ESCAPES
462                                         // "\\d" means 1 sec delay, "\\p" means 0.01 sec delay
463                                         // "\\K" means send BREAK
464                                         char c = *buf;
465                                         if ('\\' == c) {
466                                                 c = *++buf;
467                                                 if ('d' == c) {
468                                                         sleep(1);
469                                                         len--;
470                                                         continue;
471                                                 }
472                                                 if ('p' == c) {
473                                                         usleep(10000);
474                                                         len--;
475                                                         continue;
476                                                 }
477                                                 if ('K' == c) {
478                                                         tcsendbreak(STDOUT_FILENO, 0);
479                                                         len--;
480                                                         continue;
481                                                 }
482                                                 buf--;
483                                         }
484                                         if (safe_write(STDOUT_FILENO, buf, 1) != 1)
485                                                 break;
486                                         len--;
487                                         buf++;
488 #else
489                                         len -= full_write(STDOUT_FILENO, buf, len);
490 #endif
491                                 } /* while (can write) */
492                                 alarm(0);
493
494                                 // report I/O error if there still exists at least one non-sent char
495                                 if (len)
496                                         exitcode = ERR_IO;
497
498                                 // free loaded command (if any)
499                                 if (loaded)
500                                         free(loaded);
501 #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
502                                 // or terminate command with \r (if not inhibited)
503                                 else if (!nocr)
504                                         xwrite(STDOUT_FILENO, "\r", 1);
505 #endif
506                                 // bail out unless we sent command successfully
507                                 if (exitcode)
508                                         break;
509                         } /* if (*argv) */
510                 }
511         } /* while (*argv) */
512
513 #if ENABLE_FEATURE_CHAT_TTY_HIFI
514         tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
515 #endif
516
517         return exitcode;
518 }