/*
**  Send control messages to the InterNetNews daemon.
*/

#include "portable/system.h"

#include <ctype.h>
#include <errno.h>
#include <sys/stat.h>

#include "inn/innconf.h"
#include "inn/inndcomm.h"
#include "inn/libinn.h"
#include "inn/messages.h"
#include "inn/paths.h"


/*
**  Datatype for an entry in the command table.
*/
typedef struct _COMMAND {
    const char *Command;
    const char *Text;
    int argc;
    char Letter;
    bool Glue;
} COMMAND;


/* clang-format off */
static COMMAND Commands[] = {
    { "addhist",      "id arr exp post token...\tAdd history line",
      5,  SC_ADDHIST,        true },
    { "allow",        "reason...\t\tAllow remote connections",
      1,  SC_ALLOW,          true },
    { "begin",        "site\t\t\tStart newly-added site",
      1,  SC_BEGIN,          false },
    { "cancel",       "id\t\t\tCancel message locally",
      1,  SC_CANCEL,         false },
    { "changegroup",  "group status\tChange mode of group",
      2,  SC_CHANGEGROUP,    false },
    { "checkfile",    "\t\t\tCheck syntax of newsfeeds file",
      0,  SC_CHECKFILE,      false },
    { "drop",         "site\t\t\tStop feeding site",
      1,  SC_DROP,           false },
    { "feedinfo",     "site\t\t\tPrint state of feed to site*",
      1,  SC_FEEDINFO,       false },
    { "flush",        "site\t\t\tFlush feed for site*",
      1,  SC_FLUSH,          false },
    { "flushlogs",    "\t\t\tFlush log files",
      0,  SC_FLUSHLOGS,      false },
    { "go",           "reason...\t\t\tRestart after pause or throttle",
      1,  SC_GO,             true },
    { "hangup",       "channel\t\tHangup specified incoming channel",
      1,  SC_HANGUP,         false },
    { "kill",         "signal site\t\tSend signal to site's process",
      2,  SC_SIGNAL,         false },
    { "logmode",      "\t\t\tSend server mode to syslog",
      0,  SC_LOGMODE,        false },
    { "lowmark",      "filename\t\tReset active file low article marks",
      1,  SC_LOWMARK,        false },
    { "mode",         "\t\t\t\tPrint operating mode",
      0,  SC_MODE,           false },
    { "name",         "nnn\t\t\tPrint name of specified channel*",
      1,  SC_NAME,           false },
    { "newgroup",     "group [status [creator]]\tCreate new group",
      3,  SC_NEWGROUP,       false },
    { "param",        "letter value\t\tChange command-line parameters",
      2,  SC_PARAM,          false },
    { "pause",        "reason...\t\tShort-term pause in accepting articles",
      1,  SC_PAUSE,          true },
#ifdef DO_PERL
    { "perl",         "flag\t\t\tEnable or disable Perl filtering",
      1,  SC_PERL,           false },
#endif
#ifdef DO_PYTHON
    { "python",       "flag\t\t\tEnable or disable Python filtering",
      1,  SC_PYTHON,         false },
#endif
    { "readers",      "flag text...\t\tEnable or disable newsreading",
      2,  SC_READERS,        true },
    { "reject",       "reason...\t\tReject remote connections",
      1,  SC_REJECT,         true },
    { "reload",       "what reason...\t\tRe-read config files*",
      2,  SC_RELOAD,         true },
    { "renumber",     "group\t\tRenumber the active file*",
      1,  SC_RENUMBER,       false },
    { "renumberlow",  "filename\t\tReset active file low article marks",
      1,  SC_LOWMARK,        false },
    { "reserve",      "reason...\t\tReserve the next pause or throttle",
      1,  SC_RESERVE,        true },
    { "rmgroup",      "group\t\t\tRemove named group",
      1,  SC_RMGROUP,        false },
    { "send",         "feed text...\t\tSend text to exploder feed",
      2,  SC_SEND,           true },
    { "shutdown",     "reason...\t\tShut down server",
      1,  SC_SHUTDOWN,       true },
    { "stathist",     "filename|off\t\tLog into filename some history stats",
      1,  SC_STATHIST,       false },
    { "status",       "interval|off\t\tTurn innd status generation on or off",
      1,  SC_STATUS,         false },
    { "throttle",     "reason...\t\tStop accepting articles",
      1,  SC_THROTTLE,       true },
    { "timer",        "interval|off\t\tTurn performance monitoring on or off",
      1,  SC_TIMER,          false },
    { "trace",        "innd|#|nnrpd flag\tTurn tracing on or off",
      2,  SC_TRACE,          false },
    { "xabort",       "text...\t\tAbort the server",
      1,  SC_XABORT,         true },
    { "xexec",        "path\t\t\tExec new server",
      1,  SC_XEXEC,          false }
};
/* clang-format on */

static void Usage(const char *) __attribute__((__noreturn__));
static void WrongArgs(COMMAND *) __attribute__((__noreturn__));


/*
**  Print a help summary.
*/
__attribute__((__noreturn__)) static void
Help(char *p)
{
    COMMAND *cp;

    if (p == NULL) {
        printf("Usage:\n");
        printf("  ctlinnd [-hs] [-t timeout] [command [argument ...]]\n\n");
        printf("Options:\n");
        printf("  -h [command]\t\t\tPrint (specific) command summary\n");
        printf("  -s\t\t\t\tIf successful, don't print the output\n");
        printf("  -t timeout\t\t\tSet the timeout for the command\n\n");
        printf("Command summary:\n");
        printf("  help [command]\t\tPrint (specific) command summary\n");
        for (cp = Commands; cp < ARRAY_END(Commands); cp++)
            printf("  %s %s\n", cp->Command, cp->Text);
        printf("\n");
        printf("*   Empty string means all sites/groups/etc.\n");
        printf("... All trailing words are glued together.\n");
        exit(0);
    }
    for (cp = Commands; cp < ARRAY_END(Commands); cp++)
        if (strcmp(p, cp->Command) == 0) {
            printf("Command usage:\n");
            printf("  %s %s\n", cp->Command, cp->Text);
            exit(0);
        }
    printf("No such command.\n");
    exit(0);
}


/*
**  Print a command-usage message and exit.
*/
static void
WrongArgs(COMMAND *cp)
{
    printf("Wrong number of arguments -- usage:\n");
    printf("  %s %s\n", cp->Command, cp->Text);
    exit(1);
}


/*
**  Print an error message and exit.
*/
__attribute__((__noreturn__)) static void
Failed(const char *p)
{
    if (ICCfailure)
        syswarn("cannot %s (%s failure)", p, ICCfailure);
    else
        syswarn("cannot %s", p);
    ICCclose();
    exit(1);
}


/*
**  Print an error reporting incorrect usage.
*/
static void
Usage(const char *what)
{
    fprintf(stderr, "Usage error (%s) -- try -h for help.\n", what);
    exit(1);
}


int
main(int ac, char *av[])
{
    static char Y[] = NF_FLAG_OK_STRING;
    static char EMPTY[] = "";
    COMMAND *cp;
    char *p;
    int i;
    bool Silent;
    bool NeedHelp;
    char *reply;
    char *new;
    int length;
    char *nv[4];
    struct stat Sb;
    char buff[SMBUF];

    /* First thing, set up our identity. */
    message_program_name = "ctlinnd";

    /* Set defaults. */
    if (!innconf_read(NULL))
        exit(1);
    Silent = false;
    NeedHelp = false;
    ICCsettimeout(CTLINND_TIMEOUT);

    /* Parse JCL. */
    while ((i = getopt(ac, av, "hst:")) != EOF)
        switch (i) {
        default:
            Usage("bad flags");
            /* NOTREACHED */
        case 'h': /* Get help */
            NeedHelp = true;
            break;
        case 's': /* Silent -- no output */
            Silent = true;
            break;
        case 't': /* Time to wait for reply */
            ICCsettimeout(atoi(optarg));
            break;
        }
    ac -= optind;
    av += optind;
    if (NeedHelp)
        Help(av[0]);
    if (ac == 0)
        Usage("missing command");

    /* Look up the command word and move to the arguments. */
    if (strcmp(av[0], "help") == 0)
        Help(av[1]);
    for (cp = Commands; cp < ARRAY_END(Commands); cp++)
        if (strcmp(av[0], cp->Command) == 0)
            break;
    if (cp == ARRAY_END(Commands))
        Usage("unknown command");
    ac--;
    av++;

    /* Check argument count. */
    if (cp->Letter == SC_NEWGROUP) {
        /* Newgroup command has defaults. */
        switch (ac) {
        default:
            WrongArgs(cp);
            /* NOTREACHED */
        case 1:
            nv[0] = av[0];
            nv[1] = Y;
            nv[2] = EMPTY;
            nv[3] = NULL;
            av = nv;
            break;
        case 2:
            nv[0] = av[0];
            nv[1] = av[1];
            nv[2] = EMPTY;
            nv[3] = NULL;
            av = nv;
            break;
        case 3:
            break;
        }
    } else if (ac > cp->argc && cp->Glue) {
        /* Glue any extra words together. */
        for (length = 0, i = cp->argc - 1; (p = av[i++]) != NULL;)
            length += strlen(p) + 1;
        new = xmalloc(length);
        *new = '\0';
        for (i = cp->argc - 1; av[i]; i++) {
            if (i >= cp->argc)
                strlcat(new, " ", length);
            strlcat(new, av[i], length);
        }
        av[cp->argc - 1] = new;
        av[cp->argc] = NULL;
    } else if (ac != cp->argc)
        /* All other commands must have the right number of arguments. */
        WrongArgs(cp);

    /* For newgroup and changegroup, make sure the mode is valid. */
    if (cp->Letter == SC_NEWGROUP || cp->Letter == SC_CHANGEGROUP) {
        switch (av[1][0]) {
        default:
            Usage("Bad group mode");
            /* NOTREACHED */
        case NF_FLAG_ALIAS:
        case NF_FLAG_JUNK:
        case NF_FLAG_MODERATED:
        case NF_FLAG_OK:
        case NF_FLAG_NOLOCAL:
        case NF_FLAG_IGNORE:
            break;
        }
    }

    /* Make sure there are no separators in the parameters. */
    for (i = 0; (p = av[i++]) != NULL;)
        if (strchr(p, SC_SEP) != NULL)
            die("illegal character \\%03o in %s", (unsigned int) SC_SEP, p);

    /* Do the real work. */
    if (ICCopen() < 0)
        Failed("setup communication");
    i = ICCcommand(cp->Letter, (const char **) av, &reply);
    if (i < 0) {
        i = errno;
        p = concatpath(innconf->pathrun, INN_PATH_SERVERPID);
        if (stat(p, &Sb) < 0)
            warn("no innd.pid file; did server die?");
        free(p);
        snprintf(buff, sizeof(buff), "send \"%s\" command", cp->Command);
        errno = i;
        Failed(buff);
    }

    if (reply) {
        /* Skip "<exitcode><space>" part of reply. */
        for (p = reply; *p && isdigit((unsigned char) *p); p++)
            continue;
        while (*p && ISWHITE(*p))
            p++;
        if (i != 0)
            warn("%s", p);
        else if (!Silent)
            printf("%s\n", p);
    }

    if (ICCclose() < 0)
        Failed("end communication");

    exit(i);
    /* NOTREACHED */
}
