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 tarball for details.
9 */
10#include "libbb.h"
11
12// default timeout: 45 sec
13#define	DEFAULT_CHAT_TIMEOUT 45*1000
14// max length of "abort string",
15// i.e. device reply which causes termination
16#define MAX_ABORT_LEN 50
17
18// possible exit codes
19enum {
20	ERR_OK = 0,     // all's well
21	ERR_MEM,        // read too much while expecting
22	ERR_IO,         // signalled or I/O error
23	ERR_TIMEOUT,    // timed out while expecting
24	ERR_ABORT,      // first abort condition was met
25//	ERR_ABORT2,     // second abort condition was met
26//	...
27};
28
29// exit code
30#define exitcode bb_got_signal
31
32// trap for critical signals
33static void signal_handler(UNUSED_PARAM int signo)
34{
35	// report I/O error condition
36	exitcode = ERR_IO;
37}
38
39#if !ENABLE_FEATURE_CHAT_IMPLICIT_CR
40#define unescape(s, nocr) unescape(s)
41#endif
42static size_t unescape(char *s, int *nocr)
43{
44	char *start = s;
45	char *p = s;
46
47	while (*s) {
48		char c = *s;
49		// do we need special processing?
50		// standard escapes + \s for space and \N for \0
51		// \c inhibits terminating \r for commands and is noop for expects
52		if ('\\' == c) {
53			c = *++s;
54			if (c) {
55#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
56				if ('c' == c) {
57					*nocr = 1;
58					goto next;
59				}
60#endif
61				if ('N' == c) {
62					c = '\0';
63				} else if ('s' == c) {
64					c = ' ';
65#if ENABLE_FEATURE_CHAT_NOFAIL
66				// unescape leading dash only
67				// TODO: and only for expect, not command string
68				} else if ('-' == c && (start + 1 == s)) {
69					//c = '-';
70#endif
71				} else {
72					c = bb_process_escape_sequence((const char **)&s);
73					s--;
74				}
75			}
76		// ^A becomes \001, ^B -- \002 and so on...
77		} else if ('^' == c) {
78			c = *++s-'@';
79		}
80		// put unescaped char
81		*p++ = c;
82#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
83 next:
84#endif
85		// next char
86		s++;
87	}
88	*p = '\0';
89
90	return p - start;
91}
92
93int chat_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
94int chat_main(int argc UNUSED_PARAM, char **argv)
95{
96	int record_fd = -1;
97	bool echo = 0;
98	// collection of device replies which cause unconditional termination
99	llist_t *aborts = NULL;
100	// inactivity period
101	int timeout = DEFAULT_CHAT_TIMEOUT;
102	// maximum length of abort string
103#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
104	size_t max_abort_len = 0;
105#else
106#define max_abort_len MAX_ABORT_LEN
107#endif
108#if ENABLE_FEATURE_CHAT_TTY_HIFI
109	struct termios tio0, tio;
110#endif
111	// directive names
112	enum {
113		DIR_HANGUP = 0,
114		DIR_ABORT,
115#if ENABLE_FEATURE_CHAT_CLR_ABORT
116		DIR_CLR_ABORT,
117#endif
118		DIR_TIMEOUT,
119		DIR_ECHO,
120		DIR_SAY,
121		DIR_RECORD,
122	};
123
124	// make x* functions fail with correct exitcode
125	xfunc_error_retval = ERR_IO;
126
127	// trap vanilla signals to prevent process from being killed suddenly
128	bb_signals(0
129		+ (1 << SIGHUP)
130		+ (1 << SIGINT)
131		+ (1 << SIGTERM)
132		+ (1 << SIGPIPE)
133		, signal_handler);
134
135#if ENABLE_FEATURE_CHAT_TTY_HIFI
136	tcgetattr(STDIN_FILENO, &tio);
137	tio0 = tio;
138	cfmakeraw(&tio);
139	tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio);
140#endif
141
142#if ENABLE_FEATURE_CHAT_SWALLOW_OPTS
143	getopt32(argv, "vVsSE");
144	argv += optind;
145#else
146	argv++; // goto first arg
147#endif
148	// handle chat expect-send pairs
149	while (*argv) {
150		// directive given? process it
151		int key = index_in_strings(
152			"HANGUP\0" "ABORT\0"
153#if ENABLE_FEATURE_CHAT_CLR_ABORT
154			"CLR_ABORT\0"
155#endif
156			"TIMEOUT\0" "ECHO\0" "SAY\0" "RECORD\0"
157			, *argv
158		);
159		if (key >= 0) {
160			// cache directive value
161			char *arg = *++argv;
162			// OFF -> 0, anything else -> 1
163			bool onoff = (0 != strcmp("OFF", arg));
164			// process directive
165			if (DIR_HANGUP == key) {
166				// turn SIGHUP on/off
167				signal(SIGHUP, onoff ? signal_handler : SIG_IGN);
168			} else if (DIR_ABORT == key) {
169				// append the string to abort conditions
170#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
171				size_t len = strlen(arg);
172				if (len > max_abort_len)
173					max_abort_len = len;
174#endif
175				llist_add_to_end(&aborts, arg);
176#if ENABLE_FEATURE_CHAT_CLR_ABORT
177			} else if (DIR_CLR_ABORT == key) {
178				// remove the string from abort conditions
179				// N.B. gotta refresh maximum length too...
180#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
181				max_abort_len = 0;
182#endif
183				for (llist_t *l = aborts; l; l = l->link) {
184#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
185					size_t len = strlen(l->data);
186#endif
187					if (!strcmp(arg, l->data)) {
188						llist_unlink(&aborts, l);
189						continue;
190					}
191#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
192					if (len > max_abort_len)
193						max_abort_len = len;
194#endif
195				}
196#endif
197			} else if (DIR_TIMEOUT == key) {
198				// set new timeout
199				// -1 means OFF
200				timeout = atoi(arg) * 1000;
201				// 0 means default
202				// >0 means value in msecs
203				if (!timeout)
204					timeout = DEFAULT_CHAT_TIMEOUT;
205			} else if (DIR_ECHO == key) {
206				// turn echo on/off
207				// N.B. echo means dumping device input/output to stderr
208				echo = onoff;
209			} else if (DIR_RECORD == key) {
210				// turn record on/off
211				// N.B. record means dumping device input to a file
212					// close previous record_fd
213				if (record_fd > 0)
214					close(record_fd);
215				// N.B. do we have to die here on open error?
216				record_fd = (onoff) ? xopen(arg, O_WRONLY|O_CREAT|O_TRUNC) : -1;
217			} else if (DIR_SAY == key) {
218				// just print argument verbatim
219				// TODO: should we use full_write() to avoid unistd/stdio conflict?
220				bb_error_msg("%s", arg);
221			}
222			// next, please!
223			argv++;
224		// ordinary expect-send pair!
225		} else {
226			//-----------------------
227			// do expect
228			//-----------------------
229			int expect_len;
230			size_t buf_len = 0;
231			size_t max_len = max_abort_len;
232
233			struct pollfd pfd;
234#if ENABLE_FEATURE_CHAT_NOFAIL
235			int nofail = 0;
236#endif
237			char *expect = *argv++;
238
239			// sanity check: shall we really expect something?
240			if (!expect)
241				goto expect_done;
242
243#if ENABLE_FEATURE_CHAT_NOFAIL
244			// if expect starts with -
245			if ('-' == *expect) {
246				// swallow -
247				expect++;
248				// and enter nofail mode
249				nofail++;
250			}
251#endif
252
253#ifdef ___TEST___BUF___ // test behaviour with a small buffer
254#	undef COMMON_BUFSIZE
255#	define COMMON_BUFSIZE 6
256#endif
257			// expand escape sequences in expect
258			expect_len = unescape(expect, &expect_len /*dummy*/);
259			if (expect_len > max_len)
260				max_len = expect_len;
261			// sanity check:
262			// we should expect more than nothing but not more than input buffer
263			// TODO: later we'll get rid of fixed-size buffer
264			if (!expect_len)
265				goto expect_done;
266			if (max_len >= COMMON_BUFSIZE) {
267				exitcode = ERR_MEM;
268				goto expect_done;
269			}
270
271			// get reply
272			pfd.fd = STDIN_FILENO;
273			pfd.events = POLLIN;
274			while (!exitcode
275			    && poll(&pfd, 1, timeout) > 0
276			    && (pfd.revents & POLLIN)
277			) {
278#define buf bb_common_bufsiz1
279				llist_t *l;
280				ssize_t delta;
281
282				// read next char from device
283				if (safe_read(STDIN_FILENO, buf+buf_len, 1) > 0) {
284					// dump device input if RECORD fname
285					if (record_fd > 0) {
286						full_write(record_fd, buf+buf_len, 1);
287					}
288					// dump device input if ECHO ON
289					if (echo > 0) {
290//						if (buf[buf_len] < ' ') {
291//							full_write(STDERR_FILENO, "^", 1);
292//							buf[buf_len] += '@';
293//						}
294						full_write(STDERR_FILENO, buf+buf_len, 1);
295					}
296					buf_len++;
297					// move input frame if we've reached higher bound
298					if (buf_len > COMMON_BUFSIZE) {
299						memmove(buf, buf+buf_len-max_len, max_len);
300						buf_len = max_len;
301					}
302				}
303				// N.B. rule of thumb: values being looked for can
304				// be found only at the end of input buffer
305				// this allows to get rid of strstr() and memmem()
306
307				// TODO: make expect and abort strings processed uniformly
308				// abort condition is met? -> bail out
309				for (l = aborts, exitcode = ERR_ABORT; l; l = l->link, ++exitcode) {
310					size_t len = strlen(l->data);
311					delta = buf_len-len;
312					if (delta >= 0 && !memcmp(buf+delta, l->data, len))
313						goto expect_done;
314				}
315				exitcode = ERR_OK;
316
317				// expected reply received? -> goto next command
318				delta = buf_len - expect_len;
319				if (delta >= 0 && !memcmp(buf+delta, expect, expect_len))
320					goto expect_done;
321#undef buf
322			} /* while (have data) */
323
324			// device timed out or unexpected reply received
325			exitcode = ERR_TIMEOUT;
326 expect_done:
327#if ENABLE_FEATURE_CHAT_NOFAIL
328			// on success and when in nofail mode
329			// we should skip following subsend-subexpect pairs
330			if (nofail) {
331				if (!exitcode) {
332					// find last send before non-dashed expect
333					while (*argv && argv[1] && '-' == argv[1][0])
334						argv += 2;
335					// skip the pair
336					// N.B. do we really need this?!
337					if (!*argv++ || !*argv++)
338						break;
339				}
340				// nofail mode also clears all but IO errors (or signals)
341				if (ERR_IO != exitcode)
342					exitcode = ERR_OK;
343			}
344#endif
345			// bail out unless we expected successfully
346			if (exitcode)
347				break;
348
349			//-----------------------
350			// do send
351			//-----------------------
352			if (*argv) {
353#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
354				int nocr = 0; // inhibit terminating command with \r
355#endif
356				char *loaded = NULL; // loaded command
357				size_t len;
358				char *buf = *argv++;
359
360				// if command starts with @
361				// load "real" command from file named after @
362				if ('@' == *buf) {
363					// skip the @ and any following white-space
364					trim(++buf);
365					buf = loaded = xmalloc_xopen_read_close(buf, NULL);
366				}
367				// expand escape sequences in command
368				len = unescape(buf, &nocr);
369
370				// send command
371				alarm(timeout);
372				pfd.fd = STDOUT_FILENO;
373				pfd.events = POLLOUT;
374				while (len && !exitcode
375				    && poll(&pfd, 1, -1) > 0
376				    && (pfd.revents & POLLOUT)
377				) {
378#if ENABLE_FEATURE_CHAT_SEND_ESCAPES
379					// "\\d" means 1 sec delay, "\\p" means 0.01 sec delay
380					// "\\K" means send BREAK
381					char c = *buf;
382					if ('\\' == c) {
383						c = *++buf;
384						if ('d' == c) {
385							sleep(1);
386							len--;
387							continue;
388						}
389						if ('p' == c) {
390							usleep(10000);
391							len--;
392							continue;
393						}
394						if ('K' == c) {
395							tcsendbreak(STDOUT_FILENO, 0);
396							len--;
397							continue;
398						}
399						buf--;
400					}
401					if (safe_write(STDOUT_FILENO, buf, 1) != 1)
402						break;
403					len--;
404					buf++;
405#else
406					len -= full_write(STDOUT_FILENO, buf, len);
407#endif
408				} /* while (can write) */
409				alarm(0);
410
411				// report I/O error if there still exists at least one non-sent char
412				if (len)
413					exitcode = ERR_IO;
414
415				// free loaded command (if any)
416				if (loaded)
417					free(loaded);
418#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
419				// or terminate command with \r (if not inhibited)
420				else if (!nocr)
421					xwrite(STDOUT_FILENO, "\r", 1);
422#endif
423				// bail out unless we sent command successfully
424				if (exitcode)
425					break;
426			} /* if (*argv) */
427		}
428	} /* while (*argv) */
429
430#if ENABLE_FEATURE_CHAT_TTY_HIFI
431	tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
432#endif
433
434	return exitcode;
435}
436