|
@@ -0,0 +1,437 @@
|
|
|
+/**
|
|
|
+ * Copyright (c) 2014 Baidu.com, 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.
|
|
|
+ *
|
|
|
+ * @file src/http_client.js
|
|
|
+ * @author leeight
|
|
|
+ */
|
|
|
+
|
|
|
+/* eslint-env node */
|
|
|
+/* eslint max-params:[0,10] */
|
|
|
+/* globals ArrayBuffer */
|
|
|
+
|
|
|
+var http = require('http');
|
|
|
+var https = require('https');
|
|
|
+var util = require('util');
|
|
|
+var stream = require('stream');
|
|
|
+var EventEmitter = require('events').EventEmitter;
|
|
|
+
|
|
|
+var u = require('underscore');
|
|
|
+var Q = require('q');
|
|
|
+var debug = require('debug')('bce-sdk:HttpClient');
|
|
|
+
|
|
|
+var H = require('./headers');
|
|
|
+var Auth = require('./auth');
|
|
|
+
|
|
|
+/**
|
|
|
+ * The HttpClient
|
|
|
+ *
|
|
|
+ * @constructor
|
|
|
+ * @param {Object} config The http client configuration.
|
|
|
+ */
|
|
|
+function HttpClient(config) {
|
|
|
+ EventEmitter.call(this);
|
|
|
+
|
|
|
+ this.config = config;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * http(s) request object
|
|
|
+ * @type {Object}
|
|
|
+ */
|
|
|
+ this._req = null;
|
|
|
+}
|
|
|
+util.inherits(HttpClient, EventEmitter);
|
|
|
+
|
|
|
+/**
|
|
|
+ * Send Http Request
|
|
|
+ *
|
|
|
+ * @param {string} httpMethod GET,POST,PUT,DELETE,HEAD
|
|
|
+ * @param {string} path The http request path.
|
|
|
+ * @param {(string|Buffer|stream.Readable)=} body The request body. If `body` is a
|
|
|
+ * stream, `Content-Length` must be set explicitly.
|
|
|
+ * @param {Object=} headers The http request headers.
|
|
|
+ * @param {Object=} params The querystrings in url.
|
|
|
+ * @param {function():string=} signFunction The `Authorization` signature function
|
|
|
+ * @param {stream.Writable=} outputStream The http response body.
|
|
|
+ * @param {number=} retry The maximum number of network connection attempts.
|
|
|
+ *
|
|
|
+ * @resolve {{http_headers:Object,body:Object}}
|
|
|
+ * @reject {Object}
|
|
|
+ *
|
|
|
+ * @return {Q.defer}
|
|
|
+ */
|
|
|
+HttpClient.prototype.sendRequest = function (httpMethod, path, body, headers, params,
|
|
|
+ signFunction, outputStream) {
|
|
|
+ httpMethod = httpMethod.toUpperCase();
|
|
|
+ var requestUrl = this._getRequestUrl(path, params);
|
|
|
+ var options = require('url').parse(requestUrl);
|
|
|
+ debug('httpMethod = %s, requestUrl = %s, options = %j',
|
|
|
+ httpMethod, requestUrl, options);
|
|
|
+
|
|
|
+ // Prepare the request headers.
|
|
|
+ var defaultHeaders = {};
|
|
|
+ if (typeof navigator === 'object') {
|
|
|
+ defaultHeaders[H.USER_AGENT] = navigator.userAgent;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ defaultHeaders[H.USER_AGENT] = util.format('bce-sdk-nodejs/%s/%s/%s', require('../package.json').version,
|
|
|
+ process.platform, process.version);
|
|
|
+ }
|
|
|
+ defaultHeaders[H.X_BCE_DATE] = new Date().toISOString().replace(/\.\d+Z$/, 'Z');
|
|
|
+ defaultHeaders[H.CONNECTION] = 'close';
|
|
|
+ defaultHeaders[H.CONTENT_TYPE] = 'application/json; charset=UTF-8';
|
|
|
+ defaultHeaders[H.HOST] = options.host;
|
|
|
+
|
|
|
+ headers = u.extend({}, defaultHeaders, headers);
|
|
|
+
|
|
|
+ // if (!headers.hasOwnProperty(H.X_BCE_REQUEST_ID)) {
|
|
|
+ // headers[H.X_BCE_REQUEST_ID] = this._generateRequestId();
|
|
|
+ // }
|
|
|
+
|
|
|
+ // Check the content-length
|
|
|
+ if (!headers.hasOwnProperty(H.CONTENT_LENGTH)) {
|
|
|
+ var contentLength = this._guessContentLength(body);
|
|
|
+ if (!(contentLength === 0 && /GET|HEAD/i.test(httpMethod))) {
|
|
|
+ // 如果是 GET 或 HEAD 请求,并且 Content-Length 是 0,那么 Request Header 里面就不要出现 Content-Length
|
|
|
+ // 否则本地计算签名的时候会计算进去,但是浏览器发请求的时候不一定会有,此时导致 Signature Mismatch 的情况
|
|
|
+ headers[H.CONTENT_LENGTH] = contentLength;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var client = this;
|
|
|
+ options.method = httpMethod;
|
|
|
+ options.headers = headers;
|
|
|
+
|
|
|
+ // 通过browserify打包后,在Safari下并不能有效处理server的content-type
|
|
|
+ // 参考ISSUE:https://github.com/jhiesey/stream-http/issues/8
|
|
|
+ options.mode = 'prefer-fast';
|
|
|
+
|
|
|
+ // rejectUnauthorized: If true, the server certificate is verified against the list of supplied CAs.
|
|
|
+ // An 'error' event is emitted if verification fails.
|
|
|
+ // Verification happens at the connection level, before the HTTP request is sent.
|
|
|
+ options.rejectUnauthorized = false;
|
|
|
+
|
|
|
+ if (typeof signFunction === 'function') {
|
|
|
+ var promise = signFunction(this.config.credentials, httpMethod, path, params, headers);
|
|
|
+ if (isPromise(promise)) {
|
|
|
+ return promise.then(function (authorization, xbceDate) {
|
|
|
+ headers[H.AUTHORIZATION] = authorization;
|
|
|
+ if (xbceDate) {
|
|
|
+ headers[H.X_BCE_DATE] = xbceDate;
|
|
|
+ }
|
|
|
+ debug('options = %j', options);
|
|
|
+ return client._doRequest(options, body, outputStream);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ else if (typeof promise === 'string') {
|
|
|
+ headers[H.AUTHORIZATION] = promise;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ throw new Error('Invalid signature = (' + promise + ')');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ headers[H.AUTHORIZATION] = createSignature(this.config.credentials, httpMethod, path, params, headers);
|
|
|
+ }
|
|
|
+
|
|
|
+ debug('options = %j', options);
|
|
|
+ return client._doRequest(options, body, outputStream);
|
|
|
+};
|
|
|
+
|
|
|
+function createSignature(credentials, httpMethod, path, params, headers) {
|
|
|
+ var auth = new Auth(credentials.ak, credentials.sk);
|
|
|
+ return auth.generateAuthorization(httpMethod, path, params, headers);
|
|
|
+}
|
|
|
+
|
|
|
+function isPromise(obj) {
|
|
|
+ return obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
|
|
|
+}
|
|
|
+
|
|
|
+HttpClient.prototype._isValidStatus = function (statusCode) {
|
|
|
+ return statusCode >= 200 && statusCode < 300;
|
|
|
+};
|
|
|
+
|
|
|
+HttpClient.prototype._doRequest = function (options, body, outputStream) {
|
|
|
+ var deferred = Q.defer();
|
|
|
+ var api = options.protocol === 'https:' ? https : http;
|
|
|
+ var client = this;
|
|
|
+
|
|
|
+ var req = client._req = api.request(options, function (res) {
|
|
|
+ if (client._isValidStatus(res.statusCode) && outputStream
|
|
|
+ && outputStream instanceof stream.Writable) {
|
|
|
+ res.pipe(outputStream);
|
|
|
+ outputStream.on('finish', function () {
|
|
|
+ deferred.resolve(success(client._fixHeaders(res.headers), {}));
|
|
|
+ });
|
|
|
+ outputStream.on('error', function (error) {
|
|
|
+ deferred.reject(error);
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ deferred.resolve(client._recvResponse(res));
|
|
|
+ });
|
|
|
+
|
|
|
+ // 设置超时10s
|
|
|
+ if (typeof req.setTimeout === 'function') {
|
|
|
+ req.setTimeout(10e3);
|
|
|
+
|
|
|
+ req.on('timeout', function() {
|
|
|
+ deferred.reject(new Error('socket Timeout!'));
|
|
|
+
|
|
|
+ req.destroy();
|
|
|
+ });
|
|
|
+ } else if (req.xhr) {
|
|
|
+ req.xhr.timeout = 10e3;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (req.xhr && typeof req.xhr.upload === 'object') {
|
|
|
+ u.each(['progress', 'error', 'abort', 'timeout'], function (eventName) {
|
|
|
+ req.xhr.upload.addEventListener(eventName, function (evt) {
|
|
|
+ client.emit(eventName, evt);
|
|
|
+ }, false);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ req.on('error', function (error) {
|
|
|
+ deferred.reject(error);
|
|
|
+ });
|
|
|
+
|
|
|
+ try {
|
|
|
+ client._sendRequest(req, body);
|
|
|
+ }
|
|
|
+ catch (ex) {
|
|
|
+ deferred.reject(ex);
|
|
|
+ }
|
|
|
+ return deferred.promise;
|
|
|
+};
|
|
|
+
|
|
|
+HttpClient.prototype._generateRequestId = function () {
|
|
|
+ function chunk() {
|
|
|
+ var v = (~~(Math.random() * 0xffff)).toString(16);
|
|
|
+ if (v.length < 4) {
|
|
|
+ v += new Array(4 - v.length + 1).join('0');
|
|
|
+ }
|
|
|
+ return v;
|
|
|
+ }
|
|
|
+
|
|
|
+ return util.format('%s%s-%s-%s-%s-%s%s%s',
|
|
|
+ chunk(), chunk(), chunk(), chunk(),
|
|
|
+ chunk(), chunk(), chunk(), chunk());
|
|
|
+};
|
|
|
+
|
|
|
+HttpClient.prototype._guessContentLength = function (data) {
|
|
|
+ if (data == null) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ else if (typeof data === 'string') {
|
|
|
+ return Buffer.byteLength(data);
|
|
|
+ }
|
|
|
+ else if (typeof data === 'object') {
|
|
|
+ if (typeof Blob !== 'undefined' && data instanceof Blob) {
|
|
|
+ return data.size;
|
|
|
+ }
|
|
|
+ if (typeof ArrayBuffer !== 'undefined' && data instanceof ArrayBuffer) {
|
|
|
+ return data.byteLength;
|
|
|
+ }
|
|
|
+ if (Buffer.isBuffer(data)) {
|
|
|
+ return data.length;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ if (typeof FormData !== 'undefined' && data instanceof FormData) {
|
|
|
+ }
|
|
|
+ */
|
|
|
+ }
|
|
|
+ else if (Buffer.isBuffer(data)) {
|
|
|
+ return data.length;
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new Error('No Content-Length is specified.');
|
|
|
+};
|
|
|
+
|
|
|
+HttpClient.prototype._fixHeaders = function (headers) {
|
|
|
+ var fixedHeaders = {};
|
|
|
+
|
|
|
+ if (headers) {
|
|
|
+ Object.keys(headers).forEach(function (key) {
|
|
|
+ var value = headers[key].trim();
|
|
|
+ if (value) {
|
|
|
+ key = key.toLowerCase();
|
|
|
+ if (key === 'etag') {
|
|
|
+ value = value.replace(/"/g, '');
|
|
|
+ }
|
|
|
+ fixedHeaders[key] = value;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return fixedHeaders;
|
|
|
+};
|
|
|
+
|
|
|
+HttpClient.prototype._recvResponse = function (res) {
|
|
|
+ var responseHeaders = this._fixHeaders(res.headers);
|
|
|
+ var statusCode = res.statusCode;
|
|
|
+
|
|
|
+ function parseHttpResponseBody(raw) {
|
|
|
+ var contentType = responseHeaders['content-type'];
|
|
|
+
|
|
|
+ if (!raw.length) {
|
|
|
+ return {};
|
|
|
+ }
|
|
|
+ else if (contentType
|
|
|
+ && /(application|text)\/json/.test(contentType)) {
|
|
|
+ return JSON.parse(raw.toString());
|
|
|
+ }
|
|
|
+ return raw;
|
|
|
+ }
|
|
|
+
|
|
|
+ var deferred = Q.defer();
|
|
|
+
|
|
|
+ var payload = [];
|
|
|
+ /* eslint-disable */
|
|
|
+ res.on('data', function (chunk) {
|
|
|
+ if (Buffer.isBuffer(chunk)) {
|
|
|
+ payload.push(chunk);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ // xhr2返回的内容是 string,不是 Buffer,导致 Buffer.concat 的时候报错了
|
|
|
+ payload.push(new Buffer(chunk));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ res.on('error', function (e) {
|
|
|
+ deferred.reject(e);
|
|
|
+ });
|
|
|
+ /* eslint-enable */
|
|
|
+ res.on('end', function () {
|
|
|
+ var raw = Buffer.concat(payload);
|
|
|
+ var responseBody = null;
|
|
|
+
|
|
|
+ try {
|
|
|
+ debug('responseHeaders = %j', responseHeaders);
|
|
|
+ responseBody = parseHttpResponseBody(raw);
|
|
|
+ }
|
|
|
+ catch (e) {
|
|
|
+ debug('statusCode = %s, Parse response body error = %s', statusCode, e.message);
|
|
|
+ deferred.reject(failure(statusCode, e.message));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (statusCode >= 100 && statusCode < 200) {
|
|
|
+ deferred.reject(failure(statusCode, 'Can not handle 1xx http status code.'));
|
|
|
+ }
|
|
|
+ else if (statusCode < 100 || statusCode >= 300) {
|
|
|
+ if (responseBody.requestId) {
|
|
|
+ deferred.reject(failure(statusCode, responseBody.message,
|
|
|
+ responseBody.code, responseBody.requestId, responseHeaders.date));
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ deferred.reject(failure(statusCode, responseBody));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ deferred.resolve(success(responseHeaders, responseBody));
|
|
|
+ });
|
|
|
+
|
|
|
+ return deferred.promise;
|
|
|
+};
|
|
|
+
|
|
|
+/* eslint-disable */
|
|
|
+function isXHR2Compatible(obj) {
|
|
|
+ if (typeof Blob !== 'undefined' && obj instanceof Blob) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (typeof ArrayBuffer !== 'undefined' && obj instanceof ArrayBuffer) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (typeof FormData !== 'undefined' && obj instanceof FormData) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+}
|
|
|
+/* eslint-enable */
|
|
|
+
|
|
|
+HttpClient.prototype._sendRequest = function (req, data) {
|
|
|
+ /* eslint-disable */
|
|
|
+ if (!data) {
|
|
|
+ req.end();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (typeof data === 'string') {
|
|
|
+ data = new Buffer(data);
|
|
|
+ }
|
|
|
+ /* eslint-enable */
|
|
|
+
|
|
|
+ if (Buffer.isBuffer(data) || isXHR2Compatible(data)) {
|
|
|
+ req.write(data);
|
|
|
+ req.end();
|
|
|
+ }
|
|
|
+ else if (data instanceof stream.Readable) {
|
|
|
+ if (!data.readable) {
|
|
|
+ throw new Error('stream is not readable');
|
|
|
+ }
|
|
|
+
|
|
|
+ data.on('data', function (chunk) {
|
|
|
+ req.write(chunk);
|
|
|
+ });
|
|
|
+ data.on('end', function () {
|
|
|
+ req.end();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ throw new Error('Invalid body type = ' + typeof data);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+HttpClient.prototype.buildQueryString = function (params) {
|
|
|
+ var urlEncodeStr = require('querystring').stringify(params);
|
|
|
+ // https://en.wikipedia.org/wiki/Percent-encoding
|
|
|
+ return urlEncodeStr.replace(/[()'!~.*\-_]/g, function (char) {
|
|
|
+ return '%' + char.charCodeAt().toString(16);
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+HttpClient.prototype._getRequestUrl = function (path, params) {
|
|
|
+ var uri = path;
|
|
|
+ var qs = this.buildQueryString(params);
|
|
|
+ if (qs) {
|
|
|
+ uri += '?' + qs;
|
|
|
+ }
|
|
|
+
|
|
|
+ return this.config.endpoint + uri;
|
|
|
+};
|
|
|
+
|
|
|
+function success(httpHeaders, body) {
|
|
|
+ var response = {};
|
|
|
+
|
|
|
+ response[H.X_HTTP_HEADERS] = httpHeaders;
|
|
|
+ response[H.X_BODY] = body;
|
|
|
+
|
|
|
+ return response;
|
|
|
+}
|
|
|
+
|
|
|
+function failure(statusCode, message, code, requestId, xBceDate) {
|
|
|
+ var response = {};
|
|
|
+
|
|
|
+ response[H.X_STATUS_CODE] = statusCode;
|
|
|
+ response[H.X_MESSAGE] = Buffer.isBuffer(message) ? String(message) : message;
|
|
|
+ if (code) {
|
|
|
+ response[H.X_CODE] = code;
|
|
|
+ }
|
|
|
+ if (requestId) {
|
|
|
+ response[H.X_REQUEST_ID] = requestId;
|
|
|
+ }
|
|
|
+ if (xBceDate) {
|
|
|
+ response[H.X_BCE_DATE] = xBceDate;
|
|
|
+ }
|
|
|
+
|
|
|
+ return response;
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = HttpClient;
|
|
|
+
|