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