git-trace

romele / git-trace public

62 commits · 1 branch · 1 tag · 1 contributor (top: Romele) · first commit 3d ago · last push 11m ago

src/auth.c

#include "auth.h"
#include "crypto.h"
#include "repo.h"
#include "util.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

static int run(sqlite3 *db, const char *sql, const char *a, const char *b,
               int c, const char *d, int e)
{
    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK)
        return -1;
    if (a) sqlite3_bind_text(st, 1, a, -1, SQLITE_TRANSIENT);
    if (b) sqlite3_bind_text(st, 2, b, -1, SQLITE_TRANSIENT);
    if (c >= 0) sqlite3_bind_int(st, 3, c);
    if (d) sqlite3_bind_text(st, 4, d, -1, SQLITE_TRANSIENT);
    if (e >= 0) sqlite3_bind_int(st, 5, e);
    int rc = sqlite3_step(st);
    sqlite3_finalize(st);
    return (rc == SQLITE_DONE) ? 0 : -1;
}

int auth_user_add(sqlite3 *db, const char *name, const char *pass,
                  const char *email, int is_admin)
{
    char hash[160];
    if (pw_hash(pass, hash, sizeof hash) != 0)
        return -1;
    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db,
            "INSERT INTO users(name,email,password_hash,is_admin)"
            " VALUES(?,?,?,?)", -1, &st, NULL) != SQLITE_OK)
        return -1;
    sqlite3_bind_text(st, 1, name, -1, SQLITE_TRANSIENT);
    if (email && *email)
        sqlite3_bind_text(st, 2, email, -1, SQLITE_TRANSIENT);
    else
        sqlite3_bind_null(st, 2);
    sqlite3_bind_text(st, 3, hash, -1, SQLITE_TRANSIENT);
    sqlite3_bind_int(st, 4, is_admin);
    int rc = sqlite3_step(st);
    sqlite3_finalize(st);
    memset(hash, 0, sizeof hash);
    return rc == SQLITE_DONE ? 0 : -1;
}

int auth_user_passwd(sqlite3 *db, const char *name, const char *pass)
{
    char hash[160];
    if (pw_hash(pass, hash, sizeof hash) != 0)
        return -1;
    return run(db, "UPDATE users SET password_hash=? WHERE name=?",
               hash, name, -1, NULL, -1);
}

/* diagnostic for ops/migration checks: hash scheme name or NULL */
const char *auth_pw_scheme(sqlite3 *db, const char *name)
{
    static char sch[16];
    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db,
            "SELECT password_hash FROM users WHERE name=?",
            -1, &st, NULL) != SQLITE_OK)
        return NULL;
    sqlite3_bind_text(st, 1, name, -1, SQLITE_TRANSIENT);
    const char *rc = NULL;
    if (sqlite3_step(st) == SQLITE_ROW) {
        const char *h = (const char *)sqlite3_column_text(st, 0);
        if (h) {
            snprintf(sch, sizeof sch, "%s", pw_scheme(h));
            rc = sch;
        }
    }
    sqlite3_finalize(st);
    return rc;
}

/* ---------------- billing tiers (manual entitlement) ----------------
 * The forge stores only the entitlement (tier + expiry); payment
 * itself is an operator workflow outside the system.  Lazy expiry:
 * an expired tier reads as 'free' and is written back on first use. */

int tier_rank(const char *t)
{
    if (!t)                 return 0;
    if (!strcmp(t, "pro"))        return 1;
    if (!strcmp(t, "self"))       return 2;
    if (!strcmp(t, "enterprise")) return 3;
    return 0;                                   /* free / unknown */
}

int auth_tier_ok(const char *t)
{
    return !strcmp(t, "free") || tier_rank(t) > 0;
}

const char *auth_user_tier(sqlite3 *db, int uid, long *exp_out)
{
    static char t[16] = "free";
    long exp = 0;
    strcpy(t, "free");
    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db,
            "SELECT tier,tier_expires_at FROM users WHERE id=?",
            -1, &st, NULL) == SQLITE_OK) {
        sqlite3_bind_int(st, 1, uid);
        if (sqlite3_step(st) == SQLITE_ROW) {
            const char *s = (const char *)sqlite3_column_text(st, 0);
            if (sqlite3_column_type(st, 1) != SQLITE_NULL)
                exp = sqlite3_column_int64(st, 1);
            if (s && auth_tier_ok(s))
                snprintf(t, sizeof t, "%s", s);
        }
        sqlite3_finalize(st);
    }
    if (exp && exp < time(NULL)) {
        /* lazily sweep the expired entitlement */
        snprintf(t, sizeof t, "free");
        exp = 0;
        sqlite3_prepare_v2(db,
            "UPDATE users SET tier='free', tier_expires_at=NULL"
            " WHERE id=?", -1, &st, NULL);
        sqlite3_bind_int(st, 1, uid);
        sqlite3_step(st);
        sqlite3_finalize(st);
    }
    if (exp_out)
        *exp_out = exp;
    return t;
}

int auth_tier_set(sqlite3 *db, const char *name, const char *tier,
                  long expires)
{
    if (!auth_tier_ok(tier))
        return -1;
    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db,
            "UPDATE users SET tier=?, tier_expires_at=? WHERE name=?",
            -1, &st, NULL) != SQLITE_OK)
        return -1;
    sqlite3_bind_text(st, 1, tier, -1, SQLITE_TRANSIENT);
    if (expires > 0)
        sqlite3_bind_int64(st, 2, expires);
    else
        sqlite3_bind_null(st, 2);
    sqlite3_bind_text(st, 3, name, -1, SQLITE_TRANSIENT);
    int rc = sqlite3_step(st);
    int n = sqlite3_changes(db);
    sqlite3_finalize(st);
    return (rc == SQLITE_DONE && n == 1) ? 0 : -1;
}

int auth_is_admin(sqlite3 *db, int uid)
{
    int a = 0;
    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db, "SELECT is_admin FROM users WHERE id=?",
                           -1, &st, NULL) == SQLITE_OK) {
        sqlite3_bind_int(st, 1, uid);
        if (sqlite3_step(st) == SQLITE_ROW)
            a = sqlite3_column_int(st, 0);
        sqlite3_finalize(st);
    }
    return a;
}

int auth_user_id(sqlite3 *db, const char *name)
{
    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db, "SELECT id FROM users WHERE name=?",
                           -1, &st, NULL) != SQLITE_OK)
        return -1;
    sqlite3_bind_text(st, 1, name, -1, SQLITE_TRANSIENT);
    int uid = (sqlite3_step(st) == SQLITE_ROW) ? sqlite3_column_int(st, 0) : -1;
    sqlite3_finalize(st);
    return uid;
}

static int check_password(sqlite3 *db, const char *name, const char *secret)
{
    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db,
            "SELECT id,password_hash FROM users WHERE name=?",
            -1, &st, NULL) != SQLITE_OK)
        return -1;
    sqlite3_bind_text(st, 1, name, -1, SQLITE_TRANSIENT);
    int uid = -1, upgrade = 0;
    if (sqlite3_step(st) == SQLITE_ROW) {
        const char *h = (const char *)sqlite3_column_text(st, 1);
        if (h && pw_verify(secret, h)) {
            uid = sqlite3_column_int(st, 0);
            upgrade = pw_needs_rehash(h);
        }
    }
    sqlite3_finalize(st);
    /* transparent KDF upgrade: a legacy pbkdf2 row that just verified
     * is re-stored under the current scheme — no forced resets */
    if (uid >= 0 && upgrade) {
        char nh[160];
        if (pw_hash(secret, nh, sizeof nh) == 0) {
            sqlite3_prepare_v2(db,
                "UPDATE users SET password_hash=? WHERE id=?",
                -1, &st, NULL);
            sqlite3_bind_text(st, 1, nh, -1, SQLITE_TRANSIENT);
            sqlite3_bind_int(st, 2, uid);
            sqlite3_step(st);
            sqlite3_finalize(st);
        }
        gt_bzero(nh, sizeof nh);
    }
    return uid;
}

static int check_token(sqlite3 *db, const char *name, const char *secret)
{
    uint8_t dig[32];
    sha256(secret, strlen(secret), dig);
    char hex[65];
    to_hex(dig, 32, hex);

    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db,
            "SELECT u.id FROM auth_tokens t JOIN users u ON u.id=t.user_id"
            " WHERE u.name=? AND t.token_hash=?"
            " AND (t.expires_at IS NULL OR t.expires_at > strftime('%s','now'))",
            -1, &st, NULL) != SQLITE_OK)
        return -1;
    sqlite3_bind_text(st, 1, name, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(st, 2, hex, -1, SQLITE_TRANSIENT);
    int uid = (sqlite3_step(st) == SQLITE_ROW) ? sqlite3_column_int(st, 0) : -1;
    sqlite3_finalize(st);
    return uid;
}

int auth_token(sqlite3 *db, const char *name, const char *secret)
{
    return check_token(db, name, secret);
}

int auth_token_lookup(sqlite3 *db, const char *secret)
{
    uint8_t dig[32];
    sha256(secret, strlen(secret), dig);
    char hex[65];
    to_hex(dig, 32, hex);
    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db,
            "SELECT user_id FROM auth_tokens WHERE token_hash=?"
            " AND (expires_at IS NULL OR expires_at > strftime('%s','now'))",
            -1, &st, NULL) != SQLITE_OK)
        return -1;
    sqlite3_bind_text(st, 1, hex, -1, SQLITE_TRANSIENT);
    int uid = (sqlite3_step(st) == SQLITE_ROW) ? sqlite3_column_int(st, 0) : -1;
    sqlite3_finalize(st);
    return uid;
}

int auth_email_uid(sqlite3 *db, const char *email)
{
    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db,
            "SELECT id FROM users WHERE email=? COLLATE NOCASE",
            -1, &st, NULL) != SQLITE_OK)
        return -1;
    sqlite3_bind_text(st, 1, email, -1, SQLITE_TRANSIENT);
    int uid = (sqlite3_step(st) == SQLITE_ROW) ? sqlite3_column_int(st, 0) : -1;
    sqlite3_finalize(st);
    return uid;
}

int auth_check_secret(sqlite3 *db, const char *name, const char *secret)
{
    int uid = check_password(db, name, secret);
    if (uid < 0)
        uid = check_token(db, name, secret);
    return uid;
}

int auth_mktoken(sqlite3 *db, const char *name, const char *label,
                 char *token_out, size_t n)
{
    int uid = auth_user_id(db, name);
    if (uid < 0)
        return -1;
    char tok[65];
    rand_hex(tok, 32);
    uint8_t dig[32];
    sha256(tok, strlen(tok), dig);
    char hex[65];
    to_hex(dig, 32, hex);

    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db,
            "INSERT INTO auth_tokens(user_id,name,token_hash) VALUES(?,?,?)",
            -1, &st, NULL) != SQLITE_OK)
        return -1;
    sqlite3_bind_int(st, 1, uid);
    sqlite3_bind_text(st, 2, label, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(st, 3, hex, -1, SQLITE_TRANSIENT);
    int rc = sqlite3_step(st);
    sqlite3_finalize(st);
    if (rc != SQLITE_DONE)
        return -1;
    snprintf(token_out, n, "%s", tok);
    return 0;
}

/* ---------------- HTTP-level ---------------- */

int auth_basic(sqlite3 *db, struct request *r)
{
    char *hv = req_header_dup(r, "authorization");
    if (!hv || !starts_with(hv, "Basic ")) {
        free(hv);
        return 0;
    }
    char dec[256];
    long dl = b64_decode(hv + 6, strlen(hv + 6), dec, sizeof dec - 1);
    free(hv);
    if (dl <= 0)
        return 0;
    dec[dl] = '\0';
    char *colon = strchr(dec, ':');
    if (!colon)
        return 0;
    *colon = '\0';
    if (!name_ok(dec)) {
        memset(dec, 0, sizeof dec);
        return 0;
    }
    char uname[64];
    size_t ul = strlen(dec);            /* name_ok bounds it to 63 */
    memcpy(uname, dec, ul + 1);
    int uid = auth_check_secret(db, dec, colon + 1);
    memset(dec, 0, sizeof dec);
    if (uid > 0) {
        r->user_id = uid;
        snprintf(r->user_name, sizeof r->user_name, "%s", uname);
    }
    return uid;
}

static const char *cookie_get(struct request *r, const char *key,
                              char *out, size_t n)
{
    char *hv = req_header_dup(r, "cookie");
    if (!hv)
        return NULL;
    size_t kl = strlen(key);
    char *p = hv;
    while (*p) {
        while (*p == ' ' || *p == ';')
            p++;
        char *e = strchr(p, ';');
        size_t pl = e ? (size_t)(e - p) : strlen(p);
        const char *eq = memchr(p, '=', pl);
        if (eq && (size_t)(eq - p) == kl && strncmp(p, key, kl) == 0) {
            size_t vl = pl - (size_t)(eq - p) - 1;
            if (vl >= n)
                vl = n - 1;
            memcpy(out, eq + 1, vl);
            out[vl] = '\0';
            free(hv);
            return out;
        }
        p = e ? e + 1 : p + pl;
    }
    free(hv);
    return NULL;
}

int session_create(sqlite3 *db, int uid, char token_out[65])
{
    rand_hex(token_out, 32);
    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db,
            "INSERT INTO sessions(token,user_id,expires_at)"
            " VALUES(?,?,strftime('%s','now')+2592000)",
            -1, &st, NULL) != SQLITE_OK)
        return -1;
    sqlite3_bind_text(st, 1, token_out, -1, SQLITE_TRANSIENT);
    sqlite3_bind_int(st, 2, uid);
    int rc = sqlite3_step(st);
    sqlite3_finalize(st);
    return rc == SQLITE_DONE ? 0 : -1;
}

void session_delete(sqlite3 *db, const char *token)
{
    run(db, "DELETE FROM sessions WHERE token=?", token, NULL, -1, NULL, -1);
}

void auth_identify(sqlite3 *db, struct request *r)
{
    r->user_id = 0;
    r->user_name[0] = '\0';
    r->session_tok[0] = '\0';
    char tok[80];
    if (!cookie_get(r, "gt_session", tok, sizeof tok))
        return;
    if (strlen(tok) != 64)
        return;
    snprintf(r->session_tok, sizeof r->session_tok, "%s", tok);
    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db,
            "SELECT u.id,u.name FROM sessions s JOIN users u ON u.id=s.user_id"
            " WHERE s.token=? AND s.expires_at > strftime('%s','now')",
            -1, &st, NULL) != SQLITE_OK)
        return;
    sqlite3_bind_text(st, 1, tok, -1, SQLITE_TRANSIENT);
    if (sqlite3_step(st) == SQLITE_ROW) {
        r->user_id = sqlite3_column_int(st, 0);
        snprintf(r->user_name, sizeof r->user_name, "%s",
                 (const char *)sqlite3_column_text(st, 1));
    }
    sqlite3_finalize(st);
}

/* ---------------- ACL ---------------- */

/*
 * repo_perm resolves the effective permission of uid over owner/name.
 * Rules: owner -> 'a'; explicit repo_acl row -> its role; public -> 'r'
 * for anyone (including anonymous uid 0); otherwise 'n'.
 * A repo absent from the DB is treated as public/read-only: filesystem
 * presence is the source of truth for existence, the DB for policy.
 */
char repo_perm(sqlite3 *db, const char *owner, const char *name, int uid)
{
    sqlite3_stmt *st;
    int rc = sqlite3_prepare_v2(db,
        "SELECT r.id, r.owner_id, r.is_public FROM repos r"
        " LEFT JOIN users u ON u.id = r.owner_id"
        " WHERE r.name=? AND (u.name=? OR (r.owner_id IS NULL AND ?='_'))",
        -1, &st, NULL);
    if (rc != SQLITE_OK)
        return 'n';                 /* fail closed: a DB error must not
                                       open private repos to readers */
    sqlite3_bind_text(st, 1, name, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(st, 2, owner, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(st, 3, owner, -1, SQLITE_TRANSIENT);

    if (sqlite3_step(st) != SQLITE_ROW) {
        sqlite3_finalize(st);
        return 'r';                 /* unregistered repo: public read */
    }
    int repo_id = sqlite3_column_int(st, 0);
    int owner_id = sqlite3_column_type(st, 1) == SQLITE_NULL
                   ? 0 : sqlite3_column_int(st, 1);
    int is_public = sqlite3_column_int(st, 2);
    sqlite3_finalize(st);

    if (uid > 0 && uid == owner_id)
        return 'a';
    /* ownerless (_/) repos belong to the platform: site admins manage
     * them.  Deliberately NOT extended to user-owned repos — a global
     * admin must not gain read access to other users' private code. */
    if (owner_id == 0 && uid > 0 && auth_is_admin(db, uid))
        return 'a';

    int best = is_public ? 1 : 0;           /* rank: 'r' or 'n' */
    if (uid > 0) {
        if (sqlite3_prepare_v2(db,
                "SELECT role FROM repo_acl WHERE repo_id=? AND user_id=?",
                -1, &st, NULL) == SQLITE_OK) {
            sqlite3_bind_int(st, 1, repo_id);
            sqlite3_bind_int(st, 2, uid);
            if (sqlite3_step(st) == SQLITE_ROW) {
                const char *role = (const char *)sqlite3_column_text(st, 0);
                int rr = role ? perm_rank(role[0]) : 0;
                if (rr > best)
                    best = rr;
            }
            sqlite3_finalize(st);
        }
    }
    return perm_char(best);
}

int repo_register(sqlite3 *db, int owner_uid, const char *name,
                  int is_public, const char *desc)
{
    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db,
            "INSERT INTO repos(owner_id,name,description,is_public)"
            " VALUES(?,?,?,?)"
            " ON CONFLICT(owner_id,name) DO UPDATE SET"
            "   description=excluded.description,"
            "   is_public=excluded.is_public",
            -1, &st, NULL) != SQLITE_OK)
        return -1;
    if (owner_uid > 0)
        sqlite3_bind_int(st, 1, owner_uid);
    else
        sqlite3_bind_null(st, 1);
    sqlite3_bind_text(st, 2, name, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(st, 3, desc ? desc : "", -1, SQLITE_TRANSIENT);
    sqlite3_bind_int(st, 4, is_public);
    int rc = sqlite3_step(st);
    sqlite3_finalize(st);
    return rc == SQLITE_DONE ? 0 : -1;
}

/* ---------------- SSH keys ---------------- */

/*
 * OpenSSH fingerprint: base64(SHA256(wire_key_blob)) without padding.
 * The wire blob is the second field of a public key line.
 */
static int ssh_fingerprint(const char *blob64, char *out, size_t outlen)
{
    uint8_t blob[1024];
    long n = b64_decode(blob64, strlen(blob64), blob, sizeof blob);
    if (n <= 0)
        return -1;
    uint8_t dig[32];
    sha256(blob, (size_t)n, dig);
    char b[64];
    b64_encode(dig, 32, b, sizeof b, 1);
    return snprintf(out, outlen, "SHA256:%s", b) < (int)outlen ? 0 : -1;
}

int sshkey_add(sqlite3 *db, const char *user, const char *pubkey_line,
               const char *label)
{
    int uid = auth_user_id(db, user);
    if (uid < 0)
        return -1;

    /* line: "<type> <blob> [comment]" */
    const char *p = pubkey_line;
    while (*p == ' ' || *p == '\t')
        p++;
    const char *sp = strchr(p, ' ');
    if (!sp)
        return -1;
    const char *blob = sp + 1;
    while (*blob == ' ')
        blob++;
    const char *be = blob;
    while (*be && *be != ' ' && *be != '\t' && *be != '\r' && *be != '\n')
        be++;
    char blobbuf[1200];
    size_t bl = (size_t)(be - blob);
    if (bl == 0 || bl >= sizeof blobbuf)
        return -1;
    memcpy(blobbuf, blob, bl);
    blobbuf[bl] = '\0';

    char fp[80];
    if (ssh_fingerprint(blobbuf, fp, sizeof fp) != 0)
        return -1;

    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db,
            "INSERT INTO ssh_keys(user_id,name,pubkey,fingerprint)"
            " VALUES(?,?,?,?)", -1, &st, NULL) != SQLITE_OK)
        return -1;
    sqlite3_bind_int(st, 1, uid);
    sqlite3_bind_text(st, 2, label ? label : "key", -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(st, 3, pubkey_line, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(st, 4, fp, -1, SQLITE_TRANSIENT);
    int rc = sqlite3_step(st);
    long id = sqlite3_last_insert_rowid(db);
    sqlite3_finalize(st);
    return rc == SQLITE_DONE ? (int)id : -1;
}

int sshkey_rm(sqlite3 *db, long keyid)
{
    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db, "DELETE FROM ssh_keys WHERE id=?",
                           -1, &st, NULL) != SQLITE_OK)
        return -1;
    sqlite3_bind_int64(st, 1, keyid);
    int rc = sqlite3_step(st);
    int n = sqlite3_changes(db);
    sqlite3_finalize(st);
    return (rc == SQLITE_DONE && n > 0) ? 0 : -1;
}

/* web self-service variant: a user may only remove their own key */
int sshkey_rm_own(sqlite3 *db, int uid, long keyid)
{
    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db,
            "DELETE FROM ssh_keys WHERE id=? AND user_id=?",
            -1, &st, NULL) != SQLITE_OK)
        return -1;
    sqlite3_bind_int64(st, 1, keyid);
    sqlite3_bind_int(st, 2, uid);
    int rc = sqlite3_step(st);
    int n = sqlite3_changes(db);
    sqlite3_finalize(st);
    return (rc == SQLITE_DONE && n > 0) ? 0 : -1;
}

/* revoke only if the token belongs to uid */
int auth_token_rm(sqlite3 *db, int uid, long tokid)
{
    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db,
            "DELETE FROM auth_tokens WHERE id=? AND user_id=?",
            -1, &st, NULL) != SQLITE_OK)
        return -1;
    sqlite3_bind_int64(st, 1, tokid);
    sqlite3_bind_int(st, 2, uid);
    int rc = sqlite3_step(st);
    int n = sqlite3_changes(db);
    sqlite3_finalize(st);
    return (rc == SQLITE_DONE && n > 0) ? 0 : -1;
}

int auth_set_email(sqlite3 *db, int uid, const char *email)
{
    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db,
            "UPDATE users SET email=? WHERE id=?",
            -1, &st, NULL) != SQLITE_OK)
        return -1;
    if (email && *email)
        sqlite3_bind_text(st, 1, email, -1, SQLITE_TRANSIENT);
    else
        sqlite3_bind_null(st, 1);
    sqlite3_bind_int(st, 2, uid);
    int rc = sqlite3_step(st);
    sqlite3_finalize(st);
    return rc == SQLITE_DONE ? 0 : -1;
}

int sshkey_uid(sqlite3 *db, long keyid, char *uname, size_t unlen)
{
    sqlite3_stmt *st;
    if (sqlite3_prepare_v2(db,
            "SELECT u.id,u.name FROM ssh_keys k JOIN users u"
            " ON u.id=k.user_id WHERE k.id=?",
            -1, &st, NULL) != SQLITE_OK)
        return -1;
    sqlite3_bind_int64(st, 1, keyid);
    int uid = -1;
    if (sqlite3_step(st) == SQLITE_ROW) {
        uid = sqlite3_column_int(st, 0);
        snprintf(uname, unlen, "%s",
                 (const char *)sqlite3_column_text(st, 1));
    }
    sqlite3_finalize(st);
    return uid;
}

/* ---------------- forms ---------------- */

int form_get(const char *body, size_t blen, const char *key,
             char *out, size_t outlen)
{
    size_t kl = strlen(key);
    const char *p = body;
    const char *end = body + blen;
    while (p < end) {
        const char *amp = memchr(p, '&', (size_t)(end - p));
        const char *pe = amp ? amp : end;
        const char *eq = memchr(p, '=', (size_t)(pe - p));
        if (eq && (size_t)(eq - p) == kl && strncmp(p, key, kl) == 0) {
            char *d = url_decode(eq + 1, (size_t)(pe - eq - 1));
            int rc = snprintf(out, outlen, "%s", d);
            free(d);
            return (rc < 0 || (size_t)rc >= outlen) ? -1 : 0;
        }
        p = amp ? amp + 1 : end;
    }
    return -1;
}