1

I'm trying to upload the base64 data of an image directly through javascript to Google Storage using signed URLs as authentication, which is apparently possible to do.

According to developers.google.com/storage/docs/reference-methods#putobject there are only six headers that need to be set for this to work. Also for the header 'Authorization' I'm attempting to use the last option here:

developers.google.com/storage/docs/reference-headers#authorization

Which is 'A signature' developers.google.com/storage/docs/authentication#service_accounts

The only thing I want to use PHP for is to get the signature. Here is what I have been trying to get working with no success.

PHP & JS page/code

<?php

$theDate   = Date(DATE_RFC822);

function signedURL( $filename, $bucket, $method = 'PUT' ) {
    $signature  = "";
    $duration   = 30;
    $emailID    = "980000000000-ytyertyr@developer.gserviceaccount.com";
    $certs      = array();
    $priv_key   = file_get_contents("9999999999999999999999999999-privatekey.p12");

  if (!openssl_pkcs12_read($priv_key, $certs, 'notasecret')) { echo "Unable to parse the p12 file. OpenSSL error: " . openssl_error_string(); exit(); }

    $expires = time() + $duration;
    $to_sign = ( $method . "\n\n\n" . $expires . "\n" . "/" . $bucket . "/" . $filename ); 

    $RSAPrivateKey = openssl_pkey_get_private($certs["pkey"]);

  if (!openssl_sign( $to_sign, $signature, $RSAPrivateKey, 'sha256' ))
  {
    error_log( 'openssl_sign failed!' );
    $signature = 'failed';
  } else {
    $signature =  urlencode( base64_encode( $signature ) );
  }

  return ( 
    'http://storage.googleapis.com/' . $bucket . '/' . $filename . '?GoogleAccessId=' . $emailID . '&Expires=' . $expires . '&Signature=' . $signature
         );
    openssl_free_key($RSAPrivateKey);
} 
?>
<script>
var base64img  = 'data:image/png;base64,AAABAAIAICA....snip...A';
var xhr        = new XMLHttpRequest();
//PUT test - PUT status "(Canceled)" - OPTION status 200 (OK)
xhr.open("PUT", "<?php echo signedURL('test.png', 'mybucket'); ?>");
//xhr.setRequestHeader("Content-type", "image/png");
xhr.setRequestHeader("x-goog-acl", "public-read"); //try to set public read on file
xhr.setRequestHeader("Content-Length", base64img.length); // Chrome throws error (Refused to set unsafe header "Content-Length" )
xhr.send( base64img );
//GET test.txt temp file - working and returning 200 status (signing must be working ?)
/*
xhr.open("GET", "<?php echo signedURL('test.txt', 'mybucket', 'GET'); ?>");
xhr.send();
*/
//
</script>

Cors xml (seems to be fine) - I've set a wildcard only while testing and a low cache/maxage time

<?xml version="1.0" ?>
<CorsConfig>
    <Cors>
        <Origins>
            <Origin>*</Origin>
        </Origins>
        <Methods>
            <Method>GET</Method>
            <Method>HEAD</Method>
            <Method>OPTIONS</Method>
            <Method>PUT</Method>
        </Methods>
        <ResponseHeaders>
            <ResponseHeader>accept-encoding</ResponseHeader>
            <ResponseHeader>cache-control</ResponseHeader>
            <ResponseHeader>content-length</ResponseHeader>
            <ResponseHeader>content-type</ResponseHeader>
            <ResponseHeader>expect</ResponseHeader>
            <ResponseHeader>if-modified-since</ResponseHeader>
            <ResponseHeader>origin</ResponseHeader>
            <ResponseHeader>range</ResponseHeader>
            <ResponseHeader>referer</ResponseHeader>
            <ResponseHeader>x-goog-acl</ResponseHeader>
            <ResponseHeader>x-goog-api-version</ResponseHeader>
        </ResponseHeaders>
        <MaxAgeSec>900</MaxAgeSec>
    </Cors>
</CorsConfig>

I've tested the GET method on a file and get a 200 status back now (\n\n - fix)

Update:

Looking in Firefox it does return a 403, unlike Chrome.

GitaarLAB
  • 14,536
  • 11
  • 60
  • 80
Tom
  • 130
  • 1
  • 8

2 Answers2

3

So the following lines are weird, as the conflate signed URLs with OAuth and PUT with POST:

# This looks like a PUT to signed URL
xhr.open("PUT", '<?php echo signedURL('imgfile.png','PUT',30,'mybucketname'); ?>', true);
# But multipart requires POST
xhr.setRequestHeader("Content-type", "multipart/form-data; boundary="+boundary);
# And here's a second form of authorization
xhr.setRequestHeader("Authorization", "OAuth <?php echo $signature; ?>");

multipart/form-data uploads require POST verb and are intended for html forms: Google Cloud Storage : PUT Object vs POST Object to upload file.?.

As long as you are sending a custom headers in an XMLHttpRequest I would recommend using PUT with either OAuth credentials:

xhr.open("PUT", "https://storage.googleapis.com/mybucketname/imgfile.png");
xhr.setRequestHeader("Authorization", "OAuth Bearer 1234567abcdefg");
xhr.setRequestHeader("Content-Length", raw_img_bytes.length);
xhr.send(raw_img_bytes);

or a signed url:

xhr.open("PUT", "https://storage.googleapis.com/mybucketname/imgfile.png?" + 
                "GoogleAccessId=1234567890123@developer.gserviceaccount.com&" +
                "Expires=136891473&" +
                "Signature=BClz9e...WvPcwN%2BmWBPqwg...sQI8IQi1493mw%3D");
xhr.setRequestHeader("Content-Length", raw_img_bytes.length);
xhr.send(raw_img_bytres);
Community
  • 1
  • 1
fejta
  • 3,061
  • 2
  • 17
  • 22
  • Hi, Fejta. Thanks.. I knew there'd be a few issues because I really have no clue about this stuff yet. I'm going with your PUT method suggestion and using a signed URL. It looks to be a step closer now because I'm actually seeing the PUT call and not just the OPTIONS call. However the PUT call returns a "SignatureDoesNotMatch" error. Do you know if I'm doing the openssl stuff correctly here ? Thanks again. – Tom May 20 '13 at 20:41
  • Compare the value of $to_sign and the string to sign in the PUT response body. Do they match exactly? – fejta May 21 '13 at 00:38
  • Good catch, it wasn't the same I had to add two new lines (\n\n) to get a match. Unfortunately things still aren't working. In the Chrome dev tools under network when the PUT call is made the status comes back as "(Canceled)". In the cors I do have the responseheader 'accept-encoding' set in there I'm wondering if it will accept the data as base64 and I'm not sending the boundary jargon anymore either, is that needed ? Sorry to be a pain, but this is the furthest I've gotten with uploading to GCS without gsutil or the browser manager. I once tried with ogg files a while back and gave up. – Tom May 21 '13 at 14:10
  • So you're no longer receiving a SignatureDoesNotMatch error, but the PUT is cancelled? Also, can you fix up the example code in the question to match what you have currently? – fejta May 21 '13 at 21:37
  • Yes. I've also tested the GET method, so I -think- that part is working now. The PUT has no response or preview in Chrome, but it does have a request payload header with the base64 data in it. Thanks. – Tom May 22 '13 at 12:15
  • If you can successfully GET a private object then I suspect the deal is your CORS config. – fejta May 24 '13 at 03:03
  • Any luck with the CORs issue? I have been trying to use xhr2 uploads with google cloud storage, but no matter my CORs config, I always come up with a "Access-Control-Allow-Origin" error. See http://stackoverflow.com/questions/23248576/no-access-control-allow-origin-header-with-resumable-upload . – Steve Farthing Jul 01 '14 at 12:56
0

I gess your Content-Type is something known (like Content-Type:video/mp4 for instance)? Try to upload a file with not known extention. For me, PUT is working in this case, not when Content-Type is not empty... I don't understand why...

  • It turned out that the cors were blocking me from seeing the xml error that was coming back on this. The config of the cors is fine, but when setting up a test on the same domain I was able to see the xml error coming back. You are right about the content-type. With a PUT (unlike GET) even if you don't set the content-type header you still MUST put the content-type in the "to sign" value. The same goes for any extra headers it seems. My problem now is that the image being PUT returns "the image cannot display because of errors". It seems that every step of the way is a battle. – Tom May 31 '13 at 12:08
  • You are right, thank you ! Maybe you could edit your first post to modify your php part if someone else read this... I don't have the error return on the image, whereas I have a very similar configuration...Good luck for your code – user1988452 May 31 '13 at 15:40