+static int c_users(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ return list_response(c, "User list follows", trackdb_listusers());
+}
+
+static int c_register(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ char *cs;
+ uint32_t nonce[CONFIRM_SIZE];
+ char nonce_str[(32 * CONFIRM_SIZE) / 5 + 1];
+
+ /* The confirmation string is username/base62(nonce). The confirmation
+ * process will pick the username back out to identify them but the _whole_
+ * string is used as the confirmation string. Base 62 means we used only
+ * letters and digits, minimizing the chance of the URL being mispasted. */
+ gcry_randomize(nonce, sizeof nonce, GCRY_STRONG_RANDOM);
+ if(basen(nonce, CONFIRM_SIZE, nonce_str, sizeof nonce_str, 62)) {
+ disorder_error(0, "buffer too small encoding confirmation string");
+ sink_writes(ev_writer_sink(c->w), "550 Cannot create user\n");
+ }
+ byte_xasprintf(&cs, "%s/%s", vec[0], nonce_str);
+ if(trackdb_adduser(vec[0], vec[1], config->default_rights, vec[2], cs))
+ sink_writes(ev_writer_sink(c->w), "550 Cannot create user\n");
+ else
+ sink_printf(ev_writer_sink(c->w), "252 %s\n", quoteutf8(cs));
+ return 1;
+}
+
+static int c_confirm(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ char *user, *sep;
+ rights_type rights;
+ const char *host;
+
+ /* Get some kind of peer identifcation */
+ if(!(host = connection_host(c))) {
+ sink_writes(ev_writer_sink(c->w), "530 Authentication failure\n");
+ return 1;
+ }
+ /* Picking the LAST / means we don't (here) rule out slashes in usernames. */
+ if(!(sep = strrchr(vec[0], '/'))) {
+ sink_writes(ev_writer_sink(c->w), "550 Malformed confirmation string\n");
+ return 1;
+ }
+ user = xstrndup(vec[0], sep - vec[0]);
+ if(trackdb_confirm(user, vec[0], &rights))
+ sink_writes(ev_writer_sink(c->w), "550 Incorrect confirmation string\n");
+ else {
+ c->who = user;
+ c->cookie = 0;
+ c->rights = rights;
+ if(strcmp(host, "local"))
+ disorder_info("S%x %s confirmed from %s", c->tag, user, host);
+ else
+ c->rights |= RIGHT__LOCAL;
+ /* Response contains username so client knows who they are acting as */
+ sink_printf(ev_writer_sink(c->w), "232 %s\n", quoteutf8(user));
+ }
+ return 1;
+}
+
+static int sent_reminder(ev_source attribute((unused)) *ev,
+ pid_t attribute((unused)) pid,
+ int status,
+ const struct rusage attribute((unused)) *rusage,
+ void *u) {
+ struct conn *const c = u;
+
+ /* Tell the client what went down */
+ if(!status) {
+ sink_writes(ev_writer_sink(c->w), "250 OK\n");
+ } else {
+ disorder_error(0, "reminder subprocess %s", wstat(status));
+ sink_writes(ev_writer_sink(c->w), "550 Cannot send a reminder email\n");
+ }
+ /* Re-enable this connection */
+ ev_reader_enable(c->r);
+ return 0;
+}
+
+static int c_reminder(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ struct kvp *k;
+ const char *password, *email, *text, *encoding, *charset, *content_type;
+ const time_t *last;
+ time_t now;
+ pid_t pid;
+
+ static hash *last_reminder;
+
+ if(!config->mail_sender) {
+ disorder_error(0, "cannot send password reminders because mail_sender not set");
+ sink_writes(ev_writer_sink(c->w), "550 Cannot send a reminder email\n");
+ return 1;
+ }
+ if(!(k = trackdb_getuserinfo(vec[0]))) {
+ disorder_error(0, "reminder for user '%s' who does not exist", vec[0]);
+ sink_writes(ev_writer_sink(c->w), "550 Cannot send a reminder email\n");
+ return 1;
+ }
+ if(!(email = kvp_get(k, "email"))
+ || !email_valid(email)) {
+ disorder_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;
+ }
+ if(!(password = kvp_get(k, "password"))
+ || !*password) {
+ disorder_error(0, "user '%s' has no password", vec[0]);
+ sink_writes(ev_writer_sink(c->w), "550 Cannot send a reminder email\n");
+ return 1;
+ }
+ /* Rate-limit reminders. This hash is bounded in size by the number of
+ * users. If this is actually a problem for anyone then we can periodically
+ * clean it. */
+ if(!last_reminder)
+ last_reminder = hash_new(sizeof (time_t));
+ last = hash_find(last_reminder, vec[0]);
+ xtime(&now);
+ if(last && now < *last + config->reminder_interval) {
+ disorder_error(0, "sent a password reminder to '%s' too recently", vec[0]);
+ sink_writes(ev_writer_sink(c->w), "550 Cannot send a reminder email\n");
+ return 1;
+ }
+ /* Send the reminder */
+ /* TODO this should be templatized and to some extent merged with
+ * the code in act_register() */
+ byte_xasprintf((char **)&text,
+"Someone requested that you be sent a reminder of your DisOrder password.\n"
+"Your password is:\n"
+"\n"
+" %s\n", password);
+ if(!(text = mime_encode_text(text, &charset, &encoding)))
+ disorder_fatal(0, "cannot encode email");
+ byte_xasprintf((char **)&content_type, "text/plain;charset=%s",
+ quote822(charset, 0));
+ pid = sendmail_subprocess("", config->mail_sender, email,
+ "DisOrder password reminder",
+ encoding, content_type, text);
+ if(pid < 0) {
+ sink_writes(ev_writer_sink(c->w), "550 Cannot send a reminder email\n");
+ return 1;
+ }
+ hash_add(last_reminder, vec[0], &now, HASH_INSERT_OR_REPLACE);
+ disorder_info("sending a passsword reminder to user '%s'", vec[0]);
+ /* We can only continue when the subprocess finishes */
+ ev_child(c->ev, pid, 0, sent_reminder, 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 int c_adopt(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ struct queue_entry *q;
+
+ if(!c->who) {
+ sink_writes(ev_writer_sink(c->w), "550 no identity\n");
+ return 1;
+ }
+ if(!(q = queue_find(vec[0]))) {
+ sink_writes(ev_writer_sink(c->w), "550 no such track on the queue\n");
+ return 1;
+ }
+ if(q->origin != origin_random) {
+ sink_writes(ev_writer_sink(c->w), "550 not a random track\n");
+ return 1;
+ }
+ q->origin = origin_adopted;
+ q->submitter = xstrdup(c->who);
+ eventlog("adopted", q->id, q->submitter, (char *)0);
+ queue_write();
+ sink_writes(ev_writer_sink(c->w), "250 OK\n");
+ return 1;
+}
+
+static int playlist_response(struct conn *c,
+ int err) {
+ switch(err) {
+ case 0:
+ assert(!"cannot cope with success");
+ case EACCES:
+ sink_writes(ev_writer_sink(c->w), "550 Access denied\n");
+ break;
+ case EINVAL:
+ sink_writes(ev_writer_sink(c->w), "550 Invalid playlist name\n");
+ break;
+ case ENOENT:
+ sink_writes(ev_writer_sink(c->w), "555 No such playlist\n");
+ break;
+ default:
+ sink_writes(ev_writer_sink(c->w), "550 Error accessing playlist\n");
+ break;
+ }
+ return 1;
+}
+
+static int c_playlist_get(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ char **tracks;
+ int err;
+
+ if(!(err = trackdb_playlist_get(vec[0], c->who, &tracks, 0, 0)))
+ return list_response(c, "Playlist contents follows", tracks);
+ else
+ return playlist_response(c, err);
+}
+
+static int c_playlist_set(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ return fetch_body(c, c_playlist_set_body, vec[0]);
+}
+
+static int c_playlist_set_body(struct conn *c,
+ char **body,
+ int nbody,
+ void *u) {
+ const char *playlist = u;
+ int err;
+
+ if(!c->locked_playlist
+ || strcmp(playlist, c->locked_playlist)) {
+ sink_writes(ev_writer_sink(c->w), "550 Playlist is not locked\n");
+ return 1;
+ }
+ if(!(err = trackdb_playlist_set(playlist, c->who,
+ body, nbody, 0))) {
+ sink_printf(ev_writer_sink(c->w), "250 OK\n");
+ return 1;
+ } else
+ return playlist_response(c, err);
+}
+
+static int c_playlist_get_share(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ char *share;
+ int err;
+
+ if(!(err = trackdb_playlist_get(vec[0], c->who, 0, 0, &share))) {
+ sink_printf(ev_writer_sink(c->w), "252 %s\n", quoteutf8(share));
+ return 1;
+ } else
+ return playlist_response(c, err);
+}
+
+static int c_playlist_set_share(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ int err;
+
+ if(!(err = trackdb_playlist_set(vec[0], c->who, 0, 0, vec[1]))) {
+ sink_printf(ev_writer_sink(c->w), "250 OK\n");
+ return 1;
+ } else
+ return playlist_response(c, err);
+}
+
+static int c_playlists(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ char **p;
+
+ trackdb_playlist_list(c->who, &p, 0);
+ return list_response(c, "List of playlists follows", p);
+}
+
+static int c_playlist_delete(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ int err;
+
+ if(!(err = trackdb_playlist_delete(vec[0], c->who))) {
+ sink_writes(ev_writer_sink(c->w), "250 OK\n");
+ return 1;
+ } else
+ return playlist_response(c, err);
+}
+
+static int c_playlist_lock(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ int err;
+ struct conn *cc;
+
+ /* Check we're allowed to modify this playlist */
+ if((err = trackdb_playlist_set(vec[0], c->who, 0, 0, 0)))
+ return playlist_response(c, err);
+ /* If we hold a lock don't allow a new one */
+ if(c->locked_playlist) {
+ sink_writes(ev_writer_sink(c->w), "550 Already holding a lock\n");
+ return 1;
+ }
+ /* See if some other connection locks the same playlist */
+ for(cc = connections; cc; cc = cc->next)
+ if(cc->locked_playlist && !strcmp(cc->locked_playlist, vec[0]))
+ break;
+ if(cc) {
+ /* TODO: implement config->playlist_lock_timeout */
+ sink_writes(ev_writer_sink(c->w), "550 Already locked\n");
+ return 1;
+ }
+ c->locked_playlist = xstrdup(vec[0]);
+ time(&c->locked_when);
+ sink_writes(ev_writer_sink(c->w), "250 Acquired lock\n");
+ return 1;
+}
+
+static int c_playlist_unlock(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ if(!c->locked_playlist) {
+ sink_writes(ev_writer_sink(c->w), "550 Not holding a lock\n");
+ return 1;
+ }
+ c->locked_playlist = 0;
+ sink_writes(ev_writer_sink(c->w), "250 Released lock\n");
+ return 1;
+}