chat.c 14 KB

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