root/usr.sbin/smtpd/table_proc.c
/*      $OpenBSD: table_proc.c,v 1.24 2026/03/26 18:43:53 op Exp $      */

/*
 * Copyright (c) 2024 Omar Polo <op@openbsd.org>
 * Copyright (c) 2013 Eric Faurot <eric@openbsd.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "smtpd.h"
#include "log.h"

#define PROTOCOL_VERSION        "0.1"

struct table_proc_priv {
        FILE            *in;
        FILE            *out;
        char            *line;
        size_t           linesize;

        /*
         * The last ID used in a request.  At the moment the protocol
         * is synchronous from our point of view, so it's used to
         * assert that the table replied with the correct ID.
         */
        char             lastid[16];
};

static char *
table_proc_nextid(struct table *table)
{
        struct table_proc_priv  *priv = table->t_handle;
        int                      r;

        r = snprintf(priv->lastid, sizeof(priv->lastid), "%08x", arc4random());
        if (r < 0 || (size_t)r >= sizeof(priv->lastid))
                fatal("table-proc: snprintf");

        return (priv->lastid);
}

static void
table_proc_send(struct table *table, const char *type, int service,
    const char *param)
{
        struct table_proc_priv  *priv = table->t_handle;
        struct timeval           tv;

        gettimeofday(&tv, NULL);
        fprintf(priv->out, "table|%s|%lld.%06ld|%s|%s",
            PROTOCOL_VERSION, (long long)tv.tv_sec, (long)tv.tv_usec,
            table->t_name, type);
        if (service != -1) {
                fprintf(priv->out, "|%s|%s", table_service_name(service),
                    table_proc_nextid(table));
                if (param)
                        fprintf(priv->out, "|%s", param);
                fputc('\n', priv->out);
        } else
                fprintf(priv->out, "|%s\n", table_proc_nextid(table));

        if (fflush(priv->out) == EOF)
                fatal("table-proc: fflush");
}

static const char *
table_proc_recv(struct table *table, const char *type)
{
        struct table_proc_priv  *priv = table->t_handle;
        const char              *l;
        ssize_t                  linelen;
        size_t                   len;

        if ((linelen = getline(&priv->line, &priv->linesize, priv->in)) == -1)
                fatal("table-proc: getline");
        priv->line[strcspn(priv->line, "\n")] = '\0';
        l = priv->line;

        len = strlen(type);
        if (strncmp(l, type, len) != 0)
                goto err;
        l += len;

        if (*l != '|')
                goto err;
        l++;

        len = strlen(priv->lastid);
        if (strncmp(l, priv->lastid, len) != 0)
                goto err;
        l += len;

        if (*l != '|')
                goto err;
        return (++l);

 err:
        log_warnx("warn: table-proc: failed to parse reply");
        fatalx("table-proc: exiting");
}

/*
 * API
 */

static int
table_proc_open(struct table *table)
{
        struct table_proc_priv  *priv;
        const char              *s;
        ssize_t                  len;
        int                      service, services = 0;
        int                      fd, fdd;

        priv = xcalloc(1, sizeof(*priv));

        fd = fork_proc_backend("table", table->t_config, table->t_name, 1);
        if (fd == -1)
                fatalx("table-proc: exiting");
        if ((fdd = dup(fd)) == -1) {
                log_warnx("warn: table-proc: dup");
                fatalx("table-proc: exiting");
        }
        if ((priv->in = fdopen(fd, "r")) == NULL)
                fatalx("table-proc: fdopen");
        if ((priv->out = fdopen(fdd, "w")) == NULL)
                fatalx("table-proc: fdopen");

        fprintf(priv->out, "config|smtpd-version|"SMTPD_VERSION"\n");
        fprintf(priv->out, "config|protocol|"PROTOCOL_VERSION"\n");
        fprintf(priv->out, "config|tablename|%s\n", table->t_name);
        fprintf(priv->out, "config|ready\n");
        if (fflush(priv->out) == EOF)
                fatalx("table-proc: fflush");

        while ((len = getline(&priv->line, &priv->linesize, priv->in)) != -1) {
                priv->line[strcspn(priv->line, "\n")] = '\0';

                if (strncmp(priv->line, "register|", 9) != 0)
                        fatalx("table-proc: invalid handshake reply");

                s = priv->line + 9;
                if (!strcmp(s, "ready"))
                        break;
                service = table_service_from_name(s);
                if (service == -1 || service == K_NONE)
                        fatalx("table-proc: unknown service %s", s);

                services |= service;
        }

        if (ferror(priv->in))
                fatal("table-proc: getline");
        if (feof(priv->in))
                fatalx("table-proc: unexpected EOF during handshake");
        if (services == 0)
                fatalx("table-proc: no services registered");

        table->t_services = services;
        table->t_handle = priv;

        return (1);
}

static int
table_proc_update(struct table *table)
{
        const char              *r;

        table_proc_send(table, "update", -1, NULL);
        r = table_proc_recv(table, "update-result");
        if (!strcmp(r, "ok"))
                return (1);

        if (!strncmp(r, "error", 5)) {
                if (r[5] == '|') {
                        r += 6;
                        log_warnx("warn: table-proc: %s update failed: %s",
                            table->t_name, r);
                }
                return (0);
        }

        log_warnx("warn: table-proc: failed parse reply");
        fatalx("table-proc: exiting");
}

static void
table_proc_close(struct table *table)
{
        struct table_proc_priv  *priv = table->t_handle;

        if (fclose(priv->in) == EOF)
                fatal("table-proc: fclose");
        if (fclose(priv->out) == EOF)
                fatal("table-proc: fclose");
        free(priv->line);
        free(priv);

        table->t_handle = NULL;
}

static int
table_proc_lookup(struct table *table, enum table_service s, const char *k, char **dst)
{
        const char              *req = "lookup", *res = "lookup-result";
        const char              *r;

        if (dst == NULL) {
                req = "check";
                res = "check-result";
        }

        /* k cannot contain newlines */
        if (k[strcspn(k, "\r\n")] != '\0')
                return (-1);

        table_proc_send(table, req, s, k);
        r = table_proc_recv(table, res);

        if (!strcmp(r, "not-found"))
                return (0);

        if (!strncmp(r, "error", 5)) {
                if (r[5] == '|') {
                        r += 6;
                        log_warnx("warn: table-proc: %s %s failed: %s",
                            table->t_name, req, r);
                }
                return (-1);
        }

        if (dst == NULL) {
                /* check op */
                if (!strncmp(r, "found", 5))
                        return (1);
                log_warnx("warn: table-proc: failed to parse reply");
                fatalx("table-proc: exiting");
        }

        /* lookup op */
        if (strncmp(r, "found|", 6) != 0) {
                log_warnx("warn: table-proc: failed to parse reply");
                fatalx("table-proc: exiting");
        }
        r += 6;
        if (*r == '\0') {
                log_warnx("warn: table-proc: empty response");
                fatalx("table-proc: exiting");
        }
        if ((*dst = strdup(r)) == NULL)
                return (-1);
        return (1);
}

static int
table_proc_fetch(struct table *table, enum table_service s, char **dst)
{
        const char              *r;

        table_proc_send(table, "fetch", s, NULL);
        r = table_proc_recv(table, "fetch-result");

        if (!strcmp(r, "not-found"))
                return (0);

        if (!strncmp(r, "error", 5)) {
                if (r[5] == '|') {
                        r += 6;
                        log_warnx("warn: table-proc: %s fetch failed: %s",
                            table->t_name, r);
                }
                return (-1);
        }

        if (strncmp(r, "found|", 6) != 0) {
                log_warnx("warn: table-proc: failed to parse reply");
                fatalx("table-proc: exiting");
        }
        r += 6;
        if (*r == '\0') {
                log_warnx("warn: table-proc: empty response");
                fatalx("table-proc: exiting");
        }

        if ((*dst = strdup(r)) == NULL)
                return (-1);
        return (1);
}

struct table_backend table_backend_proc = {
        .name = "proc",
        .services = K_ANY,
        .config = NULL,
        .add = NULL,
        .dump = NULL,
        .open = table_proc_open,
        .update = table_proc_update,
        .close = table_proc_close,
        .lookup = table_proc_lookup,
        .fetch = table_proc_fetch,
};