busybox/miscutils/chat.c
<<
>>
Prefs
   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
  99enum {
 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
 113static 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
 122static 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
 173int chat_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
 174int 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