root/libexec/getty/chat.c
/*-
 * Copyright (c) 1997
 *      David L Nugent <davidn@blaze.net.au>.
 *      All rights reserved.
 *
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, is permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice immediately at the beginning of the file, without modification,
 *    this list of conditions, and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. This work was done expressly for inclusion into FreeBSD.  Other use
 *    is permitted provided this notation is included.
 * 4. Absolutely no warranty of function or purpose is made by the authors.
 * 5. Modifications may be freely made to this file providing the above
 *    conditions are met.
 *
 * Modem chat module - send/expect style functions for getty
 * For semi-intelligent modem handling.
 */

#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/utsname.h>

#include <ctype.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>

#include "gettytab.h"
#include "extern.h"

#define PAUSE_CH                (unsigned char)'\xff'   /* pause kludge */

#define CHATDEBUG_RECEIVE       0x01
#define CHATDEBUG_SEND          0x02
#define CHATDEBUG_EXPECT        0x04
#define CHATDEBUG_MISC          0x08

#define CHATDEBUG_DEFAULT       0
#define CHAT_DEFAULT_TIMEOUT    10


static int chat_debug = CHATDEBUG_DEFAULT;
static int chat_alarm = CHAT_DEFAULT_TIMEOUT; /* Default */

static volatile int alarmed = 0;


static void   chat_alrm(int);
static int    chat_unalarm(void);
static int    getdigit(char **, int, int);
static char   **read_chat(char **);
static char   *cleanchr(char **, unsigned char);
static const char *cleanstr(const char *, int);
static const char *result(int);
static int    chat_expect(const char *);
static int    chat_send(char const *);


/*
 * alarm signal handler
 * handle timeouts in read/write
 * change stdin to non-blocking mode to prevent
 * possible hang in read().
 */

static void
chat_alrm(int signo __unused)
{
        int on = 1;

        alarm(1);
        alarmed = 1;
        signal(SIGALRM, chat_alrm);
        ioctl(STDIN_FILENO, FIONBIO, &on);
}


/*
 * Turn back on blocking mode reset by chat_alrm()
 */

static int
chat_unalarm(void)
{
        int off = 0;
        return ioctl(STDIN_FILENO, FIONBIO, &off);
}


/*
 * convert a string of a given base (octal/hex) to binary
 */

static int
getdigit(char **ptr, int base, int max)
{
        int i, val = 0;
        char * q;

        static const char xdigits[] = "0123456789abcdef";

        for (i = 0, q = *ptr; i++ < max; ++q) {
                int sval;
                const char * s = strchr(xdigits, tolower(*q));

                if (s == NULL || (sval = s - xdigits) >= base)
                        break;
                val = (val * base) + sval;
        }
        *ptr = q;
        return val;
}


/*
 * read_chat()
 * Convert a whitespace delimtied string into an array
 * of strings, being expect/send pairs
 */

static char **
read_chat(char **chatstr)
{
        char *str = *chatstr;
        char **res = NULL;

        if (str != NULL) {
                char *tmp = NULL;
                int l;

                if ((l=strlen(str)) > 0 && (tmp=malloc(l + 1)) != NULL &&
                    (res=malloc(((l + 1) / 2 + 1) * sizeof(char *))) != NULL) {
                        static char ws[] = " \t";
                        char * p;

                        for (l = 0, p = strtok(strcpy(tmp, str), ws);
                             p != NULL;
                             p = strtok(NULL, ws))
                        {
                                char *q, *r;

                                /* Read escapes */
                                for (q = r = p; *r; ++q)
                                {
                                        if (*q == '\\')
                                        {
                                                /* handle special escapes */
                                                switch (*++q)
                                                {
                                                case 'a': /* bell */
                                                        *r++ = '\a';
                                                        break;
                                                case 'r': /* cr */
                                                        *r++ = '\r';
                                                        break;
                                                case 'n': /* nl */
                                                        *r++ = '\n';
                                                        break;
                                                case 'f': /* ff */
                                                        *r++ = '\f';
                                                        break;
                                                case 'b': /* bs */
                                                        *r++ = '\b';
                                                        break;
                                                case 'e': /* esc */
                                                        *r++ = 27;
                                                        break;
                                                case 't': /* tab */
                                                        *r++ = '\t';
                                                        break;
                                                case 'p': /* pause */
                                                        *r++ = PAUSE_CH;
                                                        break;
                                                case 's':
                                                case 'S': /* space */
                                                        *r++ = ' ';
                                                        break;
                                                case 'x': /* hexdigit */
                                                        ++q;
                                                        *r++ = getdigit(&q, 16, 2);
                                                        --q;
                                                        break;
                                                case '0': /* octal */
                                                        ++q;
                                                        *r++ = getdigit(&q, 8, 3);
                                                        --q;
                                                        break;
                                                default: /* literal */
                                                        *r++ = *q;
                                                        break;
                                                case 0: /* not past eos */
                                                        --q;
                                                        break;
                                                }
                                        } else {
                                                /* copy standard character */
                                                *r++ = *q;
                                        }
                                }

                                /* Remove surrounding quotes, if any
                                 */
                                if (*p == '"' || *p == '\'') {
                                        q = strrchr(p+1, *p);
                                        if (q != NULL && *q == *p && q[1] == '\0') {
                                                *q = '\0';
                                                p++;
                                        }
                                }

                                res[l++] = p;
                        }
                        res[l] = NULL;
                        *chatstr = tmp;
                        return res;
                }
                free(tmp);
        }
        return res;
}


/*
 * clean a character for display (ctrl/meta character)
 */

static char *
cleanchr(char **buf, unsigned char ch)
{
        int l;
        static char tmpbuf[5];
        char * tmp = buf ? *buf : tmpbuf;

        if (ch & 0x80) {
                strcpy(tmp, "M-");
                l = 2;
                ch &= 0x7f;
        } else
                l = 0;

        if (ch < 32) {
                tmp[l++] = '^';
                tmp[l++] = ch + '@';
        } else if (ch == 127) {
                tmp[l++] = '^';
                tmp[l++] = '?';
        } else
                tmp[l++] = ch;
        tmp[l] = '\0';

        if (buf)
                *buf = tmp + l;
        return tmp;
}


/*
 * clean a string for display (ctrl/meta characters)
 */

static const char *
cleanstr(const char *s, int l)
{
        static char * tmp = NULL;
        static int tmplen = 0;

        if (tmplen < l * 4 + 1)
                tmp = realloc(tmp, tmplen = l * 4 + 1);

        if (tmp == NULL) {
                tmplen = 0;
                return "(mem alloc error)";
        } else {
                int i = 0;
                char * p = tmp;

                while (i < l)
                        cleanchr(&p, s[i++]);
                *p = '\0';
        }

        return tmp;
}


/*
 * return result as a pseudo-english word
 */

static const char *
result(int r)
{
        static const char * results[] = {
                "OK", "MEMERROR", "IOERROR", "TIMEOUT"
        };
        return results[r & 3];
}


/*
 * chat_expect()
 * scan input for an expected string
 */

static int
chat_expect(const char *str)
{
        int len, r = 0;

        if (chat_debug & CHATDEBUG_EXPECT)
                syslog(LOG_DEBUG, "chat_expect '%s'", cleanstr(str, strlen(str)));

        if ((len = strlen(str)) > 0) {
                int i = 0;
                char * got;

                if ((got = malloc(len + 1)) == NULL)
                        r = 1;
                else {

                        memset(got, 0, len+1);
                        alarm(chat_alarm);
                        alarmed = 0;

                        while (r == 0 && i < len) {
                                if (alarmed)
                                        r = 3;
                                else {
                                        unsigned char ch;

                                        if (read(STDIN_FILENO, &ch, 1) == 1) {

                                                if (chat_debug & CHATDEBUG_RECEIVE)
                                                        syslog(LOG_DEBUG, "chat_recv '%s' m=%d",
                                                            cleanchr(NULL, ch), i);

                                                if (ch == str[i])
                                                        got[i++] = ch;
                                                else if (i > 0) {
                                                        int j = 1;

                                                        /* See if we can resync on a
                                                         * partial match in our buffer
                                                         */
                                                        while (j < i && memcmp(got + j, str, i - j) != 0)
                                                                j++;
                                                        if (j < i)
                                                                memcpy(got, got + j, i - j);
                                                        i -= j;
                                                }
                                        } else
                                                r = alarmed ? 3 : 2;
                                }
                        }
                        alarm(0);
                        chat_unalarm();
                        alarmed = 0;
                        free(got);
                }
        }

        if (chat_debug & CHATDEBUG_EXPECT)
                syslog(LOG_DEBUG, "chat_expect %s", result(r));

        return r;
}


/*
 * chat_send()
 * send a chat string
 */

static int
chat_send(char const *str)
{
        int r = 0;

        if (chat_debug & CHATDEBUG_SEND)
                syslog(LOG_DEBUG, "chat_send '%s'", cleanstr(str, strlen(str)));

        if (*str) {
                alarm(chat_alarm);
                alarmed = 0;
                while (r == 0 && *str)
                {
                        unsigned char ch = (unsigned char)*str++;

                        if (alarmed)
                                r = 3;
                        else if (ch == PAUSE_CH)
                                usleep(500000); /* 1/2 second */
                        else  {
                                usleep(10000);  /* be kind to modem */
                                if (write(STDOUT_FILENO, &ch, 1) != 1)
                                        r = alarmed ? 3 : 2;
                        }
                }
                alarm(0);
                chat_unalarm();
                alarmed = 0;
        }

        if (chat_debug & CHATDEBUG_SEND)
                syslog(LOG_DEBUG, "chat_send %s", result(r));

        return r;
}


/*
 * getty_chat()
 *
 * Termination codes:
 * -1 - no script supplied
 *  0 - script terminated correctly
 *  1 - invalid argument, expect string too large, etc.
 *  2 - error on an I/O operation or fatal error condition
 *  3 - timeout waiting for a simple string
 *
 * Parameters:
 *  char *scrstr     - unparsed chat script
 *  timeout          - seconds timeout
 *  debug            - debug value (bitmask)
 */

int
getty_chat(char *scrstr, int timeout, int debug)
{
        int r = -1;

        chat_alarm = timeout ? timeout : CHAT_DEFAULT_TIMEOUT;
        chat_debug = debug;

        if (scrstr != NULL) {
                char **script;

                if (chat_debug & CHATDEBUG_MISC)
                        syslog(LOG_DEBUG, "getty_chat script='%s'", scrstr);

                if ((script = read_chat(&scrstr)) != NULL) {
                        int i = r = 0;
                        int off = 0;
                        sig_t old_alarm;

                        /*
                         * We need to be in raw mode for all this
                         * Rely on caller...
                         */

                        old_alarm = signal(SIGALRM, chat_alrm);
                        chat_unalarm(); /* Force blocking mode at start */

                        /*
                         * This is the send/expect loop
                         */
                        while (r == 0 && script[i] != NULL)
                                if ((r = chat_expect(script[i++])) == 0 && script[i] != NULL)
                                        r = chat_send(script[i++]);

                        signal(SIGALRM, old_alarm);
                        free(script);
                        free(scrstr);

                        /*
                         * Ensure stdin is in blocking mode
                         */
                        ioctl(STDIN_FILENO, FIONBIO, &off);
                }

                if (chat_debug & CHATDEBUG_MISC)
                        syslog(LOG_DEBUG, "getty_chat %s", result(r));

        }
        return r;
}