Commit message: Discard job body bytes if the job is too big.

Previously, a malicious user could craft a job payload and inject
beanstalk commands without the client application knowing. (An
extra-careful client library could check the size of the job body before
sending the put command, but most libraries do not do this, nor should
they have to.)

Reported by Graham Barr.
CVE: CVE-2010-2060
Vulnerability description: The put command functionality in beanstalkd 1.4.5 and earlier allows remote attackers to execute arbitrary Beanstalk commands via the body in a job that is too big, which is not properly handled by the dispatch_cmd function in prot.c.

Project: beanstalkd

_skip(conn c, int n, const char *line, int len)
{
    /* Invert the meaning of in_job_read while throwing away data -- it
     * counts the bytes that remain to be thrown away. */
    c->in_job = 0;
    c->in_job_read = n;
    fill_extra_data(c);

    if (c->in_job_read == 0) return reply(c, line, len, STATE_SENDWORD);

    c->reply = line;
    c->reply_len = len;
    c->reply_sent = 0;
    c->state = STATE_BITBUCKET;
    return;
}

Project: beanstalkd

buried_job_p(tube t)
{
    return job_list_any_p(&t->buried);
}

Project: beanstalkd

cmd_len(conn c)
{
    return scan_line_end(c->cmd, c->cmd_read);
}

Project: beanstalkd

delay_q_peek()
{
    int i;
    tube t;
    job j = NULL, nj;

    for (i = 0; i < tubes.used; i++) {
        t = tubes.items[i];
        nj = pq_peek(&t->delay);
        if (!nj) continue;
        if (!j || nj->deadline_at < j->deadline_at) j = nj;
    }

    return j;
}

Project: beanstalkd

delay_q_take()
{
    job j = delay_q_peek();
    return j ? pq_take(&j->tube->delay) : NULL;
}

Project: beanstalkd

do_cmd(conn c)
{
    dispatch_cmd(c);
    fill_extra_data(c);
}

Project: beanstalkd

do_list_tubes(conn c, ms l)
{
    char *buf;
    tube t;
    size_t i, resp_z;

    /* first, measure how big a buffer we will need */
    resp_z = 6; /* initial "---\n" and final "\r\n" */
    for (i = 0; i < l->used; i++) {
        t = l->items[i];
        resp_z += 3 + strlen(t->name); /* including "- " and "\n" */
    }

    c->out_job = allocate_job(resp_z); /* fake job to hold response data */
    if (!c->out_job) return reply_serr(c, MSG_OUT_OF_MEMORY);

    /* Mark this job as a copy so it can be appropriately freed later on */
    c->out_job->state = JOB_STATE_COPY;

    /* now actually format the response */
    buf = c->out_job->body;
    buf += snprintf(buf, 5, "---\n");
    for (i = 0; i < l->used; i++) {
        t = l->items[i];
        buf += snprintf(buf, 4 + strlen(t->name), "- %s\n", t->name);
    }
    buf[0] = '\r';
    buf[1] = '\n';

    c->out_job_sent = 0;
    return reply_line(c, STATE_SENDJOB, "OK %d\r\n", resp_z - 2);
}

Project: beanstalkd

do_stats(conn c, fmt_fn fmt, void *data)
{
    int r, stats_len;

    /* first, measure how big a buffer we will need */
    stats_len = fmt(NULL, 0, data) + 16;

    c->out_job = allocate_job(stats_len); /* fake job to hold stats data */
    if (!c->out_job) return reply_serr(c, MSG_OUT_OF_MEMORY);

    /* Mark this job as a copy so it can be appropriately freed later on */
    c->out_job->state = JOB_STATE_COPY;

    /* now actually format the stats data */
    r = fmt(c->out_job->body, stats_len, data);
    /* and set the actual body size */
    c->out_job->body_size = r;
    if (r > stats_len) return reply_serr(c, MSG_INTERNAL_ERROR);

    c->out_job_sent = 0;
    return reply_line(c, STATE_SENDJOB, "OK %d\r\n", r - 2);
}

Project: beanstalkd

enqueue_incoming_job(conn c)
{
    int r;
    job j = c->in_job;

    c->in_job = NULL; /* the connection no longer owns this job */
    c->in_job_read = 0;

    /* check if the trailer is present and correct */
    if (memcmp(j->body + j->body_size - 2, "\r\n", 2)) {
        job_free(j);
        return reply_msg(c, MSG_EXPECTED_CRLF);
    }

    if (drain_mode) {
        job_free(j);
        return reply_serr(c, MSG_DRAINING);
    }

    if (j->reserved_binlog_space) return reply_serr(c, MSG_INTERNAL_ERROR);
    j->reserved_binlog_space = binlog_reserve_space_put(j);
    if (!j->reserved_binlog_space) return reply_serr(c, MSG_OUT_OF_MEMORY);

    /* we have a complete job, so let's stick it in the pqueue */
    r = enqueue_job(j, j->delay, 1);
    if (r < 0) return reply_serr(c, MSG_INTERNAL_ERROR);

    op_ct[OP_PUT]++; /* stats */
    global_stat.total_jobs_ct++;
    j->tube->stat.total_jobs_ct++;

    if (r == 1) return reply_line(c, STATE_SENDWORD, MSG_INSERTED_FMT, j->id);

    /* out of memory trying to grow the queue, so it gets buried */
    bury_job(j, 0);
    reply_line(c, STATE_SENDWORD, MSG_BURIED_FMT, j->id);
}

Project: beanstalkd

enqueue_job(job j, usec delay, char update_store)
{
    int r;

    j->reserver = NULL;
    if (delay) {
        j->deadline_at = now_usec() + delay;
        r = pq_give(&j->tube->delay, j);
        if (!r) return 0;
        j->state = JOB_STATE_DELAYED;
        set_main_delay_timeout();
    } else {
        r = pq_give(&j->tube->ready, j);
        if (!r) return 0;
        j->state = JOB_STATE_READY;
        ready_ct++;
        if (j->pri < URGENT_THRESHOLD) {
            global_stat.urgent_ct++;
            j->tube->stat.urgent_ct++;
        }
    }

    if (update_store) {
        r = binlog_write_job(j);
        if (!r) return -1;
    }

    process_queue();
    return 1;
}

Project: beanstalkd

enqueue_reserved_jobs(conn c)
{
    int r;
    job j;

    while (job_list_any_p(&c->reserved_jobs)) {
        j = job_remove(c->reserved_jobs.next);
        r = enqueue_job(j, 0, 0);
        if (r < 1) bury_job(j, 0);
        global_stat.reserved_ct--;
        j->tube->stat.reserved_ct--;
        c->soonest_job = NULL;
        if (!job_list_any_p(&c->reserved_jobs)) conn_remove(c);
    }
}

Project: beanstalkd

enqueue_waiting_conn(conn c)
{
    tube t;
    size_t i;

    global_stat.waiting_ct++;
    c->type |= CONN_TYPE_WAITING;
    for (i = 0; i < c->watch.used; i++) {
        t = c->watch.items[i];
        t->stat.waiting_ct++;
        ms_append(&t->waiting, c);
    }
}

Project: beanstalkd

enter_drain_mode(int sig)
{
    drain_mode = 1;
}

Project: beanstalkd

find_reserved_job_in_conn(conn c, job j)
{
    return (j && j->reserver == c && j->state == JOB_STATE_RESERVED) ? j : NULL;
}

Project: beanstalkd

fmt_job_stats(char *buf, size_t size, job j)
{
    usec t;
    uint64_t time_left;

    t = now_usec();
    if (j->state == JOB_STATE_RESERVED || j->state == JOB_STATE_DELAYED) {
        time_left = (j->deadline_at - t) / 1000000;
    } else {
        time_left = 0;
    }
    return snprintf(buf, size, STATS_JOB_FMT,
            j->id,
            j->tube->name,
            job_state(j),
            j->pri,
            (t - j->created_at) / 1000000,
            j->delay / 1000000,
            j->ttr / 1000000,
            time_left,
            j->reserve_ct,
            j->timeout_ct,
            j->release_ct,
            j->bury_ct,
            j->kick_ct);
}

Project: beanstalkd

fmt_stats_tube(char *buf, size_t size, tube t)
{
    uint64_t time_left;

    if (t->pause > 0) {
        time_left = (t->deadline_at - now_usec()) / 1000000;
    } else {
        time_left = 0;
    }
    return snprintf(buf, size, STATS_TUBE_FMT,
            t->name,
            t->stat.urgent_ct,
            t->ready.used,
            t->stat.reserved_ct,
            t->delay.used,
            t->stat.buried_ct,
            t->stat.total_jobs_ct,
            t->using_ct,
            t->watching_ct,
            t->stat.waiting_ct,
            t->stat.pause_ct,
            t->pause / 1000000,
            time_left);
}

Project: beanstalkd

get_delayed_job_ct()
{
    tube t;
    size_t i;
    unsigned int count = 0;

    for (i = 0; i < tubes.used; i++) {
        t = tubes.items[i];
        count += t->delay.used;
    }
    return count;
}

Project: beanstalkd

h_accept(const int fd, const short which, struct event *ev)
{
    conn c;
    int cfd, flags, r;
    socklen_t addrlen;
    struct sockaddr_in6 addr;

    if (which == EV_TIMEOUT) return h_delay();

    addrlen = sizeof addr;
    cfd = accept(fd, (struct sockaddr *)&addr, &addrlen);
    if (cfd == -1) {
        if (errno != EAGAIN && errno != EWOULDBLOCK) twarn("accept()");
        if (errno == EMFILE) brake();
        return;
    }

    flags = fcntl(cfd, F_GETFL, 0);
    if (flags < 0) return twarn("getting flags"), close(cfd), v();

    r = fcntl(cfd, F_SETFL, flags | O_NONBLOCK);
    if (r < 0) return twarn("setting O_NONBLOCK"), close(cfd), v();

    c = make_conn(cfd, STATE_WANTCOMMAND, default_tube, default_tube);
    if (!c) return twarnx("make_conn() failed"), close(cfd), brake();

    dprintf("accepted conn, fd=%d\n", cfd);
    r = conn_set_evq(c, EV_READ | EV_PERSIST, (evh) h_conn);
    if (r == -1) return twarnx("conn_set_evq() failed"), close(cfd), brake();
}

Project: beanstalkd

h_conn(const int fd, const short which, conn c)
{
    if (fd != c->fd) {
        twarnx("Argh! event fd doesn't match conn fd.");
        close(fd);
        return conn_close(c);
    }

    switch (which) {
    case EV_TIMEOUT:
        h_conn_timeout(c);
        event_add(&c->evq, NULL); /* seems to be necessary */
        break;
    case EV_READ:
        /* fall through... */
    case EV_WRITE:
        /* fall through... */
    default:
        h_conn_data(c);
    }

    while (cmd_data_ready(c) && (c->cmd_len = cmd_len(c))) do_cmd(c);
}

Project: beanstalkd

h_conn_timeout(conn c)
{
    int r, should_timeout = 0;
    job j;

    /* Check if the client was trying to reserve a job. */
    if (conn_waiting(c) && conn_has_close_deadline(c)) should_timeout = 1;

    /* Check if any reserved jobs have run out of time. We should do this
     * whether or not the client is waiting for a new reservation. */
    while ((j = soonest_job(c))) {
        if (j->deadline_at >= now_usec()) break;

        /* This job is in the middle of being written out. If we return it to
         * the ready queue, someone might free it before we finish writing it
         * out to the socket. So we'll copy it here and free the copy when it's
         * done sending. */
        if (j == c->out_job) {
            c->out_job = job_copy(c->out_job);
        }

        timeout_ct++; /* stats */
        j->timeout_ct++;
        r = enqueue_job(remove_this_reserved_job(c, j), 0, 0);
        if (r < 1) bury_job(j, 0); /* out of memory, so bury it */
        r = conn_update_evq(c, c->evq.ev_events);
        if (r == -1) return twarnx("conn_update_evq() failed"), conn_close(c);
    }

    if (should_timeout) {
        dprintf("conn_waiting(%p) = %d\n", c, conn_waiting(c));
        return reply_msg(remove_waiting_conn(c), MSG_DEADLINE_SOON);
    } else if (conn_waiting(c) && c->pending_timeout >= 0) {
        dprintf("conn_waiting(%p) = %d\n", c, conn_waiting(c));
        c->pending_timeout = -1;
        return reply_msg(remove_waiting_conn(c), MSG_TIMED_OUT);
    }
}

Project: beanstalkd

h_delay()
{
    int r;
    job j;
    usec now;
    int i;
    tube t;

    now = now_usec();
    while ((j = delay_q_peek())) {
        if (j->deadline_at > now) break;
        j = delay_q_take();
        r = enqueue_job(j, 0, 0);
        if (r < 1) bury_job(j, 0); /* out of memory, so bury it */
    }

    for (i = 0; i < tubes.used; i++) {
        t = tubes.items[i];

        dprintf("h_delay for %s t->waiting.used=%zu t->ready.used=%d t->pause=%" PRIu64 "\n",
                t->name, t->waiting.used, t->ready.used, t->pause);
        if (t->pause && t->deadline_at <= now) {
            t->pause = 0;
            process_queue();
        }
    }

    set_main_delay_timeout();
}

Project: beanstalkd

kick_buried_job(tube t)
{
    int r;
    job j;
    size_t z;

    if (!buried_job_p(t)) return 0;
    j = remove_buried_job(t->buried.next);

    z = binlog_reserve_space_update(j);
    if (!z) return pq_give(&t->delay, j), 0; /* put it back */
    j->reserved_binlog_space += z;

    j->kick_ct++;
    r = enqueue_job(j, 0, 1);
    if (r == 1) return 1;

    /* ready queue is full, so bury it */
    bury_job(j, 0);
    return 0;
}

Project: beanstalkd

kick_delayed_job(tube t)
{
    int r;
    job j;
    size_t z;

    j = pq_take(&t->delay);
    if (!j) return 0;

    z = binlog_reserve_space_update(j);
    if (!z) return pq_give(&t->delay, j), 0; /* put it back */
    j->reserved_binlog_space += z;

    j->kick_ct++;
    r = enqueue_job(j, 0, 1);
    if (r == 1) return 1;

    /* ready queue is full, so delay it again */
    r = enqueue_job(j, j->delay, 0);
    if (r == 1) return 0;

    /* last resort */
    bury_job(j, 0);
    return 0;
}

Project: beanstalkd

kick_delayed_jobs(tube t, unsigned int n)
{
    unsigned int i;
    for (i = 0; (i < n) && kick_delayed_job(t); ++i);
    return i;
}

Project: beanstalkd

kick_jobs(tube t, unsigned int n)
{
    if (buried_job_p(t)) return kick_buried_jobs(t, n);
    return kick_delayed_jobs(t, n);
}

Project: beanstalkd

maybe_enqueue_incoming_job(conn c)
{
    job j = c->in_job;

    /* do we have a complete job? */
    if (c->in_job_read == j->body_size) return enqueue_incoming_job(c);

    /* otherwise we have incomplete data, so just keep waiting */
    c->state = STATE_WANTDATA;
}

Project: beanstalkd

name_is_ok(const char *name, size_t max)
{
    size_t len = strlen(name);
    return len > 0 && len <= max &&
        strspn(name, NAME_CHARS) == len && name[0] != '-';
}

Project: beanstalkd

next_eligible_job(usec now)
{
    tube t;
    size_t i;
    job j = NULL, candidate;

    dprintf("tubes.used = %zu\n", tubes.used);
    for (i = 0; i < tubes.used; i++) {
        t = tubes.items[i];
        dprintf("for %s t->waiting.used=%zu t->ready.used=%d t->pause=%" PRIu64 "\n",
                t->name, t->waiting.used, t->ready.used, t->pause);
        if (t->pause) {
            if (t->deadline_at > now) continue;
            t->pause = 0;
        }
        if (t->waiting.used && t->ready.used) {
            candidate = pq_peek(&t->ready);
            if (!j || job_pri_cmp(candidate, j) < 0) j = candidate;
        }
        dprintf("i = %zu, tubes.used = %zu\n", i, tubes.used);
    }

    return j;
}

Project: beanstalkd

pause_tube_peek()
{
    int i;
    tube t, nt = NULL;

    for (i = 0; i < tubes.used; i++) {
        t = tubes.items[i];
        if (t->pause) {
            if (!nt || t->deadline_at < nt->deadline_at) nt = t;
        }
    }

    return nt;
}

Project: beanstalkd

peek_job(uint64_t id)
{
    return job_find(id);
}

Project: beanstalkd

process_queue()
{
    job j;
    usec now = now_usec();

    dprintf("processing queue\n");
    while ((j = next_eligible_job(now))) {
        dprintf("got eligible job %llu in %s\n", j->id, j->tube->name);
        j = pq_take(&j->tube->ready);
        ready_ct--;
        if (j->pri < URGENT_THRESHOLD) {
            global_stat.urgent_ct--;
            j->tube->stat.urgent_ct--;
        }
        reserve_job(remove_waiting_conn(ms_take(&j->tube->waiting)), j);
    }
}

Project: beanstalkd

prot_init()
{
    started_at = now_usec();
    memset(op_ct, 0, sizeof(op_ct));

    ms_init(&tubes, NULL, NULL);

    TUBE_ASSIGN(default_tube, tube_find_or_make("default"));
    if (!default_tube) twarnx("Out of memory during startup!");
}

Project: beanstalkd

prot_remove_tube(tube t)
{
    ms_remove(&tubes, t);
}

Project: beanstalkd

prot_replay_binlog(job binlog_jobs)
{
    job j, nj;
    usec delay;
    int r;

    for (j = binlog_jobs->next ; j != binlog_jobs ; j = nj) {
        nj = j->next;
        job_remove(j);
        binlog_reserve_space_update(j); /* reserve space for a delete */
        delay = 0;
        switch (j->state) {
        case JOB_STATE_BURIED:
            bury_job(j, 0);
            break;
        case JOB_STATE_DELAYED:
            if (started_at < j->deadline_at) {
                delay = j->deadline_at - started_at;
            }
            /* fall through */
        default:
            r = enqueue_job(j, delay, 0);
            if (r < 1) twarnx("error processing binlog job %llu", j->id);
        }
    }
}

Project: beanstalkd

read_delay(usec *delay, const char *buf, char **end)
{
    int r;
    unsigned int delay_sec;

    r = read_pri(&delay_sec, buf, end);
    if (r) return r;
    *delay = ((usec) delay_sec) * 1000000;
    return 0;
}

Project: beanstalkd

read_pri(unsigned int *pri, const char *buf, char **end)
{
    char *tend;
    unsigned int tpri;

    errno = 0;
    while (buf[0] == ' ') buf++;
    if (!isdigit(buf[0])) return -1;
    tpri = strtoul(buf, &tend, 10);
    if (tend == buf) return -1;
    if (errno && errno != ERANGE) return -1;
    if (!end && tend[0] != '\0') return -1;

    if (pri) *pri = tpri;
    if (end) *end = tend;
    return 0;
}

Project: beanstalkd

read_tube_name(char **tubename, char *buf, char **end)
{
    size_t len;

    while (buf[0] == ' ') buf++;
    len = strspn(buf, NAME_CHARS);
    if (len == 0) return -1;
    if (tubename) *tubename = buf;
    if (end) *end = buf + len;
    return 0;
}

Project: beanstalkd

remove_buried_job(job j)
{
    if (!j || j->state != JOB_STATE_BURIED) return NULL;
    j = job_remove(j);
    if (j) {
        global_stat.buried_ct--;
        j->tube->stat.buried_ct--;
    }
    return j;
}

Project: beanstalkd

remove_ready_job(job j)
{
    if (!j || j->state != JOB_STATE_READY) return NULL;
    j = pq_remove(&j->tube->ready, j);
    if (j) {
        ready_ct--;
        if (j->pri < URGENT_THRESHOLD) {
            global_stat.urgent_ct--;
            j->tube->stat.urgent_ct--;
        }
    }
    return j;
}

Project: beanstalkd

remove_reserved_job(conn c, job j)
{
    return remove_this_reserved_job(c, find_reserved_job_in_conn(c, j));
}

Project: beanstalkd

remove_this_reserved_job(conn c, job j)
{
    j = job_remove(j);
    if (j) {
        global_stat.reserved_ct--;
        j->tube->stat.reserved_ct--;
        j->reserver = NULL;
    }
    c->soonest_job = NULL;
    if (!job_list_any_p(&c->reserved_jobs)) conn_remove(c);
    return j;
}

Project: beanstalkd

remove_waiting_conn(conn c)
{
    tube t;
    size_t i;

    if (!conn_waiting(c)) return NULL;

    c->type &= ~CONN_TYPE_WAITING;
    global_stat.waiting_ct--;
    for (i = 0; i < c->watch.used; i++) {
        t = c->watch.items[i];
        t->stat.waiting_ct--;
        ms_remove(&t->waiting, c);
    }
    return c;
}

Project: beanstalkd

reply(conn c, const char *line, int len, int state)
{
    int r;

    if (!c) return;

    r = conn_update_evq(c, EV_WRITE | EV_PERSIST);
    if (r == -1) return twarnx("conn_update_evq() failed"), conn_close(c);

    c->reply = line;
    c->reply_len = len;
    c->reply_sent = 0;
    c->state = state;
    dprintf("sending reply: %.*s", len, line);
}

Project: beanstalkd

reply_job(conn c, job j, const char *word)
{
    /* tell this connection which job to send */
    c->out_job = j;
    c->out_job_sent = 0;

    return reply_line(c, STATE_SENDJOB, "%s %llu %u\r\n",
                      word, j->id, j->body_size - 2);
}

Project: beanstalkd

reply_line(conn c, int state, const char *fmt, ...)
{
    int r;
    va_list ap;

    va_start(ap, fmt);
    r = vsnprintf(c->reply_buf, LINE_BUF_SIZE, fmt, ap);
    va_end(ap);

    /* Make sure the buffer was big enough. If not, we have a bug. */
    if (r >= LINE_BUF_SIZE) return reply_serr(c, MSG_INTERNAL_ERROR);

    return reply(c, c->reply_buf, r, state);
}

Project: beanstalkd

reset_conn(conn c)
{
    int r;

    r = conn_update_evq(c, EV_READ | EV_PERSIST);
    if (r == -1) return twarnx("update events failed"), conn_close(c);

    /* was this a peek or stats command? */
    if (c->out_job && c->out_job->state == JOB_STATE_COPY) job_free(c->out_job);
    c->out_job = NULL;

    c->reply_sent = 0; /* now that we're done, reset this */
    c->state = STATE_WANTCOMMAND;
}

Project: beanstalkd

scan_line_end(const char *s, int size)
{
    char *match;

    match = memchr(s, '\r', size - 1);
    if (!match) return 0;

    /* this is safe because we only scan size - 1 chars above */
    if (match[1] == '\n') return match - s + 2;

    return 0;
}

Project: beanstalkd

set_main_delay_timeout()
{
    job j = delay_q_peek();
    tube t = pause_tube_peek();
    usec deadline_at = t ? t->deadline_at : 0;

    if (j && (!deadline_at || j->deadline_at < deadline_at)) deadline_at = j->deadline_at;

    dprintf("deadline_at=%" PRIu64 "\n", deadline_at);
    set_main_timeout(deadline_at);
}

Project: beanstalkd

touch_job(conn c, job j)
{
    j = find_reserved_job_in_conn(c, j);
    if (j) {
        j->deadline_at = now_usec() + j->ttr;
        c->soonest_job = NULL;
    }
    return j;
}

Project: beanstalkd

uptime()
{
    return (now_usec() - started_at) / 1000000;
}

Project: beanstalkd

which_cmd(conn c)
{
#define TEST_CMD(s,c,o) if (strncmp((s), (c), CONSTSTRLEN(c)) == 0) return (o);
    TEST_CMD(c->cmd, CMD_PUT, OP_PUT);
    TEST_CMD(c->cmd, CMD_PEEKJOB, OP_PEEKJOB);
    TEST_CMD(c->cmd, CMD_PEEK_READY, OP_PEEK_READY);
    TEST_CMD(c->cmd, CMD_PEEK_DELAYED, OP_PEEK_DELAYED);
    TEST_CMD(c->cmd, CMD_PEEK_BURIED, OP_PEEK_BURIED);
    TEST_CMD(c->cmd, CMD_RESERVE_TIMEOUT, OP_RESERVE_TIMEOUT);
    TEST_CMD(c->cmd, CMD_RESERVE, OP_RESERVE);
    TEST_CMD(c->cmd, CMD_DELETE, OP_DELETE);
    TEST_CMD(c->cmd, CMD_RELEASE, OP_RELEASE);
    TEST_CMD(c->cmd, CMD_BURY, OP_BURY);
    TEST_CMD(c->cmd, CMD_KICK, OP_KICK);
    TEST_CMD(c->cmd, CMD_TOUCH, OP_TOUCH);
    TEST_CMD(c->cmd, CMD_JOBSTATS, OP_JOBSTATS);
    TEST_CMD(c->cmd, CMD_STATS_TUBE, OP_STATS_TUBE);
    TEST_CMD(c->cmd, CMD_STATS, OP_STATS);
    TEST_CMD(c->cmd, CMD_USE, OP_USE);
    TEST_CMD(c->cmd, CMD_WATCH, OP_WATCH);
    TEST_CMD(c->cmd, CMD_IGNORE, OP_IGNORE);
    TEST_CMD(c->cmd, CMD_LIST_TUBES_WATCHED, OP_LIST_TUBES_WATCHED);
    TEST_CMD(c->cmd, CMD_LIST_TUBE_USED, OP_LIST_TUBE_USED);
    TEST_CMD(c->cmd, CMD_LIST_TUBES, OP_LIST_TUBES);
    TEST_CMD(c->cmd, CMD_QUIT, OP_QUIT);
    TEST_CMD(c->cmd, CMD_PAUSE_TUBE, OP_PAUSE_TUBE);
    return OP_UNKNOWN;
}