diff --git a/lib/common/util.js b/lib/common/util.js index b2bcf41c949..aa8c9d30189 100644 --- a/lib/common/util.js +++ b/lib/common/util.js @@ -344,7 +344,21 @@ function makeWritableStream(dup, options, onComplete) { module.exports.makeWritableStream = makeWritableStream; function makeAuthorizedRequest(config) { - var GAE_OR_GCE = !config || (!config.credentials && !config.keyFile); + var ENV = { + GAE_DEV: process.env.GAE_LONG_APP_ID && !process.env.GAE_VM, + NO_CREDENTIALS: !config || (!config.credentials && !config.keyFile) + }; + + var authorize; + + if (ENV.GAE_DEV && ENV.NO_CREDENTIALS) { + // Google App Engine Development mode doesn't require authorization. + authorize = function(reqOpts, callback) { + callback(null, reqOpts); + }; + } else { + authorize = gsa(config); + } var missingCredentialsError = new Error(); missingCredentialsError.message = [ @@ -357,8 +371,6 @@ function makeAuthorizedRequest(config) { '\n' ].join(''); - var authorize = gsa(config); - function makeRequest(reqOpts, callback) { var tokenRefreshAttempts = 0; reqOpts.headers = reqOpts.headers || {}; @@ -371,9 +383,8 @@ function makeAuthorizedRequest(config) { function onAuthorizedRequest(err, authorizedReqOpts) { if (err) { - if (GAE_OR_GCE && err.code === 'ENOTFOUND') { - // The metadata server wasn't found. The user must not actually be in - // a GAE or GCE environment. + if (ENV.NO_CREDENTIALS && err.code === 'ENOTFOUND') { + // The metadata server wasn't found. This must not be GAE or GCE. throw missingCredentialsError; } @@ -385,8 +396,7 @@ function makeAuthorizedRequest(config) { // For detecting Sign errors on io.js (1.x) (or node 0.11.x) // E.g. errors in form: error:code:PEM routines:PEM_read_bio:error_name - var pemError = err.message && - err.message.indexOf('error:') !== -1; + var pemError = err.message && err.message.indexOf('error:') !== -1; if (err.message === 'SignFinal error' || pemError) { err.message = [ @@ -412,6 +422,9 @@ function makeAuthorizedRequest(config) { } makeRequest.getCredentials = authorize.getCredentials; + makeRequest.getEnvironment = function() { + return ENV; + }; return makeRequest; } diff --git a/lib/datastore/request.js b/lib/datastore/request.js index f38fe1e1b4a..c069082c14c 100644 --- a/lib/datastore/request.js +++ b/lib/datastore/request.js @@ -20,6 +20,7 @@ 'use strict'; +var http = require('http'); var https = require('https'); var streamEvents = require('stream-events'); var through = require('through2'); @@ -209,7 +210,7 @@ DatastoreRequest.prototype.get = function(keys, callback) { * //- * var companyKey = dataset.key(['Company', 123]); * var productKey = dataset.key(['Product', 'Computer']); - * + * * dataset.save([ * { * key: companyKey, @@ -492,6 +493,8 @@ DatastoreRequest.prototype.allocateIds = function(incompleteKey, n, callback) { * Make a request to the API endpoint. Properties to indicate a transactional or * non-transactional operation are added automatically. * + * @todo Handle non-HTTP 200 cases. + * * @param {string} method - Datastore action (allocateIds, commit, etc.). * @param {object=} body - Request configuration object. * @param {function} callback - The callback function. @@ -507,7 +510,8 @@ DatastoreRequest.prototype.allocateIds = function(incompleteKey, n, callback) { * transaction.makeReq('commit', deleteRequest, function(err) {}); */ DatastoreRequest.prototype.makeReq_ = function(method, body, callback) { - // TODO: Handle non-HTTP 200 cases. + var ENV = this.makeAuthorizedRequest_.getEnvironment(); + if (!callback) { callback = body; body = {}; @@ -550,7 +554,17 @@ DatastoreRequest.prototype.makeReq_ = function(method, body, callback) { return; } - var remoteStream = https.request(authorizedReqOpts, function(resp) { + var remoteStream; + + if (ENV.GAE_DEV && ENV.NO_CREDENTIALS) { + authorizedReqOpts.host = process.env.API_HOST; + authorizedReqOpts.port = process.env.GAE_SERVER_PORT; + remoteStream = http.request(authorizedReqOpts); + } else { + remoteStream = https.request(authorizedReqOpts); + } + + remoteStream.on('response', function(resp) { var buffer = new Buffer(''); resp.on('data', function(chunk) { buffer = Buffer.concat([buffer, chunk]); diff --git a/test/datastore/request.js b/test/datastore/request.js index 488fd71c5d4..1b346ab689e 100644 --- a/test/datastore/request.js +++ b/test/datastore/request.js @@ -22,6 +22,7 @@ var assert = require('assert'); var ByteBuffer = require('bytebuffer'); var entity = require('../../lib/datastore/entity.js'); var extend = require('extend'); +var http = require('http'); var https = require('https'); var mockery = require('mockery'); var mockRespGet = require('../testdata/response_get.json'); @@ -39,6 +40,15 @@ extend(true, https, { } }); +var httpRequestCached = http.request; +var httpRequestOverride = util.noop; + +extend(true, http, { + request: function() { + return httpRequestOverride.apply(this, util.toArray(arguments)); + } +}); + // Create a protobuf "FakeMethod" request & response. pb.FakeMethodRequest = function() { this.toBuffer = function() { @@ -62,6 +72,7 @@ describe('Request', function() { before(function() { mockery.registerMock('./pb.js', pb); mockery.registerMock('https', https); + mockery.registerMock('http', http); mockery.enable({ useCleanCache: true, warnOnUnregistered: false @@ -73,6 +84,7 @@ describe('Request', function() { mockery.deregisterAll(); mockery.disable(); httpsRequestOverride = httpsRequestCached; + httpRequestOverride = httpRequestCached; }); beforeEach(function() { @@ -85,6 +97,9 @@ describe('Request', function() { request.makeAuthorizedRequest_ = function(req, callback) { (callback.onAuthorized || callback)(null, req); }; + request.makeAuthorizedRequest_.getEnvironment = function() { + return {}; + }; }); describe('get', function() { @@ -487,10 +502,13 @@ describe('Request', function() { assert.equal(opts.headers['Content-Type'], 'application/x-protobuf'); done(); }; + request.makeAuthorizedRequest_.getEnvironment = function() { + return {}; + }; request.makeReq_(method, {}, util.noop); }); - it('should make https request', function(done) { + it('should make https request if not gae dev and no creds', function(done) { var mockRequest = { mock: 'request' }; httpsRequestOverride = function(req) { assert.deepEqual(req, mockRequest); @@ -500,6 +518,45 @@ describe('Request', function() { request.makeAuthorizedRequest_ = function(opts, callback) { (callback.onAuthorized || callback)(null, mockRequest); }; + request.makeAuthorizedRequest_.getEnvironment = function() { + return { + GAE_DEV: false, + NO_CREDENTIALS: false + }; + }; + request.makeReq_('commit', {}, util.noop); + }); + + it('should make http call to API_HOST if no credentials', function(done) { + var mockRequest = { mock: 'request' }; + + var API_HOST_CACHED = process.env.API_HOST; + var GAE_SERVER_PORT_CACHED = process.env.GAE_SERVER_PORT; + + process.env.API_HOST = 'API_HOST'; + process.env.GAE_SERVER_PORT = 99; + + httpRequestOverride = function(req) { + assert.deepEqual(req, mockRequest); + assert.equal(req.host, process.env.API_HOST); + assert.equal(req.port, process.env.GAE_SERVER_PORT); + + done(); + + process.env.API_HOST = API_HOST_CACHED; + process.env.GAE_SERVER_PORT = GAE_SERVER_PORT_CACHED; + + return new stream.Writable(); + }; + request.makeAuthorizedRequest_ = function(opts, callback) { + (callback.onAuthorized || callback)(null, mockRequest); + }; + request.makeAuthorizedRequest_.getEnvironment = function() { + return { + GAE_DEV: true, + NO_CREDENTIALS: true + }; + }; request.makeReq_('commit', {}, util.noop); }); @@ -521,11 +578,14 @@ describe('Request', function() { pbFakeMethodResponseDecode = function() { done(); }; - httpsRequestOverride = function(req, callback) { - var ws = new stream.Writable(); - callback(ws); - ws.emit('end'); - return ws; + httpsRequestOverride = function() { + var requestStream = new stream.PassThrough(); + var responseStream = new stream.PassThrough(); + setImmediate(function() { + requestStream.emit('response', responseStream); + responseStream.end(); + }); + return requestStream; }; request.makeReq_('fakeMethod', util.noop); }); @@ -535,6 +595,9 @@ describe('Request', function() { request.createAuthorizedRequest_ = function(opts, callback) { (callback.onAuthorized || callback)(); }; + request.makeAuthorizedRequest_.getEnvironment = function() { + return {}; + }; }); describe('commit', function() {