version one
authorJann Horn <jannhorn@googlemail.com>
Mon, 12 Nov 2012 16:02:52 +0000 (17:02 +0100)
committerJann Horn <jannhorn@googlemail.com>
Mon, 12 Nov 2012 16:02:52 +0000 (17:02 +0100)
cgistuff.c [new file with mode: 0644]
checkstatus.c [new file with mode: 0644]
compile [new file with mode: 0755]
cwebfiles.h [new file with mode: 0644]
hex.c [new file with mode: 0644]
listdir.c [new file with mode: 0644]
login.c [new file with mode: 0644]
octalmodes [new file with mode: 0644]
octalmodes_to_js [new file with mode: 0644]
static/index.html [new file with mode: 0644]

diff --git a/cgistuff.c b/cgistuff.c
new file mode 100644 (file)
index 0000000..ac816e2
--- /dev/null
@@ -0,0 +1,121 @@
+#define _GNU_SOURCE
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <time.h>
+#include <stdio.h>
+#include "cwebfiles.h"
+#include <stdbool.h>
+#include <string.h>
+#include <stdlib.h>
+#include <pwd.h>
+#include <errno.h>
+
+extern char **environ;
+
+// don't use before you've authenticated successfully!
+struct cgi_serialized_session_status session;
+
+// when we call this function, we usually leak memory - not an issue
+static char *session_file_path(char *cookie) {
+  char *file = malloc(strlen(SESSION_FOLDER)+COOKIE_LENGTH+1);
+  memcpy(file, SESSION_FOLDER, strlen(SESSION_FOLDER));
+  memcpy(file+strlen(SESSION_FOLDER), cookie, COOKIE_LENGTH);
+  file[strlen(SESSION_FOLDER)+COOKIE_LENGTH] = '\0';
+  return file;
+}
+
+int authenticate_by_cookie(char *cookie) {
+  // check cookie string for conformity
+  if (strlen(cookie) != COOKIE_LENGTH) return 1;
+  if (!checkhex(cookie, COOKIE_LENGTH)) return 2;
+  
+  // read session data
+  int fd = open(session_file_path(cookie), O_RDONLY);
+  
+  // verify that the session exists
+  if (fd == -1) return 3;
+  int res = read(fd, &session, sizeof(session));
+  close(fd);
+  
+  // verify that the session file is valid
+  if (res != sizeof(session)) return 4;
+  session.user_name[32] = 0;
+  
+  // verify that the session is still active
+  time_t cur_time = time(NULL);
+  if (session.start_time + SESSION_LENGTH < cur_time) return 5;
+  if (session.start_time > cur_time) return 6;
+  
+  // verify that the username and the userid still match
+  struct passwd *user_data = getpwuid(session.uid);
+  if (user_data == NULL) return 7;
+  if (strcmp(user_data->pw_name, session.user_name) != 0) return 8;
+  
+  // everything seems fine! :)
+  return 0;
+}
+
+void persist_session(char *cookie) {
+  char *file = session_file_path(cookie);
+  int fd = open(file, O_RDWR|O_EXCL|O_CREAT, S_IRUSR|S_IWUSR);
+  if (fd == -1) senderr("can't create session file", MYFAULT);
+  
+  errno = 0;
+  int res = write(fd, &session, sizeof(session));
+  char errstr[128];
+  if (res != sizeof(session)) {
+    snprintf(errstr, 128, "can't write all session data (%i of %i): %s", (int)res, (int)sizeof(session), strerror(errno));
+  }
+  close(fd);
+  if (res != sizeof(session)) {
+    unlink(file);
+    senderr(errstr, MYFAULT);
+  }
+}
+
+void senderr(char *errmsg, bool myfault) {
+  if (myfault) {
+    puts("Status: 500 CGI error");
+  } else {
+    puts("Status: 400 CGI error");
+  }
+  puts("Content-Type: text/plain;charset=utf8");
+  puts("X-Frame-Options: DENY");
+  puts("");
+  puts("An error occured while processing your request:");
+  puts(errmsg);
+  exit(0);
+}
+
+// may send an error response and exit
+void grabrand(unsigned char *out, size_t len) {
+  int randfd = open(RANDDEV, O_RDONLY);
+  if (randfd == -1) senderr("can't open random device", MYFAULT);
+  if (read(randfd, out, len) != len) senderr("reading random data failed", MYFAULT);
+  close(randfd);
+}
+
+void login_and_setup() {
+  char *cookies = getenv("HTTP_COOKIE");
+  environ = NULL;
+  setuid(0);
+  
+  // Note that this doesn't validate whether this is actually a cookie name or
+  // part of a value - but that shouldn't be a problem.
+  // Note: Redo if I feel like doing it.
+  if (cookies == NULL) senderr("no cookies sent", NOTMYFAULT);
+  char *session_cookie = strstr(cookies, COOKIE_NAME"=");
+  if (session_cookie == NULL) senderr("session cookie not present", NOTMYFAULT);
+  session_cookie += strlen(COOKIE_NAME"=");
+  if (strlen(session_cookie) < COOKIE_LENGTH) senderr("invalid session cookie (error code 1)", NOTMYFAULT);
+  session_cookie[COOKIE_LENGTH] = '\0';
+  int res = authenticate_by_cookie(session_cookie);
+  if (res != 0) {
+    char errstr[128];
+    snprintf(errstr, 128, "invalid session cookie (error code 2-%i)", res);
+    senderr(errstr, NOTMYFAULT);
+  }
+}
diff --git a/checkstatus.c b/checkstatus.c
new file mode 100644 (file)
index 0000000..c620e1f
--- /dev/null
@@ -0,0 +1,13 @@
+#include <stdio.h>
+#include "cwebfiles.h"
+
+int main() {
+  FASTOUTPUT
+  
+  login_and_setup();
+  puts("Status: 200 you're authenticated");
+  puts("X-Frame-Options: DENY");
+  puts("Content-Type: text/plain;charset=utf8");
+  puts("");
+  puts("OK");
+}
diff --git a/compile b/compile
new file mode 100755 (executable)
index 0000000..68ca0e5
--- /dev/null
+++ b/compile
@@ -0,0 +1,6 @@
+#!/bin/bash
+for cgi in login checkstatus listdir; do
+  diet gcc -O3 -std=gnu99 -Wall -Werror -g -o $cgi $cgi.c cgistuff.c hex.c -lcrypt
+  sudo chown root:root $cgi
+  sudo chmod u+s $cgi
+done
diff --git a/cwebfiles.h b/cwebfiles.h
new file mode 100644 (file)
index 0000000..cada2eb
--- /dev/null
@@ -0,0 +1,39 @@
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#define SESSION_FOLDER "/var/run/cwebfiles/sessions/"
+#define SESSION_LENGTH 60 * 60 * 3 /* unit is seconds; we allow three-hour-sessions */
+
+#define COOKIE_NAME "WEBFILESESS"
+#define COOKIE_LENGTH 64
+
+#define RANDDEV "/dev/urandom"
+
+#define MYFAULT true
+#define NOTMYFAULT false
+
+struct cgi_serialized_session_status {
+  // "man useradd" says 32 chars is the length limit
+  char user_name[33];
+  uid_t uid;
+  time_t start_time;
+};
+
+extern struct cgi_serialized_session_status session;
+
+void unhex(unsigned char *out, char *in, size_t in_len);
+void hex(char *out, unsigned char *in, size_t in_len);
+bool checkhex(char *p, size_t l);
+
+int authenticate_by_cookie(char *cookie);
+void persist_session(char *cookie);
+void senderr(char *errmsg, bool myfault);
+void grabrand(unsigned char *out, size_t len);
+void login_and_setup();
+
+#define OUTBUFSIZE 64*1024
+#define FASTOUTPUT                             \
+  char outbuf[OUTBUFSIZE];                     \
+  setvbuf(stdout, outbuf, _IOFBF, OUTBUFSIZE); \
+
diff --git a/hex.c b/hex.c
new file mode 100644 (file)
index 0000000..150d4de
--- /dev/null
+++ b/hex.c
@@ -0,0 +1,35 @@
+#include "cwebfiles.h"
+
+// these routines don't respect nullbytes!
+
+// we use fast hex. start from 'a'. no checks performed (apart from length validity).
+// out must be half the size of in
+void unhex(unsigned char *out, char *in, size_t in_len) {
+  in_len = in_len & ~1; // input length mod 2 must be 0 - mask out the last bit
+  char *in_end = in + in_len;
+  while (in != in_end) {
+    unsigned char in1 = *(in++), in2 = *(in++);
+    *(out++) = (in1 - 'a') << 4 | (in2 - 'a');
+  }
+}
+
+// out must be twice the size of in
+void hex(char *out, unsigned char *in, size_t in_len) {
+  unsigned char *in_end = in+in_len;
+  while (in != in_end) {
+    *(out++) = (*in >> 4) + 'a';
+    *(out++) = (*in & 0xf) + 'a';
+    in++;
+  }
+}
+
+bool checkhex(char *p, size_t l) {
+  if ((l&1) == 1) return false;
+  char *e = p+l;
+  while (p<e) {
+    if (*p < 'a') return false;
+    if (*p >= 'a'+16) return false;
+    p++;
+  }
+  return true;
+}
diff --git a/listdir.c b/listdir.c
new file mode 100644 (file)
index 0000000..bfbd47c
--- /dev/null
+++ b/listdir.c
@@ -0,0 +1,60 @@
+#include <stdlib.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include "cwebfiles.h"
+#include <sys/types.h>
+#include <dirent.h>
+#include <unistd.h>
+#include <sys/fsuid.h>
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/stat.h>
+
+int main() {
+  FASTOUTPUT
+  
+  char *encpath = getenv("QUERY_STRING");
+  login_and_setup(); // will clear the environment
+  if (encpath == NULL) senderr("no query string received", NOTMYFAULT);
+  
+  // OK, we're authenticated. Drop our fsuid to the user's.
+  // Yes, there's no error indication apart from this kludge. Sucks.
+  setfsuid(session.uid);
+  if (setfsuid(session.uid) != session.uid) senderr("setfsuid failed", MYFAULT);
+  
+  // Do what the user wants us to do.
+  char path[strlen(encpath)/2+1];
+  unhex((unsigned char *)path, encpath, strlen(encpath));
+  path[strlen(encpath)/2] = 0;
+  if (chdir(path)) {
+    char errstr[128];
+    snprintf(errstr, 128, "can't access that path: %s", strerror(errno));
+    senderr(errstr, NOTMYFAULT);
+  }
+  DIR *dir = opendir(".");
+  if (dir == NULL) {
+    char errstr[128];
+    snprintf(errstr, 128, "can't open that path for reading: %s", strerror(errno));
+    senderr(errstr, NOTMYFAULT);
+  }
+  
+  // The directory was opened successfully - send our success header.
+  puts("Status: 200 here's your listing");
+  puts("X-Frame-Options: DENY");
+  puts("Content-Type: text/plain;charset=utf8");
+  puts("");
+  
+  // send one line per file
+  struct dirent *dent;
+  char encfilename[NAME_MAX*2+1];
+  while ((dent = readdir(dir)) != NULL) {
+    size_t name_len = strlen(dent->d_name);
+    hex(encfilename, (unsigned char *)dent->d_name, name_len);
+    encfilename[name_len*2] = 0;
+    struct stat st;
+    // if we can't really see the file anyway, skip it
+    if (lstat(dent->d_name, &st)) continue;
+    printf("%s %lu %u\n", encfilename, (unsigned long)st.st_size, (unsigned int)st.st_mode);
+  }
+}
diff --git a/login.c b/login.c
new file mode 100644 (file)
index 0000000..96b95bc
--- /dev/null
+++ b/login.c
@@ -0,0 +1,68 @@
+#define _GNU_SOURCE
+
+#include <stdlib.h>
+#include <string.h>
+#include "cwebfiles.h"
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <time.h>
+#include <unistd.h>
+#include <shadow.h>
+#include <pwd.h>
+#include <stdio.h>
+
+extern char **environ;
+
+#define BADAUTH "invalid user/pass"
+
+int main() {
+  FASTOUTPUT
+  environ = NULL;
+  setuid(0);
+  
+  char buf[1024];
+  int res = read(0, buf, 1023);
+  if (res == -1) senderr("couldn't read POST data", NOTMYFAULT);
+  buf[res] = 0;
+  char *nl = strchr(buf, '\r');
+  if (nl != NULL) *nl = 0;
+  char *user = buf;
+  char *pass = strchr(buf, ':');
+  if (pass == NULL) senderr("can't find user-pass-seperator", NOTMYFAULT);
+  *pass = 0;
+  pass++;
+  
+  // verify password
+  struct spwd *shadow_entry = getspnam(user);
+  struct passwd *passwd_entry = getpwnam(user);
+  if (passwd_entry == NULL || shadow_entry == NULL) senderr(BADAUTH, NOTMYFAULT);
+  char *enc_pass = crypt(pass, shadow_entry->sp_pwdp);
+  if (enc_pass == NULL) senderr(BADAUTH, NOTMYFAULT);
+  if (strcmp(enc_pass, shadow_entry->sp_pwdp) != 0) senderr(BADAUTH, NOTMYFAULT);
+  
+  // now create a new session
+  // create a cookie
+  unsigned char bincookie[COOKIE_LENGTH/2];
+  grabrand(bincookie, COOKIE_LENGTH/2);
+  char cookie[COOKIE_LENGTH+1];
+  hex(cookie, bincookie, COOKIE_LENGTH/2);
+  cookie[COOKIE_LENGTH] = 0;
+  
+  // set session data
+  strncpy(session.user_name, user, 33);
+  session.start_time = time(NULL);
+  session.uid = passwd_entry->pw_uid;
+  
+  // write out session data to the fs
+  persist_session(cookie);
+  
+  // phew, all done! tell the user everything is fine
+  puts("Status: 200 login successful");
+  puts("X-Frame-Options: DENY");
+  puts("Content-Type: text/plain;charset=utf8");
+  // TODO XXX XXX XXX XXX XXX XXX XXX XXX XXX XXX XXX XXX XXX SET SECURE FLAG!
+  printf("Set-Cookie: " COOKIE_NAME "=%s; HttpOnly\n", cookie);
+  puts("");
+  puts("OK");
+}
diff --git a/octalmodes b/octalmodes
new file mode 100644 (file)
index 0000000..e4e5627
--- /dev/null
@@ -0,0 +1,23 @@
+           S_IFMT     0170000   bit mask for the file type bit fields
+           S_IFSOCK   0140000   socket
+           S_IFLNK    0120000   symbolic link
+           S_IFREG    0100000   regular file
+           S_IFBLK    0060000   block device
+           S_IFDIR    0040000   directory
+           S_IFCHR    0020000   character device
+           S_IFIFO    0010000   FIFO
+           S_ISUID    0004000   set UID bit
+           S_ISGID    0002000   set-group-ID bit (see below)
+           S_ISVTX    0001000   sticky bit (see below)
+           S_IRWXU    00700     mask for file owner permissions
+           S_IRUSR    00400     owner has read permission
+           S_IWUSR    00200     owner has write permission
+           S_IXUSR    00100     owner has execute permission
+           S_IRWXG    00070     mask for group permissions
+           S_IRGRP    00040     group has read permission
+           S_IWGRP    00020     group has write permission
+           S_IXGRP    00010     group has execute permission
+           S_IRWXO    00007     mask for permissions for others (not in group)
+           S_IROTH    00004     others have read permission
+           S_IWOTH    00002     others have write permission
+           S_IXOTH    00001     others have execute permission
diff --git a/octalmodes_to_js b/octalmodes_to_js
new file mode 100644 (file)
index 0000000..107470b
--- /dev/null
@@ -0,0 +1 @@
+cat octalmodes | sed 's|^ *|var |g' | sed 's|   *| = parseInt("|' | sed 's|   *|",8) // |'
diff --git a/static/index.html b/static/index.html
new file mode 100644 (file)
index 0000000..1e6adcc
--- /dev/null
@@ -0,0 +1,179 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <script>
+function hex(str) {
+  var res = "";
+  var achar = 'a'.charCodeAt(0)
+  for (var i=0; i<str.length; i++) {
+    var c = str.charCodeAt(i)
+    res += String.fromCharCode((c>>4)+achar) + String.fromCharCode((c&0xf)+achar)
+  }
+  return res
+}
+
+function dehex(str) {
+  var res = ""
+  var achar = 'a'.charCodeAt(0)
+  for (var i=0; i<str.length; i+=2) {
+    var c1 = str.charCodeAt(i)-achar, c2 = str.charCodeAt(i+1)-achar
+    res += String.fromCharCode((c1<<4)+c2)
+  }
+  return res
+}
+
+function cgireq(script, qs, body, cb) {
+  var req = new XMLHttpRequest()
+  req.open((body!=null)?'POST':'GET', '/cgi-bin/cwebfiles/'+script+(qs?'?'+qs:''), !!cb)
+  if (cb) {
+    req.onloadend = function() {
+      cb(req.status, req.responseText)
+    }
+  }
+  req.send(body)
+  if (!cb) {
+    return {status: req.status, data: req.responseText}
+  }
+}
+
+function login(user, pass, cb) {
+  return cgireq('login', null, user+':'+pass, cb)
+}
+
+function listdir(path, cb) {
+  cgireq('listdir', hex(path), null, function(status, data) {
+    if (status != 200) return cb(status, data, null)
+    var listing = data.split('\n').filter(function(line) {
+      return line.trim().length > 0
+    }).map(function(line) {
+      var parts = line.split(' ')
+      return {name: dehex(parts[0]), size: parseInt(parts[1]), mode: parseInt(parts[2])}
+    })
+    cb(status, data, listing)
+  })
+}
+
+var S_IFMT = parseInt("0170000",8) // bit mask for the file type bit fields
+var S_IFSOCK = parseInt("0140000",8) // socket
+var S_IFLNK = parseInt("0120000",8) // symbolic link
+var S_IFREG = parseInt("0100000",8) // regular file
+var S_IFBLK = parseInt("0060000",8) // block device
+var S_IFDIR = parseInt("0040000",8) // directory
+var S_IFCHR = parseInt("0020000",8) // character device
+var S_IFIFO = parseInt("0010000",8) // FIFO
+
+var S_ISUID = parseInt("0004000",8) // set UID bit
+var S_ISGID = parseInt("0002000",8) // set-group-ID bit (see below)
+var S_ISVTX = parseInt("0001000",8) // sticky bit (see below)
+
+var S_IRWXU = parseInt("00700",8) // mask for file owner permissions
+var S_IRUSR = parseInt("00400",8) // owner has read permission
+var S_IWUSR = parseInt("00200",8) // owner has write permission
+var S_IXUSR = parseInt("00100",8) // owner has execute permission
+var S_IRWXG = parseInt("00070",8) // mask for group permissions
+var S_IRGRP = parseInt("00040",8) // group has read permission
+var S_IWGRP = parseInt("00020",8) // group has write permission
+var S_IXGRP = parseInt("00010",8) // group has execute permission
+var S_IRWXO = parseInt("00007",8) // mask for permissions for others (not in group)
+var S_IROTH = parseInt("00004",8) // others have read permission
+var S_IWOTH = parseInt("00002",8) // others have write permission
+var S_IXOTH = parseInt("00001",8) // others have execute permission
+
+
+var typechars = {}
+typechars[S_IFSOCK] = 's'
+typechars[S_IFLNK] = 'l'
+typechars[S_IFREG] = '-'
+typechars[S_IFBLK] = 'b'
+typechars[S_IFDIR] = 'd'
+typechars[S_IFCHR] = 'c'
+typechars[S_IFIFO] = 'p'
+
+function escapehtml(text) {
+  return text.replace(/&/g, '&amp;')
+              .replace(/</g, '&lt;')
+              .replace(/>/g, '&gt;')
+              .replace(/"/g, '&quot;')
+}
+
+var loc = '/'
+
+function dive(hexedname) { __listdir(loc + dehex(hexedname) + '/'); }
+
+function format_entry(entry) {
+  var modeline = ""
+  modeline += typechars[entry.mode&S_IFMT]
+  modeline += (entry.mode&S_IRUSR) ? 'r' : '-'
+  modeline += (entry.mode&S_IWUSR) ? 'w' : '-'
+  modeline += (entry.mode&S_ISUID) ? ((entry.mode&S_IXUSR) ? 's' : 'S') : ((entry.mode&S_IXUSR) ? 'x' : '-')
+  modeline += (entry.mode&S_IRGRP) ? 'r' : '-'
+  modeline += (entry.mode&S_IWGRP) ? 'w' : '-'
+  modeline += (entry.mode&S_ISGID) ? ((entry.mode&S_IXGRP) ? 's' : 'S') : ((entry.mode&S_IXGRP) ? 'x' : '-')
+  modeline += (entry.mode&S_IROTH) ? 'r' : '-'
+  modeline += (entry.mode&S_IWOTH) ? 'w' : '-'
+  modeline += (entry.mode&S_ISVTX) ? ((entry.mode&S_IXOTH) ? 's' : 'S') : ((entry.mode&S_IXOTH) ? 'x' : '-')
+  
+  var markred = (entry.mode&(S_ISUID|S_ISGID))
+  if (markred) {
+    modeline = '<span style="color: red">'+modeline+'</span>'
+  }
+  
+  var showsize = (entry.mode&S_IFMT) == S_IFREG
+  
+  var escaped_name = escapehtml(entry.name)
+  
+  var clickcode = ''
+  if ((entry.mode&S_IFMT) == S_IFDIR) clickcode = "dive('"+hex(entry.name)+"')"
+  
+  return '<tr><td>'+modeline+'</td><td '+(clickcode?'class="clickme" ':'')+'onclick="'+clickcode+'">'+escaped_name+'</td><td>'+(showsize?entry.size:'')+'</td></tr>'
+}
+
+function renderloc(path) {
+  var parts = path.split('/')
+  var curpath = '/';
+  var linkparts = parts.map(function(dir, i) {
+    var esc = (i==0) ? 'main directory' : escapehtml(dir)
+    if (i != 0) curpath += dir + '/'
+    var hexpath = hex(curpath)
+    var clickcode = "__listdir(dehex('"+hexpath+"'))"
+    return '<span class="clickme" onclick="'+clickcode+'">'+esc+'</span>'
+  })
+  return '<div id="loc">'+linkparts.join(' / ')+'</div>'
+}
+
+function __listdir(path) {
+  listdir(path, function(status, err, listing) {
+    if (status != 200) {
+      alert(err)
+      return
+    }
+    loc = path
+    var body = document.getElementsByTagName('body')[0]
+    body.innerHTML = renderloc(path)
+                   + '<table><thead><tr><th>mode</th><th>name</th><th>size</th></tr></thead><tbody>'+listing.filter(function(e) {
+      return e.name != '.' && e.name != '..'
+    }).map(function(e) {
+      return format_entry(e)
+    }).join('')+'</tbody></table>'
+    /*listing.forEach(function(e) {
+      //e.mode = e.mode.toString(8)
+      console.log(Object.keys(e).map(function(k) {
+        return k+':'+e[k]
+      }).join(', '))
+    })*/
+  })
+}
+
+
+    </script>
+    <style>
+      .clickme {
+        text-decoration: underline;
+        color: blue;
+        cursor: pointer;
+      }
+    </style>
+  </head>
+  <body onload="__listdir('/')">
+  </body>
+</html>