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