client.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. 'use strict';
  2. var debug = require('debug')('ali-oss');
  3. var crypto = require('crypto');
  4. var path = require('path');
  5. var copy = require('copy-to');
  6. var mime = require('mime');
  7. var xml = require('xml2js');
  8. var ms = require('humanize-ms');
  9. var AgentKeepalive = require('agentkeepalive');
  10. var merge = require('merge-descriptors');
  11. var urlutil = require('url');
  12. var is = require('is-type-of');
  13. var platform = require('platform');
  14. var utility = require('utility');
  15. var urllib = require('urllib');
  16. var pkg = require('./version');
  17. var dateFormat = require('dateformat');
  18. var bowser = require('bowser');
  19. var globalHttpAgent = new AgentKeepalive();
  20. /**
  21. * Expose `Client`
  22. */
  23. module.exports = Client;
  24. function Client(options, ctx) {
  25. if (!(this instanceof Client)) {
  26. return new Client(options, ctx);
  27. }
  28. if (options && options.inited) {
  29. this.options = options;
  30. } else {
  31. this.options = Client.initOptions(options);
  32. }
  33. // support custom agent and urllib client
  34. if (this.options.urllib) {
  35. this.urllib = this.options.urllib;
  36. } else {
  37. this.urllib = urllib;
  38. this.agent = this.options.agent || globalHttpAgent;
  39. }
  40. this.ctx = ctx;
  41. this.userAgent = this._getUserAgent();
  42. // record the time difference between client and server
  43. this.amendTimeSkewed = 0;
  44. }
  45. Client.initOptions = function initOptions(options) {
  46. if (!options
  47. || !options.accessKeyId
  48. || !options.accessKeySecret) {
  49. throw new Error('require accessKeyId, accessKeySecret');
  50. }
  51. var opts = {
  52. region: 'oss-cn-hangzhou',
  53. internal: false,
  54. secure: false,
  55. bucket: null,
  56. endpoint: null,
  57. cname: false,
  58. };
  59. for (var key in options) {
  60. if (options[key] === undefined) continue;
  61. opts[key] = options[key];
  62. }
  63. opts.accessKeyId = opts.accessKeyId.trim();
  64. opts.accessKeySecret = opts.accessKeySecret.trim();
  65. if (opts.timeout) {
  66. opts.timeout = ms(opts.timeout);
  67. }
  68. if (opts.endpoint) {
  69. opts.endpoint = setEndpoint(opts.endpoint);
  70. } else if (opts.region) {
  71. opts.endpoint = setRegion(
  72. opts.region, opts.internal, opts.secure);
  73. } else {
  74. throw new Error('require options.endpoint or options.region');
  75. }
  76. opts.inited = true;
  77. return opts;
  78. };
  79. /**
  80. * prototype
  81. */
  82. var proto = Client.prototype;
  83. /**
  84. * Object operations
  85. */
  86. merge(proto, require('./object'));
  87. // /**
  88. // * Bucket operations
  89. // */
  90. // merge(proto, require('./bucket'));
  91. /**
  92. * Multipart operations
  93. */
  94. merge(proto, require('./multipart'));
  95. /**
  96. * ImageClient class
  97. */
  98. // Client.ImageClient = require('./image')(Client);
  99. // /**
  100. // * Cluster Client class
  101. // */
  102. // Client.ClusterClient = require('./cluster')(Client);
  103. /**
  104. * STS Client class
  105. */
  106. // Client.STS = require('./sts');
  107. /**
  108. * Aysnc wrapper
  109. */
  110. Client.Wrapper = require('./wrapper');
  111. /**
  112. * get OSS signature
  113. * @param {String} stringToSign
  114. * @return {String} the signature
  115. */
  116. proto.signature = function signature(stringToSign) {
  117. debug('authorization stringToSign: %s', stringToSign);
  118. var signature = crypto.createHmac('sha1', this.options.accessKeySecret);
  119. signature = signature.update(new Buffer(stringToSign, 'utf8')).digest('base64');
  120. return signature;
  121. };
  122. /**
  123. * get author header
  124. *
  125. * "Authorization: OSS " + Access Key Id + ":" + Signature
  126. *
  127. * Signature = base64(hmac-sha1(Access Key Secret + "\n"
  128. * + VERB + "\n"
  129. * + CONTENT-MD5 + "\n"
  130. * + CONTENT-TYPE + "\n"
  131. * + DATE + "\n"
  132. * + CanonicalizedOSSHeaders
  133. * + CanonicalizedResource))
  134. *
  135. * @param {String} method
  136. * @param {String} resource
  137. * @param {Object} header
  138. * @return {String}
  139. *
  140. * @api private
  141. */
  142. proto.authorization = function authorization(method, resource, subres, headers) {
  143. var params = [
  144. method.toUpperCase(),
  145. headers['Content-Md5'] || '',
  146. getHeader(headers, 'Content-Type'),
  147. headers['x-oss-date']
  148. ];
  149. var ossHeaders = {};
  150. for (var key in headers) {
  151. var lkey = key.toLowerCase().trim();
  152. if (lkey.indexOf('x-oss-') === 0) {
  153. ossHeaders[lkey] = ossHeaders[lkey] || [];
  154. ossHeaders[lkey].push(String(headers[key]).trim());
  155. }
  156. }
  157. var ossHeadersList = [];
  158. Object.keys(ossHeaders).sort().forEach(function (key) {
  159. ossHeadersList.push(key + ':' + ossHeaders[key].join(','));
  160. });
  161. params = params.concat(ossHeadersList);
  162. var resourceStr = '';
  163. resourceStr += resource;
  164. var subresList = [];
  165. if (subres) {
  166. if (is.string(subres)) {
  167. subresList.push(subres);
  168. } else if (is.array(subres)) {
  169. subresList = subresList.concat(subres);
  170. } else {
  171. for (var k in subres) {
  172. var item = subres[k] ? k + '=' + subres[k] : k;
  173. subresList.push(item);
  174. }
  175. }
  176. }
  177. if (subresList.length > 0) {
  178. subresList = subresList.sort(function (a, b) {
  179. return a > b;
  180. });
  181. resourceStr += '?' + subresList.join('&');
  182. }
  183. debug('CanonicalizedResource: %s', resourceStr);
  184. params.push(resourceStr);
  185. var stringToSign = params.join('\n');
  186. var auth = 'OSS ' + this.options.accessKeyId + ':';
  187. return auth + this.signature(stringToSign);
  188. };
  189. /**
  190. * create request params
  191. * See `request`
  192. * @api private
  193. */
  194. proto.createRequest = function createRequest(params) {
  195. var headers = {
  196. 'x-oss-date': dateFormat(+new Date() + this.amendTimeSkewed, 'UTC:ddd, dd mmm yyyy HH:MM:ss \'GMT\''),
  197. 'x-oss-user-agent': this.userAgent,
  198. 'User-Agent': this.userAgent
  199. };
  200. if (this.options.stsToken) {
  201. headers['x-oss-security-token'] = this.options.stsToken;
  202. }
  203. copy(params.headers).to(headers);
  204. if (!getHeader(headers, 'Content-Type')) {
  205. if (params.mime === mime.default_type) {
  206. params.mime = '';
  207. }
  208. if (params.mime && params.mime.indexOf('/') > 0) {
  209. headers['Content-Type'] = params.mime;
  210. } else {
  211. headers['Content-Type'] = mime.getType(params.mime || path.extname(params.object || '')) || 'application/octet-stream';
  212. }
  213. }
  214. if (params.content) {
  215. headers['Content-Md5'] = crypto
  216. .createHash('md5')
  217. .update(new Buffer(params.content, 'utf8'))
  218. .digest('base64');
  219. if (!headers['Content-Length']) {
  220. headers['Content-Length'] = params.content.length;
  221. }
  222. }
  223. var authResource = this._getResource(params);
  224. headers.authorization = this.authorization(
  225. params.method, authResource, params.subres, headers);
  226. var url = this._getReqUrl(params)
  227. debug('request %s %s, with headers %j, !!stream: %s', params.method, url, headers, !!params.stream);
  228. var timeout = params.timeout || this.options.timeout;
  229. var reqParams = {
  230. agent: this.agent,
  231. method: params.method,
  232. content: params.content,
  233. stream: params.stream,
  234. headers: headers,
  235. timeout: timeout,
  236. writeStream: params.writeStream,
  237. customResponse: params.customResponse,
  238. ctx: params.ctx || this.ctx,
  239. };
  240. return {
  241. url: url,
  242. params: reqParams
  243. };
  244. };
  245. /**
  246. * request oss server
  247. * @param {Object} params
  248. * - {String} object
  249. * - {String} bucket
  250. * - {Object} [headers]
  251. * - {Object} [query]
  252. * - {Buffer} [content]
  253. * - {Stream} [stream]
  254. * - {Stream} [writeStream]
  255. * - {String} [mime]
  256. * - {Boolean} [xmlResponse]
  257. * - {Boolean} [customResponse]
  258. * - {Number} [timeout]
  259. * - {Object} [ctx] request context, default is `this.ctx`
  260. *
  261. * @api private
  262. */
  263. proto.request = function* request(params) {
  264. var reqParams = this.createRequest(params);
  265. var result;
  266. var reqErr;
  267. try {
  268. result = yield this.urllib.request(reqParams.url, reqParams.params);
  269. debug('response %s %s, got %s, headers: %j', params.method, reqParams.url, result.status, result.headers);
  270. } catch (err) {
  271. reqErr = err;
  272. }
  273. var err
  274. if (result && params.successStatuses && params.successStatuses.indexOf(result.status) === -1) {
  275. err = yield this.requestError(result);
  276. if (err.code === 'RequestTimeTooSkewed') {
  277. this.amendTimeSkewed = +new Date(err.serverTime) - new Date()
  278. return yield this.request(params);
  279. }
  280. err.params = params;
  281. } else if (reqErr) {
  282. err = yield this.requestError(reqErr);
  283. }
  284. if (err) {
  285. throw err;
  286. }
  287. if (params.xmlResponse) {
  288. result.data = yield this.parseXML(result.data);
  289. }
  290. return result;
  291. };
  292. proto._getResource = function _getResource(params) {
  293. var resource = '/';
  294. if (params.bucket) resource += params.bucket + '/';
  295. if (params.object) resource += params.object;
  296. return resource;
  297. };
  298. proto._isIP = function _isIP(host) {
  299. var ipv4Regex = /^(\d{1,3}\.){3,3}\d{1,3}$/;
  300. return ipv4Regex.test(host);
  301. };
  302. proto._escape = function _escape(name) {
  303. return utility.encodeURIComponent(name).replace(/%2F/g, '/');
  304. }
  305. proto._getReqUrl = function _getReqUrl(params) {
  306. var ep = {};
  307. copy(this.options.endpoint).to(ep);
  308. var isIP = this._isIP(ep.hostname);
  309. var isCname = this.options.cname;
  310. if (params.bucket && !isCname && !isIP) {
  311. ep.host = params.bucket + '.' + ep.host;
  312. }
  313. var path = '/';
  314. if (params.bucket && isIP) {
  315. path += params.bucket + '/';
  316. }
  317. if (params.object) {
  318. // Preserve '/' in result url
  319. path += this._escape(params.object);
  320. }
  321. ep.pathname = path;
  322. var query = {};
  323. if (params.query) {
  324. merge(query, params.query);
  325. }
  326. if (params.subres) {
  327. var subresAsQuery = {};
  328. if (is.string(params.subres)) {
  329. subresAsQuery[params.subres] = '';
  330. } else if (is.array(params.subres)) {
  331. params.subres.forEach(function (k) {
  332. subresAsQuery[k] = '';
  333. });
  334. } else {
  335. subresAsQuery = params.subres;
  336. }
  337. merge(query, subresAsQuery);
  338. }
  339. ep.query = query;
  340. // As '%20' is not recognized by OSS server, we must convert it to '+'.
  341. return urlutil.format(ep).replace(/%20/g, '+');
  342. };
  343. /*
  344. * Get User-Agent for browser & node.js
  345. * @example
  346. * aliyun-sdk-nodejs/4.1.2 Node.js 5.3.0 on Darwin 64-bit
  347. * aliyun-sdk-js/4.1.2 Safari 9.0 on Apple iPhone(iOS 9.2.1)
  348. * aliyun-sdk-js/4.1.2 Chrome 43.0.2357.134 32-bit on Windows Server 2008 R2 / 7 64-bit
  349. */
  350. proto._getUserAgent = function _getUserAgent() {
  351. var agent = (process && process.browser) ? 'js' : 'nodejs';
  352. var sdk = 'aliyun-sdk-' + agent + '/' + pkg.version;
  353. var plat = platform.description;
  354. if (!plat && process) {
  355. plat = 'Node.js ' + process.version.slice(1) + ' on ' + process.platform + ' ' + process.arch;
  356. }
  357. return sdk + ' ' + plat;
  358. };
  359. /*
  360. * Check Browser And Version
  361. * @param {String} [name] browser name: like IE, Chrome, Firefox
  362. * @param {String} [version] browser major version: like 10(IE 10.x), 55(Chrome 55.x), 50(Firefox 50.x)
  363. * @return {Bool} true or false
  364. * @api private
  365. */
  366. proto.checkBrowserAndVersion = function checkBrowserAndVersion(name, version) {
  367. return ((bowser.name == name) && (bowser.version.split('.')[0] == version));
  368. };
  369. /**
  370. * thunkify xml.parseString
  371. * @param {String|Buffer} str
  372. *
  373. * @api private
  374. */
  375. proto.parseXML = function parseXMLThunk(str) {
  376. return function parseXML(done) {
  377. if (Buffer.isBuffer(str)) {
  378. str = str.toString();
  379. }
  380. xml.parseString(str, {
  381. explicitRoot: false,
  382. explicitArray: false
  383. }, done);
  384. };
  385. };
  386. /**
  387. * generater a request error with request response
  388. * @param {Object} result
  389. *
  390. * @api private
  391. */
  392. proto.requestError = function* requestError(result) {
  393. var err;
  394. if (!result.data || !result.data.length) {
  395. if (result.status === -1 || result.status === -2) { //-1 is net error , -2 is timeout
  396. err = new Error(result.message);
  397. err.name = result.name;
  398. err.status = result.status;
  399. err.code = result.name;
  400. } else {
  401. // HEAD not exists resource
  402. if (result.status === 404) {
  403. err = new Error('Object not exists');
  404. err.name = 'NoSuchKeyError';
  405. err.status = 404;
  406. err.code = 'NoSuchKey';
  407. } else if (result.status === 412) {
  408. err = new Error('Pre condition failed');
  409. err.name = 'PreconditionFailedError';
  410. err.status = 412;
  411. err.code = 'PreconditionFailed';
  412. } else {
  413. err = new Error('Unknow error, status: ' + result.status);
  414. err.name = 'UnknowError';
  415. err.status = result.status;
  416. }
  417. err.requestId = result.headers['x-oss-request-id'];
  418. err.host = '';
  419. }
  420. } else {
  421. var message = String(result.data);
  422. debug('request response error data: %s', message);
  423. var info;
  424. try {
  425. info = yield this.parseXML(message) || {};
  426. } catch (err) {
  427. debug(message);
  428. err.message += '\nraw xml: ' + message;
  429. err.status = result.status;
  430. err.requestId = result.headers['x-oss-request-id'];
  431. return err;
  432. }
  433. var message = info.Message || ('unknow request error, status: ' + result.status);
  434. if (info.Condition) {
  435. message += ' (condition: ' + info.Condition + ')';
  436. }
  437. var err = new Error(message);
  438. err.name = info.Code ? info.Code + 'Error' : 'UnknowError';
  439. err.status = result.status;
  440. err.code = info.Code;
  441. err.requestId = info.RequestId;
  442. err.hostId = info.HostId;
  443. err.serverTime = info.ServerTime;
  444. }
  445. debug('generate error %j', err);
  446. return err;
  447. };
  448. function getHeader(headers, name) {
  449. return headers[name] || headers[name.toLowerCase()];
  450. }
  451. function setEndpoint(endpoint) {
  452. var url = urlutil.parse(endpoint);
  453. if (!url.protocol) {
  454. url = urlutil.parse('http://' + endpoint);
  455. }
  456. if (url.protocol != 'http:' && url.protocol != 'https:') {
  457. throw new Error('Endpoint protocol must be http or https.');
  458. }
  459. return url;
  460. }
  461. function setRegion(region, internal, secure) {
  462. var protocol = secure ? 'https://' : 'http://';
  463. var suffix = internal ? '-internal.aliyuncs.com' : '.aliyuncs.com';
  464. var prefix = 'vpc100-oss-cn-';
  465. // aliyun VPC region: https://help.aliyun.com/knowledge_detail/38740.html
  466. if (region.substr(0, prefix.length) === prefix) {
  467. suffix = '.aliyuncs.com';
  468. }
  469. return urlutil.parse(protocol + region + suffix);
  470. }