chiark / gitweb /
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 b545b69c1bdf68b1dc2915c24a3d9e0007603a6e..bf7876ccfa172bfebb4902dbafff86f6d8df27e3 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 da379c0f5588e8dab29ecfd247135e25cb9d28a7..56fd0947a383a3099ea27af7559c52e6bbd00e49 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 aabea873291cbc6fe6c2f0aa6ef8e3fc839d7f0f..08d325a669276bbbc32478a669ab0205107b4f5d 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 08de864f952a829859f4293a64cb542a2e3a48e6..4a6eac74f1bb99a9b18f70a76cc933da8665c293 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 ba50f3cbaa2a3414e2242b1086420e06e478b960..869f8957c79f49ff731c303f4dc866ee81385bc2 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 c5cf198f46957cb984ff902752230aa589bec766..93885689f8cfee7270f4a574fde985634edb5e65 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 8ca1e880309fa86409740936e1579b3df8e29618..b2f606c5e6138ba70e97cd5675b700de61f4d7a6 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 7849ef64afd23670327ac32bd2896d97d7b9ded0..d54def8678f5a8018cd16128118dd3f6253b4f54 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 29bac5d4910d49694fd12d97d2e973601f10a08d..81b6943aecbac6aeb7f40cd5277ec9234678609d 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 ea8c0edb12e51c62d51e3a7a701de5398ca6c70a..5e8f2208baffd6cddba8d72034fc81c2afdb1577 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 4a6bb592d2853753c282cc54e5186112de71f86b..84b306b09cc6b23205b926ed9bc7dc694f0ea5ae 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 21c7ddf05e947970591fb087258495005af06354..11474e286e9d9bdc69aad5003992b6403cce856c 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 289a0cf8e6321bf87fd8478d8b8905642123dbb9..82de2d934fb1db162c9bbcc06e58e856cd26854d 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 a1e89eeef76bfe5176dfe1a322ddecae06856a86..5c6c51513a4086b239ec05a65f4b51d3930e44ae 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>