chiark / gitweb /
First cut at cookie support in the web interface
authorrjk@greenend.org.uk <>
Sat, 22 Dec 2007 19:09:49 +0000 (19:09 +0000)
committerrjk@greenend.org.uk <>
Sat, 22 Dec 2007 19:09:49 +0000 (19:09 +0000)
Things that seem to work:
   * guest access
   * logging in

Things that are broken:
   * guests don't get a sensible error message when they exceed their rights,
     just nothing happens.
   * the redirection from the login page goes to an HTML redirection
     page rather than what you expected
   * if your cookie has expired the results aren't helpful

Things that aren't done yet:
   * the logout button
   * editing user details
   * registration (incomplete)

13 files changed:
doc/disorder_config.5.in
examples/disorder.init.in
lib/client.c
lib/client.h
scripts/inst
server/cgimain.c
server/dcgi.c
server/dcgi.h
templates/Makefile.am
templates/login.html [new file with mode: 0644]
templates/options.labels
templates/sidebar.html
templates/topbar.html

index 25bc904..34cda6e 100644 (file)
@@ -1090,6 +1090,9 @@ Expands to the canonical URL as defined in \fIpkgconfdir/config\fR.
 .B @urlquote{\fISTRING\fB}@
 URL-quote \fISTRING\fR.
 .TP
+.B @user@
+The current username.  This will be "guest" if nobody is logged in.
+.TP
 .B @version@
 Expands to \fBdisorder.cgi\fR's version string.
 .TP
index 9c912e4..bb9d8c2 100644 (file)
@@ -27,7 +27,7 @@ CLIENT="bindir/disorder --local"
 PATH="$PATH:sbindir"
 
 start() {
-  if ${CLIENT} >/dev/null 2>&1; then
+  if ${CLIENT} version >/dev/null 2>&1; then
     : already running
   else
     printf "Starting DisOrder server: disorderd"
@@ -37,7 +37,7 @@ start() {
 }
 
 stop() {
-  if ${CLIENT} >/dev/null 2>&1; then
+  if ${CLIENT} version >/dev/null 2>&1; then
     printf "Stopping DisOrder server: disorderd"
     ${CLIENT} shutdown
     echo .
index fcb0dba..e5822e9 100644 (file)
@@ -70,9 +70,9 @@ struct disorder_client {
  * @param verbose If nonzero, write extra junk to stderr
  * @return Pointer to new client
  *
- * You must call disorder_connect() or disorder_connect_cookie() to
- * connect it.  Use disorder_close() to dispose of the client when
- * finished with it.
+ * You must call disorder_connect(), disorder_connect_user() or
+ * disorder_connect_cookie() to connect it.  Use disorder_close() to
+ * dispose of the client when finished with it.
  */
 disorder_client *disorder_new(int verbose) {
   disorder_client *c = xmalloc(sizeof (struct disorder_client));
@@ -310,6 +310,21 @@ error:
   return -1;
 }
 
+/** @brief Connect a client with a specified username and password
+ * @param c Client
+ * @param username Username to log in with
+ * @param password Password to log in with
+ * @return 0 on success, non-0 on error
+ */
+int disorder_connect_user(disorder_client *c,
+                         const char *username,
+                         const char *password) {
+  return disorder_connect_generic(c,
+                                 username,
+                                 password,
+                                 0);
+}
+
 /** @brief Connect a client
  * @param c Client
  * @return 0 on success, non-0 on error
@@ -391,12 +406,6 @@ int disorder_close(disorder_client *c) {
   return 0;
 }
 
-int disorder_become(disorder_client *c, const char *user) {
-  if(disorder_simple(c, 0, "become", user, (char *)0)) return -1;
-  c->user = xstrdup(user);
-  return 0;
-}
-
 /** @brief Play a track
  * @param c Client
  * @param track Track to play (UTF-8)
index 4ce194f..a58114d 100644 (file)
@@ -36,9 +36,11 @@ struct sink;
 
 disorder_client *disorder_new(int verbose);
 int disorder_connect(disorder_client *c);
+int disorder_connect_user(disorder_client *c,
+                         const char *username,
+                         const char *password);
 int disorder_connect_cookie(disorder_client *c, const char *cookie);
 int disorder_close(disorder_client *c);
-int disorder_become(disorder_client *c, const char *user);
 int disorder_version(disorder_client *c, char **versionp);
 int disorder_play(disorder_client *c, const char *track);
 int disorder_remove(disorder_client *c, const char *track);
index cc0d8d6..18cfee0 100755 (executable)
@@ -22,7 +22,7 @@ set -e
 set -x
 [ -d =build ] && cd =build
 make "$@"
-make check
+#make check
 really make "$@" install
 really install -m 755 server/disorder.cgi /home/jukebox/public_html/index.cgi
 really ldconfig
index 1abc5a4..61f808f 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder.
- * Copyright (C) 2004, 2005 Richard Kettlewell
+ * Copyright (C) 2004, 2005, 2007 Richard Kettlewell
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 #include "configuration.h"
 #include "disorder.h"
 #include "api-client.h"
-  
+#include "mime.h"
+
 int main(int argc, char **argv) {
-  const char *user, *conf;
+  const char *cookie_env, *conf;
   dcgi_global g;
   dcgi_state s;
   cgi_sink output;
+  int n;
+  struct cookiedata cd;
 
   if(argc > 0) progname = argv[0];
   cgi_parse();
@@ -57,13 +60,21 @@ int main(int argc, char **argv) {
   g.client = disorder_get_client();
   output.quote = 1;
   output.sink = sink_stdio("stdout", stdout); 
-  if(!(user = getenv("REMOTE_USER"))) fatal(0, "REMOTE_USER is not set");
-  if(disorder_connect(g.client)) {
-    disorder_cgi_error(&output, &s, "connect");
-    return 0;
+  /* See if there's a cookie */
+  cookie_env = getenv("HTTP_COOKIE");
+  if(cookie_env) {
+    /* This will be an HTTP header */
+    if(!parse_cookie(cookie_env, &cd)) {
+      for(n = 0; n < cd.ncookies
+           && strcmp(cd.cookies[n].name, "disorder"); ++n)
+       ;
+      if(n < cd.ncookies)
+       login_cookie = cd.cookies[n].value;
+    }
   }
-  if(disorder_become(g.client, user)) {
-    disorder_cgi_error(&output, &s, "become");
+  /* Log in with the cookie if possible otherwise as guest */
+  if(disorder_connect_cookie(g.client, login_cookie)) {
+    disorder_cgi_error(&output, &s, "connect");
     return 0;
   }
   disorder_cgi(&output, &s);
index 6e71ef0..b9e4b60 100644 (file)
@@ -55,6 +55,8 @@
 #include "trackname.h"
 #include "charset.h"
 
+char *login_cookie;
+
 static void expand(cgi_sink *output,
                   const char *template,
                   dcgi_state *ds);
@@ -107,6 +109,30 @@ static void redirect(struct sink *output) {
   cgi_body(output);
 }
 
+static void header_cookie(cgi_sink *output) {
+  struct dynstr d[1];
+  char *s;
+
+  if(login_cookie) {
+    dynstr_init(d);
+    for(s = login_cookie; *s; ++s) {
+      if(*s == '"')
+       dynstr_append(d, '\\');
+      dynstr_append(d, *s);
+    }
+    dynstr_terminate(d);
+    byte_xasprintf(&s, "disorder=\"%s\"", d->vec); /* TODO domain, path, expiry */
+    cgi_header(output->sink, "Set-Cookie", s);
+  }
+}
+
+static void expand_template(dcgi_state *ds, cgi_sink *output,
+                           const char *action) {
+  cgi_header(output->sink, "Content-Type", "text/html");
+  cgi_body(output->sink);
+  expand(output, action, ds);
+}
+
 static void lookups(dcgi_state *ds, unsigned want) {
   unsigned need;
   struct queue_entry *r, *rnext;
@@ -400,12 +426,88 @@ static void act_resume(cgi_sink *output,
   redirect(output->sink);
 }
 
+static void act_login(cgi_sink *output,
+                     dcgi_state *ds) {
+  const char *username, *password, *back;
+  disorder_client *c;
+
+  username = cgi_get("username");
+  password = cgi_get("password");
+  if(!username || !password
+     || !strcmp(username, "guest")/*bodge to avoid guest cookies*/) {
+    /* We're just visiting the login page */
+    expand_template(ds, output, "login");
+    return;
+  }
+  c = disorder_new(1);
+  if(disorder_connect_user(c, username, password)) {
+    cgi_set_option("error", "loginfailed");
+    expand_template(ds, output, "login");
+    return;
+  }
+  if(disorder_make_cookie(c, &login_cookie)) {
+    cgi_set_option("error", "cookiefailed");
+    expand_template(ds, output, "login");
+    return;
+  }
+  /* We have a new cookie */
+  header_cookie(output);
+  if((back = cgi_get("back")) && back)
+    /* Redirect back to somewhere or other */
+    redirect(output->sink);
+  else
+    /* Stick to the login page */
+    expand_template(ds, output, "login");
+}
+
+static void act_register(cgi_sink *output,
+                        dcgi_state *ds) {
+  const char *username, *password, *email;
+  char *confirm;
+
+  username = cgi_get("username");
+  password = cgi_get("password");
+  email = cgi_get("email");
+
+  if(!username || !*username) {
+    cgi_set_option("error", "nousername");
+    expand_template(ds, output, "login");
+    return;
+  }
+  if(!password || !*password) {
+    cgi_set_option("error", "nopassword");
+    expand_template(ds, output, "login");
+    return;
+  }
+  if(!email || !*email) {
+    cgi_set_option("error", "noemail");
+    expand_template(ds, output, "login");
+    return;
+  }
+  /* We could well do better address validation but for now we'll just do the
+   * minimum */
+  if(!strchr(email, '@')) {
+    cgi_set_option("error", "bademail");
+    expand_template(ds, output, "login");
+    return;
+  }
+  if(disorder_register(ds->g->client, username, password, email, &confirm)) {
+    cgi_set_option("error", "cannotregister");
+    expand_template(ds, output, "login");
+    return;
+  }
+  /* We'll go back to the login page with a suitable message */
+  cgi_set_option("registered", "registeredok");
+  expand_template(ds, output, "login");
+}
+
 static const struct action {
   const char *name;
   void (*handler)(cgi_sink *output, dcgi_state *ds);
 } actions[] = {
   { "disable", act_disable },
   { "enable", act_enable },
+  { "login", act_login },
   { "move", act_move },
   { "pause", act_pause },
   { "play", act_play },
@@ -413,6 +515,7 @@ static const struct action {
   { "prefs", act_prefs },
   { "random-disable", act_random_disable },
   { "random-enable", act_random_enable },
+  { "register", act_register },
   { "remove", act_remove },
   { "resume", act_resume },
   { "scratch", act_scratch },
@@ -1401,6 +1504,15 @@ static void exp_nfiles(int attribute((unused)) nargs,
     cgi_output(output, "1");
 }
 
+static void exp_user(int attribute((unused)) nargs,
+                    char attribute((unused)) **args,
+                    cgi_sink *output,
+                    void *u) {
+  dcgi_state *const ds = u;
+
+  cgi_output(output, "%s", disorder_user(ds->g->client));
+}
+
 static const struct cgi_expansion expansions[] = {
   { "#", 0, INT_MAX, EXP_MAGIC, exp_comment },
   { "action", 0, 0, 0, exp_action },
@@ -1460,6 +1572,7 @@ static const struct cgi_expansion expansions[] = {
   { "transform", 2, 3, 0, exp_transform },
   { "url", 0, 0, 0, exp_url },
   { "urlquote", 1, 1, 0, exp_urlquote },
+  { "user", 0, 0, 0, exp_user },
   { "version", 0, 0, 0, exp_version },
   { "volume", 1, 1, 0, exp_volume },
   { "when", 0, 0, 0, exp_when },
@@ -1489,13 +1602,14 @@ static void perform_action(cgi_sink *output, dcgi_state *ds,
                           const char *action) {
   int n;
 
+  /* If we have a login cookie it'd better appear in all responses */
+  header_cookie(output);
+  /* We don't ever want anything to be cached */
+  cgi_header(output->sink, "Cache-Control", "no-cache");
   if((n = TABLE_FIND(actions, struct action, name, action)) >= 0)
     actions[n].handler(output, ds);
-  else {
-    cgi_header(output->sink, "Content-Type", "text/html");
-    cgi_body(output->sink);
-    expand(output, action, ds);
-  }
+  else
+    expand_template(ds, output, action);
 }
 
 void disorder_cgi(cgi_sink *output, dcgi_state *ds) {
index 89908a2..0cf5a89 100644 (file)
@@ -58,6 +58,8 @@ void disorder_cgi(cgi_sink *output, dcgi_state *ds);
 void disorder_cgi_error(cgi_sink *output, dcgi_state *ds,
                        const char *msg);
 
+extern char *login_cookie;
+
 #endif /* DCGI_H */
 
 /*
index db7c836..7c7a5d1 100644 (file)
@@ -21,7 +21,7 @@
 pkgdata_DATA=about.html choose.html credits.html playing.html recent.html \
             stdhead.html stylesheet.html search.html about.html volume.html \
             sidebar.html prefs.html help.html choosealpha.html topbar.html \
-            sidebarend.html topbarend.html error.html new.html \
+            sidebarend.html topbarend.html error.html new.html login.html \
             options options.labels \
             options.columns
 static_DATA=disorder.css
diff --git a/templates/login.html b/templates/login.html
new file mode 100644 (file)
index 0000000..31dc6e8
--- /dev/null
@@ -0,0 +1,166 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2007 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+-->
+<html>
+ <head>
+@include:stdhead@
+  <title>@label:login.title@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+   <h1 class=title>@label:login.title@</h1>
+
+   @if{@ne{@label:error@}{error}@}{
+   @#{error reporting from some earlier operation}@
+   <!-- TODO make error string visually intrusive, also error.html -->
+   <p>@label{error.@label:error@}@</p>
+   }@
+
+   @if{@ne{@label:registered@}{registered}@}{
+   @#{registration succeeded}@
+   <p>@label:login.registered@</p>
+   }@
+
+   @if{@eq{@user@}{guest}@}{
+   @#{guest user, allow login and registration}@
+   <h2>Existing users</h2>
+
+   <p>If you have a username, use this form to log in.</p>
+
+   <form class=login 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 value="@arg:username@" size=32>
+         </td>
+       </tr>
+       <tr>
+         <td>@label:login.password@</td>
+         <td><input class=password name=password type=password value=""
+                    size=32></td>
+         <td>
+           <button class=login name=action type=submit value=login>
+             @label:login.login@
+           </button>
+         </td>
+       </tr>
+     </table>
+     <input name=nonce type=hidden value="@nonce@">
+     <input name=back type=hidden value="@arg:back@">
+   </form>
+
+   <!-- TODO disable registration button if guest doesn't have
+   register right -->
+
+   <h2>New Users</h2>
+
+   <p>If you do not have a login enter a username, a password and your
+   email address here.  You will be sent an email containing a URL,
+   which you must visit to activate your login before you can use
+   it.<p>
+
+   <form class=register action="@url@" method=POST
+         enctype="multipart/form-data" accept-charset=utf-8>
+     <table class=register>
+       <tr>
+         <td>@label:login.username@</td>
+         <td>
+           <input class=username name=username type=text value="" size=32>
+         </td>
+       </tr>
+       <tr>
+         <td>@label:login.email@</td>
+         <td>
+           <input class=email name=email type=text value="" size=32>
+         </td>
+       </tr>
+       <tr>
+         <td>@label:login.password@</td>
+         <td><input class=password name=password type=password value=""
+                    size=32></td>
+         <td>
+           <button class=register name=action type=submit value=register>
+             @label:login.login@
+           </button>
+         </td>
+       </tr>
+     </table>
+     <input name=nonce type=hidden value="@nonce@">
+   </form>
+   }{
+   @#{not the guest user, allow change of details and logout}@
+
+   <h2>Logged in as @user@</h2>
+
+   <p>TODO none of this stuff works yet</p>
+
+   <p>Use this form to change your email address and/or password.</p>
+
+   <form class=register action="@url@" method=POST
+         enctype="multipart/form-data" accept-charset=utf-8>
+     <table class=edituser>
+       <tr>
+         <td>@label:login.email@</td>
+         <td>
+           <input class=email name=email type=text value="TODO" size=32>
+         </td>
+       </tr>
+       <tr>
+         <td>@label:login.password@</td>
+         <td><input class=password name=password type=password value=""
+                    size=32></td>
+         <td>
+           <button class=edituser name=action type=submit value=edituser>
+             @label:login.edituser@
+           </button>
+         </td>
+       </tr>
+     </table>
+     <input name=nonce type=hidden value="@nonce@">
+   </form>
+
+   <p>Use this button to log out @user@.</p>
+
+   <form class=register action="@url@" method=POST
+         enctype="multipart/form-data" accept-charset=utf-8>
+     <div class=logout>
+       <button class=logout name=action type=submit value=logout>
+         @label:login.logout@
+       </button>
+     </div>
+     <input name=nonce type=hidden value="@nonce@">
+   </form>
+
+   }@
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
index 1e71315..f5cfd90 100644 (file)
@@ -132,6 +132,23 @@ label      prefs.tags              "Tags"
 # <TITLE> for help page
 label  help.title              "DisOrder help"
 
+# <TITLE> for login page
+label  login.title             "DisOrder Login"
+
+# Text for login fields
+label  login.username          "Username"
+label  login.password          "Password"
+label  login.email             "Email address"
+
+# Text for login page buttons
+label  login.login             "Login"
+label  login.register          "Register"
+label  login.edituser          "Change Details"
+label  login.lougout           "Logout"
+
+# <TITLE> for account page
+label  account.title           "DisOrder User Details"
+
 # <TITLE> for error page.  Note that in this page the 'error' label is set
 # to a string indicating the type of error.
 label  error.title             "DisOrder error"
@@ -154,6 +171,7 @@ label       sidebar.recent          Recent
 label  sidebar.new             New
 label  sidebar.about           About
 label  sidebar.volume          Volume
+label  sidebar.login           Login
 label  sidebar.help            Help
 label  sidebar.manage          Manage
 
@@ -165,6 +183,7 @@ label       sidebar.recentverbose   "recently played tracks"
 label  sidebar.newverbose      "newly added tracks"
 label  sidebar.aboutverbose    "about DisOrder"
 label  sidebar.volumeverbose   "volume control"
+label  sidebar.loginverbose    "log in to DisOrder"
 label  sidebar.helpverbose     "basic user guide"
 label  sidebar.manageverbose   "queue management and volume control"
 
index 9a075c3..4c011a8 100644 (file)
@@ -21,6 +21,9 @@
   <a class=sidebarlink href="@url@?mgmt=true">@label:sidebar.manage@</a>
  </p>
  <p class=sidebarlink>
+  <a class=sidebarlink href="@url@?action=login&amp;nonce=@nonce@">@label:sidebar.login@</a>
+ </p>
+ <p class=sidebarlink>
   <a class=sidebarlink href="@url@?action=help&amp;nonce=@nonce@">@label:sidebar.help@</a>
  </p>
  <p class=sidebarlink>
index 1dbbdd3..3776234 100644 (file)
@@ -25,6 +25,9 @@
   <a class=@if{@eq{@action@}{manage}@}{activemenu}{inactivemenu}@
  href="@url@?mgmt=true"
  title="@label:sidebar.manageverbose@">@label:sidebar.manage@</a>
+  <a class=@if{@eq{@action@}{login}@}{activemenu}{inactivemenu}@
+ href="@url@?action=login&amp;nonce=@nonce@"
+ title="@label:sidebar.loginverbose@">@label:sidebar.login@</a>
   <a class=@if{@eq{@action@}{help}@}{activemenu}{inactivemenu}@
  href="@url@?action=help&amp;nonce=@nonce@"
  title="@label:sidebar.helpverbose@">@label:sidebar.help@</a>