changeset 856:86ec89baee01

cgi protocol
author Adam Chlipala <adamc@hcoop.net>
date Tue, 23 Jun 2009 17:59:23 -0400
parents 28e42b22424d
children 3d2f6cb6d54a
files Makefile.in include/request.h include/types.h include/urweb.h src/c/cgi.c src/c/http.c src/c/request.c src/c/urweb.c src/main.mlton.sml src/settings.sig src/settings.sml
diffstat 11 files changed, 272 insertions(+), 72 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile.in	Tue Jun 23 15:56:04 2009 -0400
+++ b/Makefile.in	Tue Jun 23 17:59:23 2009 -0400
@@ -14,7 +14,7 @@
 smlnj: src/urweb.cm
 mlton: bin/urweb
 
-OBJS := urweb request http
+OBJS := urweb request http cgi
 c: $(OBJS:%=lib/c/%.o)
 
 clean:
--- a/include/request.h	Tue Jun 23 15:56:04 2009 -0400
+++ b/include/request.h	Tue Jun 23 17:59:23 2009 -0400
@@ -7,7 +7,7 @@
 
 typedef struct uw_rc *uw_request_context;
 
-void uw_request_init(void);
+void uw_request_init(void *logger_data, uw_logger log_error, uw_logger log_debug);
 void uw_sign(const char *in, char *out);
 
 uw_request_context uw_new_request_context(void);
@@ -16,9 +16,16 @@
 request_result uw_request(uw_request_context, uw_context,
                           char *method, char *path, char *query_string,
                           char *body, size_t body_len,
+                          void (*on_success)(uw_context), void (*on_failure)(uw_context),
+                          void *logger_data, uw_logger log_error, uw_logger log_debug,
                           int sock);
 
-uw_context uw_request_new_context(void);
+uw_context uw_request_new_context(void *logger_data, uw_logger log_error, uw_logger log_debug);
+
+typedef struct {
+  void *logger_data;
+  uw_logger log_error, log_debug;
+} loggers;
 
 void *client_pruner(void *data);
 
--- a/include/types.h	Tue Jun 23 15:56:04 2009 -0400
+++ b/include/types.h	Tue Jun 23 17:59:23 2009 -0400
@@ -49,5 +49,6 @@
 #define TIMES_MAX 100
 
 typedef void (*uw_callback)(void *);
+typedef void (*uw_logger)(void*, const char *fmt, ...);
 
 #endif
--- a/include/urweb.h	Tue Jun 23 15:56:04 2009 -0400
+++ b/include/urweb.h	Tue Jun 23 17:59:23 2009 -0400
@@ -24,6 +24,7 @@
 void uw_reset_keep_error_message(uw_context);
 
 failure_kind uw_begin_init(uw_context);
+void uw_set_on_success(char *);
 void uw_set_headers(uw_context, char *(*get_header)(void *, const char *), void *get_header_data);
 failure_kind uw_begin(uw_context, char *path);
 void uw_login(uw_context);
@@ -41,6 +42,7 @@
 void uw_memstats(uw_context);
 
 int uw_send(uw_context, int sock);
+int uw_print(uw_context, int fd);
 
 int uw_set_input(uw_context, const char *name, char *value);
 int uw_set_file_input(uw_context, char *name, uw_Basis_file);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/c/cgi.c	Tue Jun 23 17:59:23 2009 -0400
@@ -0,0 +1,113 @@
+#include <stdio.h>
+#include <ctype.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <stdarg.h>
+
+#include "request.h"
+
+static char *uppercased;
+static size_t uppercased_len;
+
+static char *get_header(void *data, const char *h) {
+  size_t len = strlen(h);
+  char *s, *r;
+  const char *saved_h = h;
+
+  if (len > uppercased_len) {
+    uppercased_len = len;
+    uppercased = realloc(uppercased, len + 6);
+  }
+
+  strcpy(uppercased, "HTTP_");
+  for (s = uppercased+5; *h; ++h)
+    *s++ = *h == '-' ? '_' : toupper(*h);
+  *s = 0;
+
+  if (r = getenv(uppercased))
+    return r;
+  else if (!strcasecmp(saved_h, "Content-length")
+           || !strcasecmp(saved_h, "Content-type"))
+    return getenv(uppercased + 5);
+  else
+    return NULL;
+}
+
+static void on_success(uw_context ctx) { }
+
+static void on_failure(uw_context ctx) {
+  uw_write_header(ctx, "Status: 500 Internal Server Error\r\n");
+}
+
+static void log_error(void *data, const char *fmt, ...) {
+  va_list ap;
+  va_start(ap, fmt);
+
+  vfprintf(stderr, fmt, ap);
+}
+
+static void log_debug(void *data, const char *fmt, ...) {
+}
+
+int main(int argc, char *argv[]) {
+  uw_context ctx = uw_request_new_context(NULL, log_error, log_debug);
+  uw_request_context rc = uw_new_request_context();
+  request_result rr;
+  char *method = getenv("REQUEST_METHOD"),
+    *path = getenv("SCRIPT_NAME"), *path_info = getenv("PATH_INFO"),
+    *query_string = getenv("QUERY_STRING");
+  char *body = malloc(1);
+  ssize_t body_len = 1, body_pos = 0, res;
+
+  uppercased = malloc(6);
+
+  if (!method) {
+    log_error(NULL, "REQUEST_METHOD not set\n");
+    exit(1);
+  }
+
+  if (!path) {
+    log_error(NULL, "SCRIPT_NAME not set\n");
+    exit(1);
+  }
+
+  if (path_info) {
+    char *new_path = malloc(strlen(path) + strlen(path_info) + 1);
+    sprintf(new_path, "%s%s", path, path_info);
+    path = new_path;
+  }
+
+  if (!query_string)
+    query_string = "";
+
+  while ((res = read(0, body + body_pos, body_len - body_pos)) > 0) {
+    body_pos += res;
+
+    if (body_pos == body_len) {
+      body_len *= 2;
+      body = realloc(body, body_len);
+    }
+  }
+
+  if (res < 0) {
+    log_error(NULL, "Error reading stdin\n");
+    exit(1);
+  }
+
+  uw_set_on_success("");
+  uw_set_headers(ctx, get_header, NULL);
+  uw_request_init(NULL, log_error, log_debug);
+
+  body[body_pos] = 0;
+  rr = uw_request(rc, ctx, method, path, query_string, body, body_pos,
+                  on_success, on_failure,
+                  NULL, log_error, log_debug,
+                  -1);
+  uw_print(ctx, 1);
+
+  if (rr == SERVED)
+    return 0;
+  else
+    return 1;
+}
--- a/src/c/http.c	Tue Jun 23 15:56:04 2009 -0400
+++ b/src/c/http.c	Tue Jun 23 17:59:23 2009 -0400
@@ -8,11 +8,10 @@
 #include <netinet/in.h>
 #include <unistd.h>
 #include <signal.h>
+#include <stdarg.h>
 
 #include <pthread.h>
 
-#include <mhash.h>
-
 #include "urweb.h"
 #include "request.h"
 
@@ -73,9 +72,31 @@
   return NULL;
 }
 
+static void on_success(uw_context ctx) {
+  uw_write_header(ctx, "HTTP/1.1 200 OK\r\n");
+}
+
+static void on_failure(uw_context ctx) {
+  uw_write_header(ctx, "HTTP/1.1 500 Internal Server Error\r\n");
+}
+
+static void log_error(void *data, const char *fmt, ...) {
+  va_list ap;
+  va_start(ap, fmt);
+
+  vfprintf(stderr, fmt, ap);
+}
+
+static void log_debug(void *data, const char *fmt, ...) {
+  va_list ap;
+  va_start(ap, fmt);
+
+  vprintf(fmt, ap);
+}
+
 static void *worker(void *data) {
   int me = *(int *)data;
-  uw_context ctx = uw_request_new_context();
+  uw_context ctx = uw_request_new_context(NULL, log_error, log_debug);
   size_t buf_size = 2;
   char *buf = malloc(buf_size);
   uw_request_context rc = uw_new_request_context();
@@ -205,7 +226,10 @@
 
         uw_set_headers(ctx, get_header, headers);
 
-        rr = uw_request(rc, ctx, method, path, query_string, body, back - body, sock);
+        rr = uw_request(rc, ctx, method, path, query_string, body, back - body,
+                        on_success, on_failure,
+                        NULL, log_error, log_debug,
+                        sock);
         uw_send(ctx, sock);
 
         if (rr == SERVED || rr == FAILED)
@@ -231,6 +255,8 @@
   exit(0);
 }
 
+static loggers ls = {NULL, log_error, log_debug};
+
 int main(int argc, char *argv[]) {
   // The skeleton for this function comes from Beej's sockets tutorial.
   int sockfd;  // listen on sock_fd
@@ -277,7 +303,7 @@
     }
   }
 
-  uw_request_init();
+  uw_request_init(NULL, log_error, log_debug);
 
   names = calloc(nthreads, sizeof(int));
 
@@ -316,7 +342,7 @@
     pthread_t thread;
     int name;
 
-    if (pthread_create(&thread, NULL, client_pruner, &name)) {
+    if (pthread_create(&thread, NULL, client_pruner, &ls)) {
       fprintf(stderr, "Error creating pruner thread\n");
       return 1;
     }
--- a/src/c/request.c	Tue Jun 23 15:56:04 2009 -0400
+++ b/src/c/request.c	Tue Jun 23 17:59:23 2009 -0400
@@ -17,11 +17,11 @@
 
 #define MAX_RETRIES 5
 
-static int try_rollback(uw_context ctx) {
+static int try_rollback(uw_context ctx, void *logger_data, uw_logger log_error) {
   int r = uw_rollback(ctx);
 
   if (r) {
-    printf("Error running SQL ROLLBACK\n");
+    log_error(logger_data, "Error running SQL ROLLBACK\n");
     uw_reset(ctx);
     uw_write(ctx, "HTTP/1.1 500 Internal Server Error\n\r");
     uw_write(ctx, "Content-type: text/plain\r\n\r\n");
@@ -31,7 +31,7 @@
   return r;
 }
 
-uw_context uw_request_new_context() {
+uw_context uw_request_new_context(void *logger_data, uw_logger log_error, uw_logger log_debug) {
   uw_context ctx = uw_init();
   int retries_left = MAX_RETRIES;
 
@@ -39,25 +39,25 @@
     failure_kind fk = uw_begin_init(ctx);
 
     if (fk == SUCCESS) {
-      printf("Database connection initialized.\n");
+      log_debug(logger_data, "Database connection initialized.\n");
       break;
     } else if (fk == BOUNDED_RETRY) {
       if (retries_left) {
-        printf("Initialization error triggers bounded retry: %s\n", uw_error_message(ctx));
+        log_debug(logger_data, "Initialization error triggers bounded retry: %s\n", uw_error_message(ctx));
         --retries_left;
       } else {
-        printf("Fatal initialization error (out of retries): %s\n", uw_error_message(ctx));
+        log_error(logger_data, "Fatal initialization error (out of retries): %s\n", uw_error_message(ctx));
         uw_free(ctx);
         return NULL;
       }
     } else if (fk == UNLIMITED_RETRY)
-      printf("Initialization error triggers unlimited retry: %s\n", uw_error_message(ctx));
+      log_debug(logger_data, "Initialization error triggers unlimited retry: %s\n", uw_error_message(ctx));
     else if (fk == FATAL) {
-      printf("Fatal initialization error: %s\n", uw_error_message(ctx));
+      log_error(logger_data, "Fatal initialization error: %s\n", uw_error_message(ctx));
       uw_free(ctx);
       return NULL;
     } else {
-      printf("Unknown uw_begin_init return code!\n");
+      log_error(logger_data, "Unknown uw_begin_init return code!\n");
       uw_free(ctx);
       return NULL;
     }
@@ -78,7 +78,7 @@
 static int password[PASSSIZE];
 static unsigned char private_key[KEYSIZE];
 
-static void init_crypto() {
+static void init_crypto(void *logger_data, uw_logger log_error) {
   KEYGEN kg = {{HASH_ALGORITHM, HASH_ALGORITHM}};
   int i;
 
@@ -90,37 +90,37 @@
   if (mhash_keygen_ext(KEYGEN_ALGORITHM, kg,
                        private_key, sizeof(private_key),
                        (unsigned char*)password, sizeof(password)) < 0) {
-    printf("Key generation failed\n");
+    log_error(logger_data, "Key generation failed\n");
     exit(1);
   }
 }
 
-void uw_request_init() {
+void uw_request_init(void *logger_data, uw_logger log_error, uw_logger log_debug) {
   uw_context ctx;
   failure_kind fk;
 
   uw_global_init();
 
-  ctx = uw_request_new_context();
+  ctx = uw_request_new_context(logger_data, log_error, log_debug);
 
   if (!ctx)
     exit(1);
 
   for (fk = uw_initialize(ctx); fk == UNLIMITED_RETRY; fk = uw_initialize(ctx)) {
-    printf("Unlimited retry during init: %s\n", uw_error_message(ctx));
+    log_debug(logger_data, "Unlimited retry during init: %s\n", uw_error_message(ctx));
     uw_db_rollback(ctx);
     uw_reset(ctx);
   }
 
   if (fk != SUCCESS) {
-    printf("Failed to initialize database!  %s\n", uw_error_message(ctx));
+    log_error(logger_data, "Failed to initialize database!  %s\n", uw_error_message(ctx));
     uw_db_rollback(ctx);
     exit(1);
   }
 
   uw_free(ctx);
 
-  init_crypto();
+  init_crypto(logger_data, log_error);
 }
 
 void uw_sign(const char *in, char *out) {
@@ -131,7 +131,7 @@
   
   mhash(td, in, strlen(in));
   if (mhash_hmac_deinit(td, out) < 0)
-    printf("Signing failed");
+    fprintf(stderr, "Signing failed\n");
 }
 
 typedef struct uw_rc {
@@ -154,6 +154,8 @@
 request_result uw_request(uw_request_context rc, uw_context ctx,
                           char *method, char *path, char *query_string,
                           char *body, size_t body_len,
+                          void (*on_success)(uw_context), void (*on_failure)(uw_context),
+                          void *logger_data, uw_logger log_error, uw_logger log_debug,
                           int sock) {
   int retries_left = MAX_RETRIES;
   char *s;
@@ -166,17 +168,17 @@
   if (!strcmp(method, "POST")) {
     char *clen_s = uw_Basis_requestHeader(ctx, "Content-length");
     if (!clen_s) {
-      fprintf(stderr, "No Content-length with POST\n");
+      log_error(logger_data, "No Content-length with POST\n");
       return FAILED;
     }
     int clen = atoi(clen_s);
     if (clen < 0) {
-      fprintf(stderr, "Negative Content-length with POST\n");
+      log_error(logger_data, "Negative Content-length with POST\n");
       return FAILED;
     }
 
     if (body_len < clen) {
-      fprintf(stderr, "Request doesn't contain all POST data (according to Content-Length)\n");
+      log_error(logger_data, "Request doesn't contain all POST data (according to Content-Length)\n");
       return FAILED;
     }
 
@@ -185,7 +187,7 @@
     clen_s = uw_Basis_requestHeader(ctx, "Content-type");
     if (clen_s && !strncasecmp(clen_s, "multipart/form-data", 19)) {
       if (strncasecmp(clen_s + 19, "; boundary=", 11)) {
-        fprintf(stderr, "Bad multipart boundary spec");
+        log_error(logger_data, "Bad multipart boundary spec");
         return FAILED;
       }
 
@@ -195,7 +197,7 @@
       boundary_len = strlen(boundary);
     }
   } else if (strcmp(method, "GET")) {
-    fprintf(stderr, "Not ready for non-GET/POST command: %s\n", method);
+    log_error(logger_data, "Not ready for non-GET/POST command: %s\n", method);
     return FAILED;
   }
 
@@ -204,18 +206,18 @@
     char *pass = uw_Basis_requestHeader(ctx, "UrWeb-Pass");
 
     if (sock < 0) {
-      fprintf(stderr, ".msgs requested, but not socket supplied\n");
+      log_error(logger_data, ".msgs requested, but not socket supplied\n");
       return FAILED;
     }
 
     if (id && pass) {
       unsigned idn = atoi(id);
       uw_client_connect(idn, atoi(pass), sock);
-      fprintf(stderr, "Processed request for messages by client %u\n\n", idn);
+      log_error(logger_data, "Processed request for messages by client %u\n\n", idn);
       return KEEP_OPEN;
     }
     else {
-      fprintf(stderr, "Missing fields in .msgs request: %s, %s\n\n", id, pass);
+      log_error(logger_data, "Missing fields in .msgs request: %s, %s\n\n", id, pass);
       return FAILED;
     }
   }
@@ -226,7 +228,7 @@
 
     part = strstr(part, boundary);
     if (!part) {
-      fprintf(stderr, "Missing first multipart boundary\n");
+      log_error(logger_data, "Missing first multipart boundary\n");
       return FAILED;
     }
     part += boundary_len;
@@ -238,18 +240,18 @@
         break;
 
       if (*part != '\r') {
-        fprintf(stderr, "No \\r after multipart boundary\n");
+        log_error(logger_data, "No \\r after multipart boundary\n");
         return FAILED;
       }
       ++part;
       if (*part != '\n') {
-        fprintf(stderr, "No \\n after multipart boundary\n");
+        log_error(logger_data, "No \\n after multipart boundary\n");
         return FAILED;
       }
       ++part;
             
       if (!(after_sub_headers = strstr(part, "\r\n\r\n"))) {
-        fprintf(stderr, "Missing end of headers after multipart boundary\n");
+        log_error(logger_data, "Missing end of headers after multipart boundary\n");
         return FAILED;
       }
       after_sub_headers[2] = 0;
@@ -260,18 +262,18 @@
 
         *after_header = 0;
         if (!(colon = strchr(header, ':'))) {
-          fprintf(stderr, "Missing colon in multipart sub-header\n");
+          log_error(logger_data, "Missing colon in multipart sub-header\n");
           return FAILED;
         }
         *colon++ = 0;
         if (*colon++ != ' ') {
-          fprintf(stderr, "No space after colon in multipart sub-header\n");
+          log_error(logger_data, "No space after colon in multipart sub-header\n");
           return FAILED;
         }
         
         if (!strcasecmp(header, "Content-Disposition")) {
           if (strncmp(colon, "form-data; ", 11)) {
-            fprintf(stderr, "Multipart data is not \"form-data\"\n");
+            log_error(logger_data, "Multipart data is not \"form-data\"\n");
             return FAILED;
           }
 
@@ -279,12 +281,12 @@
             char *data;
             after_colon[0] = 0;
             if (after_colon[1] != '"') {
-              fprintf(stderr, "Disposition setting is missing initial quote\n");
+              log_error(logger_data, "Disposition setting is missing initial quote\n");
               return FAILED;
             }
             data = after_colon+2;
             if (!(after_colon = strchr(data, '"'))) {
-              fprintf(stderr, "Disposition setting is missing final quote\n");
+              log_error(logger_data, "Disposition setting is missing final quote\n");
               return FAILED;
             }
             after_colon[0] = 0;
@@ -304,7 +306,7 @@
 
       part = memmem(after_sub_headers, body + body_len - after_sub_headers, boundary, boundary_len);
       if (!part) {
-        fprintf(stderr, "Missing boundary after multipart payload\n");
+        log_error(logger_data, "Missing boundary after multipart payload\n");
         return FAILED;
       }
       part[-2] = 0;
@@ -316,11 +318,11 @@
         uw_Basis_file f = {filename, type, {part_len, after_sub_headers}};
 
         if (uw_set_file_input(ctx, name, f)) {
-          fprintf(stderr, "%s\n", uw_error_message(ctx));
+          log_error(logger_data, "%s\n", uw_error_message(ctx));
           return FAILED;
         }
       } else if (uw_set_input(ctx, name, after_sub_headers)) {
-        fprintf(stderr, "%s\n", uw_error_message(ctx));
+        log_error(logger_data, "%s\n", uw_error_message(ctx));
         return FAILED;
       }
     }
@@ -341,24 +343,24 @@
         if (value = strchr(name, '=')) {
           *value++ = 0;
           if (uw_set_input(ctx, name, value)) {
-            fprintf(stderr, "%s\n", uw_error_message(ctx));
+            log_error(logger_data, "%s\n", uw_error_message(ctx));
             return FAILED;
           }
         }
         else if (uw_set_input(ctx, name, "")) {
-          fprintf(stderr, "%s\n", uw_error_message(ctx));
+          log_error(logger_data, "%s\n", uw_error_message(ctx));
           return FAILED;
         }
       }
     }
   }
 
-  printf("Serving URI %s....\n", path);
+  log_debug(logger_data, "Serving URI %s....\n", path);
 
   while (1) {
     size_t path_len = strlen(path);
 
-    uw_write_header(ctx, "HTTP/1.1 200 OK\r\n");
+    on_success(ctx);
 
     if (path_len + 1 > rc->path_copy_size) {
       rc->path_copy_size = path_len + 1;
@@ -371,16 +373,16 @@
       return SERVED;
     } else if (fk == BOUNDED_RETRY) {
       if (retries_left) {
-        printf("Error triggers bounded retry: %s\n", uw_error_message(ctx));
+        log_debug(logger_data, "Error triggers bounded retry: %s\n", uw_error_message(ctx));
         --retries_left;
       }
       else {
-        printf("Fatal error (out of retries): %s\n", uw_error_message(ctx));
+        log_error(logger_data, "Fatal error (out of retries): %s\n", uw_error_message(ctx));
 
-        try_rollback(ctx);
+        try_rollback(ctx, logger_data, log_error);
 
         uw_reset_keep_error_message(ctx);
-        uw_write_header(ctx, "HTTP/1.1 500 Internal Server Error\n\r");
+        on_failure(ctx);
         uw_write_header(ctx, "Content-type: text/plain\r\n");
         uw_write(ctx, "Fatal error (out of retries): ");
         uw_write(ctx, uw_error_message(ctx));
@@ -389,14 +391,14 @@
         return FAILED;
       }
     } else if (fk == UNLIMITED_RETRY)
-      printf("Error triggers unlimited retry: %s\n", uw_error_message(ctx));
+      log_debug(logger_data, "Error triggers unlimited retry: %s\n", uw_error_message(ctx));
     else if (fk == FATAL) {
-      printf("Fatal error: %s\n", uw_error_message(ctx));
+      log_error(logger_data, "Fatal error: %s\n", uw_error_message(ctx));
 
-      try_rollback(ctx);
+      try_rollback(ctx, logger_data, log_error);
 
       uw_reset_keep_error_message(ctx);
-      uw_write_header(ctx, "HTTP/1.1 500 Internal Server Error\r\n");
+      on_failure(ctx);
       uw_write_header(ctx, "Content-type: text/html\r\n");
       uw_write(ctx, "<html><head><title>Fatal Error</title></head><body>");
       uw_write(ctx, "Fatal error: ");
@@ -405,27 +407,33 @@
 
       return FAILED;
     } else {
-      printf("Unknown uw_handle return code!\n");
+      log_error(logger_data, "Unknown uw_handle return code!\n");
 
-      try_rollback(ctx);
+      try_rollback(ctx, logger_data, log_error);
 
       uw_reset_keep_request(ctx);
-      uw_write_header(ctx, "HTTP/1.1 500 Internal Server Error\n\r");
+      on_failure(ctx);
       uw_write_header(ctx, "Content-type: text/plain\r\n");
       uw_write(ctx, "Unknown uw_handle return code!\n");
 
       return FAILED;
     }
 
-    if (try_rollback(ctx))
+    if (try_rollback(ctx, logger_data, log_error))
       return FAILED;
 
     uw_reset_keep_request(ctx);
   }
 }
 
+typedef struct {
+  void *logger_data;
+  uw_logger log_error, log_debug;
+} loggers;
+
 void *client_pruner(void *data) {
-  uw_context ctx = uw_request_new_context();
+  loggers *ls = (loggers *)data;
+  uw_context ctx = uw_request_new_context(ls->logger_data, ls->log_error, ls->log_debug);
 
   if (!ctx)
     exit(1);
--- a/src/c/urweb.c	Tue Jun 23 15:56:04 2009 -0400
+++ b/src/c/urweb.c	Tue Jun 23 17:59:23 2009 -0400
@@ -21,7 +21,21 @@
 
 int uw_really_send(int sock, const void *buf, size_t len) {
   while (len > 0) {
-    size_t n = send(sock, buf, len, 0);
+    ssize_t n = send(sock, buf, len, 0);
+
+    if (n < 0)
+      return n;
+
+    buf += n;
+    len -= n;
+  }
+
+  return 0;
+}
+
+int uw_really_write(int fd, const void *buf, size_t len) {
+  while (len > 0) {
+    ssize_t n = write(fd, buf, len);
 
     if (n < 0)
       return n;
@@ -164,7 +178,7 @@
 
 }
 
-static const char begin_msgs[] = "HTTP/1.1 200 OK\r\nContent-type: text/plain\r\n\r\n";
+static const char begin_msgs[] = "Content-type: text/plain\r\n\r\n";
 
 static client *find_client(unsigned id) {
   client *c;
@@ -182,6 +196,12 @@
   return c;
 }
 
+static char *on_success = "HTTP/1.1 200 OK\r\n";
+
+void uw_set_on_success(char *s) {
+  on_success = s;
+}
+
 void uw_client_connect(unsigned id, int pass, int sock) {
   client *c = find_client(id);
 
@@ -215,6 +235,7 @@
   c->last_contact = time(NULL);
 
   if (buf_used(&c->msgs) > 0) {
+    uw_really_send(sock, on_success, strlen(on_success));
     uw_really_send(sock, begin_msgs, sizeof(begin_msgs) - 1);
     uw_really_send(sock, c->msgs.start, buf_used(&c->msgs));
     buf_reset(&c->msgs);
@@ -227,8 +248,6 @@
 }
 
 static void free_client(client *c) {
-  printf("Freeing client %u\n", c->id);
-
   c->mode = UNUSED;
   c->pass = -1;
 
@@ -245,6 +264,7 @@
   pthread_mutex_lock(&c->lock);
 
   if (c->sock != -1) {
+    uw_really_send(c->sock, on_success, strlen(on_success));
     uw_really_send(c->sock, begin_msgs, sizeof(begin_msgs) - 1);
     uw_really_send(c->sock, msg->start, buf_used(msg));
     close(c->sock);
@@ -1068,6 +1088,20 @@
   return uw_really_send(sock, ctx->page.start, ctx->page.front - ctx->page.start);
 }
 
+int uw_print(uw_context ctx, int fd) {
+  int n = uw_really_write(fd, ctx->outHeaders.start, ctx->outHeaders.front - ctx->outHeaders.start);
+
+  if (n < 0)
+    return n;
+
+  n = uw_really_write(fd, "\r\n", 2);
+
+  if (n < 0)
+    return n;
+
+  return uw_really_write(fd, ctx->page.start, ctx->page.front - ctx->page.start);
+}
+
 static void uw_check_headers(uw_context ctx, size_t extra) {
   buf_check(&ctx->outHeaders, extra);
 }
@@ -2549,7 +2583,7 @@
         free_client(c);
       else {
         uw_db_rollback(ctx);
-        printf("Expunge blocked by error: %s\n", uw_error_message(ctx));
+        fprintf(stderr, "Expunge blocked by error: %s\n", uw_error_message(ctx));
       }
     }
     else
@@ -2664,7 +2698,8 @@
   buf_reset(&ctx->outHeaders);
   buf_reset(&ctx->page);
 
-  uw_write_header(ctx, "HTTP/1.1 200 OK\r\nContent-Type: ");
+  uw_write_header(ctx, on_success);
+  uw_write_header(ctx, "Content-Type: ");
   uw_write_header(ctx, mimeType);
   uw_write_header(ctx, "\r\nContent-Length: ");
   buf_check(&ctx->outHeaders, INTS_MAX);
--- a/src/main.mlton.sml	Tue Jun 23 15:56:04 2009 -0400
+++ b/src/main.mlton.sml	Tue Jun 23 17:59:23 2009 -0400
@@ -32,6 +32,9 @@
         doArgs (rest, (timing, SOME (prefix, false), sources))
       | "-guided-demo" :: prefix :: rest =>
         doArgs (rest, (timing, SOME (prefix, true), sources))
+      | "-protocol" :: name :: rest =>
+        (Settings.setProtocol name;
+         doArgs (rest, (timing, demo, sources)))
       | arg :: rest =>
         let
             val acc =
--- a/src/settings.sig	Tue Jun 23 15:56:04 2009 -0400
+++ b/src/settings.sig	Tue Jun 23 17:59:23 2009 -0400
@@ -93,8 +93,7 @@
     }
     val addProtocol : protocol -> unit
     val getProtocol : string -> protocol option
-
-    val setProtocol : protocol -> unit
+    val setProtocol : string -> unit
     val currentProtocol : unit -> protocol
 
 end
--- a/src/settings.sml	Tue Jun 23 15:56:04 2009 -0400
+++ b/src/settings.sml	Tue Jun 23 17:59:23 2009 -0400
@@ -266,9 +266,15 @@
             supportsPush = true}
 
 val () = addProtocol http
+val () = addProtocol {name = "cgi",
+                      link = clibFile "request.o" ^ " " ^ clibFile "cgi.o",
+                      supportsPush = false}
 
 val curProto = ref http
-fun setProtocol p = curProto := p
+fun setProtocol name =
+    case getProtocol name of
+        NONE => raise Fail ("Unknown protocol " ^ name)
+      | SOME p => curProto := p
 fun currentProtocol () = !curProto
 
 end