Skip to content
Closed
142 changes: 142 additions & 0 deletions lib/storage/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,148 @@ File.prototype.getSignedUrl = function(options, callback) {
});
};

/**
* Get a signed policy document to allow user to upload data
* with a POST.
*
* *[Reference](http://goo.gl/JWJEkG).*
*
* @throws {Error} if an expiration timestamp from the past is given or
* option parameter does not respect the expected format
*
* @param {object} options - Configuration object.
* @param {object} options.expiration - Timestamp (seconds since epoch)
* when this policy will expire.
* @param {object[][]=} options.equals - Array of request parameters and
* their expected value (e.g. [["$<field>", "<value>"]]). Values are

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

* translated into equality constraints in the conditions
* field of the policy document (e.g. ["eq", "$<field>", "<value>"]).
* If only one equality condition is to be specified, options.equals
* can be a one-dimensional array (e.g. ["$<field>", "<value>"])
* @param {object[][]=} options.startsWith - Array of request parameters and
* their expected prefixes (e.g. [["$<field>", "<value>"]]). Values are
* translated into starts-with constraints in the conditions field
* of the policy document (e.g. ["starts-with", "$<field>", "<value>"])
* If only one prefix condition is to be specified, options.startsWith
* can be a one-dimensional array (e.g. ["$<field>", "<value>"])
* @param {string=} options.acl - ACL for the object from possibly predefined
* ACLs
* @param {string=} options.successRedirect - The URL to which the user
* client is redirected if the upload is successfull
* @param {string=} options.successStatus - The status of the Google Storage
* response if the upload is successfull (must be string)
* @param {object=} options.contentLengthRange - Object providing
* minimum (options.contentLengthRange.min) and maximum
* (options.contentLengthRange.max) value for the request's
* content length
*
* @example
* file.getSignedPolicy({
* equals: ["$Content-Type", "image/jpeg"],
* contentLengthRange: {min: 0, max: 1024},
* expiration: Math.round(Date.now() / 1000) + (60 * 60 * 24 * 14) // 2 weeks.
* }, function(err, policy) {
* // policy.string: the policy document, plain text
* // policy.base64: the policy document, base64
* // policy.signature: the policy signature, base64
* });
*/
File.prototype.getSignedPolicy = function(options, callback) {
if (options.expiration < Math.floor(Date.now() / 1000)) {
throw new Error('An expiration date cannot be in the past.');
}

var expirationString = new Date(options.expiration).toISOString();
var conditions = [
['eq', '$key', this.name],
{
bucket: this.bucket.name
},
];

if (util.is(options.equals, 'array')) {
if (!util.is(options.equals[0], 'array')) {
options.equals = [options.equals];
}
options.equals.forEach(function(condition) {
if (!util.is(condition, 'array') || condition.length !== 2) {
throw new Error('Equals condition must be an array of 2 elements.');
}
conditions.push(['eq', condition[0], condition[1]]);
});
}

This comment was marked as spam.

This comment was marked as spam.


if (util.is(options.startsWith, 'array')) {
if (!util.is(options.startsWith[0], 'array')) {
options.startsWith = [options.startsWith];
}
options.startsWith.forEach(function(condition) {
if (!util.is(condition, 'array') || condition.length !== 2) {
throw new Error('StartsWith condition must be an array of 2 elements.');
}
conditions.push(['starts-with', condition[0], condition[1]]);
});
}

if (options.acl) {
conditions.push({
acl: options.acl
});
}

if (options.successRedirect) {
conditions.push({
success_action_redirect: options.successRedirect
});
}

if (options.successStatus) {
conditions.push({
success_action_status: options.successStatus
});
}

if (options.contentLengthRange) {
var min = options.contentLengthRange.min;
var max = options.contentLengthRange.max;
if (!util.is(min, 'number') || !util.is(max, 'number')) {
throw new Error(
'ContentLengthRange must have numeric min and max fields.'
);
}
conditions.push(['content-length-range', min, max]);
}

var policy = {
expiration: expirationString,
conditions: conditions
};

var makeAuthorizedRequest_ = this.bucket.storage.makeAuthorizedRequest_;

makeAuthorizedRequest_.getCredentials(function(err, credentials) {
if (err) {
callback(err);
return;
}

var sign = crypto.createSign('RSA-SHA256');
var policyString = JSON.stringify(policy);
var policyBase64 = new Buffer(policyString).toString('base64');

sign.update(policyBase64);

var signature = sign.sign(credentials.private_key, 'base64');

callback(null, {
string: policyString,
base64: policyBase64,
signature: signature
});
});
};

This comment was marked as spam.



/**
* Set the file's metadata.
*
Expand Down
202 changes: 202 additions & 0 deletions test/storage/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,208 @@ describe('File', function() {
});
});

describe('getSignedPolicy', function() {
var credentials = require('../testdata/privateKeyFile.json');

beforeEach(function() {
var storage = bucket.storage;
storage.makeAuthorizedRequest_.getCredentials = function(callback) {
callback(null, credentials);
};
});

it('should create a signed policy', function(done) {
file.getSignedPolicy({
expiration: Math.round(Date.now() / 1000) + 5
}, function(err, signedPolicy) {
assert.ifError(err);
assert.equal(typeof signedPolicy.string, 'string');
assert.equal(typeof signedPolicy.base64, 'string');
assert.equal(typeof signedPolicy.signature, 'string');
done();
});
});

it('should add key equality condition', function(done) {
file.getSignedPolicy({
expiration: Math.round(Date.now() / 1000) + 5
}, function(err, signedPolicy) {
var conditionString = '[\"eq\",\"$key\",\"'+file.name+'\"]';
assert.ifError(err);
assert(signedPolicy.string.indexOf(conditionString) > -1);
done();
});
});

it('should add ACL condtion', function(done) {
file.getSignedPolicy({
expiration: Math.round(Date.now() / 1000) + 5,
acl: '<acl>'
}, function(err, signedPolicy) {
var conditionString = '{\"acl\":\"<acl>\"}';
assert.ifError(err);
assert(signedPolicy.string.indexOf(conditionString) > -1);
done();
});
});

describe('expiration', function() {
it('should ISO encode expiration', function(done) {
var expiration = Math.round(Date.now() / 1000) + 5;
var expireDate = new Date(expiration);
file.getSignedPolicy({
expiration: expiration
}, function(err, signedPolicy) {
assert.ifError(err);
assert(signedPolicy.string.indexOf(expireDate.toISOString()) > -1);
done();
});
});

it('should throw if a date from the past is given', function() {
var expirationTimestamp = Math.floor(Date.now() / 1000) - 1;
assert.throws(function() {
file.getSignedPolicy({
expiration: expirationTimestamp
}, function() {});
}, /cannot be in the past/);
});
});

describe('equality condition', function() {
it('should add equality conditions (array of arrays)', function(done) {
var expiration = Math.round(Date.now() / 1000) + 5;
file.getSignedPolicy({
expiration: expiration,
equals: [['$<field>', '<value>']]
}, function(err, signedPolicy) {
var conditionString = '[\"eq\",\"$<field>\",\"<value>\"]';
assert.ifError(err);
assert(signedPolicy.string.indexOf(conditionString) > -1);
done();
});
});

This comment was marked as spam.


it('should add equality condition (array)', function(done) {
var expiration = Math.round(Date.now() / 1000) + 5;
file.getSignedPolicy({
expiration: expiration,
equals: ['$<field>', '<value>']
}, function(err, signedPolicy) {
var conditionString = '[\"eq\",\"$<field>\",\"<value>\"]';
assert.ifError(err);
assert(signedPolicy.string.indexOf(conditionString) > -1);
done();
});
});

it('should throw if equal condition is not an array', function() {
var expiration = Math.round(Date.now() / 1000) + 5;
assert.throws(function() {
file.getSignedPolicy({
expiration: expiration,
equals: [{}]
}, function() {});
}, /Equals condition must be an array/);
});

it('should throw if equal condition length is not 2', function() {
var expiration = Math.round(Date.now() / 1000) + 5;
assert.throws(function() {
file.getSignedPolicy({
expiration: expiration,
equals: [['1', '2', '3']]
}, function() {});
}, /Equals condition must be an array of 2 elements/);
});
});

describe('prefix conditions', function() {
it('should add prefix conditions (array of arrays)', function(done) {
var expiration = Math.round(Date.now() / 1000) + 5;
file.getSignedPolicy({
expiration: expiration,
startsWith: [['$<field>', '<value>']]
}, function(err, signedPolicy) {
console.log(signedPolicy);

This comment was marked as spam.

var conditionString = '[\"starts-with\",\"$<field>\",\"<value>\"]';
assert.ifError(err);
assert(signedPolicy.string.indexOf(conditionString) > -1);
done();
});
});

it('should add prefix condition (array)', function(done) {
var expiration = Math.round(Date.now() / 1000) + 5;
file.getSignedPolicy({
expiration: expiration,
startsWith: ['$<field>', '<value>']
}, function(err, signedPolicy) {
console.log(signedPolicy);

This comment was marked as spam.

var conditionString = '[\"starts-with\",\"$<field>\",\"<value>\"]';
assert.ifError(err);
assert(signedPolicy.string.indexOf(conditionString) > -1);
done();
});
});

it('should throw if prexif condition is not an array', function() {
var expiration = Math.round(Date.now() / 1000) + 5;
assert.throws(function() {
file.getSignedPolicy({
expiration: expiration,
startsWith: [{}]
}, function() {});
}, /StartsWith condition must be an array/);
});

it('should throw if prefix condition length is not 2', function() {
var expiration = Math.round(Date.now() / 1000) + 5;
assert.throws(function() {
file.getSignedPolicy({
expiration: expiration,
startsWith: [['1', '2', '3']]
}, function() {});
}, /StartsWith condition must be an array of 2 elements/);
});
});

describe('content length', function() {
it('should add content length condition', function(done) {
var expiration = Math.round(Date.now() / 1000) + 5;
file.getSignedPolicy({
expiration: expiration,
contentLengthRange: {min: 0, max: 1}
}, function(err, signedPolicy) {
var conditionString = '[\"content-length-range\",0,1]';
assert.ifError(err);
assert(signedPolicy.string.indexOf(conditionString) > -1);
done();
});
});

it('should throw if content length has no min', function() {
var expiration = Math.round(Date.now() / 1000) + 5;
assert.throws(function() {
file.getSignedPolicy({
expiration: expiration,
contentLengthRange: [{max: 1}]
}, function() {});
}, /ContentLengthRange must have numeric min and max fields/);
});

it('should throw if content length has no max', function() {
var expiration = Math.round(Date.now() / 1000) + 5;
assert.throws(function() {
file.getSignedPolicy({
expiration: expiration,
contentLengthRange: [{min: 0}]
}, function() {});
}, /ContentLengthRange must have numeric min and max fields/);
});
});
});

describe('setMetadata', function() {
var metadata = { fake: 'metadata' };

Expand Down