Skip to content

Commit

Permalink
storage: support signed urls. fixes #49.
Browse files Browse the repository at this point in the history
  • Loading branch information
stephenplusplus committed Aug 22, 2014
1 parent 00553cc commit c60331a
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 1 deletion.
59 changes: 59 additions & 0 deletions lib/storage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

'use strict';

var crypto = require('crypto');
var duplexify = require('duplexify');
var nodeutil = require('util');
var stream = require('stream');
Expand Down Expand Up @@ -230,6 +231,64 @@ Bucket.prototype.remove = function(name, callback) {
this.makeReq('DELETE', path, null, true, callback);
};

/**
* Get a signed URL to allow limited time access to a resource.
*
* {@link https://developers.google.com/storage/docs/accesscontrol#Signed-URLs}
*
* @param {object} options - Configuration object.
* @param {string} options.action - "read", "write", or "delete"
* @param {string=} options.contentMd5 - The MD5 digest value in base64. If you
* provide this, the client must provide this HTTP header with this same
* value in its request.
* @param {string=} options.contentType - If you provide this value, the client
* must provide this HTTP header set to the same value.
* @param {number} options.expires - Timestamp (seconds since epoch) when this
* link will expire.
* @param {string=} options.extensionHeaders - If these headers are used, the
* server will check to make sure that the client provides matching values.
* @param {string} options.resource - Resource to allow access to.
* @return {string}
*
* @example
* ```js
* var signedUrl = bucket.getSignedUrl({
* action: 'read',
* expires: Math.round(Date.now() / 1000) + (60 * 60 * 24 * 14), // 2 weeks.
* resource: 'my-dog.jpg'
* });
* ```
*/
Bucket.prototype.getSignedUrl = function(options) {
options.action = {
read: 'GET',
write: 'PUT',
delete: 'DELETE'
}[options.action];

options.resource = util.format('/{bucket}/{resource}', {
bucket: this.bucketName,
resource: options.resource
});

var sign = crypto.createSign('RSA-SHA256');
sign.update([
options.action,
(options.contentMd5 || ''),
(options.contentType || ''),
options.expires,
(options.extensionHeaders || '') + options.resource
].join('\n'));
var signature = sign.sign(this.conn.credentials.private_key, 'base64');

return [
'http://storage.googleapis.com' + options.resource,
'?GoogleAccessId=' + this.conn.credentials.client_email,
'&Expires=' + options.expires,
'&Signature=' + encodeURIComponent(signature)
].join('');
};

/**
* Create a readable stream to read contents of the provided remote file. It
* can be piped to a write stream, or listened to for 'data' events to read a
Expand Down
61 changes: 60 additions & 1 deletion regression/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
* limitations under the License.
*/

/*global describe, it, before, after */
/*global describe, it, before, after, beforeEach */

'use strict';

var assert = require('assert');
var async = require('async');
var crypto = require('crypto');
var fs = require('fs');
var request = require('request');
var tmp = require('tmp');

var env = require('./env.js');
Expand Down Expand Up @@ -189,4 +190,62 @@ describe('storage', function() {
}), done);
});
});

describe('sign urls', function() {
var fileName = 'LogoToSign.jpg';

beforeEach(function(done) {
fs.createReadStream(files.logo.path)
.pipe(bucket.createWriteStream(fileName))
.on('error', done)
.on('complete', done.bind(null, null));
});

it('should create a signed read url', function(done) {
var signedReadUrl = bucket.getSignedUrl({
action: 'read',
expires: Math.round(Date.now() / 1000) + 5,
resource: fileName
});
var localFile = fs.readFileSync(files.logo.path);
request.get(signedReadUrl, function(err, resp, body) {
assert.equal(body, localFile);
bucket.remove(fileName, done);
});
});

it('should create a signed delete url', function(done) {
var signedDeleteUrl = bucket.getSignedUrl({
action: 'delete',
expires: Math.round(Date.now() / 1000) + 5,
resource: fileName
});
request.del(signedDeleteUrl, function(err, resp) {
assert.equal(resp.statusCode, 204);
bucket.stat(fileName, function(err) {
assert.equal(err.code, 404);
done();
});
});
});

it('should allow control of expiration', function(done) {
var offsetSeconds = 5;
var signedReadUrl = bucket.getSignedUrl({
action: 'read',
expires: Math.round(Date.now() / 1000) + offsetSeconds,
resource: fileName
});
var localFile = fs.readFileSync(files.logo.path);
request.get(signedReadUrl, function(err, resp, body) {
assert.equal(body, localFile);
});
setTimeout(function() {
request.get(signedReadUrl, function(err, resp) {
assert.equal(resp.statusCode, 400);
bucket.remove(fileName, done);
});
}, (offsetSeconds + 1) * 1000);
});
});
});

0 comments on commit c60331a

Please sign in to comment.