root/usr.bin/ftp/main.c
/*      $OpenBSD: main.c,v 1.146 2023/12/23 23:03:00 kn Exp $   */
/*      $NetBSD: main.c,v 1.24 1997/08/18 10:20:26 lukem Exp $  */

/*
 * Copyright (C) 1997 and 1998 WIDE Project.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, 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. Neither the name of the project nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/*
 * Copyright (c) 1985, 1989, 1993, 1994
 *      The Regents of the University of California.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, 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. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/*
 * FTP User Program -- Command Interface.
 */
#include <sys/types.h>
#include <sys/socket.h>

#include <ctype.h>
#include <err.h>
#include <fcntl.h>
#include <netdb.h>
#include <pwd.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <tls.h>

#include "cmds.h"
#include "ftp_var.h"

int     trace;
int     hash;
int     mark;
int     sendport;
int     verbose;
int     connected;
int     fromatty;
int     interactive;
#ifndef SMALL
int     confirmrest;
int     debug;
int     bell;
char   *altarg;
#endif /* !SMALL */
int     doglob;
int     autologin;
int     proxy;
int     proxflag;
int     gatemode;
char   *gateserver;
int     sunique;
int     runique;
int     mcase;
int     ntflag;
int     mapflag;
int     preserve;
int     progress;
int     code;
int     crflag;
char    pasv[BUFSIZ];
int     passivemode;
int     activefallback;
char    ntin[17];
char    ntout[17];
char    mapin[PATH_MAX];
char    mapout[PATH_MAX];
char    typename[32];
int     type;
int     curtype;
char    structname[32];
int     stru;
char    formname[32];
int     form;
char    modename[32];
int     mode;
char    bytename[32];
int     bytesize;
int     anonftp;
int     dirchange;
unsigned int retry_connect;
int     ttywidth;
int     epsv4;
int     epsv4bad;

#ifndef SMALL
int       editing;
EditLine *el;
History  *hist;
char     *cursor_pos;
size_t    cursor_argc;
size_t    cursor_argo;
int       resume;
char     *srcaddr;
int       timestamp;
#endif /* !SMALL */

char     *cookiefile;

off_t   bytes;
off_t   filesize;
char   *direction;

char   *hostname;
int     unix_server;
int     unix_proxy;

char *ftpport;
char *httpport;
#ifndef NOSSL
char *httpsport;
#endif /* !SMALL */
char *httpuseragent;
char *gateport;

jmp_buf toplevel;

#ifndef SMALL
char    line[FTPBUFLEN];
char    *argbase;
char    *stringbase;
char    argbuf[FTPBUFLEN];
StringList *marg_sl;
int     margc;
int     options;
#endif /* !SMALL */

int     cpend;
int     mflag;

#ifndef SMALL
int macnum;
struct macel macros[16];
char macbuf[4096];
#endif /* !SMALL */

FILE    *ttyout;

int connect_timeout;

#ifndef SMALL
/* enable using server timestamps by default */
int server_timestamps = 1;
#endif

#ifndef NOSSL
char * const ssl_verify_opts[] = {
#define SSL_CAFILE              0
        "cafile",
#define SSL_CAPATH              1
        "capath",
#define SSL_CIPHERS             2
        "ciphers",
#define SSL_DONTVERIFY          3
        "dont",
#define SSL_DOVERIFY            4
        "do",
#define SSL_VERIFYDEPTH         5
        "depth",
#define SSL_MUSTSTAPLE          6
        "muststaple",
#define SSL_NOVERIFYTIME        7
        "noverifytime",
#define SSL_SESSION             8
        "session",
#define SSL_PROTOCOLS           9
        "protocols",
        NULL
};

struct tls_config *tls_config;
int tls_session_fd = -1;

static void
process_ssl_options(char *cp)
{
        const char *errstr;
        char *str;
        int depth;
        uint32_t protocols;

        while (*cp) {
                switch (getsubopt(&cp, ssl_verify_opts, &str)) {
                case SSL_CAFILE:
                        if (str == NULL)
                                errx(1, "missing CA file");
                        if (tls_config_set_ca_file(tls_config, str) != 0)
                                errx(1, "tls ca file failed: %s",
                                    tls_config_error(tls_config));
                        break;
                case SSL_CAPATH:
                        if (str == NULL)
                                errx(1, "missing CA directory path");
                        if (tls_config_set_ca_path(tls_config, str) != 0)
                                errx(1, "tls ca path failed: %s",
                                    tls_config_error(tls_config));
                        break;
                case SSL_CIPHERS:
                        if (str == NULL)
                                errx(1, "missing cipher list");
                        if (tls_config_set_ciphers(tls_config, str) != 0)
                                errx(1, "tls ciphers failed: %s",
                                    tls_config_error(tls_config));
                        break;
                case SSL_DONTVERIFY:
                        tls_config_insecure_noverifycert(tls_config);
                        tls_config_insecure_noverifyname(tls_config);
                        break;
                case SSL_DOVERIFY:
                        tls_config_verify(tls_config);
                        break;
                case SSL_VERIFYDEPTH:
                        if (str == NULL)
                                errx(1, "missing depth");
                        depth = strtonum(str, 0, INT_MAX, &errstr);
                        if (errstr)
                                errx(1, "certificate validation depth is %s",
                                    errstr);
                        tls_config_set_verify_depth(tls_config, depth);
                        break;
                case SSL_MUSTSTAPLE:
                        tls_config_ocsp_require_stapling(tls_config);
                        break;
                case SSL_NOVERIFYTIME:
                        tls_config_insecure_noverifytime(tls_config);
                        break;
                case SSL_SESSION:
                        if (str == NULL)
                                errx(1, "missing session file");
                        if ((tls_session_fd = open(str, O_RDWR|O_CREAT,
                            0600)) == -1)
                                err(1, "failed to open or create session file "
                                    "'%s'", str);
                        if (tls_config_set_session_fd(tls_config,
                            tls_session_fd) == -1)
                                errx(1, "failed to set session: %s",
                                    tls_config_error(tls_config));
                        break;
                case SSL_PROTOCOLS:
                        if (str == NULL)
                                errx(1, "missing protocol name");
                        if (tls_config_parse_protocols(&protocols, str) != 0)
                                errx(1, "failed to parse TLS protocols");
                        if (tls_config_set_protocols(tls_config, protocols) != 0)
                                errx(1, "failed to set TLS protocols: %s",
                                    tls_config_error(tls_config));
                        break;
                default:
                        errx(1, "unknown -S suboption `%s'",
                            suboptarg ? suboptarg : "");
                        /* NOTREACHED */
                }
        }
}
#endif /* !NOSSL */

int family = PF_UNSPEC;
int pipeout;

int
main(volatile int argc, char *argv[])
{
        int ch, rval;
#ifndef SMALL
        int top;
#endif
        struct passwd *pw = NULL;
        char *cp, homedir[PATH_MAX];
        char *outfile = NULL;
        const char *errstr;
        int dumb_terminal = 0;

        ftpport = "ftp";
        httpport = "http";
#ifndef NOSSL
        httpsport = "https";
#endif /* !NOSSL */
        gateport = getenv("FTPSERVERPORT");
        if (gateport == NULL || *gateport == '\0')
                gateport = "ftpgate";
        doglob = 1;
        interactive = 1;
        autologin = 1;
        passivemode = 1;
        activefallback = 1;
        preserve = 1;
        verbose = 0;
        progress = 0;
        gatemode = 0;
#ifndef NOSSL
        cookiefile = NULL;
#endif /* NOSSL */
#ifndef SMALL
        editing = 0;
        el = NULL;
        hist = NULL;
        resume = 0;
        timestamp = 0;
        srcaddr = NULL;
        marg_sl = sl_init();
#endif /* !SMALL */
        mark = HASHBYTES;
        epsv4 = 1;
        epsv4bad = 0;

        /* Set default operation mode based on FTPMODE environment variable */
        if ((cp = getenv("FTPMODE")) != NULL && *cp != '\0') {
                if (strcmp(cp, "passive") == 0) {
                        passivemode = 1;
                        activefallback = 0;
                } else if (strcmp(cp, "active") == 0) {
                        passivemode = 0;
                        activefallback = 0;
                } else if (strcmp(cp, "gate") == 0) {
                        gatemode = 1;
                } else if (strcmp(cp, "auto") == 0) {
                        passivemode = 1;
                        activefallback = 1;
                } else
                        warnx("unknown FTPMODE: %s.  Using defaults", cp);
        }

        if (strcmp(__progname, "gate-ftp") == 0)
                gatemode = 1;
        gateserver = getenv("FTPSERVER");
        if (gateserver == NULL)
                gateserver = "";
        if (gatemode) {
                if (*gateserver == '\0') {
                        warnx(
"Neither $FTPSERVER nor $GATE_SERVER is defined; disabling gate-ftp");
                        gatemode = 0;
                }
        }

        cp = getenv("TERM");
        dumb_terminal = (cp == NULL || *cp == '\0' || !strcmp(cp, "dumb") ||
            !strcmp(cp, "emacs") || !strcmp(cp, "su"));
        fromatty = isatty(fileno(stdin));
        if (fromatty) {
                verbose = 1;            /* verbose if from a tty */
#ifndef SMALL
                if (!dumb_terminal)
                        editing = 1;    /* editing mode on if tty is usable */
#endif /* !SMALL */
        }

        ttyout = stdout;
        if (isatty(fileno(ttyout)) && !dumb_terminal && foregroundproc())
                progress = 1;           /* progress bar on if tty is usable */

#ifndef NOSSL
        cookiefile = getenv("http_cookies");
        if (tls_config == NULL) {
                tls_config = tls_config_new();
                if (tls_config == NULL)
                        errx(1, "tls config failed");
                if (tls_config_set_protocols(tls_config,
                    TLS_PROTOCOLS_ALL) != 0)
                        errx(1, "tls set protocols failed: %s",
                            tls_config_error(tls_config));
                if (tls_config_set_ciphers(tls_config, "legacy") != 0)
                        errx(1, "tls set ciphers failed: %s",
                            tls_config_error(tls_config));
        }
#endif /* !NOSSL */

        httpuseragent = NULL;

        while ((ch = getopt(argc, argv,
                    "46AaCc:dD:EeN:gik:Mmno:pP:r:S:s:TtU:uvVw:")) != -1) {
                switch (ch) {
                case '4':
                        family = PF_INET;
                        break;
                case '6':
                        family = PF_INET6;
                        break;
                case 'A':
                        activefallback = 0;
                        passivemode = 0;
                        break;

                case 'N':
                        setprogname(optarg);
                        break;
                case 'a':
                        anonftp = 1;
                        break;

                case 'C':
#ifndef SMALL
                        resume = 1;
#endif /* !SMALL */
                        break;

                case 'c':
#ifndef SMALL
                        cookiefile = optarg;
#endif /* !SMALL */
                        break;

                case 'D':
                        action = optarg;
                        break;
                case 'd':
#ifndef SMALL
                        options |= SO_DEBUG;
                        debug++;
#endif /* !SMALL */
                        break;

                case 'E':
                        epsv4 = 0;
                        break;

                case 'e':
#ifndef SMALL
                        editing = 0;
#endif /* !SMALL */
                        break;

                case 'g':
                        doglob = 0;
                        break;

                case 'i':
                        interactive = 0;
                        break;

                case 'k':
                        keep_alive_timeout = strtonum(optarg, 0, INT_MAX,
                            &errstr);
                        if (errstr != NULL) {
                                warnx("keep alive amount is %s: %s", errstr,
                                        optarg);
                                usage();
                        }
                        break;
                case 'M':
                        progress = 0;
                        break;
                case 'm':
                        progress = -1;
                        break;

                case 'n':
                        autologin = 0;
                        break;

                case 'o':
                        outfile = optarg;
                        pipeout = strcmp(outfile, "-") == 0;
                        ttyout = pipeout ? stderr : stdout;
                        break;

                case 'p':
                        passivemode = 1;
                        activefallback = 0;
                        break;

                case 'P':
                        ftpport = optarg;
                        break;

                case 'r':
                        retry_connect = strtonum(optarg, 0, INT_MAX, &errstr);
                        if (errstr != NULL) {
                                warnx("retry amount is %s: %s", errstr,
                                        optarg);
                                usage();
                        }
                        break;

                case 'S':
#ifndef NOSSL
                        process_ssl_options(optarg);
#endif /* !NOSSL */
                        break;

                case 's':
#ifndef SMALL
                        srcaddr = optarg;
#endif /* !SMALL */
                        break;

#ifndef SMALL
                case 'T':
                        timestamp = 1;
                        break;
#endif /* !SMALL */
                case 't':
                        trace = 1;
                        break;

#ifndef SMALL
                case 'U':
                        free (httpuseragent);
                        if (strcspn(optarg, "\r\n") != strlen(optarg))
                                errx(1, "Invalid User-Agent: %s.", optarg);
                        if (asprintf(&httpuseragent, "User-Agent: %s",
                            optarg) == -1)
                                errx(1, "Can't allocate memory for HTTP(S) "
                                    "User-Agent");
                        break;
                case 'u':
                        server_timestamps = 0;
                        break;
#endif /* !SMALL */

                case 'v':
                        verbose = 1;
                        break;

                case 'V':
                        verbose = 0;
                        break;

                case 'w':
                        connect_timeout = strtonum(optarg, 0, 200, &errstr);
                        if (errstr)
                                errx(1, "-w: %s", errstr);
                        break;
                default:
                        usage();
                }
        }
        argc -= optind;
        argv += optind;

#ifndef NOSSL
        cookie_load();
#endif /* !NOSSL */
        if (httpuseragent == NULL)
                httpuseragent = HTTP_USER_AGENT;

        cpend = 0;      /* no pending replies */
        proxy = 0;      /* proxy not active */
        crflag = 1;     /* strip c.r. on ascii gets */
        sendport = -1;  /* not using ports */
        /*
         * Set up the home directory in case we're globbing.
         */
        cp = getlogin();
        if (cp != NULL) {
                pw = getpwnam(cp);
        }
        if (pw == NULL)
                pw = getpwuid(getuid());
        if (pw != NULL) {
                (void)strlcpy(homedir, pw->pw_dir, sizeof homedir);
                home = homedir;
        }

        setttywidth(0);
        (void)signal(SIGWINCH, setttywidth);

        if (argc > 0) {
                if (isurl(argv[0])) {
                        if (pipeout) {
                                if (pledge("stdio rpath dns tty inet",
                                    NULL) == -1)
                                        err(1, "pledge");
                        } else {
#ifndef SMALL
                            if (outfile == NULL) {
                                if (pledge("stdio rpath wpath cpath dns tty inet proc exec fattr",
                                    NULL) == -1)
                                        err(1, "pledge");
                            } else
#endif /* !SMALL */
                                if (pledge("stdio rpath wpath cpath dns tty inet fattr",
                                    NULL) == -1)
                                        err(1, "pledge");
                        }

                        rval = auto_fetch(argc, argv, outfile);
                        /* -1 == connected and cd-ed */
                        if (rval >= 0 || outfile != NULL)
                                exit(rval);
                } else {
#ifndef SMALL
                        char *xargv[5];

                        if (setjmp(toplevel))
                                exit(0);
                        (void)signal(SIGINT, (sig_t)intr);
                        (void)signal(SIGPIPE, (sig_t)lostpeer);
                        xargv[0] = __progname;
                        xargv[1] = argv[0];
                        xargv[2] = argv[1];
                        xargv[3] = argv[2];
                        xargv[4] = NULL;
                        do {
                                setpeer(argc+1, xargv);
                                if (!retry_connect)
                                        break;
                                if (!connected) {
                                        macnum = 0;
                                        fputs("Retrying...\n", ttyout);
                                        sleep(retry_connect);
                                }
                        } while (!connected);
                        retry_connect = 0; /* connected, stop hiding msgs */
#endif /* !SMALL */
                }
        }
#ifndef SMALL
        controlediting();
        top = setjmp(toplevel) == 0;
        if (top) {
                (void)signal(SIGINT, (sig_t)intr);
                (void)signal(SIGPIPE, (sig_t)lostpeer);
        }
        for (;;) {
                cmdscanner(top);
                top = 1;
        }
#else /* !SMALL */
        usage();
#endif /* !SMALL */
}

void
intr(void)
{
        int save_errno = errno;

        write(fileno(ttyout), "\n\r", 2);
        alarmtimer(0);

        errno = save_errno;
        longjmp(toplevel, 1);
}

void
lostpeer(void)
{
        int save_errno = errno;

        alarmtimer(0);
        if (connected) {
                if (cout != NULL) {
                        (void)shutdown(fileno(cout), SHUT_RDWR);
                        (void)fclose(cout);
                        cout = NULL;
                }
                if (data >= 0) {
                        (void)shutdown(data, SHUT_RDWR);
                        (void)close(data);
                        data = -1;
                }
                connected = 0;
        }
        pswitch(1);
        if (connected) {
                if (cout != NULL) {
                        (void)shutdown(fileno(cout), SHUT_RDWR);
                        (void)fclose(cout);
                        cout = NULL;
                }
                connected = 0;
        }
        proxflag = 0;
        pswitch(0);
        errno = save_errno;
}

#ifndef SMALL
/*
 * Generate a prompt
 */
char *
prompt(void)
{
        return ("ftp> ");
}

/*
 * Command parser.
 */
void
cmdscanner(int top)
{
        struct cmd *c;
        int num;
        HistEvent hev;

        if (!top && !editing)
                (void)putc('\n', ttyout);
        for (;;) {
                if (!editing) {
                        if (fromatty) {
                                fputs(prompt(), ttyout);
                                (void)fflush(ttyout);
                        }
                        if (fgets(line, sizeof(line), stdin) == NULL)
                                quit(0, 0);
                        num = strlen(line);
                        if (num == 0)
                                break;
                        if (line[--num] == '\n') {
                                if (num == 0)
                                        break;
                                line[num] = '\0';
                        } else if (num == sizeof(line) - 2) {
                                fputs("sorry, input line too long.\n", ttyout);
                                while ((num = getchar()) != '\n' && num != EOF)
                                        /* void */;
                                break;
                        } /* else it was a line without a newline */
                } else {
                        const char *buf;
                        cursor_pos = NULL;

                        if ((buf = el_gets(el, &num)) == NULL || num == 0) {
                                putc('\n', ttyout);
                                fflush(ttyout);
                                quit(0, 0);
                        }
                        if (buf[--num] == '\n') {
                                if (num == 0)
                                        break;
                        }
                        if (num >= sizeof(line)) {
                                fputs("sorry, input line too long.\n", ttyout);
                                break;
                        }
                        memcpy(line, buf, (size_t)num);
                        line[num] = '\0';
                        history(hist, &hev, H_ENTER, buf);
                }

                makeargv();
                if (margc == 0)
                        continue;
                c = getcmd(margv[0]);
                if (c == (struct cmd *)-1) {
                        fputs("?Ambiguous command.\n", ttyout);
                        continue;
                }
                if (c == 0) {
                        /*
                         * Give editline(3) a shot at unknown commands.
                         * XXX - bogus commands with a colon in
                         *       them will not elicit an error.
                         */
                        if (editing &&
                            el_parse(el, margc, (const char **)margv) != 0)
                                fputs("?Invalid command.\n", ttyout);
                        continue;
                }
                if (c->c_conn && !connected) {
                        fputs("Not connected.\n", ttyout);
                        continue;
                }
                confirmrest = 0;
                (*c->c_handler)(margc, margv);
                if (bell && c->c_bell)
                        (void)putc('\007', ttyout);
                if (c->c_handler != help)
                        break;
        }
        (void)signal(SIGINT, (sig_t)intr);
        (void)signal(SIGPIPE, (sig_t)lostpeer);
}

struct cmd *
getcmd(const char *name)
{
        const char *p, *q;
        struct cmd *c, *found;
        int nmatches, longest;

        if (name == NULL)
                return (0);

        longest = 0;
        nmatches = 0;
        found = 0;
        for (c = cmdtab; (p = c->c_name) != NULL; c++) {
                for (q = name; *q == *p++; q++)
                        if (*q == 0)            /* exact match? */
                                return (c);
                if (!*q) {                      /* the name was a prefix */
                        if (q - name > longest) {
                                longest = q - name;
                                nmatches = 1;
                                found = c;
                        } else if (q - name == longest)
                                nmatches++;
                }
        }
        if (nmatches > 1)
                return ((struct cmd *)-1);
        return (found);
}

/*
 * Slice a string up into argc/argv.
 */

int slrflag;

void
makeargv(void)
{
        char *argp;

        stringbase = line;              /* scan from first of buffer */
        argbase = argbuf;               /* store from first of buffer */
        slrflag = 0;
        marg_sl->sl_cur = 0;            /* reset to start of marg_sl */
        for (margc = 0; ; margc++) {
                argp = slurpstring();
                sl_add(marg_sl, argp);
                if (argp == NULL)
                        break;
        }
        if (cursor_pos == line) {
                cursor_argc = 0;
                cursor_argo = 0;
        } else if (cursor_pos != NULL) {
                cursor_argc = margc;
                cursor_argo = strlen(margv[margc-1]);
        }
}

#define INC_CHKCURSOR(x)        { (x)++ ; \
                                if (x == cursor_pos) { \
                                        cursor_argc = margc; \
                                        cursor_argo = ap-argbase; \
                                        cursor_pos = NULL; \
                                } }

/*
 * Parse string into argbuf;
 * implemented with FSM to
 * handle quoting and strings
 */
char *
slurpstring(void)
{
        int got_one = 0;
        char *sb = stringbase;
        char *ap = argbase;
        char *tmp = argbase;            /* will return this if token found */

        if (*sb == '!' || *sb == '$') { /* recognize ! as a token for shell */
                switch (slrflag) {      /* and $ as token for macro invoke */
                        case 0:
                                slrflag++;
                                INC_CHKCURSOR(stringbase);
                                return ((*sb == '!') ? "!" : "$");
                                /* NOTREACHED */
                        case 1:
                                slrflag++;
                                altarg = stringbase;
                                break;
                        default:
                                break;
                }
        }

S0:
        switch (*sb) {

        case '\0':
                goto OUT;

        case ' ':
        case '\t':
                INC_CHKCURSOR(sb);
                goto S0;

        default:
                switch (slrflag) {
                        case 0:
                                slrflag++;
                                break;
                        case 1:
                                slrflag++;
                                altarg = sb;
                                break;
                        default:
                                break;
                }
                goto S1;
        }

S1:
        switch (*sb) {

        case ' ':
        case '\t':
        case '\0':
                goto OUT;       /* end of token */

        case '\\':
                INC_CHKCURSOR(sb);
                goto S2;        /* slurp next character */

        case '"':
                INC_CHKCURSOR(sb);
                goto S3;        /* slurp quoted string */

        default:
                *ap = *sb;      /* add character to token */
                ap++;
                INC_CHKCURSOR(sb);
                got_one = 1;
                goto S1;
        }

S2:
        switch (*sb) {

        case '\0':
                goto OUT;

        default:
                *ap = *sb;
                ap++;
                INC_CHKCURSOR(sb);
                got_one = 1;
                goto S1;
        }

S3:
        switch (*sb) {

        case '\0':
                goto OUT;

        case '"':
                INC_CHKCURSOR(sb);
                goto S1;

        default:
                *ap = *sb;
                ap++;
                INC_CHKCURSOR(sb);
                got_one = 1;
                goto S3;
        }

OUT:
        if (got_one)
                *ap++ = '\0';
        argbase = ap;                   /* update storage pointer */
        stringbase = sb;                /* update scan pointer */
        if (got_one) {
                return (tmp);
        }
        switch (slrflag) {
                case 0:
                        slrflag++;
                        break;
                case 1:
                        slrflag++;
                        altarg = (char *) 0;
                        break;
                default:
                        break;
        }
        return (NULL);
}

/*
 * Help command.
 * Call each command handler with argc == 0 and argv[0] == name.
 */
void
help(int argc, char *argv[])
{
        struct cmd *c;

        if (argc == 1) {
                StringList *buf;

                buf = sl_init();
                fprintf(ttyout, "%sommands may be abbreviated.  Commands are:\n\n",
                    proxy ? "Proxy c" : "C");
                for (c = cmdtab; c < &cmdtab[NCMDS]; c++)
                        if (c->c_name && (!proxy || c->c_proxy))
                                sl_add(buf, c->c_name);
                list_vertical(buf);
                sl_free(buf, 0);
                return;
        }

#define HELPINDENT ((int) sizeof("disconnect"))

        while (--argc > 0) {
                char *arg;

                arg = *++argv;
                c = getcmd(arg);
                if (c == (struct cmd *)-1)
                        fprintf(ttyout, "?Ambiguous help command %s\n", arg);
                else if (c == NULL)
                        fprintf(ttyout, "?Invalid help command %s\n", arg);
                else
                        fprintf(ttyout, "%-*s\t%s\n", HELPINDENT,
                                c->c_name, c->c_help);
        }
}
#endif /* !SMALL */

__dead void
usage(void)
{
        fprintf(stderr, "usage: "
#ifndef SMALL
            "ftp [-46AadEegiMmnptVv] [-D title] [-k seconds] [-P port] "
            "[-r seconds]\n"
            "           [-s sourceaddr] [host [port]]\n"
            "       ftp [-C] [-N name] [-o output] [-s sourceaddr]\n"
            "           ftp://[user:password@]host[:port]/file[/] ...\n"
            "       ftp [-CTu] [-c cookie] [-N name] [-o output] [-S ssl_options] "
            "[-s sourceaddr]\n"
            "           [-U useragent] [-w seconds] "
            "http[s]://[user:password@]host[:port]/file ...\n"
            "       ftp [-C] [-N name] [-o output] [-s sourceaddr] file:file ...\n"
            "       ftp [-C] [-N name] [-o output] [-s sourceaddr] host:/file[/] ...\n"
#else /* !SMALL */
            "ftp [-N name] [-o output] "
            "ftp://[user:password@]host[:port]/file[/] ...\n"
#ifndef NOSSL
            "       ftp [-N name] [-o output] [-S ssl_options] [-w seconds] "
            "http[s]://[user:password@]host[:port]/file ...\n"
#else
            "       ftp [-N name] [-o output] [-w seconds] http://host[:port]/file ...\n"
#endif /* NOSSL */
            "       ftp [-N name] [-o output] file:file ...\n"
            "       ftp [-N name] [-o output] host:/file[/] ...\n"
#endif /* !SMALL */
            );
        exit(1);
}