UNPKG

10.6 kBJavaScriptView Raw
1
2const debug = require('debug')('ali-oss');
3const sendToWormhole = require('stream-wormhole');
4const xml = require('xml2js');
5const AgentKeepalive = require('agentkeepalive');
6const HttpsAgentKeepalive = require('agentkeepalive').HttpsAgent;
7const merge = require('merge-descriptors');
8const platform = require('platform');
9const utility = require('utility');
10const urllib = require('urllib');
11const pkg = require('../package.json');
12const bowser = require('bowser');
13const signUtils = require('./common/signUtils');
14const _initOptions = require('./common/client/initOptions');
15const { createRequest } = require('./common/utils/createRequest');
16const { encoder } = require('./common/utils/encoder');
17const { getReqUrl } = require('./common/client/getReqUrl');
18const { setSTSToken } = require('./common/utils/setSTSToken');
19const { retry } = require('./common/utils/retry');
20
21const globalHttpAgent = new AgentKeepalive();
22const globalHttpsAgent = new HttpsAgentKeepalive();
23
24function Client(options, ctx) {
25 if (!(this instanceof Client)) {
26 return new Client(options, ctx);
27 }
28
29 if (options && options.inited) {
30 this.options = options;
31 } else {
32 this.options = Client.initOptions(options);
33 }
34
35 // support custom agent and urllib client
36 if (this.options.urllib) {
37 this.urllib = this.options.urllib;
38 } else {
39 this.urllib = urllib;
40 this.agent = this.options.agent || globalHttpAgent;
41 this.httpsAgent = this.options.httpsAgent || globalHttpsAgent;
42 }
43 this.ctx = ctx;
44 this.userAgent = this._getUserAgent();
45}
46
47/**
48 * Expose `Client`
49 */
50
51module.exports = Client;
52
53Client.initOptions = function initOptions(options) {
54 return _initOptions(options);
55};
56
57/**
58 * prototype
59 */
60
61const proto = Client.prototype;
62
63/**
64 * Object operations
65 */
66merge(proto, require('./common/object'));
67merge(proto, require('./object'));
68merge(proto, require('./common/image'));
69/**
70 * Bucket operations
71 */
72merge(proto, require('./common/bucket'));
73merge(proto, require('./bucket'));
74// multipart upload
75merge(proto, require('./managed-upload'));
76/**
77 * RTMP operations
78 */
79merge(proto, require('./rtmp'));
80
81/**
82 * common multipart-copy support node and browser
83 */
84merge(proto, require('./common/multipart-copy'));
85/**
86 * Common module parallel
87 */
88merge(proto, require('./common/parallel'));
89/**
90 * Multipart operations
91 */
92merge(proto, require('./common/multipart'));
93/**
94 * ImageClient class
95 */
96Client.ImageClient = require('./image')(Client);
97/**
98 * Cluster Client class
99 */
100Client.ClusterClient = require('./cluster')(Client);
101
102/**
103 * STS Client class
104 */
105Client.STS = require('./sts');
106
107/**
108 * get OSS signature
109 * @param {String} stringToSign
110 * @return {String} the signature
111 */
112proto.signature = function signature(stringToSign) {
113 debug('authorization stringToSign: %s', stringToSign);
114
115 return signUtils.computeSignature(this.options.accessKeySecret, stringToSign, this.options.headerEncoding);
116};
117
118proto._getReqUrl = getReqUrl;
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
141proto.authorization = function authorization(method, resource, subres, headers) {
142 const stringToSign = signUtils.buildCanonicalString(method.toUpperCase(), resource, {
143 headers,
144 parameters: subres
145 });
146
147 return signUtils.authorization(this.options.accessKeyId, this.options.accessKeySecret, stringToSign, this.options.headerEncoding);
148};
149
150/**
151 * request oss server
152 * @param {Object} params
153 * - {String} object
154 * - {String} bucket
155 * - {Object} [headers]
156 * - {Object} [query]
157 * - {Buffer} [content]
158 * - {Stream} [stream]
159 * - {Stream} [writeStream]
160 * - {String} [mime]
161 * - {Boolean} [xmlResponse]
162 * - {Boolean} [customResponse]
163 * - {Number} [timeout]
164 * - {Object} [ctx] request context, default is `this.ctx`
165 *
166 * @api private
167 */
168
169proto.request = async function (params) {
170 const isAvailableStream = params.stream ? params.stream.readable : true;
171 if (this.options.retryMax && isAvailableStream) {
172 this.request = retry(request.bind(this), this.options.retryMax, {
173 errorHandler: (err) => {
174 const _errHandle = (_err) => {
175 const statusErr = [-1, -2].includes(_err.status);
176 const requestErrorRetryHandle = this.options.requestErrorRetryHandle || (() => true);
177 return statusErr && requestErrorRetryHandle(_err);
178 };
179 if (_errHandle(err)) return true;
180 return false;
181 }
182 });
183 } else {
184 this.request = request.bind(this);
185 }
186
187 return await this.request(params);
188};
189
190async function request(params) {
191 const reqParams = createRequest.call(this, params);
192 let result;
193 let reqErr;
194 try {
195 result = await this.urllib.request(reqParams.url, reqParams.params);
196 debug('response %s %s, got %s, headers: %j', params.method, reqParams.url, result.status, result.headers);
197 } catch (err) {
198 reqErr = err;
199 }
200 let err;
201 if (result && params.successStatuses && params.successStatuses.indexOf(result.status) === -1) {
202 err = await this.requestError(result);
203 err.params = params;
204 } else if (reqErr) {
205 err = await this.requestError(reqErr);
206 }
207
208 if (err) {
209 if (params.customResponse && result && result.res) {
210 // consume the response stream
211 await sendToWormhole(result.res);
212 }
213
214 if (err.status === 403 && err.code === 'InvalidAccessKeyId' &&
215 this.options.accessKeyId.startsWith('STS.') &&
216 typeof this.options.refreshSTSToken === 'function') {
217 // prevent infinite loop, only trigger once within 10 seconds
218 if (!this._setOptions || Date.now() - this._setOptions > 10000) {
219 this._setOptions = Date.now();
220 await setSTSToken.call(this);
221 return this.request(params);
222 }
223 }
224
225 if (err.name === 'ResponseTimeoutError') {
226 err.message = `${err.message.split(',')[0]}, please increase the timeout or use multipartDownload.`;
227 }
228 throw err;
229 }
230
231 if (params.xmlResponse) {
232 result.data = await this.parseXML(result.data);
233 }
234 return result;
235};
236
237proto._getResource = function _getResource(params) {
238 let resource = '/';
239 if (params.bucket) resource += `${params.bucket}/`;
240 if (params.object) resource += encoder(params.object, this.options.headerEncoding);
241
242 return resource;
243};
244
245proto._escape = function _escape(name) {
246 return utility.encodeURIComponent(name).replace(/%2F/g, '/');
247};
248
249/*
250 * Get User-Agent for browser & node.js
251 * @example
252 * aliyun-sdk-nodejs/4.1.2 Node.js 5.3.0 on Darwin 64-bit
253 * aliyun-sdk-js/4.1.2 Safari 9.0 on Apple iPhone(iOS 9.2.1)
254 * aliyun-sdk-js/4.1.2 Chrome 43.0.2357.134 32-bit on Windows Server 2008 R2 / 7 64-bit
255 */
256
257proto._getUserAgent = function _getUserAgent() {
258 const agent = (process && process.browser) ? 'js' : 'nodejs';
259 const sdk = `aliyun-sdk-${agent}/${pkg.version}`;
260 let plat = platform.description;
261 if (!plat && process) {
262 plat = `Node.js ${process.version.slice(1)} on ${process.platform} ${process.arch}`;
263 }
264
265 return this._checkUserAgent(`${sdk} ${plat}`);
266};
267
268proto._checkUserAgent = function _checkUserAgent(ua) {
269 const userAgent = ua.replace(/\u03b1/, 'alpha').replace(/\u03b2/, 'beta');
270 return userAgent;
271};
272
273/*
274 * Check Browser And Version
275 * @param {String} [name] browser name: like IE, Chrome, Firefox
276 * @param {String} [version] browser major version: like 10(IE 10.x), 55(Chrome 55.x), 50(Firefox 50.x)
277 * @return {Bool} true or false
278 * @api private
279 */
280
281proto.checkBrowserAndVersion = function checkBrowserAndVersion(name, version) {
282 return ((bowser.name === name) && (bowser.version.split('.')[0] === version));
283};
284
285/**
286 * thunkify xml.parseString
287 * @param {String|Buffer} str
288 *
289 * @api private
290 */
291
292proto.parseXML = function parseXMLThunk(str) {
293 return new Promise((resolve, reject) => {
294 if (Buffer.isBuffer(str)) {
295 str = str.toString();
296 }
297 xml.parseString(str, {
298 explicitRoot: false,
299 explicitArray: false
300 }, (err, result) => {
301 if (err) {
302 reject(err);
303 } else {
304 resolve(result);
305 }
306 });
307 });
308};
309
310/**
311 * generater a request error with request response
312 * @param {Object} result
313 *
314 * @api private
315 */
316
317proto.requestError = async function requestError(result) {
318 let err = null;
319 if (result.name === 'ResponseTimeoutError') {
320 err = new Error(result.message);
321 err.name = result.name;
322 } else if (!result.data || !result.data.length) {
323 if (result.status === -1 || result.status === -2) { // -1 is net error , -2 is timeout
324 err = new Error(result.message);
325 err.name = result.name;
326 err.status = result.status;
327 err.code = result.name;
328 } else {
329 // HEAD not exists resource
330 if (result.status === 404) {
331 err = new Error('Object not exists');
332 err.name = 'NoSuchKeyError';
333 err.status = 404;
334 err.code = 'NoSuchKey';
335 } else if (result.status === 412) {
336 err = new Error('Pre condition failed');
337 err.name = 'PreconditionFailedError';
338 err.status = 412;
339 err.code = 'PreconditionFailed';
340 } else {
341 err = new Error(`Unknow error, status: ${result.status}`);
342 err.name = 'UnknowError';
343 err.status = result.status;
344 }
345 err.requestId = result.headers['x-oss-request-id'];
346 err.host = '';
347 }
348 } else {
349 const message = String(result.data);
350 debug('request response error data: %s', message);
351
352 let info;
353 try {
354 info = await this.parseXML(message) || {};
355 } catch (error) {
356 debug(message);
357 error.message += `\nraw xml: ${message}`;
358 error.status = result.status;
359 error.requestId = result.headers['x-oss-request-id'];
360 return error;
361 }
362
363 let msg = info.Message || (`unknow request error, status: ${result.status}`);
364 if (info.Condition) {
365 msg += ` (condition: ${info.Condition})`;
366 }
367 err = new Error(msg);
368 err.name = info.Code ? `${info.Code}Error` : 'UnknowError';
369 err.status = result.status;
370 err.code = info.Code;
371 err.requestId = info.RequestId;
372 err.hostId = info.HostId;
373 }
374
375 debug('generate error %j', err);
376 return err;
377};
378
379proto.setSLDEnabled = function setSLDEnabled(enable) {
380 this.options.sldEnable = !!enable;
381 return this;
382};