chiark / gitweb /
update CHANGES.html
[disorder] / server / server.c
index e03ff9c3ea1a4c5b935b7f3723eed704ced337ec..c4c889a448f909d6b9bcff26dc14d90d6760735b 100644 (file)
  * USA
  */
 
-#include <config.h>
-#include "types.h"
-
-#include <pwd.h>
-#include <sys/types.h>
-#include <sys/socket.h>
-#include <sys/time.h>
-#include <unistd.h>
-#include <stdlib.h>
-#include <string.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <gcrypt.h>
-#include <stddef.h>
-#include <time.h>
-#include <limits.h>
-#include <pcre.h>
-#include <netdb.h>
-#include <netinet/in.h>
-
-#include "event.h"
-#include "server.h"
-#include "syscalls.h"
-#include "queue.h"
-#include "server-queue.h"
-#include "play.h"
-#include "log.h"
-#include "mem.h"
-#include "state.h"
-#include "charset.h"
-#include "split.h"
-#include "configuration.h"
-#include "hex.h"
-#include "rights.h"
-#include "trackdb.h"
-#include "table.h"
-#include "kvp.h"
-#include "mixer.h"
-#include "sink.h"
-#include "authhash.h"
-#include "plugin.h"
-#include "printf.h"
-#include "trackname.h"
-#include "eventlog.h"
-#include "defs.h"
-#include "cache.h"
-#include "unicode.h"
-#include "cookies.h"
-#include "base64.h"
-#include "hash.h"
-#include "mime.h"
-#include "sendmail.h"
-#include "wstat.h"
+#include "disorder-server.h"
 
 #ifndef NONCE_SIZE
 # define NONCE_SIZE 16
@@ -124,6 +72,8 @@ struct conn {
   rights_type rights;
   /** @brief Next connection */
   struct conn *next;
+  /** @brief True if pending rescan had 'wait' set */
+  int rescan_wait;
 };
 
 /** @brief Linked list of connections */
@@ -355,13 +305,107 @@ static int c_reconfigure(struct conn *c,
   return 1;                            /* completed */
 }
 
+static void finished_rescan(void *ru) {
+  struct conn *const c = ru;
+
+  sink_writes(ev_writer_sink(c->w), "250 rescan completed\n");
+  /* Turn this connection back on */
+  ev_reader_enable(c->r);
+}
+
+static void start_fresh_rescan(void *ru) {
+  struct conn *const c = ru;
+
+  if(trackdb_rescan_underway()) {
+    /* Some other waiter beat us to it.  However in this case we're happy to
+     * piggyback; the requirement is that a new rescan be started, not that it
+     * was _our_ rescan. */
+    if(c->rescan_wait) {
+      /* We block until the rescan completes */
+      trackdb_add_rescanned(finished_rescan, c);
+    } else {
+      /* We report that the new rescan has started */
+      sink_writes(ev_writer_sink(c->w), "250 rescan initiated\n");
+      /* Turn this connection back on */
+      ev_reader_enable(c->r);
+    }
+  } else {
+    /* We are the first connection to get a callback so we must start a
+     * rescan. */
+    if(c->rescan_wait) {
+      /* We want to block until the new rescan completes */
+      trackdb_rescan(c->ev, 1/*check*/, finished_rescan, c);
+    } else {
+      /* We can report back immediately */
+      trackdb_rescan(c->ev, 1/*check*/, 0, 0);
+      sink_writes(ev_writer_sink(c->w), "250 rescan initiated\n");
+      /* Turn this connection back on */
+      ev_reader_enable(c->r);
+    }
+  }
+}
+
 static int c_rescan(struct conn *c,
-                   char attribute((unused)) **vec,
-                   int attribute((unused)) nvec) {
-  info("S%x rescan by %s", c->tag, c->who);
-  trackdb_rescan(c->ev, 1/*check*/);
-  sink_writes(ev_writer_sink(c->w), "250 initiated rescan\n");
-  return 1;                            /* completed */
+                   char **vec,
+                   int nvec) {
+  int flag_wait = 0, flag_fresh = 0, n;
+
+  /* Parse flags */
+  for(n = 0; n < nvec; ++n) {
+    if(!strcmp(vec[n], "wait"))
+      flag_wait = 1;                   /* wait for rescan to complete */
+#if 0
+    /* Currently disabled because untested (and hard to test). */
+    else if(!strcmp(vec[n], "fresh"))
+      flag_fresh = 1;                  /* don't piggyback underway rescan */
+#endif
+    else {
+      sink_writes(ev_writer_sink(c->w), "550 unknown flag\n");
+      return 1;                                /* completed */
+    }
+  }
+  /* Report what was requested */
+  info("S%x rescan by %s (%s %s)", c->tag, c->who,
+       flag_wait ? "wait" : "",
+       flag_fresh ? "fresh" : "");
+  if(trackdb_rescan_underway()) {
+    if(flag_fresh) {
+      /* We want a fresh rescan but there is already one underway.  Arrange a
+       * callback when it completes and then set off a new one. */
+      c->rescan_wait = flag_wait;
+      trackdb_add_rescanned(start_fresh_rescan, c);
+      if(flag_wait)
+       return 0;
+      else {
+       sink_writes(ev_writer_sink(c->w), "250 rescan queued\n");
+       return 1;
+      }
+    } else {
+      /* There's a rescan underway, and it's acceptable to piggyback on it */
+      if(flag_wait) {
+       /* We want to block until completion. */
+       trackdb_add_rescanned(finished_rescan, c);
+       return 0;
+      } else {
+       /* We don't want to block.  So we just report that things are in
+        * hand. */
+       sink_writes(ev_writer_sink(c->w), "250 rescan already underway\n");
+       return 1;
+      }
+    }
+  } else {
+    /* No rescan is underway.  fresh is therefore irrelevant. */
+    if(flag_wait) {
+      /* We want to block until completion */
+      trackdb_rescan(c->ev, 1/*check*/, finished_rescan, c);
+      return 0;
+    } else {
+      /* We don't want to block. */
+      trackdb_rescan(c->ev, 1/*check*/, 0, 0);
+      sink_writes(ev_writer_sink(c->w), "250 rescan initiated\n");
+      return 1;                                /* completed */
+    }
+  }
 }
 
 static int c_version(struct conn *c,
@@ -614,9 +658,13 @@ static int c_allfiles(struct conn *c,
 static int c_get(struct conn *c,
                 char **vec,
                 int attribute((unused)) nvec) {
-  const char *v;
+  const char *v, *track;
 
-  if(vec[1][0] != '_' && (v = trackdb_get(vec[0], vec[1])))
+  if(!(track = trackdb_resolve(vec[0]))) {
+    sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
+    return 1;
+  }
+  if(vec[1][0] != '_' && (v = trackdb_get(track, vec[1])))
     sink_printf(ev_writer_sink(c->w), "252 %s\n", quoteutf8(v));
   else
     sink_writes(ev_writer_sink(c->w), "555 not found\n");
@@ -642,7 +690,13 @@ static int c_length(struct conn *c,
 static int c_set(struct conn *c,
                 char **vec,
                 int attribute((unused)) nvec) {
-  if(vec[1][0] != '_' && !trackdb_set(vec[0], vec[1], vec[2]))
+  const char *track;
+
+  if(!(track = trackdb_resolve(vec[0]))) {
+    sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
+    return 1;
+  }
+  if(vec[1][0] != '_' && !trackdb_set(track, vec[1], vec[2]))
     sink_writes(ev_writer_sink(c->w), "250 OK\n");
   else
     sink_writes(ev_writer_sink(c->w), "550 not found\n");
@@ -653,8 +707,13 @@ static int c_prefs(struct conn *c,
                   char **vec,
                   int attribute((unused)) nvec) {
   struct kvp *k;
+  const char *track;
 
-  k = trackdb_get_all(vec[0]);
+  if(!(track = trackdb_resolve(vec[0]))) {
+    sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
+    return 1;
+  }
+  k = trackdb_get_all(track);
   sink_writes(ev_writer_sink(c->w), "253 prefs follow\n");
   for(; k; k = k->next)
     if(k->name[0] != '_')              /* omit internal values */
@@ -667,6 +726,7 @@ static int c_prefs(struct conn *c,
 static int c_exists(struct conn *c,
                    char **vec,
                    int attribute((unused)) nvec) {
+  /* trackdb_exists() does its own alias checking */
   sink_printf(ev_writer_sink(c->w), "252 %s\n", noyes[trackdb_exists(vec[0])]);
   return 1;
 }
@@ -767,7 +827,7 @@ static int c_volume(struct conn *c,
     sink_writes(ev_writer_sink(c->w), "510 Prohibited\n");
     return 1;
   }
-  if(mixer_control(&l, &r, set))
+  if(mixer_control(-1/*as configured*/, &l, &r, set))
     sink_writes(ev_writer_sink(c->w), "550 error accessing mixer\n");
   else {
     sink_printf(ev_writer_sink(c->w), "252 %d %d\n", l, r);
@@ -816,8 +876,19 @@ static void logclient(const char *msg, void *user) {
   if(!c->w || !c->r) {
     /* This connection has gone up in smoke for some reason */
     eventlog_remove(c->lo);
+    c->lo = 0;
     return;
   }
+  /* user_* messages are restricted */
+  if(!strncmp(msg, "user_", 5)) {
+    /* They are only sent to admin users */
+    if(!(c->rights & RIGHT_ADMIN))
+      return;
+    /* They are not sent over TCP connections unless remote user-management is
+     * enabled */
+    if(!config->remote_userman && !(c->rights & RIGHT__LOCAL))
+      return;
+  }
   sink_printf(ev_writer_sink(c->w), "%"PRIxMAX" %s\n",
              (uintmax_t)time(0), msg);
 }
@@ -931,8 +1002,14 @@ static int c_moveafter(struct conn *c,
 static int c_part(struct conn *c,
                  char **vec,
                  int attribute((unused)) nvec) {
+  const char *track;
+
+  if(!(track = trackdb_resolve(vec[0]))) {
+    sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
+    return 1;
+  }
   sink_printf(ev_writer_sink(c->w), "252 %s\n",
-             quoteutf8(trackdb_getpart(vec[0], vec[1], vec[2])));
+             quoteutf8(trackdb_getpart(track, vec[1], vec[2])));
   return 1;
 }
 
@@ -1096,6 +1173,11 @@ static int c_adduser(struct conn *c,
                     int nvec) {
   const char *rights;
 
+  if(!config->remote_userman && !(c->rights & RIGHT__LOCAL)) {
+    error(0, "S%x: remote adduser", c->tag);
+    sink_writes(ev_writer_sink(c->w), "550 Remote user management is disabled\n");
+    return 1;
+  }
   if(nvec > 2) {
     rights = vec[2];
     if(parse_rights(vec[2], 0, 1)) {
@@ -1117,6 +1199,11 @@ static int c_deluser(struct conn *c,
                     int attribute((unused)) nvec) {
   struct conn *d;
 
+  if(!config->remote_userman && !(c->rights & RIGHT__LOCAL)) {
+    error(0, "S%x: remote deluser", c->tag);
+    sink_writes(ev_writer_sink(c->w), "550 Remote user management is disabled\n");
+    return 1;
+  }
   if(trackdb_deluser(vec[0])) {
     sink_writes(ev_writer_sink(c->w), "550 Cannot delete user\n");
     return 1;
@@ -1134,6 +1221,11 @@ static int c_edituser(struct conn *c,
                      int attribute((unused)) nvec) {
   struct conn *d;
 
+  if(!config->remote_userman && !(c->rights & RIGHT__LOCAL)) {
+    error(0, "S%x: remote edituser", c->tag);
+    sink_writes(ev_writer_sink(c->w), "550 Remote user management is disabled\n");
+    return 1;
+  }
   /* RIGHT_ADMIN can do anything; otherwise you can only set your own email
    * address and password. */
   if((c->rights & RIGHT_ADMIN)
@@ -1153,10 +1245,21 @@ static int c_edituser(struct conn *c,
       /* Update rights for this user */
       rights_type r;
 
-      if(parse_rights(vec[2], &r, 1))
-       for(d = connections; d; d = d->next)
-         if(!strcmp(d->who, vec[0]))
+      if(!parse_rights(vec[2], &r, 1)) {
+        const char *new_rights = rights_string(r);
+       for(d = connections; d; d = d->next) {
+         if(!strcmp(d->who, vec[0])) {
+            /* Update rights */
            d->rights = r;
+            /* Notify any log connections */
+            if(d->lo)
+              sink_printf(ev_writer_sink(d->w),
+                          "%"PRIxMAX" rights_changed %s\n",
+                          (uintmax_t)time(0),
+                          quoteutf8(new_rights));
+          }
+        }
+      }
     }
     sink_writes(ev_writer_sink(c->w), "250 OK\n");
   } else {
@@ -1172,6 +1275,15 @@ static int c_userinfo(struct conn *c,
   struct kvp *k;
   const char *value;
 
+  /* We allow remote querying of rights so that clients can figure out what
+   * they're allowed to do */
+  if(!config->remote_userman
+     && !(c->rights & RIGHT__LOCAL)
+     && strcmp(vec[1], "rights")) {
+    error(0, "S%x: remote userinfo %s %s", c->tag, vec[0], vec[1]);
+    sink_writes(ev_writer_sink(c->w), "550 Remote user management is disabled\n");
+    return 1;
+  }
   /* RIGHT_ADMIN allows anything; otherwise you can only get your own email
    * address and rights list. */
   if((c->rights & RIGHT_ADMIN)
@@ -1315,7 +1427,7 @@ static int c_reminder(struct conn *c,
     return 1;
   }
   if(!(email = kvp_get(k, "email"))
-     || !strchr(email, '@')) {
+     || !email_valid(email)) {
     error(0, "user '%s' has no valid email address", vec[0]);
     sink_writes(ev_writer_sink(c->w), "550 Cannot send a reminder email\n");
     return 1;
@@ -1364,6 +1476,105 @@ static int c_reminder(struct conn *c,
   return 0;
 }
 
+static int c_schedule_list(struct conn *c,
+                          char attribute((unused)) **vec,
+                          int attribute((unused)) nvec) {
+  char **ids = schedule_list(0);
+  sink_writes(ev_writer_sink(c->w), "253 ID list follows\n");
+  while(*ids)
+    sink_printf(ev_writer_sink(c->w), "%s\n", *ids++);
+  sink_writes(ev_writer_sink(c->w), ".\n");
+  return 1;                            /* completed */
+}
+
+static int c_schedule_get(struct conn *c,
+                         char **vec,
+                         int attribute((unused)) nvec) {
+  struct kvp *actiondata = schedule_get(vec[0]), *k;
+
+  if(!actiondata) {
+    sink_writes(ev_writer_sink(c->w), "555 No such event\n");
+    return 1;                          /* completed */
+  }
+  /* Scheduled events are public information.  Anyone with RIGHT_READ can see
+   * them. */
+  sink_writes(ev_writer_sink(c->w), "253 Event information follows\n");
+  for(k = actiondata; k; k = k->next)
+    sink_printf(ev_writer_sink(c->w), " %s %s\n",
+               quoteutf8(k->name),  quoteutf8(k->value));
+  sink_writes(ev_writer_sink(c->w), ".\n");
+  return 1;                            /* completed */
+}
+
+static int c_schedule_del(struct conn *c,
+                         char **vec,
+                         int attribute((unused)) nvec) {
+  struct kvp *actiondata = schedule_get(vec[0]);
+
+  if(!actiondata) {
+    sink_writes(ev_writer_sink(c->w), "555 No such event\n");
+    return 1;                          /* completed */
+  }
+  /* If you have admin rights you can delete anything.  If you don't then you
+   * can only delete your own scheduled events. */
+  if(!(c->rights & RIGHT_ADMIN)) {
+    const char *who = kvp_get(actiondata, "who");
+
+    if(!who || !c->who || strcmp(who, c->who)) {
+      sink_writes(ev_writer_sink(c->w), "551 Not authorized\n");
+      return 1;                                /* completed */
+    }
+  }
+  if(schedule_del(vec[0]))
+    sink_writes(ev_writer_sink(c->w), "550 Could not delete scheduled event\n");
+  else
+    sink_writes(ev_writer_sink(c->w), "250 Deleted\n");
+  return 1;                            /* completed */
+}
+
+static int c_schedule_add(struct conn *c,
+                         char **vec,
+                         int nvec) {
+  struct kvp *actiondata = 0;
+  const char *id;
+
+  /* Standard fields */
+  kvp_set(&actiondata, "who", c->who);
+  kvp_set(&actiondata, "when", vec[0]);
+  kvp_set(&actiondata, "priority", vec[1]);
+  kvp_set(&actiondata, "action", vec[2]);
+  /* Action-dependent fields */
+  if(!strcmp(vec[2], "play")) {
+    if(nvec != 4) {
+      sink_writes(ev_writer_sink(c->w), "550 Wrong number of arguments\n");
+      return 1;
+    }
+    if(!trackdb_exists(vec[3])) {
+      sink_writes(ev_writer_sink(c->w), "550 Track is not in database\n");
+      return 1;
+    }
+    kvp_set(&actiondata, "track", vec[3]);
+  } else if(!strcmp(vec[2], "set-global")) {
+    if(nvec < 4 || nvec > 5) {
+      sink_writes(ev_writer_sink(c->w), "550 Wrong number of arguments\n");
+      return 1;
+    }
+    kvp_set(&actiondata, "key", vec[3]);
+    if(nvec > 4)
+      kvp_set(&actiondata, "value", vec[4]);
+  } else {
+    sink_writes(ev_writer_sink(c->w), "550 Unknown action\n");
+    return 1;
+  }
+  /* schedule_add() checks user rights */
+  id = schedule_add(c->ev, actiondata);
+  if(!id)
+    sink_writes(ev_writer_sink(c->w), "550 Cannot add scheduled event\n");
+  else
+    sink_printf(ev_writer_sink(c->w), "252 %s\n", id);
+  return 1;
+}
+
 static const struct command {
   /** @brief Command name */
   const char *name;
@@ -1419,11 +1630,15 @@ static const struct command {
   { "register",       3, 3,       c_register,       RIGHT_REGISTER|RIGHT__LOCAL },
   { "reminder",       1, 1,       c_reminder,       RIGHT__LOCAL },
   { "remove",         1, 1,       c_remove,         RIGHT_REMOVE__MASK },
-  { "rescan",         0, 0,       c_rescan,         RIGHT_RESCAN },
+  { "rescan",         0, INT_MAX, c_rescan,         RIGHT_RESCAN },
   { "resolve",        1, 1,       c_resolve,        RIGHT_READ },
   { "resume",         0, 0,       c_resume,         RIGHT_PAUSE },
   { "revoke",         0, 0,       c_revoke,         RIGHT_READ },
   { "rtp-address",    0, 0,       c_rtp_address,    0 },
+  { "schedule-add",   3, INT_MAX, c_schedule_add,   RIGHT_READ },
+  { "schedule-del",   1, 1,       c_schedule_del,   RIGHT_READ },
+  { "schedule-get",   1, 1,       c_schedule_get,   RIGHT_READ },
+  { "schedule-list",  0, 0,       c_schedule_list,  RIGHT_READ },
   { "scratch",        0, 1,       c_scratch,        RIGHT_SCRATCH__MASK },
   { "search",         1, 1,       c_search,         RIGHT_READ },
   { "set",            3, 3,       c_set,            RIGHT_PREFS, },
@@ -1465,7 +1680,7 @@ static int command(struct conn *c, char *line) {
     sink_writes(ev_writer_sink(c->w), "500 do what?\n");
     return 1;
   }
-  if((n = TABLE_FIND(commands, struct command, name, vec[0])) < 0)
+  if((n = TABLE_FIND(commands, name, vec[0])) < 0)
     sink_writes(ev_writer_sink(c->w), "500 unknown command\n");
   else {
     if(commands[n].rights
@@ -1558,6 +1773,7 @@ static int listen_callback(ev_source *ev,
   D(("server listen_callback fd %d (%s)", fd, l->name));
   nonblock(fd);
   cloexec(fd);
+  c->next = connections;
   c->tag = tags++;
   c->ev = ev;
   c->w = ev_writer_new(ev, fd, writer_error, c,
@@ -1569,6 +1785,7 @@ static int listen_callback(ev_source *ev,
   c->reader = reader_callback;
   c->l = l;
   c->rights = 0;
+  connections = c;
   gcry_randomize(c->nonce, sizeof c->nonce, GCRY_STRONG_RANDOM);
   sink_printf(ev_writer_sink(c->w), "231 %d %s %s\n",
              2,