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