changeset 1347:b106ca8200b1

postBody type
author Adam Chlipala <adam@chlipala.net>
date Sat, 18 Dec 2010 10:56:31 -0500
parents faad7d01b200
children 8a169fc0838b
files doc/manual.tex include/types.h include/urweb.h lib/ur/basis.urs src/c/request.c src/c/urweb.c src/cjr_print.sml src/corify.sml src/effectize.sml src/elaborate.sml src/export.sig src/export.sml src/marshalcheck.sml tests/post.ur tests/post.urp tests/post.urs
diffstat 16 files changed, 150 insertions(+), 59 deletions(-) [+]
line wrap: on
line diff
--- a/doc/manual.tex	Thu Dec 16 18:40:49 2010 -0500
+++ b/doc/manual.tex	Sat Dec 18 10:56:31 2010 -0500
@@ -2077,6 +2077,8 @@
 
 A web application is built from a series of modules, with one module, the last one appearing in the \texttt{.urp} file, designated as the main module.  The signature of the main module determines the URL entry points to the application.  Such an entry point should have type $\mt{t1} \to \ldots \to \mt{tn} \to \mt{transaction} \; \mt{page}$, for any integer $n \geq 0$, where $\mt{page}$ is a type synonym for top-level HTML pages, defined in $\mt{Basis}$.  If such a function is at the top level of main module $M$, with $n = 0$, it will be accessible at URI \texttt{/M/f}, and so on for more deeply-nested functions, as described in Section \ref{tag} below.  Arguments to an entry-point function are deserialized from the part of the URI following \texttt{f}.
 
+Normal links are accessible via HTTP \texttt{GET}, which the relevant standard says should never cause side effects.  To export a page which may cause side effects, accessible only via HTTP \texttt{POST}, include one argument of the page handler of type $\mt{Basis.postBody}$.  When the handler is called, this argument will receive a value that can be deconstructed into a MIME type (with $\mt{Basis.postType}$) and payload (with $\mt{Basis.postData}$).  This kind of handler will only work with \texttt{POST} payloads of MIME types besides those associated with HTML forms; for these, use Ur/Web's built-in support, as described below.
+
 When the standalone web server receives a request for a known page, it calls the function for that page, ``running'' the resulting transaction to produce the page to return to the client.  Pages link to other pages with the \texttt{link} attribute of the \texttt{a} HTML tag.  A link has type $\mt{transaction} \; \mt{page}$, and the semantics of a link are that this transaction should be run to compute the result page, when the link is followed.  Link targets are assigned URL names in the same way as top-level entry points.
 
 HTML forms are handled in a similar way.  The $\mt{action}$ attribute of a $\mt{submit}$ form tag takes a value of type $\$\mt{use} \to \mt{transaction} \; \mt{page}$, where $\mt{use}$ is a kind-$\{\mt{Type}\}$ record of the form fields used by this action handler.  Action handlers are assigned URL patterns in the same way as above.
@@ -2087,7 +2089,7 @@
 
 \medskip
 
-The HTTP standard suggests that GET requests only be used in ways that generate no side effects.  Side effecting operations should use POST requests instead.  The Ur/Web compiler enforces this rule strictly, via a simple conservative program analysis.  Any page that may have a side effect must be accessed through a form, all of which use POST requests.  A page is judged to have a side effect if its code depends syntactically on any of the side-effecting, server-side FFI functions.  Links, forms, and most client-side event handlers are not followed during this syntactic traversal, but \texttt{<body onload=\{...\}>} handlers \emph{are} examined, since they run right away and could just as well be considered parts of main page handlers.
+The HTTP standard suggests that GET requests only be used in ways that generate no side effects.  Side effecting operations should use POST requests instead.  The Ur/Web compiler enforces this rule strictly, via a simple conservative program analysis.  Any page that may have a side effect must be accessed through a form, all of which use POST requests, or via a direct call to a page handler with some argument of type $\mt{Basis.postBody}$.  A page is judged to have a side effect if its code depends syntactically on any of the side-effecting, server-side FFI functions.  Links, forms, and most client-side event handlers are not followed during this syntactic traversal, but \texttt{<body onload=\{...\}>} handlers \emph{are} examined, since they run right away and could just as well be considered parts of main page handlers.
 
 Ur/Web includes a kind of automatic protection against cross site request forgery attacks.  Whenever any page execution can have side effects and can also read at least one cookie value, all cookie values must be signed cryptographically, to ensure that the user has come to the current page by submitting a form on a real page generated by the proper server.  Signing and signature checking are inserted automatically by the compiler.  This prevents attacks like phishing schemes where users are directed to counterfeit pages with forms that submit to your application, where a user's cookies might be submitted without his knowledge, causing some undesired side effect.
 
--- a/include/types.h	Thu Dec 16 18:40:49 2010 -0500
+++ b/include/types.h	Sat Dec 18 10:56:31 2010 -0500
@@ -40,6 +40,10 @@
   uw_Basis_blob data;
 } uw_Basis_file;
 
+typedef struct uw_Basis_postBody {
+  uw_Basis_string type, data;
+} uw_Basis_postBody;
+
 typedef enum { SUCCESS, FATAL, BOUNDED_RETRY, UNLIMITED_RETRY, RETURN_INDIRECTLY } failure_kind;
 
 typedef enum { SERVED, KEEP_OPEN, FAILED } request_result;
--- a/include/urweb.h	Thu Dec 16 18:40:49 2010 -0500
+++ b/include/urweb.h	Sat Dec 18 10:56:31 2010 -0500
@@ -221,6 +221,13 @@
 uw_Basis_int uw_Basis_blobSize(uw_context, uw_Basis_blob);
 uw_Basis_blob uw_Basis_textBlob(uw_context, uw_Basis_string);
 
+uw_Basis_string uw_Basis_postType(uw_context, uw_Basis_postBody);
+uw_Basis_string uw_Basis_postData(uw_context, uw_Basis_postBody);
+void uw_noPostBody(uw_context);
+void uw_postBody(uw_context, uw_Basis_postBody);
+int uw_hasPostBody(uw_context);
+uw_Basis_postBody uw_getPostBody(uw_context);
+
 __attribute__((noreturn)) void uw_return_blob(uw_context, uw_Basis_blob, uw_Basis_string mimeType);
 __attribute__((noreturn)) void uw_redirect(uw_context, uw_Basis_string url);
 
--- a/lib/ur/basis.urs	Thu Dec 16 18:40:49 2010 -0500
+++ b/lib/ur/basis.urs	Sat Dec 18 10:56:31 2010 -0500
@@ -737,6 +737,10 @@
 val blobSize : blob -> int
 val textBlob : string -> blob
 
+type postBody
+val postType : postBody -> string
+val postData : postBody -> string
+
 con radio = [Body, Radio]
 val radio : formTag string radio [Id = string]
 val radioOption : unit -> tag ([Value = string, Checked = bool] ++ boxAttrs) radio [] [] []
--- a/src/c/request.c	Thu Dec 16 18:40:49 2010 -0500
+++ b/src/c/request.c	Sat Dec 18 10:56:31 2010 -0500
@@ -192,6 +192,9 @@
       boundary[0] = '-';
       boundary[1] = '-';
       boundary_len = strlen(boundary);
+    } else if (clen_s && strcasecmp(clen_s, "application/x-www-form-urlencoded")) {
+      uw_Basis_postBody pb = {clen_s, body};
+      uw_postBody(ctx, pb);
     }
   } else if (strcmp(method, "GET")) {
     log_error(logger_data, "Not ready for non-GET/POST command: %s\n", method);
@@ -325,7 +328,7 @@
       }
     }
   }
-  else {
+  else if (!uw_hasPostBody(ctx)) {
     inputs = is_post ? body : query_string;
 
     if (inputs) {
--- a/src/c/urweb.c	Thu Dec 16 18:40:49 2010 -0500
+++ b/src/c/urweb.c	Sat Dec 18 10:56:31 2010 -0500
@@ -445,6 +445,9 @@
   void *logger_data;
   uw_logger log_debug;
 
+  int hasPostBody;
+  uw_Basis_postBody postBody;
+
   char error_message[ERROR_BUF_LEN];
 };
 
@@ -507,6 +510,8 @@
   ctx->logger_data = logger_data;
   ctx->log_debug = log_debug;
 
+  ctx->hasPostBody = 0;
+
   return ctx;
 }
 
@@ -583,6 +588,7 @@
   ctx->cur_container = NULL;
   ctx->used_transactionals = 0;
   ctx->script_header = "";
+  ctx->hasPostBody = 0;
 }
 
 void uw_reset_keep_request(uw_context ctx) {
@@ -3200,6 +3206,14 @@
   return f.data;
 }
 
+uw_Basis_string uw_Basis_postType(uw_context ctx, uw_Basis_postBody pb) {
+  return pb.type;
+}
+
+uw_Basis_string uw_Basis_postData(uw_context ctx, uw_Basis_postBody pb) {
+  return pb.data;
+}
+
 __attribute__((noreturn)) void uw_return_blob(uw_context ctx, uw_Basis_blob b, uw_Basis_string mimeType) {
   cleanup *cl;
   int len;
@@ -3458,3 +3472,23 @@
   uw_Basis_int n = abs(rand());
   return n;
 }
+
+void uw_noPostBody(uw_context ctx) {
+  ctx->hasPostBody = 0;
+}
+
+void uw_postBody(uw_context ctx, uw_Basis_postBody pb) {
+  ctx->hasPostBody = 1;
+  ctx->postBody = pb;
+}
+
+int uw_hasPostBody(uw_context ctx) {
+  return ctx->hasPostBody;
+}
+
+uw_Basis_postBody uw_getPostBody(uw_context ctx) {
+  if (ctx->hasPostBody)
+    return ctx->postBody;
+  else
+    uw_error(ctx, FATAL, "Asked for POST body when none exists");
+}
--- a/src/cjr_print.sml	Thu Dec 16 18:40:49 2010 -0500
+++ b/src/cjr_print.sml	Sat Dec 18 10:56:31 2010 -0500
@@ -2246,22 +2246,21 @@
 
         val fields = foldl (fn ((ek, _, _, ts, _, _, _), fields) =>
                                case ek of
-                                   Link => fields
-                                 | Rpc _ => fields
-                                 | Action eff =>
-                                   case List.nth (ts, length ts - 2) of
-                                       (TRecord i, loc) =>
-                                       let
-                                           val xts = E.lookupStruct env i
-                                           val extra = case eff of
-                                                           ReadCookieWrite => [sigName xts]
-                                                       | _ => []
-                                       in
-                                           case flatFields extra (TRecord i, loc) of
-                                               NONE => raise Fail "CjrPrint: flatFields impossible"
-                                             | SOME fields' => List.revAppend (fields', fields)
-                                       end
-                                     | _ => raise Fail "CjrPrint: Last argument of action isn't record")
+                                   Action eff =>
+                                   (case List.nth (ts, length ts - 2) of
+                                        (TRecord i, loc) =>
+                                        let
+                                            val xts = E.lookupStruct env i
+                                            val extra = case eff of
+                                                            ReadCookieWrite => [sigName xts]
+                                                          | _ => []
+                                        in
+                                            case flatFields extra (TRecord i, loc) of
+                                                NONE => raise Fail "CjrPrint: flatFields impossible"
+                                              | SOME fields' => List.revAppend (fields', fields)
+                                        end
+                                      | _ => raise Fail "CjrPrint: Last argument of action isn't record")
+                                 | _ => fields)
                            [] ps
 
         val fields = foldl (fn (xts, fields) =>
@@ -2544,49 +2543,49 @@
             let
                 val (ts, defInputs, inputsVar, fields) =
                     case ek of
-                        Core.Link => (List.take (ts, length ts - 1), string "", string "", NONE)
-                      | Core.Rpc _ => (List.take (ts, length ts - 1), string "", string "", NONE)
-                      | Core.Action _ =>
-                        case List.nth (ts, length ts - 2) of
-                            (TRecord i, _) =>
-                            let
-                                val xts = E.lookupStruct env i
-                            in
-                                (List.take (ts, length ts - 2),
-                                 box [box (map (fn (x, t) => box [p_typ env t,
-                                                                  space,
-                                                                  string "uw_input_",
-                                                                  p_ident x,
-                                                                  string ";",
-                                                                  newline]) xts),
-                                      newline,
-                                      box (map getInput xts),
-                                      string "struct __uws_",
-                                      string (Int.toString i),
-                                      space,
-                                      string "uw_inputs",
-                                      space,
-                                      string "= {",
-                                      newline,
-                                      box (map (fn (x, _) => box [string "uw_input_",
-                                                                  p_ident x,
-                                                                  string ",",
-                                                                  newline]) xts),
-                                      string "};",
-                                      newline],
-                                 box [string ",",
-                                      space,
-                                      string "uw_inputs"],
-                                 SOME xts)
-                            end
+                        Core.Action _ =>
+                        (case List.nth (ts, length ts - 2) of
+                             (TRecord i, _) =>
+                             let
+                                 val xts = E.lookupStruct env i
+                             in
+                                 (List.take (ts, length ts - 2),
+                                  box [box (map (fn (x, t) => box [p_typ env t,
+                                                                   space,
+                                                                   string "uw_input_",
+                                                                   p_ident x,
+                                                                   string ";",
+                                                                   newline]) xts),
+                                       newline,
+                                       box (map getInput xts),
+                                       string "struct __uws_",
+                                       string (Int.toString i),
+                                       space,
+                                       string "uw_inputs",
+                                       space,
+                                       string "= {",
+                                       newline,
+                                       box (map (fn (x, _) => box [string "uw_input_",
+                                                                   p_ident x,
+                                                                   string ",",
+                                                                   newline]) xts),
+                                       string "};",
+                                       newline],
+                                  box [string ",",
+                                       space,
+                                       string "uw_inputs"],
+                                  SOME xts)
+                             end
 
-                          | _ => raise Fail "CjrPrint: Last argument to an action isn't a record"
+                           | _ => raise Fail "CjrPrint: Last argument to an action isn't a record")
+                      | _ => (List.take (ts, length ts - 1), string "", string "", NONE)
 
                 fun couldWrite ek =
                     case ek of
                         Link => false
                       | Action ef => ef = ReadCookieWrite
                       | Rpc ef => ef = ReadCookieWrite
+                      | Extern ef => ef = ReadCookieWrite
 
                 val s =
                     case Settings.getUrlPrefix () of
@@ -2693,7 +2692,9 @@
                                                                 space,
                                                                 string "=",
                                                                 space,
-                                                                unurlify false env t,
+                                                                case #1 t of
+                                                                    TFfi ("Basis", "postBody") => string "uw_getPostBody(ctx)"
+                                                                  | _ => unurlify false env t,
                                                                 string ";",
                                                                 newline]) ts),
                           defInputs,
--- a/src/corify.sml	Thu Dec 16 18:40:49 2010 -0500
+++ b/src/corify.sml	Sat Dec 18 10:56:31 2010 -0500
@@ -1011,11 +1011,19 @@
                                                                                          t, tf, e), loc),
                                                                                 (L.TFun (t, tf), loc)))
                                                            ((L.EApp (ef, ea), loc), ranT) args
+
+                                             val expKind = if List.exists (fn t =>
+                                                                              case corifyCon st t of
+                                                                                  (L'.CFfi ("Basis", "postBody"), _) => true
+                                                                                | _ => false) args then
+                                                               L'.Extern L'.ReadCookieWrite
+                                                           else
+                                                               L'.Link
                                          in
                                              ((L.DVal ("wrap_" ^ s, 0, tf, e), loc) :: wds,
                                               (fn st =>
                                                   case #1 (corifyExp st (L.EModProj (en, [], "wrap_" ^ s), loc)) of
-                                                      L'.ENamed n => (L'.DExport (L'.Link, n, false), loc)
+                                                      L'.ENamed n => (L'.DExport (expKind, n, false), loc)
                                                     | _ => raise Fail "Corify: Value to export didn't corify properly")
                                               :: eds)
                                          end
--- a/src/effectize.sml	Thu Dec 16 18:40:49 2010 -0500
+++ b/src/effectize.sml	Sat Dec 18 10:56:31 2010 -0500
@@ -168,6 +168,15 @@
                                 else
                                     ReadOnly), n, IM.inDomain (pushers, n)), #2 d),
                  evs)
+              | DExport (Extern _, n, _) =>
+                ((DExport (Extern (if IM.inDomain (writers, n) then
+                                       if IM.inDomain (readers, n) then
+                                           ReadCookieWrite
+                                       else
+                                           ReadWrite
+                                   else
+                                       ReadOnly), n, IM.inDomain (pushers, n)), #2 d),
+                 evs)
               | _ => (d, evs)
 
         val (file, _) = ListUtil.foldlMap doDecl (IM.empty, IM.empty, IM.empty) file
--- a/src/elaborate.sml	Thu Dec 16 18:40:49 2010 -0500
+++ b/src/elaborate.sml	Sat Dec 18 10:56:31 2010 -0500
@@ -3834,8 +3834,14 @@
                                                                                       (L'.CModProj
                                                                                            (basis, [], "transaction"), loc),
                                                                                       t), loc)
+
+                                                                             fun normArgs t =
+                                                                                 case hnormCon env t of
+                                                                                     (L'.TFun (dom, ran), loc) =>
+                                                                                     (L'.TFun (hnormCon env dom, normArgs ran), loc)
+                                                                                   | t' => t'
                                                                          in
-                                                                             (L'.SgiVal (x, n, makeRes t), loc)
+                                                                             (L'.SgiVal (x, n, normArgs (makeRes t)), loc)
                                                                          end
                                                                        | _ => all)
                                                                   | _ => all)
--- a/src/export.sig	Thu Dec 16 18:40:49 2010 -0500
+++ b/src/export.sig	Sat Dec 18 10:56:31 2010 -0500
@@ -36,6 +36,7 @@
          Link
        | Action of effect
        | Rpc of effect
+       | Extern of effect
 
 val p_effect : effect Print.printer
 val p_export_kind : export_kind Print.printer
--- a/src/export.sml	Thu Dec 16 18:40:49 2010 -0500
+++ b/src/export.sml	Sat Dec 18 10:56:31 2010 -0500
@@ -39,6 +39,7 @@
          Link
        | Action of effect
        | Rpc of effect
+       | Extern of effect
 
 fun p_effect ef =
     case ef of
@@ -51,5 +52,6 @@
         Link => string "link"
       | Action ef => box [string "action(", p_effect ef, string ")"]
       | Rpc ef => box [string "rpc(", p_effect ef, string ")"]
+      | Extern ef => box [string "extern(", p_effect ef, string ")"]
 
 end
--- a/src/marshalcheck.sml	Thu Dec 16 18:40:49 2010 -0500
+++ b/src/marshalcheck.sml	Sat Dec 18 10:56:31 2010 -0500
@@ -1,4 +1,4 @@
-(* Copyright (c) 2009, Adam Chlipala
+(* Copyright (c) 2009-2010, Adam Chlipala
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -96,7 +96,10 @@
                                    let
                                        fun makeS (t, _) =
                                            case t of
-                                               TFun (dom, ran) => PS.union (sins cmap dom, makeS ran)
+                                               TFun (dom, ran) =>
+                                               (case #1 dom of
+                                                    CFfi ("Basis", "postBody") => makeS ran
+                                                  | _ => PS.union (sins cmap dom, makeS ran))
                                              | _ => PS.empty
                                        val s = makeS t
                                    in
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/post.ur	Sat Dec 18 10:56:31 2010 -0500
@@ -0,0 +1,5 @@
+fun callMe n s pb = return <xml><body>
+  n = {[n]}<br/>
+  s = {[s]}<br/>
+  pb : {[postType pb]} = {[postData pb]}
+</body></xml>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/post.urp	Sat Dec 18 10:56:31 2010 -0500
@@ -0,0 +1,1 @@
+post
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/post.urs	Sat Dec 18 10:56:31 2010 -0500
@@ -0,0 +1,1 @@
+val callMe : int -> string -> postBody -> transaction page