| /* vi: set sw=4 ts=4: */ | 
 | /* | 
 |  * bare bones chat utility | 
 |  * inspired by ppp's chat | 
 |  * | 
 |  * Copyright (C) 2008 by Vladimir Dronnikov <dronnikov@gmail.com> | 
 |  * | 
 |  * Licensed under GPLv2, see file LICENSE in this source tree. | 
 |  */ | 
 |  | 
 | //usage:#define chat_trivial_usage | 
 | //usage:       "EXPECT [SEND [EXPECT [SEND...]]]" | 
 | //usage:#define chat_full_usage "\n\n" | 
 | //usage:       "Useful for interacting with a modem connected to stdin/stdout.\n" | 
 | //usage:       "A script consists of one or more \"expect-send\" pairs of strings,\n" | 
 | //usage:       "each pair is a pair of arguments. Example:\n" | 
 | //usage:       "chat '' ATZ OK ATD123456 CONNECT '' ogin: pppuser word: ppppass '~'" | 
 |  | 
 | #include "libbb.h" | 
 |  | 
 | // default timeout: 45 sec | 
 | #define DEFAULT_CHAT_TIMEOUT 45*1000 | 
 | // max length of "abort string", | 
 | // i.e. device reply which causes termination | 
 | #define MAX_ABORT_LEN 50 | 
 |  | 
 | // possible exit codes | 
 | enum { | 
 | 	ERR_OK = 0,     // all's well | 
 | 	ERR_MEM,        // read too much while expecting | 
 | 	ERR_IO,         // signalled or I/O error | 
 | 	ERR_TIMEOUT,    // timed out while expecting | 
 | 	ERR_ABORT,      // first abort condition was met | 
 | //	ERR_ABORT2,     // second abort condition was met | 
 | //	... | 
 | }; | 
 |  | 
 | // exit code | 
 | #define exitcode bb_got_signal | 
 |  | 
 | // trap for critical signals | 
 | static void signal_handler(UNUSED_PARAM int signo) | 
 | { | 
 | 	// report I/O error condition | 
 | 	exitcode = ERR_IO; | 
 | } | 
 |  | 
 | #if !ENABLE_FEATURE_CHAT_IMPLICIT_CR | 
 | #define unescape(s, nocr) unescape(s) | 
 | #endif | 
 | static size_t unescape(char *s, int *nocr) | 
 | { | 
 | 	char *start = s; | 
 | 	char *p = s; | 
 |  | 
 | 	while (*s) { | 
 | 		char c = *s; | 
 | 		// do we need special processing? | 
 | 		// standard escapes + \s for space and \N for \0 | 
 | 		// \c inhibits terminating \r for commands and is noop for expects | 
 | 		if ('\\' == c) { | 
 | 			c = *++s; | 
 | 			if (c) { | 
 | #if ENABLE_FEATURE_CHAT_IMPLICIT_CR | 
 | 				if ('c' == c) { | 
 | 					*nocr = 1; | 
 | 					goto next; | 
 | 				} | 
 | #endif | 
 | 				if ('N' == c) { | 
 | 					c = '\0'; | 
 | 				} else if ('s' == c) { | 
 | 					c = ' '; | 
 | #if ENABLE_FEATURE_CHAT_NOFAIL | 
 | 				// unescape leading dash only | 
 | 				// TODO: and only for expect, not command string | 
 | 				} else if ('-' == c && (start + 1 == s)) { | 
 | 					//c = '-'; | 
 | #endif | 
 | 				} else { | 
 | 					c = bb_process_escape_sequence((const char **)&s); | 
 | 					s--; | 
 | 				} | 
 | 			} | 
 | 		// ^A becomes \001, ^B -- \002 and so on... | 
 | 		} else if ('^' == c) { | 
 | 			c = *++s-'@'; | 
 | 		} | 
 | 		// put unescaped char | 
 | 		*p++ = c; | 
 | #if ENABLE_FEATURE_CHAT_IMPLICIT_CR | 
 |  next: | 
 | #endif | 
 | 		// next char | 
 | 		s++; | 
 | 	} | 
 | 	*p = '\0'; | 
 |  | 
 | 	return p - start; | 
 | } | 
 |  | 
 | int chat_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; | 
 | int chat_main(int argc UNUSED_PARAM, char **argv) | 
 | { | 
 | 	int record_fd = -1; | 
 | 	bool echo = 0; | 
 | 	// collection of device replies which cause unconditional termination | 
 | 	llist_t *aborts = NULL; | 
 | 	// inactivity period | 
 | 	int timeout = DEFAULT_CHAT_TIMEOUT; | 
 | 	// maximum length of abort string | 
 | #if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN | 
 | 	size_t max_abort_len = 0; | 
 | #else | 
 | #define max_abort_len MAX_ABORT_LEN | 
 | #endif | 
 | #if ENABLE_FEATURE_CHAT_TTY_HIFI | 
 | 	struct termios tio0, tio; | 
 | #endif | 
 | 	// directive names | 
 | 	enum { | 
 | 		DIR_HANGUP = 0, | 
 | 		DIR_ABORT, | 
 | #if ENABLE_FEATURE_CHAT_CLR_ABORT | 
 | 		DIR_CLR_ABORT, | 
 | #endif | 
 | 		DIR_TIMEOUT, | 
 | 		DIR_ECHO, | 
 | 		DIR_SAY, | 
 | 		DIR_RECORD, | 
 | 	}; | 
 |  | 
 | 	// make x* functions fail with correct exitcode | 
 | 	xfunc_error_retval = ERR_IO; | 
 |  | 
 | 	// trap vanilla signals to prevent process from being killed suddenly | 
 | 	bb_signals(0 | 
 | 		+ (1 << SIGHUP) | 
 | 		+ (1 << SIGINT) | 
 | 		+ (1 << SIGTERM) | 
 | 		+ (1 << SIGPIPE) | 
 | 		, signal_handler); | 
 |  | 
 | #if ENABLE_FEATURE_CHAT_TTY_HIFI | 
 | 	tcgetattr(STDIN_FILENO, &tio); | 
 | 	tio0 = tio; | 
 | 	cfmakeraw(&tio); | 
 | 	tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio); | 
 | #endif | 
 |  | 
 | #if ENABLE_FEATURE_CHAT_SWALLOW_OPTS | 
 | 	getopt32(argv, "vVsSE"); | 
 | 	argv += optind; | 
 | #else | 
 | 	argv++; // goto first arg | 
 | #endif | 
 | 	// handle chat expect-send pairs | 
 | 	while (*argv) { | 
 | 		// directive given? process it | 
 | 		int key = index_in_strings( | 
 | 			"HANGUP\0" "ABORT\0" | 
 | #if ENABLE_FEATURE_CHAT_CLR_ABORT | 
 | 			"CLR_ABORT\0" | 
 | #endif | 
 | 			"TIMEOUT\0" "ECHO\0" "SAY\0" "RECORD\0" | 
 | 			, *argv | 
 | 		); | 
 | 		if (key >= 0) { | 
 | 			// cache directive value | 
 | 			char *arg = *++argv; | 
 | 			// OFF -> 0, anything else -> 1 | 
 | 			bool onoff = (0 != strcmp("OFF", arg)); | 
 | 			// process directive | 
 | 			if (DIR_HANGUP == key) { | 
 | 				// turn SIGHUP on/off | 
 | 				signal(SIGHUP, onoff ? signal_handler : SIG_IGN); | 
 | 			} else if (DIR_ABORT == key) { | 
 | 				// append the string to abort conditions | 
 | #if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN | 
 | 				size_t len = strlen(arg); | 
 | 				if (len > max_abort_len) | 
 | 					max_abort_len = len; | 
 | #endif | 
 | 				llist_add_to_end(&aborts, arg); | 
 | #if ENABLE_FEATURE_CHAT_CLR_ABORT | 
 | 			} else if (DIR_CLR_ABORT == key) { | 
 | 				llist_t *l; | 
 | 				// remove the string from abort conditions | 
 | 				// N.B. gotta refresh maximum length too... | 
 | # if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN | 
 | 				max_abort_len = 0; | 
 | # endif | 
 | 				for (l = aborts; l; l = l->link) { | 
 | # if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN | 
 | 					size_t len = strlen(l->data); | 
 | # endif | 
 | 					if (strcmp(arg, l->data) == 0) { | 
 | 						llist_unlink(&aborts, l); | 
 | 						continue; | 
 | 					} | 
 | # if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN | 
 | 					if (len > max_abort_len) | 
 | 						max_abort_len = len; | 
 | # endif | 
 | 				} | 
 | #endif | 
 | 			} else if (DIR_TIMEOUT == key) { | 
 | 				// set new timeout | 
 | 				// -1 means OFF | 
 | 				timeout = atoi(arg) * 1000; | 
 | 				// 0 means default | 
 | 				// >0 means value in msecs | 
 | 				if (!timeout) | 
 | 					timeout = DEFAULT_CHAT_TIMEOUT; | 
 | 			} else if (DIR_ECHO == key) { | 
 | 				// turn echo on/off | 
 | 				// N.B. echo means dumping device input/output to stderr | 
 | 				echo = onoff; | 
 | 			} else if (DIR_RECORD == key) { | 
 | 				// turn record on/off | 
 | 				// N.B. record means dumping device input to a file | 
 | 					// close previous record_fd | 
 | 				if (record_fd > 0) | 
 | 					close(record_fd); | 
 | 				// N.B. do we have to die here on open error? | 
 | 				record_fd = (onoff) ? xopen(arg, O_WRONLY|O_CREAT|O_TRUNC) : -1; | 
 | 			} else if (DIR_SAY == key) { | 
 | 				// just print argument verbatim | 
 | 				// TODO: should we use full_write() to avoid unistd/stdio conflict? | 
 | 				bb_error_msg("%s", arg); | 
 | 			} | 
 | 			// next, please! | 
 | 			argv++; | 
 | 		// ordinary expect-send pair! | 
 | 		} else { | 
 | 			//----------------------- | 
 | 			// do expect | 
 | 			//----------------------- | 
 | 			int expect_len; | 
 | 			size_t buf_len = 0; | 
 | 			size_t max_len = max_abort_len; | 
 |  | 
 | 			struct pollfd pfd; | 
 | #if ENABLE_FEATURE_CHAT_NOFAIL | 
 | 			int nofail = 0; | 
 | #endif | 
 | 			char *expect = *argv++; | 
 |  | 
 | 			// sanity check: shall we really expect something? | 
 | 			if (!expect) | 
 | 				goto expect_done; | 
 |  | 
 | #if ENABLE_FEATURE_CHAT_NOFAIL | 
 | 			// if expect starts with - | 
 | 			if ('-' == *expect) { | 
 | 				// swallow - | 
 | 				expect++; | 
 | 				// and enter nofail mode | 
 | 				nofail++; | 
 | 			} | 
 | #endif | 
 |  | 
 | #ifdef ___TEST___BUF___ // test behaviour with a small buffer | 
 | #	undef COMMON_BUFSIZE | 
 | #	define COMMON_BUFSIZE 6 | 
 | #endif | 
 | 			// expand escape sequences in expect | 
 | 			expect_len = unescape(expect, &expect_len /*dummy*/); | 
 | 			if (expect_len > max_len) | 
 | 				max_len = expect_len; | 
 | 			// sanity check: | 
 | 			// we should expect more than nothing but not more than input buffer | 
 | 			// TODO: later we'll get rid of fixed-size buffer | 
 | 			if (!expect_len) | 
 | 				goto expect_done; | 
 | 			if (max_len >= COMMON_BUFSIZE) { | 
 | 				exitcode = ERR_MEM; | 
 | 				goto expect_done; | 
 | 			} | 
 |  | 
 | 			// get reply | 
 | 			pfd.fd = STDIN_FILENO; | 
 | 			pfd.events = POLLIN; | 
 | 			while (!exitcode | 
 | 			    && poll(&pfd, 1, timeout) > 0 | 
 | 			    && (pfd.revents & POLLIN) | 
 | 			) { | 
 | #define buf bb_common_bufsiz1 | 
 | 				llist_t *l; | 
 | 				ssize_t delta; | 
 |  | 
 | 				// read next char from device | 
 | 				if (safe_read(STDIN_FILENO, buf+buf_len, 1) > 0) { | 
 | 					// dump device input if RECORD fname | 
 | 					if (record_fd > 0) { | 
 | 						full_write(record_fd, buf+buf_len, 1); | 
 | 					} | 
 | 					// dump device input if ECHO ON | 
 | 					if (echo > 0) { | 
 | //						if (buf[buf_len] < ' ') { | 
 | //							full_write(STDERR_FILENO, "^", 1); | 
 | //							buf[buf_len] += '@'; | 
 | //						} | 
 | 						full_write(STDERR_FILENO, buf+buf_len, 1); | 
 | 					} | 
 | 					buf_len++; | 
 | 					// move input frame if we've reached higher bound | 
 | 					if (buf_len > COMMON_BUFSIZE) { | 
 | 						memmove(buf, buf+buf_len-max_len, max_len); | 
 | 						buf_len = max_len; | 
 | 					} | 
 | 				} | 
 | 				// N.B. rule of thumb: values being looked for can | 
 | 				// be found only at the end of input buffer | 
 | 				// this allows to get rid of strstr() and memmem() | 
 |  | 
 | 				// TODO: make expect and abort strings processed uniformly | 
 | 				// abort condition is met? -> bail out | 
 | 				for (l = aborts, exitcode = ERR_ABORT; l; l = l->link, ++exitcode) { | 
 | 					size_t len = strlen(l->data); | 
 | 					delta = buf_len-len; | 
 | 					if (delta >= 0 && !memcmp(buf+delta, l->data, len)) | 
 | 						goto expect_done; | 
 | 				} | 
 | 				exitcode = ERR_OK; | 
 |  | 
 | 				// expected reply received? -> goto next command | 
 | 				delta = buf_len - expect_len; | 
 | 				if (delta >= 0 && !memcmp(buf+delta, expect, expect_len)) | 
 | 					goto expect_done; | 
 | #undef buf | 
 | 			} /* while (have data) */ | 
 |  | 
 | 			// device timed out or unexpected reply received | 
 | 			exitcode = ERR_TIMEOUT; | 
 |  expect_done: | 
 | #if ENABLE_FEATURE_CHAT_NOFAIL | 
 | 			// on success and when in nofail mode | 
 | 			// we should skip following subsend-subexpect pairs | 
 | 			if (nofail) { | 
 | 				if (!exitcode) { | 
 | 					// find last send before non-dashed expect | 
 | 					while (*argv && argv[1] && '-' == argv[1][0]) | 
 | 						argv += 2; | 
 | 					// skip the pair | 
 | 					// N.B. do we really need this?! | 
 | 					if (!*argv++ || !*argv++) | 
 | 						break; | 
 | 				} | 
 | 				// nofail mode also clears all but IO errors (or signals) | 
 | 				if (ERR_IO != exitcode) | 
 | 					exitcode = ERR_OK; | 
 | 			} | 
 | #endif | 
 | 			// bail out unless we expected successfully | 
 | 			if (exitcode) | 
 | 				break; | 
 |  | 
 | 			//----------------------- | 
 | 			// do send | 
 | 			//----------------------- | 
 | 			if (*argv) { | 
 | #if ENABLE_FEATURE_CHAT_IMPLICIT_CR | 
 | 				int nocr = 0; // inhibit terminating command with \r | 
 | #endif | 
 | 				char *loaded = NULL; // loaded command | 
 | 				size_t len; | 
 | 				char *buf = *argv++; | 
 |  | 
 | 				// if command starts with @ | 
 | 				// load "real" command from file named after @ | 
 | 				if ('@' == *buf) { | 
 | 					// skip the @ and any following white-space | 
 | 					trim(++buf); | 
 | 					buf = loaded = xmalloc_xopen_read_close(buf, NULL); | 
 | 				} | 
 | 				// expand escape sequences in command | 
 | 				len = unescape(buf, &nocr); | 
 |  | 
 | 				// send command | 
 | 				alarm(timeout); | 
 | 				pfd.fd = STDOUT_FILENO; | 
 | 				pfd.events = POLLOUT; | 
 | 				while (len && !exitcode | 
 | 				    && poll(&pfd, 1, -1) > 0 | 
 | 				    && (pfd.revents & POLLOUT) | 
 | 				) { | 
 | #if ENABLE_FEATURE_CHAT_SEND_ESCAPES | 
 | 					// "\\d" means 1 sec delay, "\\p" means 0.01 sec delay | 
 | 					// "\\K" means send BREAK | 
 | 					char c = *buf; | 
 | 					if ('\\' == c) { | 
 | 						c = *++buf; | 
 | 						if ('d' == c) { | 
 | 							sleep(1); | 
 | 							len--; | 
 | 							continue; | 
 | 						} | 
 | 						if ('p' == c) { | 
 | 							usleep(10000); | 
 | 							len--; | 
 | 							continue; | 
 | 						} | 
 | 						if ('K' == c) { | 
 | 							tcsendbreak(STDOUT_FILENO, 0); | 
 | 							len--; | 
 | 							continue; | 
 | 						} | 
 | 						buf--; | 
 | 					} | 
 | 					if (safe_write(STDOUT_FILENO, buf, 1) != 1) | 
 | 						break; | 
 | 					len--; | 
 | 					buf++; | 
 | #else | 
 | 					len -= full_write(STDOUT_FILENO, buf, len); | 
 | #endif | 
 | 				} /* while (can write) */ | 
 | 				alarm(0); | 
 |  | 
 | 				// report I/O error if there still exists at least one non-sent char | 
 | 				if (len) | 
 | 					exitcode = ERR_IO; | 
 |  | 
 | 				// free loaded command (if any) | 
 | 				if (loaded) | 
 | 					free(loaded); | 
 | #if ENABLE_FEATURE_CHAT_IMPLICIT_CR | 
 | 				// or terminate command with \r (if not inhibited) | 
 | 				else if (!nocr) | 
 | 					xwrite(STDOUT_FILENO, "\r", 1); | 
 | #endif | 
 | 				// bail out unless we sent command successfully | 
 | 				if (exitcode) | 
 | 					break; | 
 | 			} /* if (*argv) */ | 
 | 		} | 
 | 	} /* while (*argv) */ | 
 |  | 
 | #if ENABLE_FEATURE_CHAT_TTY_HIFI | 
 | 	tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0); | 
 | #endif | 
 |  | 
 | 	return exitcode; | 
 | } |