You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

221 lines
6.9 KiB

/**
* @license
* Copyright 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var url = require('url');
var Task = require('./task');
exports.inject = function(options) {
var key = options.key || process.env.GOOGLE_MAPS_API_KEY;
var channel = options.channel;
var clientId = options.clientId || process.env.GOOGLE_MAPS_API_CLIENT_ID;
var clientSecret = options.clientSecret || process.env.GOOGLE_MAPS_API_CLIENT_SECRET;
var rate = options.rate || {};
var rateLimit = rate.limit || 10; // 10 requests per ratePeriod.
var ratePeriod = rate.period || 1000; // 1 second.
var makeUrlRequest = options.makeUrlRequest || require('./make-url-request');
var mySetTimeout = options.setTimeout || setTimeout;
var myClearTimeout = options.clearTimeout || clearTimeout;
var getTime = options.getTime || function() {return new Date().getTime();};
var wait = require('./wait').inject(mySetTimeout, myClearTimeout);
var attempt = require('./attempt').inject(wait).attempt;
var ThrottledQueue = require('./throttled-queue').inject(wait, getTime);
var requestQueue = ThrottledQueue.create(rateLimit, ratePeriod);
/**
* Makes an API request using the injected makeUrlRequest.
*
* Inserts the API key (or client ID and signature) into the query
* parameters. Retries requests when the status code requires it.
* Parses the response body as JSON.
*
* The callback is given either an error or a response. The response
* is an object with the following entries:
* {
* status: number,
* body: string,
* json: Object
* }
*
* @param {string} path
* @param {Object} query This function mutates the query object.
* @param {Function} callback
* @return {{
* cancel: function(),
* finally: function(function()),
* asPromise: function(): Promise
* }}
*/
return function(path, query, callback) {
callback = callback || function() {};
var retryOptions = query.retryOptions || options.retryOptions || {};
delete query.retryOptions;
var timeout = query.timeout || options.timeout || 60 * 1000;
delete query.timeout;
var useClientId = query.supportsClientId && clientId && clientSecret;
delete query.supportsClientId;
var queryOptions = query.options || {};
delete query.options;
var isPost = queryOptions.method === 'POST'
var requestUrl = formatRequestUrl(path, isPost ? {} : query, useClientId);
if (isPost) {
queryOptions.body = query;
}
// Determines whether a response indicates a retriable error.
var canRetry = queryOptions.canRetry || function(response) {
return (
response == null
|| response.status === 500
|| response.status === 503
|| response.status === 504
|| (response.json && (
response.json.status === 'OVER_QUERY_LIMIT' ||
response.json.status === 'RESOURCE_EXHAUSTED')));
};
delete queryOptions.canRetry;
// Determines whether a response indicates success.
var isSuccessful = queryOptions.isSuccessful || function(response) {
return response.status === 200 && (
response.json == undefined ||
response.json.status === undefined ||
response.json.status === 'OK' ||
response.json.status === 'ZERO_RESULTS');
};
delete queryOptions.isSuccessful;
function rateLimitedGet() {
return requestQueue.add(function() {
return Task.start(function(resolve, reject) {
return makeUrlRequest(requestUrl, resolve, reject, queryOptions);
});
});
}
var timeoutTask = wait(timeout).thenDo(function() {
throw 'timeout';
});
var requestTask = attempt({
'do': rateLimitedGet,
until: function(response) { return !canRetry(response); },
interval: retryOptions.interval,
increment: retryOptions.increment,
jitter: retryOptions.jitter
});
var task =
Task.race([timeoutTask, requestTask])
.thenDo(function(response) {
// We add the request url and the original query to the response
// to be able to use them when debugging errors.
response.requestUrl = requestUrl;
response.query = query;
if (isSuccessful(response)) {
return Task.withValue(response);
} else {
return Task.withError(response);
}
})
.thenDo(
function(response) { callback(null, response); },
function(err) { callback(err); });
if (options.Promise) {
var originalCallback = callback;
var promise = new options.Promise(function(resolve, reject) {
callback = function(err, result) {
if (err != null) {
reject(err);
} else {
resolve(result);
}
originalCallback(err, result);
};
});
task.asPromise = function() { return promise; };
}
delete task.thenDo;
return task;
};
/**
* Adds auth information to the query, and formats it into a URL.
* @param {string} path
* @param {Object} query
* @param {boolean} useClientId
* @return {string} The formatted URL.
*/
function formatRequestUrl(path, query, useClientId) {
if (channel) {
query.channel = channel;
}
if (useClientId) {
query.client = clientId;
} else if (key && key.indexOf('AIza') == 0) {
query.key = key;
} else {
throw 'Missing either a valid API key, or a client ID and secret';
}
var requestUrl = url.format({pathname: path, query: query});
// When using client ID, generate and append the signature param.
if (useClientId) {
var secret = new Buffer(clientSecret, 'base64');
var payload = url.parse(requestUrl).path;
var signature = computeSignature(secret, payload);
requestUrl += '&signature=' + encodeURIComponent(signature);
}
return requestUrl;
}
/**
* @param {string} secret
* @param {string} payload
* @return {string}
*/
function computeSignature(secret, payload) {
var signature =
new Buffer(
require('crypto')
.createHmac('sha1', secret)
.update(payload)
.digest('base64'))
.toString()
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
while (signature.length % 4) {
signature += '=';
}
return signature;
}
};