Google cloud storage and CORS


This is part of the writeup on Using Google Cloud Storage with Apps Script. By now you should have completed all the steps in Setting up or creating a console projectEnabling APIs and OAuth2 and Using the service account to enable access to cloud storage and now have a project and credentials you can use to create your storage buckets. You can also read more about the background of how all this works at GcsStore overview - Google Cloud Storage and Apps Script, and see some  examples at GcsStore examples

CORS

If you are accessing Google Store objects (perhaps created with Apps Script) from client apps, then you are likely to run into Cross Origin Resource sharing problems. For more info on what this is see Cross Origin Resource sharing (CORS)

One way of getting round this is to create as proxy as described in Apps Script as a proxy, but this introduces a double hop for your data and another dependency.  Google Cloud storage supports CORS, so you can use the GcsStore library to easily set that up from Apps Script. 

Some examples

One of the places you'll find this handy is if you use codepen.io or some other tools like that to test your client code. If you try to access data stored on google cloud storage, you'll get the usual cross domain origin error reported. We'll use codepen as an example.

Write some data

This could be any kind of data, but for the test we'll just write an object and pick it up from CodePen. I'm assuming you're already familiar with GcsStore and have taklen care of setting up authentication as described in Setting up or creating a console projectEnabling APIs and OAuth2 

Getting started

Let's first of all initialize GcsStore on the bucket we're sharing, using this code, changing the bucket name to yours.
function cors() {
  
  // test cors getting.setting
  var bucket = 'xliberation-store';
  var packageName = bucket;
  
  // this consumes up the Oauth2 authentication that was set up once off
  var goa = cGoa.make (packageName,PropertiesService.getScriptProperties());
  
  // you can get the project id & accessToken from goa
  var accessToken = goa.getToken();
  
  // create a new store manager
  var gcs = new cGcsStore.GcsStore()
  .setAccessToken (accessToken)
  .setBucket(bucket); 

Now we'll write some data, to a particular folder, set it for public sharing, and get its public url
/// set up an object for public reading 
  gcs.setFolderKey("corstest")
  .put("testkey",{hello:"there"})
  .patchPredefinedAcl("testkey","publicRead");
  
  // show its public link
  Logger.log(gcs.getPublicLink ("testkey"));

That will create this object in the cloud store
and log its public link
 https://storage.googleapis.com/xliberation-store/corstest/testkey


Disabling CORS


Disabling cors is easy - just call patchCors() with no arguments

  // to disable all cors sharing and show
  gcs.patchCors();
  Logger.log(gcs.getCors());
You'll get this logged
[]

Allow s.codepen.io to access this bucket for GET operations

  // set one origin using defaults
  gcs.patchCors ("https://s.codepen.io");
  Logger.log(gcs.getCors());

Logs
[{method=[GET], origin=[https://s.codepen.io], responseHeader=[Content-Type, Origin], maxAgeSeconds=1800}]

Client side Cors

Here's how to use CORS with plain JavaScript. The example is in the codepen below

And reproduced here.
 testCors();

   function testCors() {
     doCors("https://storage.googleapis.com/xliberation-store/corstest/testkey")
       .then(function(response) {
         console.log(JSON.parse(response.responseText));
       })
       .catch(function(err) {
         console.log(err);
       });

   }

   /**
    *do a cors request
    *@param {string}  url the url
    *@param {string} method the method (default GET)
    *@param {*} payload the optional payload
    *@return {object} the response
    */
   function doCors(url, method, payload) {
     return new Promise(function(resolve, reject) {
       return corsRequest(resolve, reject, url, method, payload);
     });
   }
   /**
    *do a cors request
    *thanks to http://enable-cors.org/ for info on cors/html5
    *@param {function} callback the load callback
    *@param {function} errorCallback the error callback
    *@param {string}  url the url
    *@param {string} method the method (default GET)
    *@param {*} payload the optional payload
    *@return {object} the response
    */
   function corsRequest(callback, errorCallback, url, method, payload) {

     // get the appropriate xhr
     var xhr = getXhr();
     if (!xhr) throw 'cant do cors with this browser';

     // now we can go
     xhr.open(method || "GET", url, true);

     // set up callbacks
     xhr.onload = function(evt) {
       // meed to catch this since it doesnt actually catch http errors
       if (evt.target.status < 200 || evt.target.status >= 300) {
         errorCallback(evt.target.responseText);
       } else {
         callback(evt.target);
       }

     }
     xhr.onerror = function(response) {
       errorCallback("request error- check that " + url + "exists and has CORS enabled");
     }

     // execute
     return xhr.send(payload);

     /**
      * get the correct xhr object for the browser being used
      * @return {XDomainRequest|XMLHttpReQuest} the xhr
      */
     function getXhr() {

       // likely to be this, unless its IE
       var xhr = new XMLHttpRequest();
       return isDefined(xhr.withCredentials) ?
         xhr : (isDefined(XDomainRequest) ? new XDomainRequest() : undefined);
     }

     function isDefined(ob) {
       return typeof ob !== typeof undefined;
     }

   }

As you can see, I can now access my data from another domain without needing to use a proxy.
Object {
  hello: "there"
}

Setting multiple origins and other options

GcsStore provides various defaults to simplify the setting of cors attributes. However you can get more complicated by providing some or all of the cors resource described here.

Some examples
// set multiple origins using defaults
  gcs.patchCors (["https://s.codepen.com","*.mcpher.com"]);
  Logger.log(gcs.getCors());
  
  // specify full thing
  gcs.patchCors([{method:["GET"], origin:["http://xliberation.com"]}]);
  Logger.log(gcs.getCors());

  // set even more things
  gcs.patchCors ([{
    method:["GET","POST"], 
    origin:["https://s.codepen.io"],
    responseHeader:["Content-Type","Origin"],
    maxAgeSeconds:3600
  }]);
  
  Logger.log(gcs.getCors());

}


MaxAgeSeconds

This is how long the CORS preflight conversation is valid for. This means that you can disable CORS and find you can still access it for while. If you are debugging, it's worth setting this to a lower number to clear previous settings still alive by virtue of the maxAgeSeconds property they were initiated with.





For more like this, see Google Apps Scripts snippets. Why not join our forum, follow the blog or follow me on twitter to ensure you get updates when they are available.
Comments