X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/blobdiff_plain/f0feb22e80bfe438c16d212a7cc8be6d2282b6ac..91d9a42d3d2a8382f1b3729b2f4c6c2aafe4126d:/lib/trackdb.c diff --git a/lib/trackdb.c b/lib/trackdb.c index d3e6e40..f5d5836 100644 --- a/lib/trackdb.c +++ b/lib/trackdb.c @@ -17,7 +17,7 @@ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA */ -/** @file server/trackdb.c +/** @file lib/trackdb.c * @brief Track database * * This file is getting in desparate need of splitting up... @@ -49,6 +49,7 @@ #include "kvp.h" #include "log.h" #include "vector.h" +#include "rights.h" #include "trackdb.h" #include "configuration.h" #include "syscalls.h" @@ -63,7 +64,7 @@ #include "hash.h" #include "unicode.h" #include "unidata.h" -#include "mime.h" +#include "base64.h" #define RESCAN "disorder-rescan" #define DEADLOCK "disorder-deadlock" @@ -73,7 +74,6 @@ static const char *getpart(const char *track, const char *part, const struct kvp *p, int *used_db); -static int trackdb_alltags_tid(DB_TXN *tid, char ***taglistp); static char **trackdb_new_tid(int *ntracksp, int maxtracks, DB_TXN *tid); @@ -113,7 +113,7 @@ DB *trackdb_prefsdb; * - Values are UTF-8(NFC(unicode(path name))) * - There can be more than one value per key * - Presence of key,value means that path matches the search terms - * - Only tracks fond in @ref tracks_tracksdb are represented here + * - Only tracks fond in @ref trackdb_tracksdb are represented here * - This database can be reconstructed, it contains no user data */ DB *trackdb_searchdb; @@ -259,10 +259,23 @@ static int reap_db_deadlock(ev_source attribute((unused)) *ev, return 0; } -static pid_t subprogram(ev_source *ev, const char *prog, - int outputfd) { +static pid_t subprogram(ev_source *ev, int outputfd, const char *prog, + ...) { pid_t pid; - + va_list ap; + const char *args[1024], **argp, *a; + + argp = args; + *argp++ = prog; + *argp++ = "--config"; + *argp++ = configfile; + *argp++ = debugging ? "--debug" : "--no-debug"; + *argp++ = log_default == &log_syslog ? "--syslog" : "--no-syslog"; + va_start(ap, prog); + while((a = va_arg(ap, const char *))) + *argp++ = a; + va_end(ap); + *argp = 0; /* If we're in the background then trap subprocess stdout/stderr */ if(!(pid = xfork())) { exitfn = _exit; @@ -279,10 +292,7 @@ static pid_t subprogram(ev_source *ev, const char *prog, /* If we were negatively niced, undo it. We don't bother checking for * error, it's not that important. */ setpriority(PRIO_PROCESS, 0, 0); - execlp(prog, prog, "--config", configfile, - debugging ? "--debug" : "--no-debug", - log_default == &log_syslog ? "--syslog" : "--no-syslog", - (char *)0); + execvp(prog, (char **)args); fatal(errno, "error invoking %s", prog); } return pid; @@ -291,7 +301,7 @@ static pid_t subprogram(ev_source *ev, const char *prog, /* start deadlock manager */ void trackdb_master(ev_source *ev) { assert(db_deadlock_pid == -1); - db_deadlock_pid = subprogram(ev, DEADLOCK, -1); + db_deadlock_pid = subprogram(ev, -1, DEADLOCK, (char *)0); ev_child(ev, db_deadlock_pid, 0, reap_db_deadlock, 0); D(("started deadlock manager")); } @@ -349,7 +359,7 @@ static DB *open_db(const char *path, } /** @brief Open track databases - * @param Flags flags word + * @param flags Flags flags word * * @p flags should have one of: * - @p TRACKDB_NO_UPGRADE, if no upgrade should be attempted @@ -396,7 +406,7 @@ void trackdb_open(int flags) { /* This database needs upgrading */ info("invoking disorder-dbupgrade to upgrade from %ld to %ld", oldversion, config->dbversion); - pid = subprogram(0, "disorder-dbupgrade", -1); + pid = subprogram(0, -1, "disorder-dbupgrade", (char *)0); while(waitpid(pid, &err, 0) == -1 && errno == EINTR) ; if(err) @@ -1353,7 +1363,7 @@ void trackdb_stats_subprocess(ev_source *ev, d->done = done; d->u = u; xpipe(p); - pid = subprogram(ev, "disorder-stats", p[1]); + pid = subprogram(ev, p[1], "disorder-stats", (char *)0); xclose(p[1]); ev_child(ev, pid, 0, stats_finished, d); ev_reader_new(ev, p[0], stats_read, stats_error, d, "disorder-stats reader"); @@ -1520,42 +1530,40 @@ fail: /* return the list of tags */ char **trackdb_alltags(void) { - DB_TXN *tid; - int err; - char **taglist; + int e; + struct vector v[1]; - for(;;) { - tid = trackdb_begin_transaction(); - err = trackdb_alltags_tid(tid, &taglist); - if(!err) break; - trackdb_abort_transaction(tid); - } - trackdb_commit_transaction(tid); - return taglist; + vector_init(v); + WITH_TRANSACTION(trackdb_listkeys(trackdb_tagsdb, v, tid)); + return v->vec; } -static int trackdb_alltags_tid(DB_TXN *tid, char ***taglistp) { - struct vector v; - DBC *c; +/** @brief List all the keys in @p db + * @param db Database + * @param v Vector to store keys in + * @param tid Transaction ID + * @return 0 or DB_LOCK_DEADLOCK + */ +int trackdb_listkeys(DB *db, struct vector *v, DB_TXN *tid) { + int e; DBT k, d; - int err; + DBC *const c = trackdb_opencursor(db, tid); - vector_init(&v); - c = trackdb_opencursor(trackdb_tagsdb, tid); + v->nvec = 0; memset(&k, 0, sizeof k); - while(!(err = c->c_get(c, &k, prepare_data(&d), DB_NEXT_NODUP))) - vector_append(&v, xstrndup(k.data, k.size)); - switch(err) { + while(!(e = c->c_get(c, &k, prepare_data(&d), DB_NEXT_NODUP))) + vector_append(v, xstrndup(k.data, k.size)); + switch(e) { case DB_NOTFOUND: break; case DB_LOCK_DEADLOCK: - return err; + return e; default: - fatal(0, "c->c_get: %s", db_strerror(err)); + fatal(0, "c->c_get: %s", db_strerror(e)); } - if((err = trackdb_closecursor(c))) return err; - vector_terminate(&v); - *taglistp = v.vec; + if((e = trackdb_closecursor(c))) + return e; + vector_terminate(v); return 0; } @@ -2148,14 +2156,20 @@ static int reap_rescan(ev_source attribute((unused)) *ev, return 0; } -void trackdb_rescan(ev_source *ev) { +/** @brief Initiate a rescan + * @param ev Event loop or 0 to block + * @param check 1 to recheck lengths, 0 to suppress check + */ +void trackdb_rescan(ev_source *ev, int check) { int w; if(rescan_pid != -1) { error(0, "rescan already underway"); return; } - rescan_pid = subprogram(ev, RESCAN, -1); + rescan_pid = subprogram(ev, -1, RESCAN, + check ? "--check" : "--no-check", + (char *)0); if(ev) { ev_child(ev, rescan_pid, 0, reap_rescan, 0); D(("started rescanner")); @@ -2423,67 +2437,26 @@ static int trusted(const char *user) { return n < config->trust.n; } -static const struct { - rights_type bit; - const char *name; -} rights_names[] = { - { RIGHT_READ, "read" }, - { RIGHT_PLAY, "play" }, - { RIGHT_MOVE_ANY, "move any" }, - { RIGHT_MOVE_MINE, "move mine" }, - { RIGHT_MOVE_RANDOM, "move random" }, - { RIGHT_REMOVE_ANY, "remove any" }, - { RIGHT_REMOVE_MINE, "remove mine" }, - { RIGHT_REMOVE_RANDOM, "remove random" }, - { RIGHT_SCRATCH_ANY, "scratch any" }, - { RIGHT_SCRATCH_MINE, "scratch mine" }, - { RIGHT_SCRATCH_RANDOM, "scratch random" }, - { RIGHT_VOLUME, "volume" }, - { RIGHT_ADMIN, "admin" }, - { RIGHT_RESCAN, "rescan" }, - { RIGHT_REGISTER, "register" }, - { RIGHT_USERINFO, "userinfo" }, - { RIGHT_PREFS, "prefs" }, - { RIGHT_GLOBAL_PREFS, "global prefs" } -}; -#define NRIGHTS (sizeof rights_names / sizeof *rights_names) - -/** @brief Convert a rights word to a string */ -static char *rights_string(rights_type r) { - struct dynstr d[1]; - size_t n; - - dynstr_init(d); - for(n = 0; n < NRIGHTS; ++n) { - if(r & rights_names[n].bit) { - if(d->nvec) - dynstr_append(d, ','); - dynstr_append_string(d, rights_names[n].name); - } +/** @brief Return non-zero for a valid username + * + * Currently we only allow the letters and digits in ASCII. We could be more + * liberal than this but it is a nice simple test. It is critical that + * semicolons are never allowed. + */ +static int valid_username(const char *user) { + if(!*user) + return 0; + while(*user) { + const uint8_t c = *user++; + /* For now we are very strict */ + if((c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9')) + /* ok */; + else + return 0; } - dynstr_terminate(d); - return d->vec; -} - -/** @brief Compute default rights for a new user */ -rights_type default_rights(void) { - /* TODO get rights from config. This is probably in the wrong place but it - * will do for now... */ - rights_type r = RIGHTS__MASK & ~(RIGHT_ADMIN|RIGHT_REGISTER - |RIGHT_MOVE__MASK - |RIGHT_SCRATCH__MASK - |RIGHT_REMOVE__MASK); - if(config->restrictions & RESTRICT_SCRATCH) - r |= RIGHT_SCRATCH_MINE|RIGHT_SCRATCH_RANDOM; - else - r |= RIGHT_SCRATCH_ANY; - if(!(config->restrictions & RESTRICT_MOVE)) - r |= RIGHT_MOVE_ANY; - if(config->restrictions & RESTRICT_REMOVE) - r |= RIGHT_REMOVE_MINE; - else - r |= RIGHT_REMOVE_ANY; - return r; + return 1; } /** @brief Add a user */ @@ -2491,17 +2464,29 @@ static int create_user(const char *user, const char *password, const char *rights, const char *email, + const char *confirmation, DB_TXN *tid, uint32_t flags) { struct kvp *k = 0; char s[64]; + /* sanity check user */ + if(!valid_username(user)) { + error(0, "invalid username '%s'", user); + return -1; + } + if(parse_rights(rights, 0, 1)) { + error(0, "invalid rights string"); + return -1; + } /* data for this user */ if(password) kvp_set(&k, "password", password); kvp_set(&k, "rights", rights); if(email) kvp_set(&k, "email", email); + if(confirmation) + kvp_set(&k, "confirmation", confirmation); snprintf(s, sizeof s, "%jd", (intmax_t)time(0)); kvp_set(&k, "created", s); return trackdb_putdata(trackdb_usersdb, user, k, tid, flags); @@ -2520,11 +2505,18 @@ static int one_old_user(const char *user, const char *password, /* pick rights */ if(!strcmp(user, "root")) rights = "all"; - else if(trusted(user)) - rights = rights_string(default_rights()|RIGHT_ADMIN); - else - rights = rights_string(default_rights()); - return create_user(user, password, rights, 0/*email*/, tid, DB_NOOVERWRITE); + else if(trusted(user)) { + rights_type r; + + parse_rights(config->default_rights, &r, 1); + r &= ~(rights_type)(RIGHT_SCRATCH__MASK|RIGHT_MOVE__MASK|RIGHT_REMOVE__MASK); + r |= (RIGHT_ADMIN|RIGHT_RESCAN + |RIGHT_SCRATCH_ANY|RIGHT_MOVE_ANY|RIGHT_REMOVE_ANY); + rights = rights_string(r); + } else + rights = config->default_rights; + return create_user(user, password, rights, 0/*email*/, 0/*confirmation*/, + tid, DB_NOOVERWRITE); } static int trackdb_old_users_tid(DB_TXN *tid) { @@ -2567,8 +2559,9 @@ void trackdb_create_root(void) { gcry_randomize(pwbin, sizeof pwbin, GCRY_STRONG_RANDOM); pw = mime_to_base64(pwbin, sizeof pwbin); /* Create the root user if it does not exist */ - WITH_TRANSACTION(create_user("root", pw, "all", 0/*email*/, tid, - DB_NOOVERWRITE)); + WITH_TRANSACTION(create_user("root", pw, "all", + 0/*email*/, 0/*confirmation*/, + tid, DB_NOOVERWRITE)); if(e == 0) info("created root user"); } @@ -2597,34 +2590,35 @@ const char *trackdb_get_password(const char *user) { * @param user Username * @param password Password or NULL * @param rights Initial rights - * @param email Email address + * @param email Email address or NULL + * @param confirmation Confirmation string or NULL * @return 0 on success, non-0 on error */ int trackdb_adduser(const char *user, const char *password, - rights_type rights, - const char *email) { + const char *rights, + const char *email, + const char *confirmation) { int e; - const char *r = rights_string(rights); - WITH_TRANSACTION(create_user(user, password, r, email, + WITH_TRANSACTION(create_user(user, password, rights, email, confirmation, tid, DB_NOOVERWRITE)); if(e) { - error(0, "cannot created user '%s' because they already exist", user); + error(0, "cannot create user '%s' because they already exist", user); return -1; } else { if(email) info("created user '%s' with rights '%s' and email address '%s'", - user, r, email); + user, rights, email); else - info("created user '%s' with rights '%s'", user, r); + info("created user '%s' with rights '%s'", user, rights); return 0; } } /** @brief Delete a user * @param user User to delete - * @param 0 on success, non-0 if the user didn't exist anyway + * @return 0 on success, non-0 if the user didn't exist anyway */ int trackdb_deluser(const char *user) { int e; @@ -2685,11 +2679,106 @@ int trackdb_edituserinfo(const char *user, const char *key, const char *value) { int e; + if(!strcmp(key, "rights")) { + if(!value) { + error(0, "cannot remove 'rights' key from user '%s'", user); + return -1; + } + if(parse_rights(value, 0, 1)) { + error(0, "invalid rights string"); + return -1; + } + } else if(!strcmp(key, "email")) { + if(!strchr(value, '@')) { + error(0, "invalid email address '%s' for user '%s'", user, value); + return -1; + } + } else if(!strcmp(key, "created")) { + error(0, "cannot change creation date for user '%s'", user); + return -1; + } else if(strcmp(key, "password") + && !strcmp(key, "confirmation")) { + error(0, "unknown user info key '%s' for user '%s'", key, user); + return -1; + } WITH_TRANSACTION(trackdb_edituserinfo_tid(user, key, value, tid)); - if(e) + if(e) { + error(0, "unknown user '%s'", user); return -1; - else + } else + return 0; +} + +/** @brief List all users + * @return NULL-terminated list of users + */ +char **trackdb_listusers(void) { + int e; + struct vector v[1]; + + vector_init(v); + WITH_TRANSACTION(trackdb_listkeys(trackdb_usersdb, v, tid)); + return v->vec; +} + +/** @brief Confirm a user registration + * @param user Username + * @param confirmation Confirmation string + * @param rightsp Where to put user rights + * @param tid Transaction ID + * @return 0 on success, non-0 on error + */ +static int trackdb_confirm_tid(const char *user, const char *confirmation, + rights_type *rightsp, + DB_TXN *tid) { + const char *stored_confirmation; + struct kvp *k; + int e; + const char *rights; + + if((e = trackdb_getdata(trackdb_usersdb, user, &k, tid))) + return e; + if(!(stored_confirmation = kvp_get(k, "confirmation"))) { + error(0, "already confirmed user '%s'", user); + /* DB claims -30,800 to -30,999 so -1 should be a safe bet */ + return -1; + } + if(!(rights = kvp_get(k, "rights"))) { + error(0, "no rights for unconfirmed user '%s'", user); + return -1; + } + if(parse_rights(rights, rightsp, 1)) + return -1; + if(strcmp(confirmation, stored_confirmation)) { + error(0, "wrong confirmation string for user '%s'", user); + return -1; + } + /* 'sall good */ + kvp_set(&k, "confirmation", 0); + return trackdb_putdata(trackdb_usersdb, user, k, tid, 0); +} + +/** @brief Confirm a user registration + * @param user Username + * @param confirmation Confirmation string + * @param rightsp Where to put user rights + * @return 0 on success, non-0 on error + */ +int trackdb_confirm(const char *user, const char *confirmation, + rights_type *rightsp) { + int e; + + WITH_TRANSACTION(trackdb_confirm_tid(user, confirmation, rightsp, tid)); + switch(e) { + case 0: + info("registration confirmed for user '%s'", user); return 0; + case DB_NOTFOUND: + error(0, "confirmation for nonexistent user '%s'", user); + return -1; + default: /* already reported */ + return -1; + } } /*