client.js 13 KB

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