Skip to content

Commit

Permalink
#5 - Added JSON API integration for Google Cloud.
Browse files Browse the repository at this point in the history
There are some remaining CORS issues though.
  • Loading branch information
gsuess committed Jan 18, 2015
1 parent cbf8d17 commit 59e1bdf
Show file tree
Hide file tree
Showing 14 changed files with 296 additions and 56 deletions.
8 changes: 5 additions & 3 deletions .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"dojo" : false, // Dojo Toolkit
"jquery" : false, // jQuery
"mootools" : false, // MooTools
"node" : false, // Node.js
"node" : true, // Node.js
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
"prototypejs" : false, // Prototype and Scriptaculous
"rhino" : false, // Rhino
Expand Down Expand Up @@ -124,8 +124,10 @@
"$": false,

//Internals
"S3Policy": false,
"Slingshot": true,
"matchAllowedFileTypes": false
"oauth": false,
"matchAllowedFileTypes": false,
"rsaSha256": false,
"hmac256": false
}
}
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ Slingshot Changelog
* Added region parameters to S3. The default is `us-east-1`. This fixes bucketUrl problems [#33](https://github.com/CulturalMe/meteor-slingshot/issues/33).
* Upgrade to `AWS4-HMAC-256` for S3 policy signing to make slingshot compatible with new AWS datacenters, such as Frankfurt. [#33](https://github.com/CulturalMe/meteor-slingshot/issues/33)
* Added Rackspace Cloud Files support [#17](https://github.com/CulturalMe/meteor-slingshot/issues/17).
* Added dependency to the core HTTP package on the server side.
* Added resumable upload support for Google Cloud Storage.
* Upload instructions returned by upload services now must include an upload method (`PUT` or `POST`)

## Version 0.3.0

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ Meteor core packages:
* tracker
* reactive-var
* check
* http

## API Reference

Expand Down
14 changes: 7 additions & 7 deletions lib/directive.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,15 @@ _.extend(Slingshot.Directive.prototype, {
check(instructions, {
upload: String,
download: String,
postData: [{
postData: Match.Optional([{
name: String,
value: Match.OneOf(String, Number, null)
}],
headers: Match.Optional(Object)
}]),
headers: Match.Optional(Object),
method: Match.Where(function (method) {
check(method, String);
return method === "POST" || method === "PUT";
})
});

return instructions;
Expand Down Expand Up @@ -232,7 +236,3 @@ Meteor.methods({
return directive.getInstructions(this, file, meta);
}
});

function quoteString(string, quotes) {
return quotes + string.replace(quotes, '\\' + quotes) + quotes;
}
107 changes: 107 additions & 0 deletions lib/oauth2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/* global oauth: true */


oauth = {

Jwt: function (claim, scope) {
_.defaults(claim, {
aud: "https://www.googleapis.com/oauth2/v3/token"
});

check(claim, {
"iss": String,
"aud": String,
"sub": Match.Optional(String)
});

check(scope, [String]);

claim.scope = scope.join(" ");

_.extend(this, {
_claim: claim,
_header: this.encode({
"alg": "RS256",
"typ": "JWT"
})
});
},

TokenGenerator: function (jwt, key) {
check(jwt, oauth.Jwt);
check(key, String);

this.endpoint = "https://www.googleapis.com/oauth2/v3/token";
this.jwt = jwt;
this.key = key;
this.grantType = "urn:ietf:params:oauth:grant-type:jwt-bearer";
}
};

_.extend(oauth.Jwt.prototype, {

encode: function (object) {
return new Buffer(JSON.stringify(object)).toString("base64");
},

sign: rsaSha256,

makeClaim: function (iat, exp) {
return this.encode(_.extend({
exp: Math.round(exp.getTime() / 1000),
iat: Math.round(iat.getTime() / 1000)
}, this._claim));
},

compile: function (key, iat, exp) {
var jwt = this._header + "." + this.makeClaim(iat, exp);
return jwt + "." + this.sign(jwt, key);
}
});

_.extend(oauth.TokenGenerator.prototype, {

timeout: function () {
if (!this._expires)
return 0;

return Math.max(this._expires.getTime() - Date.now(), 0);
},

getToken: Meteor.wrapAsync(function (callback) {
if (!this._token || this.timeout() < 60000)
this.requestNewToken(callback);
else
callback(null, this._token);
}),

requestNewToken: Meteor.wrapAsync(function (callback) {
var self = this,
querystring = Npm.require("querystring"),
iat = new Date(),
exp = new Date(iat.getTime() + 3600 * 1000);

delete this._token;
delete this._expires;

HTTP.post(this.endpoint, {
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},

content: querystring.stringify({
grant_type: this.grantType,
assertion: this.jwt.compile(this.key, iat, exp)
})
}, function (error, response) {
if (!error && response) {
self._expires = new Date(iat.getTime() +
response.data.expires_in * 1000);
self._token = response.data.access_token;
}

callback(error, self._token);
});
})

});
1 change: 0 additions & 1 deletion lib/storage-policy.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ Slingshot.StoragePolicy = function () {
*/

stringify: function (encoding) {
/* global Buffer: false */
return Buffer(JSON.stringify(policy), "utf-8")
.toString(encoding || "base64");
}
Expand Down
10 changes: 8 additions & 2 deletions lib/upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,19 @@ Slingshot.Upload = function (directive, metaData) {
"The upload has been aborted by the user"));
});

xhr.open("POST", self.instructions.upload, true);
xhr.open(self.instructions.method, self.instructions.upload, true);

_.each(self.instructions.headers, function (value, key) {
xhr.setRequestHeader(key, value);
});

xhr.send(buildFormData());
switch (self.instructions.method) {
case "POST":
xhr.send(buildFormData());
break;
case "PUT":
xhr.send(self.file);
}

return self;
},
Expand Down
17 changes: 17 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
var crypto = Npm.require("crypto");

/* global rsaSha256: true */
rsaSha256 = function (data, key, encoding) {
return crypto
.createSign('RSA-SHA256')
.update(data)
.sign(key, encoding || "base64");
};

/* global hmac256: true */
hmac256 = function (key, data, encoding) {
return crypto
.createHmac("sha256", key)
.update(new Buffer(data, "utf-8"))
.digest(encoding);
};
4 changes: 4 additions & 0 deletions package.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Package.describe({
Package.on_use(function (api) {
api.versionsFrom('[email protected]');

api.use("http", "server");
api.use(["underscore", "check"]);
api.use(["tracker", "reactive-var"], "client");

Expand All @@ -19,10 +20,13 @@ Package.on_use(function (api) {
api.add_files("lib/upload.js", "client");

api.add_files([
"lib/utils.js",
"lib/directive.js",
"lib/storage-policy.js",
"lib/oauth2.js",
"services/aws-s3.js",
"services/google-cloud.js",
"services/google-cloud-resumable.js",
"services/rackspace.js"
], "server");

Expand Down
90 changes: 52 additions & 38 deletions services/aws-s3.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,39 +65,16 @@ Slingshot.S3Storage = {
*/

upload: function (method, directive, file, meta) {
var url = Npm.require("url"),

policy = new Slingshot.StoragePolicy()
.expireIn(directive.expire)
.contentLength(0, Math.min(file.size, directive.maxSize || Infinity)),

payload = {
key: _.isFunction(directive.key) ?
directive.key.call(method, file, meta) : directive.key,

bucket: directive.bucket,

"Content-Type": file.type,
"acl": directive.acl,

"Cache-Control": directive.cacheControl,
"Content-Disposition": directive.contentDisposition || file.name &&
"inline; filename=" + quoteString(file.name, '"')
},

bucketUrl = _.isFunction(directive.bucketUrl) ?
directive.bucketUrl(directive.bucket, directive.region) :
directive.bucketUrl,

download = _.extend(url.parse(directive.cdn || bucketUrl), {
pathname: payload.key
});
var policy = this.createPolicy(directive, file),
payload = this.getPayload(method, directive, file, meta),
bucketUrl = this.bucketUrl(directive),
download = this.downloadUrl(directive, payload.key);

this.applySignature(payload, policy, directive);

return {
upload: bucketUrl,
download: url.format(download),
download: download,
postData: [{
name: "key",
value: payload.key
Expand All @@ -106,10 +83,56 @@ Slingshot.S3Storage = {
name: name,
value: value
};
}).compact().value())
}).compact().value()),
method: "POST"
};
},

createPolicy: function (directive, file) {
return new Slingshot.StoragePolicy()
.expireIn(directive.expire)
.contentLength(0, Math.min(file.size, directive.maxSize || Infinity));
},

getPayload: function (method, directive, file, meta) {
return {
key: this.objectName(method, directive, file, meta),

bucket: directive.bucket,

"Content-Type": file.type,
"acl": directive.acl,

"Cache-Control": directive.cacheControl,
"Content-Disposition": this.contentDisposition(directive, file)
};
},

objectName: function (method, directive, file, meta) {
return _.isFunction(directive.key) ?
directive.key.call(method, file, meta) : directive.key;
},

contentDisposition: function (directive, file) {
return directive.contentDisposition || file.name &&
"inline; filename=" + quoteString(file.name, '"');
},

downloadUrl: function (directive, key) {
var url = Npm.require("url"),
bucketUrl = this.bucketUrl(directive);

return url.format(_.extend(url.parse(directive.cdn || bucketUrl), {
pathname: key
}));
},

bucketUrl: function (directive) {
return _.isFunction(directive.bucketUrl) ?
directive.bucketUrl(directive.bucket, directive.region) :
directive.bucketUrl;
},

/** Applies signature an upload payload
*
* @param {Object} payload - Data to be upload along with file
Expand Down Expand Up @@ -170,12 +193,3 @@ function formatNumber(num, digits) {
return Array(digits - string.length + 1).join("0").concat(string);
}

var crypto = Npm.require("crypto");

function hmac256(key, data, encoding) {
/* global Buffer: false */
return crypto
.createHmac("sha256", key)
.update(new Buffer(data, "utf-8"))
.digest(encoding);
}
Loading

0 comments on commit 59e1bdf

Please sign in to comment.