Search code examples
phpcorsfine-uploader

Fine Uploader Invalid Response


Current Page: http://www.typhooncloud.com/fineuploader

When attempting to upload a simple jpg file, i receive the error:

'Invalid policy document or request headers!'

Here is my index.html, s3handler.php, CORS policy and console output after attempting an upload.

All changes that had been recommended have been made.

HTML:

    <!DOCTYPE html>
    <head>
        <link href="http://typhooncloud.com/fineuploader/fineuploader-3.9.0-3.css" rel="stylesheet">
    </head> 
    <body>

        <!-- The element where Fine Uploader will exist. -->
        <div id="fine-uploader">
       </div>

        <!-- jQuery version 1.10.x (if you are using the jQuery plugin -->
        <script src="http://code.jquery.com/jquery-1.10.2.min.js" type="text/javascript"></script>

        <!-- Fine Uploader-jQuery -->
        <script src="http://typhooncloud.com/fineuploader/s3.jquery.fineuploader-3.9.0-3.js"></script>

        <script type="text/javascript">
            $(document).ready(function ()
            {
                $('#fine-uploader').fineUploaderS3({
                    debug: true,

                    request: {
                        endpoint: "getalink1001.s3.amazonaws.com",
                        accessKey: "MYACCESSKEY"
                    },
                    signature: {
                        endpoint: "http://typhooncloud.com/fineuploader/s3handler.php"
                    },
                    uploadSuccess: {
                        endpoint: "http://typhooncloud.com/fineuploader/s3handler.php?success"
                    },
                    iframeSupport: {
                        localBlankPagePath: "http://typhooncloud.com/fineuploader/success.html"
                    },
                    validation: {
                        allowedExtensions: ["gif", "jpeg", "jpg", "png"],
                        acceptFiles: "image/gif, image/jpeg, image/png",
                        sizeLimit: 5000000,
                        itemLimit: 3
                    },
                    retry: {
                        showButton: true
                    },
                    chunking: {
                        enabled: true
                    },
                    resume: {
                        enabled: true
                    },
                    deleteFile: {
                        enabled: true,
                        method: "POST",
                        endpoint: "http://typhooncloud.com/fineuploader/s3handler.php"
                    },
                    paste: {
                        targetElement: $(document),
                        promptForName: true
                    }
                });
            });
        </script>

    </body>
    </html>

SERVER:

    <?php
    /**
     * PHP Server-Side Example for Fine Uploader S3.
     * Maintained by Widen Enterprises.
     *
     * Note: This is the exact server-side code used by the S3 example
     * on fineuploader.com.
     *
     * This example:
     *  - handles both CORS and non-CORS environments
     *  - handles delete file requests for both DELETE and POST methods
     *  - Performs basic inspections on the policy documents and REST headers before signing them
     *  - Ensures again the file size does not exceed the max (after file is in S3)
     *  - signs policy documents (simple uploads) and REST requests
     *    (chunked/multipart uploads)
     *
     * Requirements:
     *  - PHP 5.3 or newer
     *  - Amazon PHP SDK (only if utilizing the AWS SDK for deleting files or otherwise examining them)
     *
     * If you need to install the AWS SDK, see http://docs.aws.amazon.com/aws-sdk-php-2/guide/latest/installation.html.
     */

    // You can remove these two lines if you are not using Fine Uploader's
    // delete file feature
    require("aws/aws-autoloader.php");

    use Aws\S3\S3Client;


    // These assume you have the associated AWS keys stored in
    // the associated system environment variables
    $clientPrivateKey = $_SERVER['MYSECRETKEY'];
    // These two keys are only needed if the delete file feature is enabled
    // or if you are, for example, confirming the file size in a successEndpoint
    // handler via S3's SDK, as we are doing in this example.
    $serverPublicKey = $_SERVER['MYPRIVATEKEY'];
    $serverPrivateKey = $_SERVER['MYSECRETKEY'];

    // The following variables are used when validating the policy document
    // sent by the uploader: 
    $expectedBucketName = "getalink1001";
    // $expectedMaxSize is the value you set the sizeLimit property of the 
    // validation option. We assume it is `null` here. If you are performing
    // validation, then change this to match the integer value you specified
    // otherwise your policy document will be invalid.
    // http://docs.fineuploader.com/branch/develop/api/options.html#validation-option
    $expectedMaxSize = 15000000;

    $method = getRequestMethod();

    // This first conditional will only ever evaluate to true in a
    // CORS environment
    if ($method == 'OPTIONS') {
        handlePreflight();
    }
    // This second conditional will only ever evaluate to true if
    // the delete file feature is enabled
    else if ($method == "DELETE") {
        handleCorsRequest(); // only needed in a CORS environment
        deleteObject();
    }
    // This is all you really need if not using the delete file feature
    // and not working in a CORS environment
    else if ($method == 'POST') {
        handleCorsRequest();

        // Assumes the successEndpoint has a parameter of "success" associated with it,
        // to allow the server to differentiate between a successEndpoint request
        // and other POST requests (all requests are sent to the same endpoint in this example).
        // This condition is not needed if you don't require a callback on upload success.
        if (isset($_REQUEST["success"])) {
            verifyFileInS3();
        }
        else {
            signRequest();
        }
    }

    // This will retrieve the "intended" request method.  Normally, this is the
    // actual method of the request.  Sometimes, though, the intended request method
    // must be hidden in the parameters of the request.  For example, when attempting to
    // send a DELETE request in a cross-origin environment in IE9 or older, it is not
    // possible to send a DELETE request.  So, we send a POST with the intended method,
    // DELETE, in a "_method" parameter.
    function getRequestMethod() {
        global $HTTP_RAW_POST_DATA;

        // This should only evaluate to true if the Content-Type is undefined
        // or unrecognized, such as when XDomainRequest has been used to
        // send the request.
        if(isset($HTTP_RAW_POST_DATA)) {
          parse_str($HTTP_RAW_POST_DATA, $_POST);
        }

        if ($_POST['_method'] != null) {
            return $_POST['_method'];
        }

        return $_SERVER['REQUEST_METHOD'];
    }

    // Only needed in cross-origin setups
    function handleCorsRequest() {
        // If you are relying on CORS, you will need to adjust the allowed domain here.
        header('Access-Control-Allow-Origin: http://typhoonupload.com');
    }

    // Only needed in cross-origin setups
    function handlePreflight() {
        handleCorsRequest();
        header('Access-Control-Allow-Methods: POST');
        header('Access-Control-Allow-Headers: Content-Type');
    }

    function getS3Client() {
        global $serverPublicKey, $serverPrivateKey;

        return S3Client::factory(array(
            'key' => $serverPublicKey,
            'secret' => $serverPrivateKey
        ));
    }

    // Only needed if the delete file feature is enabled
    function deleteObject() {
        getS3Client()->deleteObject(array(
            'Bucket' => $_POST['bucket'],
            'Key' => $_POST['key']
        ));
    }

    function signRequest() {
        header('Content-Type: application/json');

        $responseBody = file_get_contents('php://input');
        $contentAsObject = json_decode($responseBody, true);
        $jsonContent = json_encode($contentAsObject);

        $headersStr = $contentAsObject["headers"];
        if ($headersStr) {
            signRestRequest($headersStr);
        }
        else {
            signPolicy($jsonContent);
        }
    }

    function signRestRequest($headersStr) {
        if (isValidRestRequest($headersStr)) {
            $response = array('signature' => sign($headersStr));
            echo json_encode($response);
        }
        else {
            echo json_encode(array("invalid" => true));
        }
    }

    function isValidRestRequest($headersStr) {
        global $expectedBucketName;

        $pattern = "/\/$expectedBucketName\/.+$/";
        preg_match($pattern, $headersStr, $matches);

        return count($matches) > 0;
    }

    function signPolicy($policyStr) {
        $policyObj = json_decode($policyStr, true);

        if (isPolicyValid($policyObj)) {
            $encodedPolicy = base64_encode($policyStr);
            $response = array('policy' => $encodedPolicy, 'signature' => sign($encodedPolicy));
            echo json_encode($response);
        }
        else {
            echo json_encode(array("invalid" => true));
        }
    }

    function isPolicyValid($policy) {
        global $expectedMaxSize, $expectedBucketName;

        $conditions = $policy["conditions"];
        $bucket = null;
        $parsedMaxSize = null;

        for ($i = 0; $i < count($conditions); ++$i) {
            $condition = $conditions[$i];

            if (isset($condition["bucket"])) {
                $bucket = $condition["bucket"];
            }
            else if (isset($condition[0]) && $condition[0] == "content-length-range") {
                $parsedMaxSize = $condition[2];
            }
        }

        return $bucket == $expectedBucketName && $parsedMaxSize == (string)$expectedMaxSize;
    }

    function sign($stringToSign) {
        global $clientPrivateKey;

        return base64_encode(hash_hmac(
                'sha1',
                $stringToSign,
                $clientPrivateKey,
                true
            ));
    }

    // This is not needed if you don't require a callback on upload success.
    function verifyFileInS3() {
        global $expectedMaxSize;

        $bucket = $_POST["bucket"];
        $key = $_POST["key"];

        // If utilizing CORS, we return a 200 response with the error message in the body
        // to ensure Fine Uploader can parse the error message in IE9 and IE8,
        // since XDomainRequest is used on those browsers for CORS requests.  XDomainRequest
        // does not allow access to the response body for non-success responses.
        if (getObjectSize($bucket, $key) > $expectedMaxSize) {
            // You can safely uncomment this next line if you are not depending on CORS
            //header("HTTP/1.0 500 Internal Server Error");
            deleteObject();
            echo json_encode(array("error" => "File is too big!"));
        }
        else {
            echo json_encode(array("tempLink" => getTempLink($bucket, $key)));
        }
    }

    // Provide a time-bombed public link to the file.
    function getTempLink($bucket, $key) {
        $client = getS3Client();
        $url = "{$bucket}/{$key}";
        $request = $client->get($url);

        return $client->createPresignedUrl($request, '+15 minutes');
    }

    function getObjectSize($bucket, $key) {
        $objInfo = getS3Client()->headObject(array(
                'Bucket' => $bucket,
                'Key' => $key
            ));
        return $objInfo['ContentLength'];
    }
    ?>

The CORS buckey policy is as follows:

    <?xml version="1.0" encoding="UTF-8"?>
    <CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
        <CORSRule>
            <AllowedOrigin>*</AllowedOrigin>
            <AllowedMethod>POST</AllowedMethod>
            <AllowedMethod>PUT</AllowedMethod>
            <AllowedMethod>DELETE</AllowedMethod>
            <MaxAgeSeconds>3000</MaxAgeSeconds>
            <ExposeHeader>ETag</ExposeHeader>
            <AllowedHeader>*</AllowedHeader>
        </CORSRule>
    </CORSConfiguration>

Chrome console:

      [FineUploader 3.9.0-3] Received 1 files or inputs. s3.jquery.fineuploader-3.9.0-3.js:164
      [FineUploader 3.9.0-3] Submitting S3 signature request for 0 s3.jquery.fineuploader-3.9.0-3.js:164
      [FineUploader 3.9.0-3] Sending POST request for 0 s3.jquery.fineuploader-3.9.0-3.js:164
      [FineUploader 3.9.0-3] Invalid policy document or request headers! s3.jquery.fineuploader-3.9.0-3.js:169
      [FineUploader 3.9.0-3] Policy signing failed.  Invalid policy document or request headers! s3.jquery.fineuploader-3.9.0-3.js:169
      [FineUploader 3.9.0-3] Received response status 0 with body:  s3.jquery.fineuploader-3.9.0-3.js:164
      [FineUploader 3.9.0-3] Upload attempt for file ID 0 to S3 is complete s3.jquery.fineuploader-3.9.0-3.js:164

Fiddler Output:

      POST http://typhooncloud.com/fineuploader/s3handler.php HTTP/1.1
      Host: typhooncloud.com
      Connection: keep-alive
      Content-Length: 287
      Origin: http://typhooncloud.com
      User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.76 Safari/537.36
      Content-Type: application/json; charset=UTF-8
      Accept: */*
      Referer: http://typhooncloud.com/fineuploader/
      Accept-Encoding: gzip,deflate,sdch
      Accept-Language: en-US,en;q=0.8
      Cookie: __utma=33700260.900109978.1379619256.1379708113.1380038992.6; __utmb=33700260.1.10.1380038992; __utmc=33700260; __utmz=33700260.1380038992.6.2.utmcsr=stackoverflow.com|utmccn=(referral)|utmcmd=referral|utmcct=/questions/18971119/fine-uploader-invalid-response
      AlexaToolbar-ALX_NS_PH: AlexaToolbar/alxg-3.2

      {"expiration":"2013-09-24T16:26:35.948Z","conditions":[{"acl":"private"},{"bucket":"getalink1001"},{"Content-Type":"image/gif"},{"success_action_status":"200"},{"key":"f7e280c4-8cae-4bd7-82c4-d19e71186def.gif"},{"x-amz-meta-qqfilename":"319.gif"},["content-length-range","0","5000000"]]}


      HTTP/1.1 200 OK
      Date: Tue, 24 Sep 2013 16:21:49 GMT
      Server: Apache
      Access-Control-Allow-Origin: http://typhoonupload.com
      Keep-Alive: timeout=5, max=100
      Connection: Keep-Alive
      Transfer-Encoding: chunked
      Content-Type: application/json

      10
      {"invalid":true}
      0

Solution

  • It looks like when you copy and pasted the PHP example server code, you neglected to adjust some of the values. In this case, the handlePreflightedRequest method was not modified to reflect your domain.

    It should read:

    function handlePreflightedRequest() {
        // If you are relying on CORS, you will need to adjust the allowed domain here.
        header('Access-Control-Allow-Origin: http://typhooncloud.com');
    }
    

    Also, the handlePreflightedRequest method should probably be renamed handleCorsRequest to avoid confusion. I've done this just now in the server repo. You don't really need to do this though, as it won't affect the behavior of the code.