From 6207d2f3bcf38c072c2bcaa7c9e8dbd469b5e8e6 Mon Sep 17 00:00:00 2001 Message-Id: <6207d2f3bcf38c072c2bcaa7c9e8dbd469b5e8e6.1715468771.git.mdw@distorted.org.uk> From: Mark Wooding Date: Sun, 6 Jan 2008 12:14:09 +0000 Subject: [PATCH] The Login page now includes a form to request a password reminder 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. Organization: Straylight/Edgeware From: rjk@greenend.org.uk <> There is also a new configuration command reminder_interval, used to limit the rate at which reminders can be sent to any one user. --- doc/disorder_config.5.in | 4 ++ doc/disorder_protocol.5.in | 5 ++ lib/client.c | 9 ++++ lib/client.h | 1 + lib/configuration.c | 2 + lib/configuration.h | 3 ++ lib/sendmail.c | 31 ++++++++++++ lib/sendmail.h | 7 +++ server/dcgi.c | 18 +++++++ server/server.c | 97 +++++++++++++++++++++++++++++++++++++- templates/disorder.css | 5 ++ templates/login.html | 33 +++++++++++-- templates/options.labels | 3 ++ templates/topbar.html | 1 + 14 files changed, 215 insertions(+), 4 deletions(-) diff --git a/doc/disorder_config.5.in b/doc/disorder_config.5.in index b545b69..bf7876c 100644 --- a/doc/disorder_config.5.in +++ b/doc/disorder_config.5.in @@ -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 +.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: diff --git a/doc/disorder_protocol.5.in b/doc/disorder_protocol.5.in index da379c0..56fd094 100644 --- a/doc/disorder_protocol.5.in +++ b/doc/disorder_protocol.5.in @@ -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 +.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 diff --git a/lib/client.c b/lib/client.c index aabea87..08d325a 100644 --- a/lib/client.c +++ b/lib/client.c @@ -1166,6 +1166,15 @@ int disorder_revoke(disorder_client *c) { 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 diff --git a/lib/client.h b/lib/client.h index 08de864..4a6eac7 100644 --- a/lib/client.h +++ b/lib/client.h @@ -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_reminder(disorder_client *c, const char *user); #endif /* CLIENT_H */ diff --git a/lib/configuration.c b/lib/configuration.c index ba50f3c..869f895 100644 --- a/lib/configuration.c +++ b/lib/configuration.c @@ -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(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 }, @@ -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->reminder_interval = 600; /* 10m */ /* Default stopwords */ if(config_set(&cs, (int)NDEFAULT_STOPWORDS, (char **)default_stopwords)) exit(1); diff --git a/lib/configuration.h b/lib/configuration.h index c5cf198..9388568 100644 --- a/lib/configuration.h +++ b/lib/configuration.h @@ -264,6 +264,9 @@ struct config { /** @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 */ diff --git a/lib/sendmail.c b/lib/sendmail.c index 8ca1e88..b2f606c 100644 --- a/lib/sendmail.c +++ b/lib/sendmail.c @@ -246,6 +246,37 @@ int sendmail(const char *sender, 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 diff --git a/lib/sendmail.h b/lib/sendmail.h index 7849ef6..d54def8 100644 --- a/lib/sendmail.h +++ b/lib/sendmail.h @@ -28,6 +28,13 @@ int sendmail(const char *sender, 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 */ diff --git a/server/dcgi.c b/server/dcgi.c index 29bac5d..81b6943 100644 --- a/server/dcgi.c +++ b/server/dcgi.c @@ -645,6 +645,23 @@ static void act_edituser(cgi_sink *output, 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; @@ -664,6 +681,7 @@ static const struct action { { "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 }, diff --git a/server/server.c b/server/server.c index ea8c0ed..5e8f220 100644 --- a/server/server.c +++ b/server/server.c @@ -67,6 +67,10 @@ #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 @@ -1270,7 +1274,97 @@ static int c_confirm(struct conn *c, } 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; @@ -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 }, + { "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 }, diff --git a/templates/disorder.css b/templates/disorder.css index 4a6bb59..84b306b 100644 --- a/templates/disorder.css +++ b/templates/disorder.css @@ -532,6 +532,11 @@ form.login { 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 */ diff --git a/templates/login.html b/templates/login.html index 21c7ddf..11474e2 100644 --- a/templates/login.html +++ b/templates/login.html @@ -65,7 +65,7 @@ USA - + @@ -77,6 +77,33 @@ USA +

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.

+ +
+ + + + + + + + + + + +
+ @right{register}{

New Users

@@ -121,7 +148,7 @@ USA @label:login.registerpassword2extra@ - + @@ -177,7 +204,7 @@ USA @label:login.edituserpassword2extra@ - + diff --git a/templates/options.labels b/templates/options.labels index 289a0cf..82de2d9 100644 --- a/templates/options.labels +++ b/templates/options.labels @@ -160,6 +160,7 @@ label login.login "Login" 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." @@ -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.reminded "You have been sent a reminder email." # 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.reminderfailed "Cannot send a reminder." # Text appended to all error pages label error.generic "" diff --git a/templates/topbar.html b/templates/topbar.html index a1e89ee..5c6c515 100644 --- a/templates/topbar.html +++ b/templates/topbar.html @@ -34,6 +34,7 @@ <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&nonce=@nonce@" title="@label:sidebar.loginverbose@">@label:sidebar.login@</a> -- [mdw]