Add support for authenticated uploads from browser-based web apps using form POST...
authorpastith <devnull@localhost>
Thu, 5 Mar 2009 14:45:19 +0000 (14:45 +0000)
committerpastith <devnull@localhost>
Thu, 5 Mar 2009 14:45:19 +0000 (14:45 +0000)
gss/src/gr/ebs/gss/server/rest/FilesHandler.java
gss/src/gr/ebs/gss/server/rest/RequestHandler.java
gss/test/rest-api-test.html

index b791628..cf8e7c0 100644 (file)
@@ -66,6 +66,9 @@ import org.apache.commons.fileupload.FileItemStream;
 import org.apache.commons.fileupload.FileUploadException;
 import org.apache.commons.fileupload.ProgressListener;
 import org.apache.commons.fileupload.servlet.ServletFileUpload;
+import org.apache.commons.fileupload.util.Streams;
+import org.apache.commons.httpclient.util.DateParseException;
+import org.apache.commons.httpclient.util.DateUtil;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.json.JSONArray;
@@ -93,6 +96,16 @@ public class FilesHandler extends RequestHandler {
        private static final int TRACK_PROGRESS_PERCENT = 5;
 
        /**
+        * The form parameter name that contains the signature in a browser POST upload.
+        */
+       private static final String AUTHORIZATION_PARAMETER = "Authorization";
+
+       /**
+        * The form parameter name that contains the date in a browser POST upload.
+        */
+       private static final String DATE_PARAMETER = "Date";
+
+       /**
         * The logger.
         */
        private static Log logger = LogFactory.getLog(FilesHandler.class);
@@ -414,13 +427,24 @@ public class FilesHandler extends RequestHandler {
      * @exception IOException if an input/output error occurs
         */
        void postResource(HttpServletRequest req, HttpServletResponse resp) throws IOException {
-       if (req.getParameterMap().size() > 1) {
+               boolean authDeferred = getAuthDeferred(req);
+       if (!authDeferred && req.getParameterMap().size() > 1) {
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
                return;
        }
         String path = getInnerPath(req, PATH_FILES);
        path = path.endsWith("/")? path: path + '/';
        path = URLDecoder.decode(path, "UTF-8");
+       // We only defer authenticating multipart POST requests.
+       if (authDeferred) {
+                       if (!ServletFileUpload.isMultipartContent(req)) {
+                       resp.sendError(HttpServletResponse.SC_FORBIDDEN);
+                       return;
+               }
+                       handleMultipart(req, resp, path);
+                       return;
+               }
+
        String newName = req.getParameter(NEW_FOLDER_PARAMETER);
        boolean hasUpdateParam = req.getParameterMap().containsKey(RESOURCE_UPDATE_PARAMETER);
        boolean hasTrashParam = req.getParameterMap().containsKey(RESOURCE_TRASH_PARAMETER);
@@ -440,8 +464,6 @@ public class FilesHandler extends RequestHandler {
                        copyResource(req, resp, path, copyTo);
                else if (moveTo != null)
                        moveResource(req, resp, path, moveTo);
-               else if (ServletFileUpload.isMultipartContent(req))
-                       handleMultipart(req, resp, path);
                else
                        resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
        }
@@ -460,7 +482,6 @@ public class FilesHandler extends RequestHandler {
        if (logger.isDebugEnabled())
                        logger.debug("Multipart POST for resource: " + path);
 
-       User user = getUser(request);
        User owner = getOwner(request);
        boolean exists = true;
         Object resource = null;
@@ -510,10 +531,72 @@ public class FilesHandler extends RequestHandler {
                        StatusProgressListener progressListener = new StatusProgressListener(getService());
                        upload.setProgressListener(progressListener);
                        iter = upload.getItemIterator(request);
+                       String dateParam = null;
+                       String auth = null;
                        while (iter.hasNext()) {
                                FileItemStream item = iter.next();
+                               String name = item.getFieldName();
                                InputStream stream = item.openStream();
-                               if (!item.isFormField()) {
+                               if (item.isFormField()) {
+                                       final String value = Streams.asString(stream);
+                                       if (name.equals(DATE_PARAMETER))
+                                               dateParam = value;
+                                       else if (name.equals(AUTHORIZATION_PARAMETER))
+                                               auth = value;
+
+                                       if (logger.isDebugEnabled())
+                                               logger.debug(name + ":" + value);
+                               } else {
+                                       // Fetch the timestamp used to guard against replay attacks.
+                               if (dateParam == null) {
+                                       response.sendError(HttpServletResponse.SC_FORBIDDEN, "No Date parameter");
+                                       return;
+                               }
+
+                               long timestamp;
+                                       try {
+                                               timestamp = DateUtil.parseDate(dateParam).getTime();
+                                       } catch (DateParseException e) {
+                                       response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
+                                       return;
+                                       }
+                               if (!isTimeValid(timestamp)) {
+                                       response.sendError(HttpServletResponse.SC_FORBIDDEN);
+                                       return;
+                               }
+
+                                       // Fetch the Authorization parameter and find the user specified in it.
+                               if (auth == null) {
+                                       response.sendError(HttpServletResponse.SC_FORBIDDEN, "No Authorization parameter");
+                                       return;
+                               }
+                                       String[] authParts = auth.split(" ");
+                                       if (authParts.length != 2) {
+                                       response.sendError(HttpServletResponse.SC_FORBIDDEN);
+                                       return;
+                               }
+                                       String username = authParts[0];
+                                       String signature = authParts[1];
+                                       User user = null;
+                                       try {
+                                               user = getService().findUser(username);
+                                       } catch (RpcException e) {
+                                       response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
+                                               return;
+                                       }
+                                       if (user == null) {
+                                       response.sendError(HttpServletResponse.SC_FORBIDDEN);
+                                       return;
+                               }
+                                       request.setAttribute(USER_ATTRIBUTE, user);
+
+                                       // Validate the signature in the Authorization parameter.
+                                       String data = request.getMethod() + dateParam + URLEncoder.encode(request.getPathInfo(), "UTF-8");
+                                       if (!isSignatureValid(signature, user, data)) {
+                                       response.sendError(HttpServletResponse.SC_FORBIDDEN);
+                                       return;
+                               }
+
                                        progressListener.setUserId(user.getId());
                                        progressListener.setFilename(fileName);
                                        String contentType = item.getContentType();
index 3d4089d..f545cf2 100644 (file)
@@ -199,7 +199,8 @@ public class RequestHandler extends Webdav {
             logger.debug("[" + method + "] " + path);
 
                if (!isRequestValid(request)) {
-                       if (!method.equals(METHOD_GET) && !method.equals(METHOD_HEAD)) {
+                       if (!method.equals(METHOD_GET) && !method.equals(METHOD_HEAD) &&
+                                               !method.equals(METHOD_POST)) {
                                response.sendError(HttpServletResponse.SC_FORBIDDEN);
                                return;
                        }
@@ -403,14 +404,26 @@ public class RequestHandler extends Webdav {
 
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+               boolean authDeferred = getAuthDeferred(req);
        // Strip the username part
        String path;
                try {
                        path = getUserPath(req);
                } catch (ObjectNotFoundException e) {
+                       if (authDeferred) {
+                               // We do not want to leak information if the request
+                               // was not authenticated.
+                               resp.sendError(HttpServletResponse.SC_FORBIDDEN);
+                               return;
+                       }
                        resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
                        return;
                }
+               if (authDeferred && !path.startsWith(PATH_FILES)) {
+                       // Only POST to files may be authenticated without an Authorization header.
+                       resp.sendError(HttpServletResponse.SC_FORBIDDEN);
+                       return;
+               }
 
                if (path.startsWith(PATH_OTHERS)) {
             resp.addHeader("Allow", methodsAllowed.get(PATH_OTHERS));
@@ -567,14 +580,10 @@ public class RequestHandler extends Webdav {
                } catch (IllegalArgumentException e) {
                        return false;
                }
-               if (timestamp == -1)
-                       return false;
-               Calendar cal = Calendar.getInstance();
-               if (logger.isDebugEnabled())
-                       logger.debug("Time: server=" + cal.getTimeInMillis() + ", client=" + timestamp);
-               // Ignore the request if the timestamp is too far off.
-               if (Math.abs(timestamp - cal.getTimeInMillis()) > TIME_SKEW)
+               if (!isTimeValid(timestamp))
                        return false;
+
+               // Fetch the Authorization header and find the user specified in it.
                String auth = request.getHeader(AUTHORIZATION_HEADER);
                String[] authParts = auth.split(" ");
                if (authParts.length != 2)
@@ -591,14 +600,30 @@ public class RequestHandler extends Webdav {
                        return false;
 
                request.setAttribute(USER_ATTRIBUTE, user);
+
+               // Validate the signature in the Authorization header.
                String dateHeader = useGssDateHeader? request.getHeader(GSS_DATE_HEADER):
-                               request.getHeader(DATE_HEADER);
+                       request.getHeader(DATE_HEADER);
                String data;
                try {
                        data = request.getMethod() + dateHeader + URLEncoder.encode(request.getPathInfo(), "UTF-8");
                } catch (UnsupportedEncodingException e) {
                        throw new RuntimeException(e);
                }
+               return isSignatureValid(signature, user, data);
+       }
+
+       /**
+        * Calculates the signature for the specified data String and then
+        * compares it against the provided signature. If the signatures match,
+        * the method returns true. Otherwise it returns false.
+        *
+        * @param signature the signature to compare against
+        * @param user the current user
+        * @param data the data to sign
+        * @return true if the calculated signature matches the supplied one
+        */
+       protected boolean isSignatureValid(String signature, User user, String data) {
                if (logger.isDebugEnabled())
                        logger.debug("server pre-signing data: "+data);
                String serverSignature = null;
@@ -627,6 +652,27 @@ public class RequestHandler extends Webdav {
                return true;
        }
 
+       /**
+        * A helper method that checks if the timestamp of the request
+        * is within TIME_SKEW milliseconds of the current time. If
+        * the timestamp is older (or even newer) than that, it is
+        * considered invalid.
+        *
+        * @param timestamp the time of the request
+        * @return true if the timestamp is valid
+        */
+       protected boolean isTimeValid(long timestamp) {
+               if (timestamp == -1)
+                       return false;
+               Calendar cal = Calendar.getInstance();
+               if (logger.isDebugEnabled())
+                       logger.debug("Time: server=" + cal.getTimeInMillis() + ", client=" + timestamp);
+               // Ignore the request if the timestamp is too far off.
+               if (Math.abs(timestamp - cal.getTimeInMillis()) > TIME_SKEW)
+                       return false;
+               return true;
+       }
+
        protected boolean getAuthDeferred(HttpServletRequest req) {
                Boolean attr = (Boolean) req.getAttribute(AUTH_DEFERRED_ATTR);
                return attr == null? false: attr;
index a852a55..58dffa5 100644 (file)
@@ -17,6 +17,7 @@ function send() {
        var file = document.getElementById("file").value;\r
        var form = document.getElementById("form").value;\r
        var update = document.getElementById("update").value;\r
+       var formfile = document.getElementById('formfile');\r
        var params = null;\r
        var now = (new Date()).toUTCString();\r
        var q = resource.indexOf('?');\r
@@ -28,6 +29,22 @@ function send() {
        else if (update)\r
                params = update;\r
 \r
+       // Browser upload with POST.\r
+       if (formfile.value) {\r
+               var formdate = document.getElementById('formdate');\r
+               var formauth = document.getElementById('formauth');\r
+               res = resource+formfile.value;\r
+               data = 'POST' + now + encodeURIComponent(decodeURIComponent(res));\r
+               sig = b64_hmac_sha1(atob(token), data);\r
+               formauth.value = user + " " + sig;\r
+               formdate.value = now;\r
+               var upload = document.upload;\r
+               upload.action = '/gss/rest'+res;\r
+               upload.submit();\r
+               return;\r
+       }\r
+\r
+       // All other API operations.\r
        var req = new XMLHttpRequest();\r
        req.open(method, '/gss/rest'+resource, true);\r
        req.onreadystatechange = function (event) {\r
@@ -75,6 +92,12 @@ function send() {
 <tr><td>POST form </td><td><input id="form"></td></tr>\r
 <tr><td>POST JSON update </td><td><input id="update"></td></tr>\r
 </table>\r
+<form id="upload" name="upload" method="post" action="/gss/rest" enctype="multipart/form-data">\r
+<input id="formdate" type="hidden" name="Date" value="">\r
+<input id="formauth" type="hidden" name="Authorization" value="">\r
+File upload<input id="formfile" type="file" name="formfile">\r
+<input type="submit">\r
+</form>\r
 <button onclick="send()">send</button><br>\r
 <div id="result" style="width: 200px; height: 200px"></div>\r
 </body>\r