The Login page now includes a form to request a password reminder
authorrjk@greenend.org.uk <>
Sun, 6 Jan 2008 12:14:09 +0000 (12:14 +0000)
committerrjk@greenend.org.uk <>
Sun, 6 Jan 2008 12:14:09 +0000 (12:14 +0000)
email.  This implies a new server command 'reminder' to do the
sending, which in turn takes advantage of a new sendmail_subprocess()
to send the mail without wedging the server.

There is also a new configuration command reminder_interval, used to
limit the rate at which reminders can be sent to any one user.

14 files changed:
doc/disorder_config.5.in
doc/disorder_protocol.5.in
lib/client.c
lib/client.h
lib/configuration.c
lib/configuration.h
lib/sendmail.c
lib/sendmail.h
server/dcgi.c
server/server.c
templates/disorder.css
templates/login.html
templates/options.labels
templates/topbar.html

index b545b69..bf7876c 100644 (file)
@@ -537,6 +537,10 @@ to 3600, i.e. one hour.
 The target size of the queue.  If random play is enabled then randomly picked
 tracks will be added until the queue is at least this big.  The default is 10.
 .TP
 The target size of the queue.  If random play is enabled then randomly picked
 tracks will be added until the queue is at least this big.  The default is 10.
 .TP
+.B reminder_interval \fISECONDS\fR
+The minimum number of seconds that must elapse between password reminders.  The
+default is 600, i.e. 10 minutes.
+.TP
 .B sample_format \fIBITS\fB/\fIRATE\fB/\fICHANNELS
 Describes the sample format expected by the \fBspeaker_command\fR (below).  The
 components of the format specification are as follows:
 .B sample_format \fIBITS\fB/\fIRATE\fB/\fICHANNELS
 Describes the sample format expected by the \fBspeaker_command\fR (below).  The
 components of the format specification are as follows:
index da379c0..56fd094 100644 (file)
@@ -223,6 +223,11 @@ Register a new user.  Requires the \fBregister\fR right.  The result contains a
 confirmation string; the user will be be able to log in until this has been
 presented back to the server via the \fBconfirm\fR command.
 .TP
 confirmation string; the user will be be able to log in until this has been
 presented back to the server via the \fBconfirm\fR command.
 .TP
+.B reminder \fIUSER\fR
+Send a password reminder to \fIUSER\fR.  If the user has no valid email
+address, or no password, or a reminder has been sent too recently, then no
+reminder will be sent.
+.TP
 .B remove \fIID\fR
 Remove the track identified by \fIID\fR.  Requires one of the \fBremove
 mine\fR, \fBremove random\fR or \fBremove any\fR rights depending on how the
 .B remove \fIID\fR
 Remove the track identified by \fIID\fR.  Requires one of the \fBremove
 mine\fR, \fBremove random\fR or \fBremove any\fR rights depending on how the
index aabea87..08d325a 100644 (file)
@@ -1166,6 +1166,15 @@ int disorder_revoke(disorder_client *c) {
   return disorder_simple(c, 0, "revoke", (char *)0);
 }
 
   return disorder_simple(c, 0, "revoke", (char *)0);
 }
 
+/** @brief Request a password reminder email
+ * @param c Client
+ * @param user Username
+ * @return 0 on success, non-0 on error
+ */
+int disorder_reminder(disorder_client *c, const char *user) {
+  return disorder_simple(c, 0, "reminder", user, (char *)0);
+}
+
 /*
 Local Variables:
 c-basic-offset:2
 /*
 Local Variables:
 c-basic-offset:2
index 08de864..4a6eac7 100644 (file)
@@ -115,6 +115,7 @@ int disorder_confirm(disorder_client *c, const char *confirm);
 int disorder_make_cookie(disorder_client *c, char **cookiep);
 const char *disorder_last(disorder_client *c);
 int disorder_revoke(disorder_client *c);
 int disorder_make_cookie(disorder_client *c, char **cookiep);
 const char *disorder_last(disorder_client *c);
 int disorder_revoke(disorder_client *c);
+int disorder_reminder(disorder_client *c, const char *user);
 
 #endif /* CLIENT_H */
 
 
 #endif /* CLIENT_H */
 
index ba50f3c..869f895 100644 (file)
@@ -958,6 +958,7 @@ static const struct conf conf[] = {
   { C(prefsync),         &type_integer,          validate_positive },
   { C(queue_pad),        &type_integer,          validate_positive },
   { C(refresh),          &type_integer,          validate_positive },
   { C(prefsync),         &type_integer,          validate_positive },
   { C(queue_pad),        &type_integer,          validate_positive },
   { C(refresh),          &type_integer,          validate_positive },
+  { C(reminder_interval), &type_integer,         validate_positive },
   { C2(restrict, restrictions),         &type_restrict,         validate_any },
   { C(sample_format),    &type_sample_format,    validate_sample_format },
   { C(scratch),          &type_string_accum,     validate_isreg },
   { C2(restrict, restrictions),         &type_restrict,         validate_any },
   { C(sample_format),    &type_sample_format,    validate_sample_format },
   { C(scratch),          &type_string_accum,     validate_isreg },
@@ -1190,6 +1191,7 @@ static struct config *config_default(void) {
   c->cookie_key_lifetime = 86400 * 7;
   c->smtp_server = xstrdup("127.0.0.1");
   c->new_max = 100;
   c->cookie_key_lifetime = 86400 * 7;
   c->smtp_server = xstrdup("127.0.0.1");
   c->new_max = 100;
+  c->reminder_interval = 600;          /* 10m */
   /* Default stopwords */
   if(config_set(&cs, (int)NDEFAULT_STOPWORDS, (char **)default_stopwords))
     exit(1);
   /* Default stopwords */
   if(config_set(&cs, (int)NDEFAULT_STOPWORDS, (char **)default_stopwords))
     exit(1);
index c5cf198..9388568 100644 (file)
@@ -264,6 +264,9 @@ struct config {
 
   /** @brief Maximum number of tracks in response to 'new' */
   long new_max;
 
   /** @brief Maximum number of tracks in response to 'new' */
   long new_max;
+
+  /** @brief Minimum interval between password reminder emails */
+  long reminder_interval;
   
   /* derived values: */
   int nparts;                          /* number of distinct name parts */
   
   /* derived values: */
   int nparts;                          /* number of distinct name parts */
index 8ca1e88..b2f606c 100644 (file)
@@ -246,6 +246,37 @@ int sendmail(const char *sender,
   return rc;
 }
 
   return rc;
 }
 
+/** @brief Start a subproces to send a mail message
+ * @param sender Sender address (can be "")
+ * @param pubsender Visible sender address (must not be "")
+ * @param recipient Recipient address
+ * @param subject Subject string
+ * @param encoding Body encoding
+ * @param content_type Content-type of body
+ * @param body Text of body (encoded, but \n for newline)
+ * @return Subprocess PID on success, -1 on error
+ */
+pid_t sendmail_subprocess(const char *sender,
+                          const char *pubsender,
+                          const char *recipient,
+                          const char *subject,
+                          const char *encoding,
+                          const char *content_type,
+                          const char *body) {
+  pid_t pid;
+
+  if(!(pid = fork())) {
+    exitfn = _exit;
+    if(sendmail(sender, pubsender, recipient, subject,
+                encoding, content_type, body))
+      _exit(1);
+    _exit(0);
+  }
+  if(pid < 0)
+    error(errno, "error calling fork");
+  return pid;
+}
+
 /*
 Local Variables:
 c-basic-offset:2
 /*
 Local Variables:
 c-basic-offset:2
index 7849ef6..d54def8 100644 (file)
@@ -28,6 +28,13 @@ int sendmail(const char *sender,
             const char *encoding,
             const char *content_type,
             const char *body);
             const char *encoding,
             const char *content_type,
             const char *body);
+pid_t sendmail_subprocess(const char *sender,
+                          const char *pubsender,
+                          const char *recipient,
+                          const char *subject,
+                          const char *encoding,
+                          const char *content_type,
+                          const char *body);
 
 #endif /* SENDMAIL_H */
 
 
 #endif /* SENDMAIL_H */
 
index 29bac5d..81b6943 100644 (file)
@@ -645,6 +645,23 @@ static void act_edituser(cgi_sink *output,
   expand_template(ds, output, "login");  
 }
 
   expand_template(ds, output, "login");  
 }
 
+static void act_reminder(cgi_sink *output,
+                        dcgi_state *ds) {
+  const char *const username = cgi_get("username");
+
+  if(!username || !*username) {
+    cgi_set_option("error", "nousername");
+    expand_template(ds, output, "login");
+    return;
+  }
+  if(disorder_reminder(ds->g->client, username)) {
+    cgi_set_option("error", "reminderfailed");
+    expand_template(ds, output, "login");
+    return;
+  }
+  cgi_set_option("status", "reminded");
+  expand_template(ds, output, "login");  
+}
 
 static const struct action {
   const char *name;
 
 static const struct action {
   const char *name;
@@ -664,6 +681,7 @@ static const struct action {
   { "random-disable", act_random_disable },
   { "random-enable", act_random_enable },
   { "register", act_register },
   { "random-disable", act_random_disable },
   { "random-enable", act_random_enable },
   { "register", act_register },
+  { "reminder", act_reminder },
   { "remove", act_remove },
   { "resume", act_resume },
   { "scratch", act_scratch },
   { "remove", act_remove },
   { "resume", act_resume },
   { "scratch", act_scratch },
index ea8c0ed..5e8f220 100644 (file)
 #include "unicode.h"
 #include "cookies.h"
 #include "base64.h"
 #include "unicode.h"
 #include "cookies.h"
 #include "base64.h"
+#include "hash.h"
+#include "mime.h"
+#include "sendmail.h"
+#include "wstat.h"
 
 #ifndef NONCE_SIZE
 # define NONCE_SIZE 16
 
 #ifndef NONCE_SIZE
 # define NONCE_SIZE 16
@@ -1270,7 +1274,97 @@ static int c_confirm(struct conn *c,
   }
   return 1;
 }
   }
   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 {
+    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) {
+    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]))) {
+    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"))
+     || !strchr(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;
+  }
+  if(!(password = kvp_get(k, "password"))
+     || !*password) {
+    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]);
+  time(&now);
+  if(last && now < *last + config->reminder_interval) {
+    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)))
+    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);
+  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 const struct command {
   /** @brief Command name */
   const char *name;
 static const struct command {
   /** @brief Command name */
   const char *name;
@@ -1324,6 +1418,7 @@ static const struct command {
   { "recent",         0, 0,       c_recent,         RIGHT_READ },
   { "reconfigure",    0, 0,       c_reconfigure,    RIGHT_ADMIN },
   { "register",       3, 3,       c_register,       RIGHT_REGISTER|RIGHT__LOCAL },
   { "recent",         0, 0,       c_recent,         RIGHT_READ },
   { "reconfigure",    0, 0,       c_reconfigure,    RIGHT_ADMIN },
   { "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 },
   { "resolve",        1, 1,       c_resolve,        RIGHT_READ },
   { "remove",         1, 1,       c_remove,         RIGHT_REMOVE__MASK },
   { "rescan",         0, 0,       c_rescan,         RIGHT_RESCAN },
   { "resolve",        1, 1,       c_resolve,        RIGHT_READ },
index 4a6bb59..84b306b 100644 (file)
@@ -532,6 +532,11 @@ form.login {
   background-color: #e0ffe0    /* pastel green */
 }
 
   background-color: #e0ffe0    /* pastel green */
 }
 
+form.reminder {
+  border: 1px solid black;
+  background-color: #e0e0ff    /* pastel blue */
+}
+
 form.register {
   border: 1px solid black;
   background-color: #e0e0ff    /* pastel blue */
 form.register {
   border: 1px solid black;
   background-color: #e0e0ff    /* pastel blue */
index 21c7ddf..11474e2 100644 (file)
@@ -65,7 +65,7 @@ USA
          </td>
        </tr>
        <tr>
          </td>
        </tr>
        <tr>
-         <td>
+         <td colspan=2>
            <button class=login name=button type=submit>
              @label:login.login@
            </button>
            <button class=login name=button type=submit>
              @label:login.login@
            </button>
@@ -77,6 +77,33 @@ USA
      <input name=back type=hidden value="@arg:back@">
    </form>
 
      <input name=back type=hidden value="@arg:back@">
    </form>
 
+   <p>If you've forgotten your password, use this form to request an
+   email reminder.  A reminder can only be sent if you registered with
+   your email address, and if a reminder has been sent too recently
+   then it won't be possible to send one.</p>
+
+   <form class=reminder action="@url@" method=POST
+         enctype="multipart/form-data" accept-charset=utf-8>
+     <table class=login>
+       <tr>
+         <td>@label:login.username@</td>
+         <td>
+           <input class=username name=username type=text size=32
+                 value="@arg:username@">
+         </td>
+       </tr>
+       <tr>
+         <td colspan=2>
+           <button class=login name=button type=submit>
+             @label:login.reminder@
+           </button>
+         </td>
+       </tr>
+     </table>
+     <input name=action type=hidden value=reminder>
+     <input name=nonce type=hidden value="@nonce@">
+   </form>
+
    @right{register}{
    <h2>New Users</h2>
 
    @right{register}{
    <h2>New Users</h2>
 
@@ -121,7 +148,7 @@ USA
          <td class=extra>@label:login.registerpassword2extra@</td>
        </tr>
        <tr>
          <td class=extra>@label:login.registerpassword2extra@</td>
        </tr>
        <tr>
-         <td>
+         <td colspan=3>
            <button class=register name=button>
              @label:login.register@
            </button>
            <button class=register name=button>
              @label:login.register@
            </button>
@@ -177,7 +204,7 @@ USA
          <td class=extra>@label:login.edituserpassword2extra@</td>
        </tr>
        <tr>
          <td class=extra>@label:login.edituserpassword2extra@</td>
        </tr>
        <tr>
-         <td>
+         <td colspan=3>
            <button class=edituser name=submit type=submit>
              @label:login.edituser@
            </button>
            <button class=edituser name=submit type=submit>
              @label:login.edituser@
            </button>
index 289a0cf..82de2d9 100644 (file)
@@ -160,6 +160,7 @@ label       login.login             "Login"
 label  login.register          "Register"
 label  login.edituser          "Change Details"
 label  login.logout            "Logout"
 label  login.register          "Register"
 label  login.edituser          "Change Details"
 label  login.logout            "Logout"
+label  login.reminder          "Send reminder"
 
 # Text for login page responses
 label  login.loginok           "You are now logged in."
 
 # Text for login page responses
 label  login.loginok           "You are now logged in."
@@ -167,6 +168,7 @@ label       login.logoutok          "You are now logged out."
 label  login.registered        "Your new login has been registered.  Please check your email."
 label  login.confirmed         "Your new login has been confirmed.  You are now logged in."
 label  login.edited            "Your details have been changed."
 label  login.registered        "Your new login has been registered.  Please check your email."
 label  login.confirmed         "Your new login has been confirmed.  You are now logged in."
 label  login.edited            "Your details have been changed."
+label  login.reminded          "You have been sent a reminder email."
 
 # <TITLE> for account page
 label  account.title           "DisOrder User Details"
 
 # <TITLE> for account page
 label  account.title           "DisOrder User Details"
@@ -190,6 +192,7 @@ label       error.cannotregister    "Unable to register user."
 label  error.noconfirm         "Missing confirmation string."
 label  error.badconfirm        "Invalid confirmation string."
 label  error.badedit           "Cannot edit user details."
 label  error.noconfirm         "Missing confirmation string."
 label  error.badconfirm        "Invalid confirmation string."
 label  error.badedit           "Cannot edit user details."
+label  error.reminderfailed    "Cannot send a reminder."
 
 # Text appended to all error pages
 label  error.generic           ""
 
 # Text appended to all error pages
 label  error.generic           ""
index a1e89ee..5c6c515 100644 (file)
@@ -34,6 +34,7 @@
   <a class=@if{@or{@eq{@action@}{login}@}
                   {@eq{@action@}{logout}@}
                   {@eq{@action@}{register}@}
   <a class=@if{@or{@eq{@action@}{login}@}
                   {@eq{@action@}{logout}@}
                   {@eq{@action@}{register}@}
+                  {@eq{@action@}{reminder}@}
                   {@eq{@action@}{edituser}@}@}{activemenu}{inactivemenu}@
  href="@url@?action=login&amp;nonce=@nonce@"
  title="@label:sidebar.loginverbose@">@label:sidebar.login@</a>
                   {@eq{@action@}{edituser}@}@}{activemenu}{inactivemenu}@
  href="@url@?action=login&amp;nonce=@nonce@"
  title="@label:sidebar.loginverbose@">@label:sidebar.login@</a>