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        // make x* functions fail with correct exitcode
 205        xfunc_error_retval = ERR_IO;
 206
 207        // trap vanilla signals to prevent process from being killed suddenly
 208        bb_signals(0
 209                + (1 << SIGHUP)
 210                + (1 << SIGINT)
 211                + (1 << SIGTERM)
 212                + (1 << SIGPIPE)
 213                , signal_handler);
 214
 215#if ENABLE_FEATURE_CHAT_TTY_HIFI
 216//TODO: use set_termios_to_raw()
 217        tcgetattr(STDIN_FILENO, &tio);
 218        tio0 = tio;
 219        cfmakeraw(&tio);
 220        tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio);
 221#endif
 222
 223#if ENABLE_FEATURE_CHAT_SWALLOW_OPTS
 224        getopt32(argv, "vVsSE");
 225        argv += optind;
 226#else
 227        argv++; // goto first arg
 228#endif
 229        // handle chat expect-send pairs
 230        while (*argv) {
 231                // directive given? process it
 232                int key = index_in_strings(
 233                        "HANGUP\0" "ABORT\0"
 234#if ENABLE_FEATURE_CHAT_CLR_ABORT
 235                        "CLR_ABORT\0"
 236#endif
 237                        "TIMEOUT\0" "ECHO\0" "SAY\0" "RECORD\0"
 238                        , *argv
 239                );
 240                if (key >= 0) {
 241                        bool onoff;
 242                        // cache directive value
 243                        char *arg = *++argv;
 244
 245                        if (!arg) {
 246#if ENABLE_FEATURE_CHAT_TTY_HIFI
 247                                tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
 248#endif
 249                                bb_show_usage();
 250                        }
 251                        // OFF -> 0, anything else -> 1
 252                        onoff = (0 != strcmp("OFF", arg));
 253                        // process directive
 254                        if (DIR_HANGUP == key) {
 255                                // turn SIGHUP on/off
 256                                signal(SIGHUP, onoff ? signal_handler : SIG_IGN);
 257                        } else if (DIR_ABORT == key) {
 258                                // append the string to abort conditions
 259#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
 260                                size_t len = strlen(arg);
 261                                if (len > max_abort_len)
 262                                        max_abort_len = len;
 263#endif
 264                                llist_add_to_end(&aborts, arg);
 265#if ENABLE_FEATURE_CHAT_CLR_ABORT
 266                        } else if (DIR_CLR_ABORT == key) {
 267                                llist_t *l;
 268                                // remove the string from abort conditions
 269                                // N.B. gotta refresh maximum length too...
 270# if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
 271                                max_abort_len = 0;
 272# endif
 273                                for (l = aborts; l; l = l->link) {
 274# if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
 275                                        size_t len = strlen(l->data);
 276# endif
 277                                        if (strcmp(arg, l->data) == 0) {
 278                                                llist_unlink(&aborts, l);
 279                                                continue;
 280                                        }
 281# if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
 282                                        if (len > max_abort_len)
 283                                                max_abort_len = len;
 284# endif
 285                                }
 286#endif
 287                        } else if (DIR_TIMEOUT == key) {
 288                                // set new timeout
 289                                // -1 means OFF
 290                                timeout = atoi(arg) * 1000;
 291                                // 0 means default
 292                                // >0 means value in msecs
 293                                if (!timeout)
 294                                        timeout = DEFAULT_CHAT_TIMEOUT;
 295                        } else if (DIR_ECHO == key) {
 296                                // turn echo on/off
 297                                // N.B. echo means dumping device input/output to stderr
 298                                echo = onoff;
 299                        } else if (DIR_RECORD == key) {
 300                                // turn record on/off
 301                                // N.B. record means dumping device input to a file
 302                                        // close previous record_fd
 303                                if (record_fd > 0)
 304                                        close(record_fd);
 305                                // N.B. do we have to die here on open error?
 306                                record_fd = (onoff) ? xopen(arg, O_WRONLY|O_CREAT|O_TRUNC) : -1;
 307                        } else if (DIR_SAY == key) {
 308                                // just print argument verbatim
 309                                // TODO: should we use full_write() to avoid unistd/stdio conflict?
 310                                bb_error_msg("%s", arg);
 311                        }
 312                        // next, please!
 313                        argv++;
 314                // ordinary expect-send pair!
 315                } else {
 316                        //-----------------------
 317                        // do expect
 318                        //-----------------------
 319                        int expect_len;
 320                        size_t buf_len = 0;
 321                        size_t max_len = max_abort_len;
 322
 323                        struct pollfd pfd;
 324#if ENABLE_FEATURE_CHAT_NOFAIL
 325                        int nofail = 0;
 326#endif
 327                        char *expect = *argv++;
 328
 329                        // sanity check: shall we really expect something?
 330                        if (!expect)
 331                                goto expect_done;
 332
 333#if ENABLE_FEATURE_CHAT_NOFAIL
 334                        // if expect starts with -
 335                        if ('-' == *expect) {
 336                                // swallow -
 337                                expect++;
 338                                // and enter nofail mode
 339                                nofail++;
 340                        }
 341#endif
 342
 343#ifdef ___TEST___BUF___ // test behaviour with a small buffer
 344#       undef COMMON_BUFSIZE
 345#       define COMMON_BUFSIZE 6
 346#endif
 347                        // expand escape sequences in expect
 348                        expect_len = unescape(expect, &expect_len /*dummy*/);
 349                        if (expect_len > max_len)
 350                                max_len = expect_len;
 351                        // sanity check:
 352                        // we should expect more than nothing but not more than input buffer
 353                        // TODO: later we'll get rid of fixed-size buffer
 354                        if (!expect_len)
 355                                goto expect_done;
 356                        if (max_len >= COMMON_BUFSIZE) {
 357                                exitcode = ERR_MEM;
 358                                goto expect_done;
 359                        }
 360
 361                        // get reply
 362                        pfd.fd = STDIN_FILENO;
 363                        pfd.events = POLLIN;
 364                        while (!exitcode
 365                            && poll(&pfd, 1, timeout) > 0
 366                            && (pfd.revents & POLLIN)
 367                        ) {
 368                                llist_t *l;
 369                                ssize_t delta;
 370#define buf bb_common_bufsiz1
 371                                setup_common_bufsiz();
 372
 373                                // read next char from device
 374                                if (safe_read(STDIN_FILENO, buf+buf_len, 1) > 0) {
 375                                        // dump device input if RECORD fname
 376                                        if (record_fd > 0) {
 377                                                full_write(record_fd, buf+buf_len, 1);
 378                                        }
 379                                        // dump device input if ECHO ON
 380                                        if (echo) {
 381//                                              if (buf[buf_len] < ' ') {
 382//                                                      full_write(STDERR_FILENO, "^", 1);
 383//                                                      buf[buf_len] += '@';
 384//                                              }
 385                                                full_write(STDERR_FILENO, buf+buf_len, 1);
 386                                        }
 387                                        buf_len++;
 388                                        // move input frame if we've reached higher bound
 389                                        if (buf_len > COMMON_BUFSIZE) {
 390                                                memmove(buf, buf+buf_len-max_len, max_len);
 391                                                buf_len = max_len;
 392                                        }
 393                                }
 394                                // N.B. rule of thumb: values being looked for can
 395                                // be found only at the end of input buffer
 396                                // this allows to get rid of strstr() and memmem()
 397
 398                                // TODO: make expect and abort strings processed uniformly
 399                                // abort condition is met? -> bail out
 400                                for (l = aborts, exitcode = ERR_ABORT; l; l = l->link, ++exitcode) {
 401                                        size_t len = strlen(l->data);
 402                                        delta = buf_len-len;
 403                                        if (delta >= 0 && !memcmp(buf+delta, l->data, len))
 404                                                goto expect_done;
 405                                }
 406                                exitcode = ERR_OK;
 407
 408                                // expected reply received? -> goto next command
 409                                delta = buf_len - expect_len;
 410                                if (delta >= 0 && !memcmp(buf+delta, expect, expect_len))
 411                                        goto expect_done;
 412#undef buf
 413                        } /* while (have data) */
 414
 415                        // device timed out or unexpected reply received
 416                        exitcode = ERR_TIMEOUT;
 417 expect_done:
 418#if ENABLE_FEATURE_CHAT_NOFAIL
 419                        // on success and when in nofail mode
 420                        // we should skip following subsend-subexpect pairs
 421                        if (nofail) {
 422                                if (!exitcode) {
 423                                        // find last send before non-dashed expect
 424                                        while (*argv && argv[1] && '-' == argv[1][0])
 425                                                argv += 2;
 426                                        // skip the pair
 427                                        // N.B. do we really need this?!
 428                                        if (!*argv++ || !*argv++)
 429                                                break;
 430                                }
 431                                // nofail mode also clears all but IO errors (or signals)
 432                                if (ERR_IO != exitcode)
 433                                        exitcode = ERR_OK;
 434                        }
 435#endif
 436                        // bail out unless we expected successfully
 437                        if (exitcode)
 438                                break;
 439
 440                        //-----------------------
 441                        // do send
 442                        //-----------------------
 443                        if (*argv) {
 444#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
 445                                int nocr = 0; // inhibit terminating command with \r
 446#endif
 447                                char *loaded = NULL; // loaded command
 448                                size_t len;
 449                                char *buf = *argv++;
 450
 451                                // if command starts with @
 452                                // load "real" command from file named after @
 453                                if ('@' == *buf) {
 454                                        // skip the @ and any following white-space
 455                                        trim(++buf);
 456                                        buf = loaded = xmalloc_xopen_read_close(buf, NULL);
 457                                }
 458                                // expand escape sequences in command
 459                                len = unescape(buf, &nocr);
 460
 461                                // send command
 462                                alarm(timeout);
 463                                pfd.fd = STDOUT_FILENO;
 464                                pfd.events = POLLOUT;
 465                                while (len && !exitcode
 466                                    && poll(&pfd, 1, -1) > 0
 467                                    && (pfd.revents & POLLOUT)
 468                                ) {
 469#if ENABLE_FEATURE_CHAT_SEND_ESCAPES
 470                                        // "\\d" means 1 sec delay, "\\p" means 0.01 sec delay
 471                                        // "\\K" means send BREAK
 472                                        char c = *buf;
 473                                        if ('\\' == c) {
 474                                                c = *++buf;
 475                                                if ('d' == c) {
 476                                                        sleep(1);
 477                                                        len--;
 478                                                        continue;
 479                                                }
 480                                                if ('p' == c) {
 481                                                        usleep(10000);
 482                                                        len--;
 483                                                        continue;
 484                                                }
 485                                                if ('K' == c) {
 486                                                        tcsendbreak(STDOUT_FILENO, 0);
 487                                                        len--;
 488                                                        continue;
 489                                                }
 490                                                buf--;
 491                                        }
 492                                        if (safe_write(STDOUT_FILENO, buf, 1) != 1)
 493                                                break;
 494                                        len--;
 495                                        buf++;
 496#else
 497                                        len -= full_write(STDOUT_FILENO, buf, len);
 498#endif
 499                                } /* while (can write) */
 500                                alarm(0);
 501
 502                                // report I/O error if there still exists at least one non-sent char
 503                                if (len)
 504                                        exitcode = ERR_IO;
 505
 506                                // free loaded command (if any)
 507                                if (loaded)
 508                                        free(loaded);
 509#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
 510                                // or terminate command with \r (if not inhibited)
 511                                else if (!nocr)
 512                                        xwrite(STDOUT_FILENO, "\r", 1);
 513#endif
 514                                // bail out unless we sent command successfully
 515                                if (exitcode)
 516                                        break;
 517                        } /* if (*argv) */
 518                }
 519        } /* while (*argv) */
 520
 521#if ENABLE_FEATURE_CHAT_TTY_HIFI
 522        tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
 523#endif
 524
 525        return exitcode;
 526}
 527