UNPKG

ks3

Version:

本代码库为`金山云存储KS3`服务.主要提供`KS3 nodejs SDK`和`命令行工具`.

413 lines (385 loc) 14.4 kB
var requestPromise = require('request'); var config = require('./../config'); var util = require('./util'); var xml2json = util.xml2json; var debug = require('debug')('ks3:request'); var UAParser = require('ua-parser-js'); var dns = require('dns'); var auth = require('./auth'); var { RetryState } = require('./retry'); var { RETRY_STRATEGY } = require('./const'); // 内存缓存,存储DNS查询结果 var dnsCache = new Map(); var dnsCacheTimeout = null; var enableDnsCache = false; function makeRequest(req, body, headers = {}, cb) { let self = this; var maxRetryTimes = this.maxRetryTimes; // 最大重试次数 req.retryTimes = req.retryTimes || 0; let uri = req.uri; enableDnsCache = this.dnsCache; //是否启用DNS缓存 dnsCacheTimeout = this.dnsCacheTimeout; // DNS缓存时间 let options = { uri, headers: headers, method: req.method, timeout: req.timeout || 1000 * 60 * 2, } //配置代理 if (this.proxyUrl) { options.proxy = this.proxyUrl; } // 配置代理认证的Proxy-Authorization头,因为ks3的认证header也是Authorization,所以这里用Proxy-Authorization if (this.proxyAuth && this.proxyAuth.pass && this.proxyAuth.user) { options.headers['Proxy-Authorization'] = 'Basic ' + Buffer.from( this.proxyAuth.user + ':' + this.proxyAuth.pass ).toString('base64'); } options.followRedirect = this.followRedirect; let readStreamBody = null; if (body && typeof body.pipe === 'function') { // 如果body是一个流对象 则将其赋值给readStreamBody 便于pipe readStreamBody = body; options.body = null; }else if (Buffer.isBuffer(body) || typeof body === 'string') { options.body = body; options.headers['Content-Length'] = Buffer.byteLength(body, 'utf8') } else if (util.isObject(body)) { options.data = body; } let hasReturned = false; // 如果启用DNS缓存,解析域名获取family if(enableDnsCache){ //node高版本默认ipv6,为兼容版本问题,默认只使用ipv4 const family = 4; options.family = family; options.lookup = (hostname, options, callback) => customDnsLookup(hostname, options.family, callback, enableDnsCache); } let sender = requestPromise(options); if (readStreamBody) { readStreamBody.pipe(sender); } const streamHandler = function (err, response, body) { if (hasReturned) { cb(err, null, response, body) return } hasReturned = true debug('err', err) if (response) debug('statusCode', response.statusCode) if (!cb) { return } let data = '' if (!!body && JSON.stringify(body) !== '{}') { data = body if (!req.rawBody) { let dataStr = body.toString(); data = dataStr; const dataType = req.dataType || config.dataType; if (dataType === 'json') { dataStr = dataStr.replace(/\s\w+\:\w+=\"\S+\"/g, ''); const json = xml2json.parser(dataStr) ? xml2json.parser(dataStr) : dataStr; data = json; } } } if (sender) { sender.removeAllListeners && sender.removeAllListeners(); sender.on('error', function () {}); sender = null; } debug('data', data) // 增加原始数据流,因为在写文件的时候 // `buf.toString` 会有问题 // err: 异常信息 // data: 转格式后的数据 // response: response // body: 原始数据 cb(err, data, response, body) } sender.on('error', function (err) { const canRetry = shouldRetry(err, self.retryStrategy); if (canRetry && !readStreamBody && maxRetryTimes > req.retryTimes) { headers.date = util.getDate(); req.date = headers.date; const token = auth.generateAuth(self.ak, self.sk, req, body); headers.Authorization = token; setTimeout(() => { req.retryTimes++; makeRequest.call(self, req, body, headers, cb); }, getRetryDelay.call(self, req.retryTimes)); return } streamHandler(util.error(err)); }) sender.on('response', function (response) { let statusCode = response.statusCode; let requestId = response.caseless.get('x-kss-request-id'); let responseHeaders = response.headers; let curReqHost = response.client._host; // 当前请求的host let isSuccess = statusCode >= 200 && statusCode < 300; let dataList = []; //如果有 Output,直接将流pipe到 Output if (req.Output) { req.Output.on('error', function (err) { sender && sender.abort && sender.abort(); streamHandler(err, null, null); }) sender.pipe(req.Output) //此时直接将数据流送往 Output //提前返回,不再占用 dataList 内存 return sender; } // 手动处理重定向 if ( statusCode > 300 && statusCode < 400 && options.followRedirect && response.headers.location && !readStreamBody ) { sender.removeAllListeners && sender.removeAllListeners(); sender.abort(); req.uri = response.headers.location; console.log('redirect to:', response.headers, req.uri); return makeRequest.call(self, req, body, headers, cb); } //增加重试机制 const canRetry = shouldRetry(response, self.retryStrategy) if (canRetry && !readStreamBody && maxRetryTimes > req.retryTimes) { headers.date = util.getDate(); req.date = headers.date; const token = auth.generateAuth(self.ak, self.sk, req, body); headers.Authorization = token; setTimeout(() => { req.retryTimes++; console.log('retry:', req.retryTimes, req); makeRequest.call(self, req, body, headers, cb); }, getRetryDelay.call(self, req.retryTimes)); return } sender.on('data', function (data) { dataList.push(data); }) sender.on('end', function () { let bodyBuf = Buffer.from(''); try { bodyBuf = Buffer.concat(dataList); } catch (e) { console.error('end error:', e); streamHandler(util.error(e)); return } let body = bodyBuf; // 如果返回有结果则需要解析xml let json = {}; try { if (!isSuccess) { body = bodyBuf.toString(); json = (body && body.indexOf('<') > -1 && body.indexOf('>') > -1 && xml2json.parser(body)) || {}; } } catch (e) {} // 处理返回值 let xmlError = json && json.Error; if (isSuccess) { streamHandler(null, response, body); } else if (xmlError) { // 正常返回了 xml body,且有 Error 节点 xmlError = util.lowerFirst(xmlError); streamHandler( util.error(xmlError, responseHeaders, { requestId: requestId, host: curReqHost, statusCode: statusCode, code: statusCode, error: body, }), response, body ) } else if (statusCode) { // 有错误的状态码 如307 streamHandler( util.error( { message: response.statusMessage, code: '' + statusCode, }, responseHeaders, { host: curReqHost, statusCode: statusCode, requestId: requestId, code: statusCode, } ), response, body ) } else { // 无状态码,或者获取不到状态码 streamHandler( util.error(new Error('statusCode error')), response, body ) } dataList = null; // streamHandler(null, response, body) }) }) // upload progress if (req.onProgress && typeof req.onProgress === 'function') { let contentLength = data.headers['Content-Length']; let time0 = Date.now(); let size0 = 0; sender.on('drain', function () { let time1 = Date.now(); let loaded = 0; try { // 已经上传的字节数 = socket当前累计发送的字节数 - 头部长度 - socket以前发送的字节数 loaded = sender.req.connection.bytesWritten - sender.req._header.length - (sender.req.connection._lastBytesWritten || 0) } catch (e) {} let total = contentLength; let speed = parseInt(((loaded - size0) / ((time1 - time0) / 1000)) * 100) / 100 || 0; let percent = parseInt((loaded / total) * 100) / 100 || 0; time0 = time1; size0 = loaded; req.onProgress({ loaded: loaded, total: total, speed: speed, percent: percent, }) }) } // download progress if ( req.onDownloadProgress && typeof req.onDownloadProgress === 'function' ) { let time0 = Date.now(); let size0 = 0; let loaded = 0; let total = 0; sender.on('response', function (res) { total = res.headers['content-length']; sender.on('data', function (chunk) { loaded += chunk.length; let time1 = Date.now(); let speed = parseInt( ((loaded - size0) / ((time1 - time0) / 1000)) * 100 ) / 100 || 0; let percent = parseInt((loaded / total) * 100) / 100 || 0; time0 = time1; size0 = loaded; req.onDownloadProgress({ loaded: loaded, total: total, speed: speed, percent: percent, }) }) }) } return sender } function sendRequest(reqParams, body, token, cb) { let ua = ''; let uaParser = new UAParser(); if (uaParser.getResult().ua) { ua = config.ua + ' ' + uaParser.getResult().ua; } // add debug mode requestPromise.debug = config.isDebug; const headers = { 'Content-Type': reqParams.type || config.contentType, 'User-Agent': ua || reqParams.ua || config.ua, // warning: 这个地方需要注意,因为后端的token产生需要`date`,所以需要在请求的时候,把用于计算授权的`date`再传给后端, // 不能自己生成,否则token就报错了 date: reqParams.date, ...(reqParams.headers || {}), } // console.log('this.sk:', this.sk, 'this.ak:', this.ak) // console.log('headers:', headers) if (this.ak && this.sk) { headers.Authorization = token; } // set default timeout from init config reqParams.timeout = this.timeout return makeRequest.call(this, reqParams, body, headers, cb); } function customDnsLookup(hostname, family, callback, enableDnsCache = true) { if (!hostname) { return callback(new Error('Hostname is required')); } hostname = util.convertToASCII(hostname); // 将域名转换为 ASCII 格式 const now = Date.now(); // 当前时间戳 // 如果启用了DNS缓存,并且缓存中存在有效的记录 if (enableDnsCache && dnsCache.has(hostname)) { const cacheEntry = dnsCache.get(hostname); if (cacheEntry.expires > now) { const { address, family } = cacheEntry; return callback(null, address, family); } else { // 移除过期的缓存记录 dnsCache.delete(hostname); } } // 执行实际的DNS查询 dns.lookup(hostname, { family }, (err, address) => { if (err) { return callback(err); } // 如果启用了 DNS 缓存 if (enableDnsCache) { dnsCache.set(hostname, { address, family, expires: Date.now() + dnsCacheTimeout, }) } return callback(null, address, family); }) } function shouldRetry(response, retryStrategy) { if (!retryStrategy) { return false; } if (!response) { return true; } if (response && !response.statusCode) { return true; } //通过状态码判断是否需要重试 if ([408, 429, 500, 502, 503, 504].indexOf(response.statusCode) > -1) { return true; } return false; } function getRetryDelay(retryTimes) { // 可以根据实际情况自定义哪些错误可以重试 const retryStrategyClass = RETRY_STRATEGY[this.retryStrategy]; var retryWaitStrategy = new retryStrategyClass(); var retryState = new RetryState(retryTimes + 1); // 调用策略的 invoke 方法 var retryDelay = retryWaitStrategy.invoke(retryState); return retryDelay; } module.exports = sendRequest;