From f0feb22e80bfe438c16d212a7cc8be6d2282b6ac Mon Sep 17 00:00:00 2001 Message-Id: From: Mark Wooding Date: Thu, 20 Dec 2007 16:02:39 +0000 Subject: [PATCH] Use users.db. trackdb* moves to lib/, as it's now used by client.c to pick out root's password (or others if it could read them, but it can't). Organization: Straylight/Edgeware From: Richard Kettlewell --- CHANGES | 6 + README | 4 +- README.upgrades | 11 ++ clients/Makefile.am | 4 +- clients/authorize.c | 27 ++- clients/authorize.h | 2 +- clients/disorder.c | 25 ++- doc/disorder_config.5.in | 8 - lib/Makefile.am | 1 + lib/client.c | 35 ++-- lib/client.h | 4 + lib/cookies.c | 23 +-- {server => lib}/trackdb-int.h | 18 ++ {server => lib}/trackdb.c | 353 +++++++++++++++++++++++++++++++--- {server => lib}/trackdb.h | 84 +++++++- python/disorder.py.in | 24 ++- scripts/completion.bash | 2 +- server/Makefile.am | 15 +- server/api-server.c | 1 + server/disorderd.c | 9 +- server/server.c | 56 +++++- server/setup.c | 98 ---------- server/setup.h | 35 ---- tests/Makefile.am | 2 +- tests/cookie.py | 1 + tests/dbversion.py | 1 + tests/dtest.py | 46 +++-- tests/dump.py | 29 +-- tests/files.py | 1 + tests/play.py | 3 +- tests/queue.py | 1 + tests/search.py | 1 + tests/user-upgrade.py | 41 ++++ tests/user.py | 47 +++++ 34 files changed, 740 insertions(+), 278 deletions(-) rename {server => lib}/trackdb-int.h (84%) rename {server => lib}/trackdb.c (88%) rename {server => lib}/trackdb.h (67%) delete mode 100644 server/setup.c delete mode 100644 server/setup.h create mode 100755 tests/user-upgrade.py create mode 100755 tests/user.py diff --git a/CHANGES b/CHANGES index cb8df1a..ada5cf9 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,11 @@ See ChangeLog.d/* for detailed revision history. +* Changes up to version 2.1 + +** Server + +Users are now stored in the database rather than a configuration file. + * Changes up to version 2.0 ** General diff --git a/README b/README index f6872cb..e3d9c2c 100644 --- a/README +++ b/README @@ -160,8 +160,8 @@ skip steps 1 to 6 and configure it via debconf. This is strongly recommended! disorder authorize USERNAME - This will automatically choose a random password and add new line to - /etc/disorder/config.private and create /etc/disorder/config.USERNAME. + This will automatically choose a random password and create + /etc/disorder/config.USERNAME. Those users should now be able to access the server from the same host as it runs on, either via the disorder command or Disobedience. To run diff --git a/README.upgrades b/README.upgrades index 915a794..5bea1cd 100644 --- a/README.upgrades +++ b/README.upgrades @@ -14,6 +14,17 @@ upgrading between particular versions. Minor versions are not explicitly mentioned; a version number like 1.1 implicitly includes all 1.1.x versions. +* 2.0 -> 2.1 + +** Authentication + +Users are now stored in the database rather than in 'allow' directives in a +private configuration file. 'allow' is still understood in this version, but +is only used to populate the database on startup. After the first (successful) +run of the server the remaining 'allow' directives can be deleted. + +'allow' will stop working entirely in a future version. + * 1.4/1.5 -> 2.0 ** 'transform' and 'namepart' directives diff --git a/clients/Makefile.am b/clients/Makefile.am index 16f7437..7baa667 100644 --- a/clients/Makefile.am +++ b/clients/Makefile.am @@ -27,7 +27,7 @@ AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib disorder_SOURCES=disorder.c authorize.c authorize.h \ ../lib/memgc.c disorder_LDADD=$(LIBOBJS) ../lib/libdisorder.a \ - $(LIBGC) $(LIBGCRYPT) $(LIBPCRE) + $(LIBGC) $(LIBGCRYPT) $(LIBPCRE) $(LIBDB) disorder_DEPENDENCIES=$(LIBOBJS) ../lib/libdisorder.a disorderfm_SOURCES=disorderfm.c \ @@ -38,7 +38,7 @@ disorderfm_DEPENDENCIES=$(LIBOBJS) ../lib/libdisorder.a disorder_playrtp_SOURCES=playrtp.c playrtp.h playrtp-mem.c \ playrtp-alsa.c playrtp-coreaudio.c playrtp-oss.c disorder_playrtp_LDADD=$(LIBOBJS) ../lib/libdisorder.a \ - $(LIBASOUND) $(LIBPCRE) $(LIBICONV) $(LIBGCRYPT) $(COREAUDIO) + $(LIBASOUND) $(LIBPCRE) $(LIBICONV) $(LIBGCRYPT) $(COREAUDIO) $(LIBDB) disorder_playrtp_DEPENDENCIES=$(LIBOBJS) ../lib/libdisorder.a filename_bytes_SOURCES=filename-bytes.c diff --git a/clients/authorize.c b/clients/authorize.c index 22c321f..8697abe 100644 --- a/clients/authorize.c +++ b/clients/authorize.c @@ -29,13 +29,19 @@ #include #include +#include "client.h" #include "authorize.h" #include "log.h" #include "configuration.h" #include "printf.h" #include "hex.h" -int authorize(const char *user) { +/** @brief Create a DisOrder login for the calling user, called @p user + * @param client DisOrder client + * @param user Username to create + * @return 0 on success, non-0 on error + */ +int authorize(disorder_client *client, const char *user) { uint8_t pwbin[10]; const struct passwd *pw, *jbpw; gid_t jbgid; @@ -75,21 +81,10 @@ int authorize(const char *user) { if(rename(t, c) < 0) fatal(errno, "error renaming %s to %s", t, c); - /* append to config.private. We might create it along the way (though this - * is unlikely) in which case it had better be 640 root:jukebox */ - if(!(c = config_private())) - fatal(0, "cannot determine private config file"); - if((fd = open(c, O_WRONLY|O_APPEND|O_CREAT, 0600)) < 0) - fatal(errno, "error opening %s", c); - if(fchown(fd, 0, jbgid) < 0) - fatal(errno, "error chowning %s", c); - if(fchmod(fd, 0640) < 0) - fatal(errno, "error chmoding %s", t); - if(!(fp = fdopen(fd, "a"))) - fatal(errno, "error calling fdopen"); - if(fprintf(fp, "allow %s %s\n", user, pwhex) < 0 - || fclose(fp) < 0) - fatal(errno, "error appending to %s", c); + /* create the user on the server */ + if(disorder_adduser(client, user, pwhex)) + return -1; + return 0; } diff --git a/clients/authorize.h b/clients/authorize.h index ad78deb..07e87ef 100644 --- a/clients/authorize.h +++ b/clients/authorize.h @@ -20,7 +20,7 @@ #ifndef AUTHORIZE_H #define AUTHORIZE_H -int authorize(const char *user); +int authorize(disorder_client *client, const char *user); #endif /* AUTHORIZE_H */ diff --git a/clients/disorder.c b/clients/disorder.c index 7ee9865..7d15afe 100644 --- a/clients/disorder.c +++ b/clients/disorder.c @@ -64,6 +64,8 @@ static const struct option options[] = { { "local", no_argument, 0, 'l' }, { "no-per-user-config", no_argument, 0, 'N' }, { "help-commands", no_argument, 0, 'H' }, + { "user", required_argument, 0, 'u' }, + { "password", required_argument, 0, 'p' }, { 0, 0, 0, 0 } }; @@ -334,9 +336,9 @@ static int isarg_filename(const char *s) { return s[0] == '/'; } -static void cf_authorize(disorder_client attribute((unused)) *c, +static void cf_authorize(disorder_client *c, char **argv) { - if(!authorize(argv[0])) + if(!authorize(c, argv[0])) auto_reconfigure = 1; } @@ -410,6 +412,12 @@ static void cf_rtp_address(disorder_client *c, xprintf("address: %s\nport: %s\n", address, port); } +static void cf_adduser(disorder_client *c, + char **argv) { + if(disorder_adduser(c, argv[0], argv[1])) + exit(EXIT_FAILURE); +} + static const struct command { const char *name; int min, max; @@ -417,6 +425,8 @@ static const struct command { int (*isarg)(const char *); const char *argstr, *desc; } commands[] = { + { "adduser", 2, 2, cf_adduser, 0, "USER PASSWORD", + "Create a new user" }, { "allfiles", 1, 2, cf_allfiles, isarg_regexp, "DIR [~REGEXP]", "List all files and directories in DIR" }, { "authorize", 1, 1, cf_authorize, 0, "USER", @@ -538,13 +548,14 @@ int main(int argc, char **argv) { disorder_client *c = 0; int status = 0; struct vector args; + const char *user = 0, *password = 0; mem_init(); /* garbage-collect PCRE's memory */ pcre_malloc = xmalloc; pcre_free = xfree; if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale"); - while((n = getopt_long(argc, argv, "hVc:dHlN", options, 0)) >= 0) { + while((n = getopt_long(argc, argv, "hVc:dHlNu:p:", options, 0)) >= 0) { switch(n) { case 'h': help(); case 'H': help_commands(); @@ -553,10 +564,18 @@ int main(int argc, char **argv) { case 'd': debugging = 1; break; case 'l': local = 1; break; case 'N': config_per_user = 0; break; + case 'u': user = optarg; break; + case 'p': password = optarg; break; default: fatal(0, "invalid option"); } } if(config_read(0)) fatal(0, "cannot read configuration"); + if(user) { + config->username = user; + config->password = 0; + } + if(password) + config->password = password; if(local) config->connect.n = 0; if(!(c = disorder_new(1))) exit(EXIT_FAILURE); diff --git a/doc/disorder_config.5.in b/doc/disorder_config.5.in index 6cb4a78..6eb57b8 100644 --- a/doc/disorder_config.5.in +++ b/doc/disorder_config.5.in @@ -1,4 +1,3 @@ - .\" .\" Copyright (C) 2004, 2005, 2006, 2007 Richard Kettlewell .\" @@ -566,13 +565,6 @@ This must be the full URL, e.g. \fBhttp://myhost/cgi-bin/jukebox\fR and not \fB/cgi-bin/jukebox\fR. .SS "Authentication Configuration" .TP -.B allow \fIUSERNAME\fR \fIPASSWORD\fR -Specify a username/password pair. -.IP -If -.B allow -is used without arguments, the list of allowed users is cleared. -.TP .B password \fIPASSWORD\fR Specify password. .TP diff --git a/lib/Makefile.am b/lib/Makefile.am index 13bafc6..7c792d8 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -62,6 +62,7 @@ libdisorder_a_SOURCES=charset.c charset.h \ types.h \ table.c table.h \ timeval.h \ + trackdb.h trackdb.c trackdb-int.h \ trackname.c trackname.h \ user.h user.c \ unicode.h unicode.c \ diff --git a/lib/client.c b/lib/client.c index 27b7beb..e4e28e8 100644 --- a/lib/client.c +++ b/lib/client.c @@ -31,6 +31,7 @@ #include #include #include +#include #include "log.h" #include "mem.h" @@ -49,6 +50,7 @@ #include "addr.h" #include "authhash.h" #include "client-common.h" +#include "trackdb.h" struct disorder_client { FILE *fpin, *fpout; @@ -154,22 +156,24 @@ static int connect_sock(void *vc, const char *ident) { const char *username, *password; disorder_client *c = vc; - int n; if(!(username = config->username)) { error(0, "no username configured"); return -1; } - if(!(password = config->password)) { - for(n = 0; (n < config->allow.n - && strcmp(config->allow.s[n].s[0], username)); ++n) - ; - if(n < config->allow.n) - password = config->allow.s[n].s[1]; - else { - error(0, "no password configured"); - return -1; - } + password = config->password; + if(!password) { + /* Maybe we can read the database */ + /* TODO failure to open the database should not be fatal */ + trackdb_init(TRACKDB_NO_RECOVER|TRACKDB_NO_UPGRADE); + trackdb_open(TRACKDB_READ_ONLY); + password = trackdb_get_password(username); + trackdb_close(); + } + if(!password) { + /* Oh well */ + error(0, "no password configured"); + return -1; } return disorder_connect_sock(c, sa, len, username, password, ident); } @@ -646,6 +650,15 @@ int disorder_rtp_address(disorder_client *c, char **addressp, char **portp) { return 0; } +int disorder_adduser(disorder_client *c, + const char *user, const char *password) { + return disorder_simple(c, 0, "adduser", user, password, (char *)0); +} + +int disorder_deluser(disorder_client *c, const char *user) { + return disorder_simple(c, 0, "deluser", user, (char *)0); +} + /* Local Variables: c-basic-offset:2 diff --git a/lib/client.h b/lib/client.h index 39a6830..ab93115 100644 --- a/lib/client.h +++ b/lib/client.h @@ -188,6 +188,10 @@ int disorder_new_tracks(disorder_client *c, int disorder_rtp_address(disorder_client *c, char **addressp, char **portp); +int disorder_adduser(disorder_client *c, + const char *user, const char *password); +int disorder_deluser(disorder_client *c, const char *user); + #endif /* CLIENT_H */ /* diff --git a/lib/cookies.c b/lib/cookies.c index 4ac9f65..666c6ce 100644 --- a/lib/cookies.c +++ b/lib/cookies.c @@ -30,6 +30,7 @@ #include #include #include +#include #include "cookies.h" #include "hash.h" @@ -39,6 +40,7 @@ #include "mime.h" #include "configuration.h" #include "kvp.h" +#include "trackdb.h" /** @brief Hash function used in signing HMAC */ #define ALGO GCRY_MD_SHA1 @@ -112,10 +114,9 @@ static char *sign(const uint8_t *key, * @return Cookie or NULL */ char *make_cookie(const char *user) { - char *password; + const char *password; time_t now; char *b, *bp, *c, *g; - int n; /* semicolons aren't allowed in usernames */ if(strchr(user, ';')) { @@ -123,14 +124,11 @@ char *make_cookie(const char *user) { return 0; } /* look up the password */ - for(n = 0; n < config->allow.n - && strcmp(config->allow.s[n].s[0], user); ++n) - ; - if(n >= config->allow.n) { + password = trackdb_get_password(user); + if(!password) { error(0, "make_cookie for nonexistent user"); return 0; } - password = config->allow.s[n].s[1]; /* make sure we have a valid signing key */ time(&now); if(now >= signing_key_validity_limit) @@ -155,8 +153,8 @@ char *verify_cookie(const char *cookie) { char *c1, *c2; intmax_t t; time_t now; - char *user, *bp, *password, *sig; - int n; + char *user, *bp, *sig; + const char *password; /* check the revocation list */ if(revoked && hash_find(revoked, cookie)) { @@ -189,14 +187,11 @@ char *verify_cookie(const char *cookie) { return 0; } /* look up the password */ - for(n = 0; n < config->allow.n - && strcmp(config->allow.s[n].s[0], user); ++n) - ; - if(n >= config->allow.n) { + password = trackdb_get_password(user); + if(!password) { error(0, "verify_cookie for nonexistent user"); return 0; } - password = config->allow.s[n].s[1]; /* construct the expected subject. We re-encode the timestamp and the * password. */ byte_xasprintf(&bp, "%jx;%s;%s", t, urlencodestring(user), password); diff --git a/server/trackdb-int.h b/lib/trackdb-int.h similarity index 84% rename from server/trackdb-int.h rename to lib/trackdb-int.h index 441a056..1d31cfe 100644 --- a/server/trackdb-int.h +++ b/lib/trackdb-int.h @@ -53,6 +53,24 @@ void trackdb_abort_transaction(DB_TXN *tid); void trackdb_commit_transaction(DB_TXN *tid); /* begin, abort or commit a transaction */ +/** @brief Evaluate @p expr in a transaction, looping on deadlock + * + * @c tid will be the transaction handle. @p e will be the error code. + */ +#define WITH_TRANSACTION(expr) do { \ + DB_TXN *tid; \ + \ + tid = trackdb_begin_transaction(); \ + while((e = (expr)) == DB_LOCK_DEADLOCK) { \ + trackdb_abort_transaction(tid); \ + tid = trackdb_begin_transaction(); \ + } \ + if(e) \ + trackdb_abort_transaction(tid); \ + else \ + trackdb_commit_transaction(tid); \ +} while(0) + int trackdb_getdata(DB *db, const char *track, struct kvp **kp, diff --git a/server/trackdb.c b/lib/trackdb.c similarity index 88% rename from server/trackdb.c rename to lib/trackdb.c index 4c00e28..d3e6e40 100644 --- a/server/trackdb.c +++ b/lib/trackdb.c @@ -18,7 +18,10 @@ * USA */ /** @file server/trackdb.c - * @brief Track database */ + * @brief Track database + * + * This file is getting in desparate need of splitting up... + */ #include #include "types.h" @@ -39,6 +42,7 @@ #include #include #include +#include #include "event.h" #include "mem.h" @@ -59,6 +63,7 @@ #include "hash.h" #include "unicode.h" #include "unidata.h" +#include "mime.h" #define RESCAN "disorder-rescan" #define DEADLOCK "disorder-deadlock" @@ -190,8 +195,6 @@ void trackdb_init(int flags) { struct stat st; char *p; - /* create home directory if it does not exist */ - mkdir(config->home, 0755); /* Remove world/group permissions on any regular files already in the * database directory. Actually we don't care about all of them but it's * easier to just do the lot. This can be revisited if it's a serious @@ -206,7 +209,7 @@ void trackdb_init(int flags) { if(lstat(p, &st) == 0 && S_ISREG(st.st_mode) && (st.st_mode & 077)) { - if(chmod(p, st.st_mode & (~(mode_t)077) & 07777) < 0) + if(chmod(p, st.st_mode & 07700) < 0) fatal(errno, "cannot chmod %s", p); } xfree(p); @@ -270,8 +273,11 @@ static pid_t subprogram(ev_source *ev, const char *prog, xdup2(outputfd, 1); xclose(outputfd); } - /* If we were negatively niced, undo it. We don't bother checking for - * error, it's not that important. */ + /* ensure we don't leak privilege anywhere */ + if(setuid(geteuid()) < 0) + fatal(errno, "error calling setuid"); + /* 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", @@ -345,20 +351,23 @@ static DB *open_db(const char *path, /** @brief Open track databases * @param Flags flags word * - * @p flags should be one of: + * @p flags should have one of: * - @p TRACKDB_NO_UPGRADE, if no upgrade should be attempted * - @p TRACKDB_CAN_UPGRADE, if an upgrade may be attempted * - @p TRACKDB_OPEN_FOR_UPGRADE, if this is disorder-dbupgrade + * Also it may have: + * - @p TRACKDB_READ_ONLY, read only access */ void trackdb_open(int flags) { int err; pid_t pid; + uint32_t dbflags = flags & TRACKDB_READ_ONLY ? DB_RDONLY : DB_CREATE; /* sanity checks */ assert(opened == 0); ++opened; /* check the database version first */ - trackdb_globaldb = open_db("global.db", 0, DB_HASH, 0, 0666); + trackdb_globaldb = open_db("global.db", 0, DB_HASH, DB_RDONLY, 0666); if(trackdb_globaldb) { /* This is an existing database */ const char *s; @@ -415,17 +424,17 @@ void trackdb_open(int flags) { } /* open the databases */ trackdb_tracksdb = open_db("tracks.db", - DB_RECNUM, DB_BTREE, DB_CREATE, 0666); + DB_RECNUM, DB_BTREE, dbflags, 0666); trackdb_searchdb = open_db("search.db", - DB_DUP|DB_DUPSORT, DB_HASH, DB_CREATE, 0666); + DB_DUP|DB_DUPSORT, DB_HASH, dbflags, 0666); trackdb_tagsdb = open_db("tags.db", - DB_DUP|DB_DUPSORT, DB_HASH, DB_CREATE, 0666); - trackdb_prefsdb = open_db("prefs.db", 0, DB_HASH, DB_CREATE, 0666); - trackdb_globaldb = open_db("global.db", 0, DB_HASH, DB_CREATE, 0666); + DB_DUP|DB_DUPSORT, DB_HASH, dbflags, 0666); + trackdb_prefsdb = open_db("prefs.db", 0, DB_HASH, dbflags, 0666); + trackdb_globaldb = open_db("global.db", 0, DB_HASH, dbflags, 0666); trackdb_noticeddb = open_db("noticed.db", - DB_DUPSORT, DB_BTREE, DB_CREATE, 0666); + DB_DUPSORT, DB_BTREE, dbflags, 0666); trackdb_usersdb = open_db("users.db", - 0, DB_HASH, DB_CREATE, 0600); + 0, DB_HASH, dbflags, 0600); if(!trackdb_existing_database) { /* Stash the database version */ char buf[32]; @@ -440,7 +449,7 @@ void trackdb_open(int flags) { /* close track databases */ void trackdb_close(void) { int err; - + /* sanity checks */ assert(opened == 1); --opened; @@ -513,7 +522,12 @@ int trackdb_putdata(DB *db, } } -/* delete a database entry */ +/** @brief Delete a database entry + * @param db Database + * @param track Key to delete + * @param tid Transaction ID + * @return 0, DB_NOTFOUND or DB_LOCK_DEADLOCK + */ int trackdb_delkey(DB *db, const char *track, DB_TXN *tid) { @@ -918,7 +932,7 @@ static int gettrackdata(const char *track, int err; const char *actual = track; struct kvp *t = 0, *p = 0; - + if((err = trackdb_getdata(trackdb_tracksdb, track, &t, tid))) goto done; if((actual = kvp_get(t, "_alias_for"))) { if(flags & GTD_NOALIAS) { @@ -950,7 +964,7 @@ int trackdb_notice(const char *track, const char *path) { int err; DB_TXN *tid; - + for(;;) { tid = trackdb_begin_transaction(); err = trackdb_notice_tid(track, path, tid); @@ -1043,11 +1057,12 @@ int trackdb_obsolete(const char *track, DB_TXN *tid) { return err; else if(err == DB_NOTFOUND) return 0; /* compute the alias, if any, and delete it */ - if(compute_alias(&alias, track, p, tid)) return err; + if((err = compute_alias(&alias, track, p, tid))) return err; if(alias) { /* if the alias points to some other track then compute_alias won't * return it */ - if(trackdb_delkey(trackdb_tracksdb, alias, tid)) + if((err = trackdb_delkey(trackdb_tracksdb, alias, tid)) + && err != DB_NOTFOUND) return err; } /* update search.db */ @@ -1236,7 +1251,7 @@ static int search_league(struct vector *v, int count, DB_TXN *tid) { char **trackdb_stats(int *nstatsp) { DB_TXN *tid; struct vector v; - + vector_init(&v); for(;;) { tid = trackdb_begin_transaction(); @@ -1356,7 +1371,7 @@ int trackdb_set(const char *track, if(value) { /* TODO: if value matches default then set value=0 */ } - + for(;;) { tid = trackdb_begin_transaction(); if((err = gettrackdata(track, &t, &p, 0, @@ -1384,7 +1399,8 @@ int trackdb_set(const char *track, || (oldalias && newalias && !strcmp(oldalias, newalias)))) { /* adjust alias records to fit change */ if(oldalias - && trackdb_delkey(trackdb_tracksdb, oldalias, tid)) goto fail; + && trackdb_delkey(trackdb_tracksdb, oldalias, tid) == DB_LOCK_DEADLOCK) + goto fail; if(newalias) { a = 0; kvp_set(&a, "_alias_for", track); @@ -1465,7 +1481,7 @@ fail: const char *trackdb_resolve(const char *track) { DB_TXN *tid; const char *actual; - + for(;;) { tid = trackdb_begin_transaction(); if(gettrackdata(track, 0, 0, &actual, 0, tid) == DB_LOCK_DEADLOCK) @@ -1827,7 +1843,7 @@ static int do_list(struct vector *v, const char *dir, size_t l, last_dir_len = 0; char *last_dir = 0, *track, *alias; struct kvp *p; - + dl = strlen(dir); cursor = trackdb_opencursor(trackdb_tracksdb, tid); make_key(&k, dir); @@ -1913,7 +1929,7 @@ fail: if(np) *np = v.nvec; return v.vec; -} +} /* If S is tag:something, return something. Else return 0. */ static const char *checktag(const char *s) { @@ -1946,7 +1962,7 @@ char **trackdb_search(char **wordlist, int nwordlist, int *ntracks) { for(n = 0; n < nwordlist; ++n) { uint32_t *w32; size_t nw32; - + w[n] = utf8_casefold_compat(wordlist[n], strlen(wordlist[n]), 0); if(checktag(w[n])) { ++ntags; /* count up tags */ @@ -2395,6 +2411,287 @@ void trackdb_gc(void) { * long-term storage requirements than record the db logfiles. */ } +/* user database *************************************************************/ + +/** @brief Return true if @p user is trusted */ +static int trusted(const char *user) { + int n; + + for(n = 0; (n < config->trust.n + && strcmp(config->trust.s[n], user)); ++n) + ; + 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); + } + } + 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; +} + +/** @brief Add a user */ +static int create_user(const char *user, + const char *password, + const char *rights, + const char *email, + DB_TXN *tid, + uint32_t flags) { + struct kvp *k = 0; + char s[64]; + + /* data for this user */ + if(password) + kvp_set(&k, "password", password); + kvp_set(&k, "rights", rights); + if(email) + kvp_set(&k, "email", email); + snprintf(s, sizeof s, "%jd", (intmax_t)time(0)); + kvp_set(&k, "created", s); + return trackdb_putdata(trackdb_usersdb, user, k, tid, flags); +} + +/** @brief Add one pre-existing user */ +static int one_old_user(const char *user, const char *password, + DB_TXN *tid) { + const char *rights; + + /* www-data doesn't get added */ + if(!strcmp(user, "www-data")) { + info("not adding www-data to user database"); + return 0; + } + /* 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); +} + +static int trackdb_old_users_tid(DB_TXN *tid) { + int n; + + for(n = 0; n < config->allow.n; ++n) { + switch(one_old_user(config->allow.s[n].s[0], config->allow.s[n].s[1], + tid)) { + case 0: + info("created user %s from 'allow' directive", config->allow.s[n].s[0]); + break; + case DB_KEYEXIST: + error(0, "user %s already exists, delete 'allow' directive", + config->allow.s[n].s[0]); + /* This won't ever become fatal - eventually 'allow' will be + * disabled. */ + break; + case DB_LOCK_DEADLOCK: + return DB_LOCK_DEADLOCK; + } + } + return 0; +} + +/** @brief Read old 'allow' directives and copy them to the users database */ +void trackdb_old_users(void) { + int e; + + if(config->allow.n) + WITH_TRANSACTION(trackdb_old_users_tid(tid)); +} + +/** @brief Create a root user in the user database if there is none */ +void trackdb_create_root(void) { + int e; + uint8_t pwbin[12]; + char *pw; + + /* Choose a new root password */ + 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)); + if(e == 0) + info("created root user"); +} + +/** @brief Find a user's password from the database + * @param user Username + * @return Password or NULL + * + * Only works if running as a user that can read the database! + * + * If the user exists but has no password, "" is returned. + */ +const char *trackdb_get_password(const char *user) { + int e; + struct kvp *k; + const char *password; + + WITH_TRANSACTION(trackdb_getdata(trackdb_usersdb, user, &k, tid)); + if(e) + return 0; + password = kvp_get(k, "password"); + return password ? password : ""; +} + +/** @brief Add a new user + * @param user Username + * @param password Password or NULL + * @param rights Initial rights + * @param email Email address + * @return 0 on success, non-0 on error + */ +int trackdb_adduser(const char *user, + const char *password, + rights_type rights, + const char *email) { + int e; + const char *r = rights_string(rights); + + WITH_TRANSACTION(create_user(user, password, r, email, + tid, DB_NOOVERWRITE)); + if(e) { + error(0, "cannot created 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); + else + info("created user '%s' with rights '%s'", user, r); + return 0; + } +} + +/** @brief Delete a user + * @param user User to delete + * @param 0 on success, non-0 if the user didn't exist anyway + */ +int trackdb_deluser(const char *user) { + int e; + + WITH_TRANSACTION(trackdb_delkey(trackdb_usersdb, user, tid)); + if(e) { + error(0, "cannot delete user '%s' because they do not exist", user); + return -1; + } + info("deleted user '%s'", user); + return 0; +} + +/** @brief Get user information + * @param user User to query + * @return Linked list of user information or NULL if user does not exist + * + * Every user has at least a @c rights entry so NULL can be used to mean no + * such user safely. + */ +struct kvp *trackdb_getuserinfo(const char *user) { + int e; + struct kvp *k; + + WITH_TRANSACTION(trackdb_getdata(trackdb_usersdb, user, &k, tid)); + if(e) + return 0; + else + return k; +} + +/** @brief Edit user information + * @param user User to edit + * @param key Key to change + * @param value Value to set, or NULL to remove + * @param tid Transaction ID + * @return 0, DB_LOCK_DEADLOCK or DB_NOTFOUND + */ +static int trackdb_edituserinfo_tid(const char *user, const char *key, + const char *value, DB_TXN *tid) { + struct kvp *k; + int e; + + if((e = trackdb_getdata(trackdb_usersdb, user, &k, tid))) + return e; + if(!kvp_set(&k, key, value)) + return 0; /* no change */ + return trackdb_putdata(trackdb_usersdb, user, k, tid, 0); +} + +/** @brief Edit user information + * @param user User to edit + * @param key Key to change + * @param value Value to set, or NULL to remove + * @return 0 on success, non-0 on error + */ +int trackdb_edituserinfo(const char *user, + const char *key, const char *value) { + int e; + + WITH_TRANSACTION(trackdb_edituserinfo_tid(user, key, value, tid)); + if(e) + return -1; + else + return 0; +} + /* Local Variables: c-basic-offset:2 diff --git a/server/trackdb.h b/lib/trackdb.h similarity index 67% rename from server/trackdb.h rename to lib/trackdb.h index d9d7ceb..b317974 100644 --- a/server/trackdb.h +++ b/lib/trackdb.h @@ -17,7 +17,7 @@ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA */ -/** @file server/trackdb.h +/** @file lib/trackdb.h * @brief Track database public interface */ #ifndef TRACKDB_H @@ -29,6 +29,72 @@ extern const struct cache_type cache_files_type; extern unsigned long cache_files_hits, cache_files_misses; /* Cache entry type and tracking for regexp-based lookups */ +/** @brief User can perform read-only operations */ +#define RIGHT_READ 0x00000001 + +/** @brief User can add tracks to the queue */ +#define RIGHT_PLAY 0x00000002 + +/** @brief User can move any track */ +#define RIGHT_MOVE_ANY 0x00000004 + +/** @brief User can move their own tracks */ +#define RIGHT_MOVE_MINE 0x00000008 + +/** @brief User can move randomly chosen tracks */ +#define RIGHT_MOVE_RANDOM 0x00000010 + +#define RIGHT_MOVE__MASK 0x0000001c + +/** @brief User can remove any track */ +#define RIGHT_REMOVE_ANY 0x00000020 + +/** @brief User can remove their own tracks */ +#define RIGHT_REMOVE_MINE 0x00000040 + +/** @brief User can remove randomly chosen tracks */ +#define RIGHT_REMOVE_RANDOM 0x00000080 + +#define RIGHT_REMOVE__MASK 0x000000e0 + +/** @brief User can scratch any track */ +#define RIGHT_SCRATCH_ANY 0x00000100 + +/** @brief User can scratch their own tracks */ +#define RIGHT_SCRATCH_MINE 0x00000200 + +/** @brief User can scratch randomly chosen tracks */ +#define RIGHT_SCRATCH_RANDOM 0x00000400 + +#define RIGHT_SCRATCH__MASK 0x00000700 + +/** @brief User can change the volume */ +#define RIGHT_VOLUME 0x00000800 + +/** @brief User can perform admin operations */ +#define RIGHT_ADMIN 0x00001000 + +/** @brief User can initiate a rescan */ +#define RIGHT_RESCAN 0x00002000 + +/** @brief User can register new users */ +#define RIGHT_REGISTER 0x00004000 + +/** @brief User can edit their own userinfo */ +#define RIGHT_USERINFO 0x00008000 + +/** @brief User can modify track preferences */ +#define RIGHT_PREFS 0x00010000 + +/** @brief User can modify global preferences */ +#define RIGHT_GLOBAL_PREFS 0x00020000 + +/** @brief Current rights mask */ +#define RIGHTS__MASK 0x0003ffff + +/** @brief Unsigned type big enough for rights */ +typedef uint32_t rights_type; + /** @brief Do not attempt database recovery (trackdb_init()) */ #define TRACKDB_NO_RECOVER 0x0000 @@ -56,6 +122,9 @@ extern unsigned long cache_files_hits, cache_files_misses; /** @brief May create database environment (trackdb_init()) */ #define TRACKDB_MAY_CREATE 0x0010 +/** @brief Read-only access (trackdb_open()) */ +#define TRACKDB_READ_ONLY 0x0020 + void trackdb_init(int flags); void trackdb_deinit(void); /* close/close environment */ @@ -153,6 +222,19 @@ const char *trackdb_get_global(const char *name); char **trackdb_new(int *ntracksp, int maxtracks); void trackdb_expire_noticed(time_t when); +void trackdb_old_users(void); +void trackdb_create_root(void); +const char *trackdb_get_password(const char *user); +int trackdb_adduser(const char *user, + const char *password, + rights_type rights, + const char *email); +int trackdb_deluser(const char *user); +struct kvp *trackdb_getuserinfo(const char *user); +int trackdb_edituserinfo(const char *user, + const char *key, const char *value); + +rights_type default_rights(void); #endif /* TRACKDB_H */ diff --git a/python/disorder.py.in b/python/disorder.py.in index c501be5..6478981 100644 --- a/python/disorder.py.in +++ b/python/disorder.py.in @@ -276,7 +276,7 @@ class client: debug_proto = 0x0001 debug_body = 0x0002 - def __init__(self): + def __init__(self, user=None, password=None): """Constructor for DisOrder client class. The constructor reads the configuration file, but does not connect @@ -294,6 +294,8 @@ class client: self.config = { 'collections': [], 'username': pw.pw_name, 'home': _dbhome } + self.user = user + self.password = password home = os.getenv("HOME") if not home: home = pw.pw_dir @@ -375,10 +377,18 @@ class client: self.r = s.makefile("rb") (res, challenge) = self._simple() if cookie is None: + if self.user is None: + user = self.config['username'] + else: + user = self.user + if self.password is None: + password = self.config['password'] + else: + password = self.password h = sha.sha() - h.update(self.config['password']) + h.update(password) h.update(binascii.unhexlify(challenge)) - self._simple("user", self.config['username'], h.hexdigest()) + self._simple("user", user, h.hexdigest()) else: self._simple("cookie", cookie) self.state = 'connected' @@ -850,6 +860,14 @@ class client: """Revoke a login cookie""" self._simple("revoke") + def adduser(self, user, password): + """Create a user""" + self._simple("adduser", user, password) + + def deluser(self, user): + """Delete a user""" + self._simple("deluser", user) + ######################################################################## # I/O infrastructure diff --git a/scripts/completion.bash b/scripts/completion.bash index 3f863ee..3c13c29 100644 --- a/scripts/completion.bash +++ b/scripts/completion.bash @@ -31,7 +31,7 @@ complete -o default \ random-enable recent reconfigure remove rescan scratch search set set-volume shutdown stats unset version resolve part pause resume scratch-id get-global set-global unset-global - tags new rtp-address + tags new rtp-address adduser -h --help -H --help-commands --version -V --config -c --length --debug -d" \ disorder diff --git a/server/Makefile.am b/server/Makefile.am index 4332ed4..5d38b56 100644 --- a/server/Makefile.am +++ b/server/Makefile.am @@ -32,17 +32,15 @@ disorderd_SOURCES=disorderd.c \ play.c play.h \ server.c server.h \ server-queue.c server-queue.h \ - setup.c setup.h \ state.c state.h \ - trackdb.c trackdb.h trackdb-int.h exports.c \ + exports.c \ ../lib/memgc.c disorderd_LDADD=$(LIBOBJS) ../lib/libdisorder.a \ $(LIBPCRE) $(LIBDB) $(LIBAO) $(LIBGC) $(LIBGCRYPT) $(LIBICONV) disorderd_LDFLAGS=-export-dynamic disorderd_DEPENDENCIES=../lib/libdisorder.a -disorder_deadlock_SOURCES=deadlock.c \ - trackdb.c trackdb.h +disorder_deadlock_SOURCES=deadlock.c disorder_deadlock_LDADD=$(LIBOBJS) ../lib/libdisorder.a \ $(LIBDB) $(LIBPCRE) $(LIBICONV) $(LIBGCRYPT) disorder_deadlock_DEPENDENCIES=../lib/libdisorder.a @@ -69,26 +67,25 @@ disorder_normalize_DEPENDENCIES=../lib/libdisorder.a disorder_rescan_SOURCES=rescan.c \ api.c api-server.c \ - trackdb.c trackdb.h exports.c \ + exports.c \ ../lib/memgc.c disorder_rescan_LDADD=$(LIBOBJS) ../lib/libdisorder.a \ $(LIBDB) $(LIBGC) $(LIBPCRE) $(LIBICONV) $(LIBGCRYPT) disorder_rescan_LDFLAGS=-export-dynamic disorder_rescan_DEPENDENCIES=../lib/libdisorder.a -disorder_stats_SOURCES=stats.c trackdb.c trackdb.h +disorder_stats_SOURCES=stats.c disorder_stats_LDADD=$(LIBOBJS) ../lib/libdisorder.a \ $(LIBDB) $(LIBPCRE) $(LIBICONV) $(LIBGCRYPT) disorder_stats_DEPENDENCIES=../lib/libdisorder.a disorder_dump_SOURCES=dump.c \ - trackdb.c trackdb.h \ ../lib/memgc.c disorder_dump_LDADD=$(LIBOBJS) ../lib/libdisorder.a \ $(LIBPCRE) $(LIBDB) $(LIBICONV) $(LIBGC) $(LIBGCRYPT) disorder_dump_DEPENDENCIES=$(LIBOBJS) ../lib/libdisorder.a -disorder_dbupgrade_SOURCES=dbupgrade.c trackdb.c trackdb.h ../lib/memgc.c +disorder_dbupgrade_SOURCES=dbupgrade.c ../lib/memgc.c disorder_dbupgrade_LDADD=$(LIBOBJS) ../lib/libdisorder.a \ $(LIBDB) $(LIBGC) $(LIBPCRE) $(LIBICONV) $(LIBGCRYPT) disorder_dbupgrade_DEPENDENCIES=../lib/libdisorder.a @@ -97,7 +94,7 @@ disorder_cgi_SOURCES=dcgi.c dcgi.h \ api.c api-client.c api-client.h \ cgi.c cgi.h cgimain.c exports.c disorder_cgi_LDADD=../lib/libdisorder.a \ - $(LIBPCRE) $(LIBGCRYPT) $(LIBDL) + $(LIBPCRE) $(LIBGCRYPT) $(LIBDL) $(LIBDB) disorder_cgi_LDFLAGS=-export-dynamic disorder_cgi_DEPENDENCIES=../lib/libdisorder.a diff --git a/server/api-server.c b/server/api-server.c index 04c0f25..77960a3 100644 --- a/server/api-server.c +++ b/server/api-server.c @@ -19,6 +19,7 @@ */ #include +#include "types.h" #include #include diff --git a/server/disorderd.c b/server/disorderd.c index 3d77d73..c1f578e 100644 --- a/server/disorderd.c +++ b/server/disorderd.c @@ -56,7 +56,6 @@ #include "mixer.h" #include "eventlog.h" #include "printf.h" -#include "setup.h" static ev_source *ev; @@ -249,8 +248,6 @@ int main(int argc, char **argv) { fatal(0, "cannot read configuration"); /* make sure the home directory exists and has suitable permissions */ make_home(); - /* create the default login */ - make_root_login(); /* Start the speaker process (as root! - so it can choose its nice value) */ speaker_setup(ev); /* set server nice value _after_ starting the speaker, so that they @@ -279,8 +276,12 @@ int main(int argc, char **argv) { /* initialize database environment */ trackdb_init(TRACKDB_NORMAL_RECOVER|TRACKDB_MAY_CREATE); trackdb_master(ev); - /* install new config */ + /* install new config (calls trackdb_open()) */ reconfigure(ev, 0); + /* pull in old users */ + trackdb_old_users(); + /* create a root login */ + trackdb_create_root(); /* re-read config if we receive a SIGHUP */ if(ev_signal(ev, SIGHUP, handle_sighup, 0)) fatal(0, "ev_signal failed"); /* exit on SIGINT/SIGTERM */ diff --git a/server/server.c b/server/server.c index cd97435..145bfe2 100644 --- a/server/server.c +++ b/server/server.c @@ -403,8 +403,7 @@ static const char *connection_host(struct conn *c) { static int c_user(struct conn *c, char **vec, int attribute((unused)) nvec) { - int n; - const char *res, *host; + const char *res, *host, *password; if(c->who) { sink_writes(ev_writer_sink(c->w), "530 already authenticated\n"); @@ -416,16 +415,15 @@ static int c_user(struct conn *c, return 1; } /* find the user */ - for(n = 0; n < config->allow.n - && strcmp(config->allow.s[n].s[0], vec[0]); ++n) - ; - /* if it's a real user check whether the response is right */ - if(n >= config->allow.n) { + password = trackdb_get_password(vec[0]); + /* reject nonexistent users */ + if(!password) { info("S%x unknown user '%s' from %s", c->tag, vec[0], host); sink_writes(ev_writer_sink(c->w), "530 authentication failed\n"); return 1; } - res = authhash(c->nonce, sizeof c->nonce, config->allow.s[n].s[1], + /* check whether the response is right */ + res = authhash(c->nonce, sizeof c->nonce, password, config->authorization_algorithm); if(wideopen || (res && !strcmp(res, vec[1]))) { c->who = vec[0]; @@ -1029,6 +1027,42 @@ static int c_revoke(struct conn *c, return 1; } +static int c_adduser(struct conn *c, + char **vec, + int attribute((unused)) nvec) { + /* TODO local only */ + if(trackdb_adduser(vec[0], vec[1], default_rights(), 0)) + sink_writes(ev_writer_sink(c->w), "550 Cannot create user\n"); + else + sink_writes(ev_writer_sink(c->w), "250 User created\n"); + return 1; +} + +static int c_deluser(struct conn *c, + char **vec, + int attribute((unused)) nvec) { + /* TODO local only */ + if(trackdb_deluser(vec[0])) + sink_writes(ev_writer_sink(c->w), "550 Cannot deleted user\n"); + else + sink_writes(ev_writer_sink(c->w), "250 User deleted\n"); + return 1; +} + +static int c_edituser(struct conn *c, + char attribute((unused)) **vec, + int attribute((unused)) nvec) { + sink_writes(ev_writer_sink(c->w), "550 Not implemented\n"); /* TODO */ + return 1; +} + +static int c_userinfo(struct conn *c, + char attribute((unused)) **vec, + int attribute((unused)) nvec) { + sink_writes(ev_writer_sink(c->w), "550 Not implemented\n"); /* TODO */ + return 1; +} + #define C_AUTH 0001 /* must be authenticated */ #define C_TRUSTED 0002 /* must be trusted user */ @@ -1038,11 +1072,14 @@ static const struct command { int (*fn)(struct conn *, char **, int); unsigned flags; } commands[] = { + { "adduser", 2, 2, c_adduser, C_AUTH|C_TRUSTED }, { "allfiles", 0, 2, c_allfiles, C_AUTH }, { "become", 1, 1, c_become, C_AUTH|C_TRUSTED }, { "cookie", 1, 1, c_cookie, 0 }, + { "deluser", 1, 1, c_deluser, C_AUTH|C_TRUSTED }, { "dirs", 0, 2, c_dirs, C_AUTH }, { "disable", 0, 1, c_disable, C_AUTH }, + { "edituser", 3, 3, c_edituser, C_AUTH }, { "enable", 0, 0, c_enable, C_AUTH }, { "enabled", 0, 0, c_enabled, C_AUTH }, { "exists", 1, 1, c_exists, C_AUTH }, @@ -1081,8 +1118,9 @@ static const struct command { { "stats", 0, 0, c_stats, C_AUTH }, { "tags", 0, 0, c_tags, C_AUTH }, { "unset", 2, 2, c_set, C_AUTH }, - { "unset-global", 1, 1, c_set_global, C_AUTH }, + { "unset-global", 1, 1, c_set_global, C_AUTH }, { "user", 2, 2, c_user, 0 }, + { "userinfo", 2, 2, c_userinfo, C_AUTH }, { "version", 0, 0, c_version, C_AUTH }, { "volume", 0, 2, c_volume, C_AUTH } }; diff --git a/server/setup.c b/server/setup.c deleted file mode 100644 index 4ccb00e..0000000 --- a/server/setup.c +++ /dev/null @@ -1,98 +0,0 @@ -/* - * This file is part of DisOrder. - * Copyright (C) 2007 Richard Kettlewell - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 - * USA - */ -/** @file server/setup.c - * @brief Automated setup functions - */ - -#include -#include "types.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "log.h" -#include "mem.h" -#include "printf.h" -#include "configuration.h" -#include "setup.h" -#include "hex.h" -#include "defs.h" - -/** @brief Create config.private with a login for root */ -void make_root_login(void) { - struct stat sb; - char *privconfig, *privconfignew; - int fd; - FILE *fp; - struct passwd *pw; - uint8_t pwbin[10]; - char *pwhex; - - if(config->user) { - if(!(pw = getpwnam(config->user))) - fatal(0, "cannot find user %s", config->user); - } else - pw = 0; - /* Compute filenames */ - byte_xasprintf(&privconfig, "%s/config.private", pkgconfdir); - byte_xasprintf(&privconfignew, "%s/config.private.new", pkgconfdir); - /* If config.private already exists don't overwrite it */ - if(stat(privconfig, &sb) == 0) - return; - /* Choose a new root password */ - gcry_randomize(pwbin, sizeof pwbin, GCRY_STRONG_RANDOM); - pwhex = hex(pwbin, sizeof pwbin); - /* Create the file */ - if((fd = open(privconfignew, O_WRONLY|O_CREAT, 0600)) < 0) { - error(errno, "error creating %s", privconfignew); - return; /* not fatal! */ - } - /* Fix permissions */ - if(pw) { - if(fchown(fd, 0, pw->pw_gid) < 0) - fatal(errno, "error setting owner/group for %s", privconfignew); - if(fchmod(fd, 0640) < 0) - fatal(errno, "error setting permissions for %s", privconfignew); - } - /* Write the required 'allow' line */ - if(!(fp = fdopen(fd, "w"))) - fatal(errno, "fdopen"); - if(fprintf(fp, "allow root %s\n", pwhex) < 0 - || fclose(fp) < 0) - fatal(errno, "error writing %s", privconfignew); - /* Rename into place */ - if(rename(privconfignew, privconfig) < 0) - fatal(errno, "error renaming %s", privconfignew); -} - -/* -Local Variables: -c-basic-offset:2 -comment-column:40 -fill-column:79 -indent-tabs-mode:nil -End: -*/ diff --git a/server/setup.h b/server/setup.h deleted file mode 100644 index ea31106..0000000 --- a/server/setup.h +++ /dev/null @@ -1,35 +0,0 @@ -/* - * This file is part of DisOrder - * Copyright (C) 2007 Richard Kettlewell - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 - * USA - */ - -#ifndef SETUP_H -#define SETUP_H - -void make_root_login(void); - -#endif /* SETUP_H */ - -/* -Local Variables: -c-basic-offset:2 -comment-column:40 -fill-column:79 -indent-tabs-mode:nil -End: -*/ diff --git a/tests/Makefile.am b/tests/Makefile.am index 9527a55..c78401a 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -30,4 +30,4 @@ check: ${PYTHON} ${srcdir}/alltests EXTRA_DIST=alltests dtest.py dbversion.py search.py \ - queue.py dump.py play.py cookie.py + queue.py dump.py play.py cookie.py user-upgrade.py user.py diff --git a/tests/cookie.py b/tests/cookie.py index 7f00454..c0b71e0 100755 --- a/tests/cookie.py +++ b/tests/cookie.py @@ -23,6 +23,7 @@ import dtest,disorder def test(): """Exercise cookie protocol""" dtest.start_daemon() + dtest.create_user() print " connecting" c = disorder.client() v = c.version() diff --git a/tests/dbversion.py b/tests/dbversion.py index 35f71ad..4ae71d7 100755 --- a/tests/dbversion.py +++ b/tests/dbversion.py @@ -28,6 +28,7 @@ def test(): dtest.copyfile(config, configsave) open(config, "a").write("dbversion 1\n") dtest.start_daemon() + dtest.create_user() dtest.stop_daemon() # Revert to default configuration dtest.copyfile(configsave, config) diff --git a/tests/dtest.py b/tests/dtest.py index ef25fc4..5ab56ed 100644 --- a/tests/dtest.py +++ b/tests/dtest.py @@ -166,21 +166,8 @@ def bindable(p): except: return False -def common_setup(): - remove_dir(testroot) - os.mkdir(testroot) - # Choose a port - global port - port = random.randint(49152, 65535) - while not bindable(port + 1): - print "port %d is not bindable, trying another" % (port + 1) - port = random.randint(49152, 65535) - # Log anything sent to that port - packetlog = "%s/packetlog" % testroot - subprocess.Popen(["disorder-udplog", - "--output", packetlog, - "127.0.0.1", "%d" % port]) - # disorder-udplog will quit when its parent process terminates +def default_config(): + """Write the default config""" open("%s/config" % testroot, "w").write( """home %s collection fs UTF-8 %s/tracks @@ -193,8 +180,7 @@ stopword 21 22 23 24 25 26 27 28 29 30 stopword the a an and to too in on of we i am as im for is username fred password fredpass -allow fred fredpass -trust fred +trust fred root plugins plugins %s/plugins plugins %s/plugins/.libs @@ -211,8 +197,25 @@ broadcast 127.0.0.1 %d broadcast_from 127.0.0.1 %d """ % (testroot, testroot, testroot, top_builddir, top_builddir, port, port + 1)) + +def common_setup(): + remove_dir(testroot) + os.mkdir(testroot) + # Choose a port + global port + port = random.randint(49152, 65535) + while not bindable(port + 1): + print "port %d is not bindable, trying another" % (port + 1) + port = random.randint(49152, 65535) + # Log anything sent to that port + packetlog = "%s/packetlog" % testroot + subprocess.Popen(["disorder-udplog", + "--output", packetlog, + "127.0.0.1", "%d" % port]) + # disorder-udplog will quit when its parent process terminates copyfile("%s/sounds/scratch.ogg" % top_srcdir, "%s/scratch.ogg" % testroot) + default_config() def start_daemon(): """start_daemon() @@ -253,6 +256,15 @@ Start the daemon.""" if waited > 0: print " took about %ds for socket to appear" % waited +def create_user(username="fred", password="fredpass"): + """create_user(USERNAME, PASSWORD) + + Create a user, abusing direct database access to do so.""" + print " creating user %s" % username + command(["disorder", + "--config", disorder._configfile, "--no-per-user-config", + "--user", "root", "adduser", username, password]) + def stop_daemon(): """stop_daemon() diff --git a/tests/dump.py b/tests/dump.py index ff28c63..f3122c2 100755 --- a/tests/dump.py +++ b/tests/dump.py @@ -23,16 +23,17 @@ import dtest,time,disorder,re def test(): """Exercise database dumper""" dtest.start_daemon() + dtest.create_user() c = disorder.client() track = "%s/Joe Bloggs/First Album/02:Second track.ogg" % dtest.tracks dump = "%s/dumpfile" % dtest.testroot - print "setting a track pref" + print " setting a track pref" c.set(track, "foo", "before") assert c.get(track, "foo") == "before", "checking track foo=before" - print "setting a global pref" + print " setting a global pref" c.setglobal("foo", "before"); assert c.getglobal("foo") == "before", "checking global foo=before" - print "adding a tag" + print " adding a tag" # Exercise the tags-changed code c.set(track, "tags", " first tag, Another Tag") assert dtest.lists_have_same_contents(c.tags(), @@ -42,25 +43,25 @@ def test(): assert dtest.lists_have_same_contents(c.tags(), [u"another tag", u"wibble"]),\ "checking tag list(2)" - print "checking track appears in tag search" + print " checking track appears in tag search" tracks = c.search(["tag:wibble"]) assert len(tracks) == 1, "checking there is exactly one search result(1)" assert tracks[0] == track, "checking for right search result(1)" tracks = c.search(["tag: another tAg "]) assert len(tracks) == 1, "checking there is exactly one search result(2)" assert tracks[0] == track, "checking for right search result(2)" - print "dumping database" + print " dumping database" print dtest.command(["disorder-dump", "--config", disorder._configfile, "--dump", dump]) - print "changing track pref" + print " changing track pref" c.set(track, "foo", "after"); assert c.get(track, "foo") == "after", "checking track foo=before" - print "changing global pref" + print " changing global pref" c.setglobal("foo", "after"); assert c.getglobal("foo") == "after", "checking global foo=before" - print "adding fresh track pref" + print " adding fresh track pref" c.set(track, "bar", "after") - print "adding fresh global pref" + print " adding fresh global pref" c.setglobal("bar", "after") dtest.stop_daemon(); print "restoring database" @@ -68,15 +69,15 @@ def test(): "--undump", dump]) dtest.start_daemon(); c = disorder.client() - print "checking track pref" + print " checking track pref" assert c.get(track, "foo") == "before", "checking track foo=before after undump" - print "checking global pref" + print " checking global pref" assert c.getglobal("foo") == "before", "checking global foo=before after undump" - print "checking fresh track pref" + print " checking fresh track pref" assert c.get(track, "bar") is None, "checking fresh track pref has gone" - print "checking fresh global pref" + print " checking fresh global pref" assert c.getglobal("bar") is None, "checking fresh global pref has gone" - print "checking tag search still works" + print " checking tag search still works" tracks = c.search(["tag:wibble"]) assert len(tracks) == 1, "checking there is exactly one search result" assert tracks[0] == track, "checking for right search result(3)" diff --git a/tests/files.py b/tests/files.py index 69e98d4..7c22ffc 100755 --- a/tests/files.py +++ b/tests/files.py @@ -23,6 +23,7 @@ import dtest,time,disorder,sys def test(): """Check that the file listing comes out right""" dtest.start_daemon() + dtest.create_user() assert dtest.check_files() == 0, "dtest.check_files" print " checking regexp file listing" c = disorder.client() diff --git a/tests/play.py b/tests/play.py index 92bb664..9292dfc 100755 --- a/tests/play.py +++ b/tests/play.py @@ -23,9 +23,10 @@ import dtest,time,disorder,re def test(): """Play some tracks""" dtest.start_daemon() + dtest.create_user() c = disorder.client() track = u"%s/Joe Bloggs/First Album/02:Second track.ogg" % dtest.tracks - print "adding track to queue" + print " adding track to queue" c.play(track) print " checking track turned up in queue" q = c.queue() diff --git a/tests/queue.py b/tests/queue.py index cc0b619..c78d6ed 100755 --- a/tests/queue.py +++ b/tests/queue.py @@ -23,6 +23,7 @@ import dtest,time,disorder,re def test(): """Check the queue is padded to the (default) configured length""" dtest.start_daemon() + dtest.create_user() c = disorder.client() print " getting queue via python module" q = c.queue() diff --git a/tests/search.py b/tests/search.py index 01ab893..3be6ec9 100755 --- a/tests/search.py +++ b/tests/search.py @@ -41,6 +41,7 @@ def check_search_results(terms, expected): def test(): """Check that the search produces the right results""" dtest.start_daemon() + dtest.create_user() time.sleep(2) # give rescan a chance global client client = disorder.client() diff --git a/tests/user-upgrade.py b/tests/user-upgrade.py new file mode 100755 index 0000000..6141714 --- /dev/null +++ b/tests/user-upgrade.py @@ -0,0 +1,41 @@ +#! /usr/bin/env python +# +# This file is part of DisOrder. +# Copyright (C) 2007 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# +import dtest,disorder + +def test(): + """Test upgrade to new user database""" + print " testing upgrade from old versions" + open("%s/config" % dtest.testroot, "a").write( + """allow fred fredpass +""") + dtest.start_daemon() + print " checking can log in after upgrade" + c = disorder.client() + c.version() + dtest.stop_daemon() + dtest.default_config() + dtest.start_daemon() + print " checking can log in after removing 'allow'" + c = disorder.client() + c.version() + +if __name__ == '__main__': + dtest.run() diff --git a/tests/user.py b/tests/user.py new file mode 100755 index 0000000..8c13232 --- /dev/null +++ b/tests/user.py @@ -0,0 +1,47 @@ +#! /usr/bin/env python +# +# This file is part of DisOrder. +# Copyright (C) 2007 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# +import dtest,disorder + +def test(): + """Test user database""" + dtest.start_daemon() + dtest.create_user() + print " checking user creation" + c = disorder.client() + c.adduser("bob", "bobpass") + print " checking new user can log in" + c = disorder.client(user="bob", password="bobpass") + c.version() + print " checking user deletion" + c = disorder.client() + c.deluser("bob") + print " checking new user can no longer log in" + c = disorder.client(user="bob", password="bobpass") + try: + c.version() + print "*** should not be able to log in after deletion ***" + assert False + except disorder.operationError: + pass # good + print " deleted user could no longer log in." + +if __name__ == '__main__': + dtest.run() -- [mdw]