Tom
Tom

Reputation: 130

Google Storage REST PUT With Signed URLs

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    = "[email protected]";
    $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.

Upvotes: 1

Views: 2393

Answers (2)

user1988452
user1988452

Reputation: 11

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...

Upvotes: 0

fejta
fejta

Reputation: 3121

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?" + 
                "[email protected]&" +
                "Expires=136891473&" +
                "Signature=BClz9e...WvPcwN%2BmWBPqwg...sQI8IQi1493mw%3D");
xhr.setRequestHeader("Content-Length", raw_img_bytes.length);
xhr.send(raw_img_bytres);

Upvotes: 3

Related Questions