You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
335 lines
8.6 KiB
335 lines
8.6 KiB
const url = require('url');
|
|
const crypto = require('crypto');
|
|
const zone = require('./zone');
|
|
|
|
// Check Timestamp Expired or not
|
|
exports.isTimestampExpired = function (timestamp) {
|
|
return timestamp < parseInt(Date.now() / 1000);
|
|
};
|
|
|
|
// Format Data
|
|
exports.formatDateUTC = function (date, layout) {
|
|
function pad (num, digit) {
|
|
const d = digit || 2;
|
|
let result = num.toString();
|
|
while (result.length < d) {
|
|
result = '0' + result;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
const d = new Date(date);
|
|
const year = d.getUTCFullYear();
|
|
const month = d.getUTCMonth() + 1;
|
|
const day = d.getUTCDate();
|
|
const hour = d.getUTCHours();
|
|
const minute = d.getUTCMinutes();
|
|
const second = d.getUTCSeconds();
|
|
const millisecond = d.getUTCMilliseconds();
|
|
|
|
let result = layout || 'YYYY-MM-DDTHH:MM:ss.SSSZ';
|
|
|
|
result = result.replace(/YYYY/g, year.toString())
|
|
.replace(/MM/g, pad(month))
|
|
.replace(/DD/g, pad(day))
|
|
.replace(/HH/g, pad(hour))
|
|
.replace(/mm/g, pad(minute))
|
|
.replace(/ss/g, pad(second))
|
|
.replace(/SSS/g, pad(millisecond, 3));
|
|
|
|
return result;
|
|
};
|
|
|
|
// Encoded Entry
|
|
exports.encodedEntry = function (bucket, key) {
|
|
return exports.urlsafeBase64Encode(bucket + (key ? ':' + key : ''));
|
|
};
|
|
|
|
// Get accessKey from uptoken
|
|
exports.getAKFromUptoken = function (uploadToken) {
|
|
var sepIndex = uploadToken.indexOf(':');
|
|
return uploadToken.substring(0, sepIndex);
|
|
};
|
|
|
|
// Get bucket from uptoken
|
|
exports.getBucketFromUptoken = function (uploadToken) {
|
|
var sepIndex = uploadToken.lastIndexOf(':');
|
|
var encodedPutPolicy = uploadToken.substring(sepIndex + 1);
|
|
var putPolicy = exports.urlSafeBase64Decode(encodedPutPolicy);
|
|
var putPolicyObj = JSON.parse(putPolicy);
|
|
var scope = putPolicyObj.scope;
|
|
var scopeSepIndex = scope.indexOf(':');
|
|
if (scopeSepIndex == -1) {
|
|
return scope;
|
|
} else {
|
|
return scope.substring(0, scopeSepIndex);
|
|
}
|
|
};
|
|
|
|
exports.base64ToUrlSafe = function (v) {
|
|
return v.replace(/\//g, '_').replace(/\+/g, '-');
|
|
};
|
|
|
|
exports.urlSafeToBase64 = function (v) {
|
|
return v.replace(/_/g, '/').replace(/-/g, '+');
|
|
};
|
|
|
|
// UrlSafe Base64 Decode
|
|
exports.urlsafeBase64Encode = function (jsonFlags) {
|
|
var encoded = Buffer.from(jsonFlags).toString('base64');
|
|
return exports.base64ToUrlSafe(encoded);
|
|
};
|
|
|
|
// UrlSafe Base64 Decode
|
|
exports.urlSafeBase64Decode = function (fromStr) {
|
|
return Buffer.from(exports.urlSafeToBase64(fromStr), 'base64').toString();
|
|
};
|
|
|
|
// Hmac-sha1 Crypt
|
|
exports.hmacSha1 = function (encodedFlags, secretKey) {
|
|
/*
|
|
*return value already encoded with base64
|
|
* */
|
|
var hmac = crypto.createHmac('sha1', secretKey);
|
|
hmac.update(encodedFlags);
|
|
return hmac.digest('base64');
|
|
};
|
|
|
|
// get md5
|
|
exports.getMd5 = function (data) {
|
|
var md5 = crypto.createHash('md5');
|
|
return md5.update(data).digest('hex');
|
|
};
|
|
|
|
// 创建 AccessToken 凭证
|
|
// @param mac AK&SK对象
|
|
// @param requestURI 请求URL
|
|
// @param reqBody 请求Body,仅当请求的 ContentType 为
|
|
// application/x-www-form-urlencoded时才需要传入该参数
|
|
exports.generateAccessToken = function (mac, requestURI, reqBody) {
|
|
var u = new url.URL(requestURI);
|
|
var path = u.pathname + u.search;
|
|
var access = path + '\n';
|
|
|
|
if (reqBody) {
|
|
access += reqBody;
|
|
}
|
|
|
|
var digest = exports.hmacSha1(access, mac.secretKey);
|
|
var safeDigest = exports.base64ToUrlSafe(digest);
|
|
return 'QBox ' + mac.accessKey + ':' + safeDigest;
|
|
};
|
|
|
|
const isTokenTable = {
|
|
'!': true,
|
|
'#': true,
|
|
$: true,
|
|
'%': true,
|
|
'&': true,
|
|
'\\': true,
|
|
'*': true,
|
|
'+': true,
|
|
'-': true,
|
|
'.': true,
|
|
0: true,
|
|
1: true,
|
|
2: true,
|
|
3: true,
|
|
4: true,
|
|
5: true,
|
|
6: true,
|
|
7: true,
|
|
8: true,
|
|
9: true,
|
|
A: true,
|
|
B: true,
|
|
C: true,
|
|
D: true,
|
|
E: true,
|
|
F: true,
|
|
G: true,
|
|
H: true,
|
|
I: true,
|
|
J: true,
|
|
K: true,
|
|
L: true,
|
|
M: true,
|
|
N: true,
|
|
O: true,
|
|
P: true,
|
|
Q: true,
|
|
R: true,
|
|
S: true,
|
|
T: true,
|
|
U: true,
|
|
W: true,
|
|
V: true,
|
|
X: true,
|
|
Y: true,
|
|
Z: true,
|
|
'^': true,
|
|
_: true,
|
|
'`': true,
|
|
a: true,
|
|
b: true,
|
|
c: true,
|
|
d: true,
|
|
e: true,
|
|
f: true,
|
|
g: true,
|
|
h: true,
|
|
i: true,
|
|
j: true,
|
|
k: true,
|
|
l: true,
|
|
m: true,
|
|
n: true,
|
|
o: true,
|
|
p: true,
|
|
q: true,
|
|
r: true,
|
|
s: true,
|
|
t: true,
|
|
u: true,
|
|
v: true,
|
|
w: true,
|
|
x: true,
|
|
y: true,
|
|
z: true,
|
|
'|': true,
|
|
'~': true
|
|
};
|
|
/**
|
|
* 是否合法的 header field name 字符
|
|
* @param ch string
|
|
* @return boolean|undefined
|
|
*/
|
|
function validHeaderKeyChar (ch) {
|
|
if (ch.charCodeAt(0) >= 128) {
|
|
return false;
|
|
}
|
|
return isTokenTable[ch];
|
|
}
|
|
|
|
/**
|
|
* 规范化 header field name
|
|
* @param fieldName string
|
|
* @return string
|
|
*/
|
|
exports.canonicalMimeHeaderKey = function (fieldName) {
|
|
for (const ch of fieldName) {
|
|
if (!validHeaderKeyChar(ch)) {
|
|
return fieldName;
|
|
}
|
|
}
|
|
return fieldName.split('-')
|
|
.map(function (text) {
|
|
return text.substring(0, 1).toUpperCase() + text.substring(1).toLowerCase();
|
|
})
|
|
.join('-');
|
|
};
|
|
|
|
// 创建 AccessToken 凭证
|
|
// @param mac AK&SK对象
|
|
// @param requestURI 请求URL
|
|
// @param reqMethod 请求方法,例如 GET,POST
|
|
// @param reqContentType 请求类型,例如 application/json 或者 application/x-www-form-urlencoded
|
|
// @param reqBody 请求Body,仅当请求的 ContentType 为 application/json 或者
|
|
// application/x-www-form-urlencoded 时才需要传入该参数
|
|
exports.generateAccessTokenV2 = function (mac, requestURI, reqMethod, reqContentType, reqBody, reqHeaders) {
|
|
var u = new url.URL(requestURI);
|
|
var path = u.pathname;
|
|
var search = u.search;
|
|
var host = u.host;
|
|
var port = u.port;
|
|
|
|
var access = reqMethod.toUpperCase() + ' ' + path;
|
|
if (search) {
|
|
access += search;
|
|
}
|
|
// add host
|
|
access += '\nHost: ' + host;
|
|
// add port
|
|
if (port) {
|
|
access += ':' + port;
|
|
}
|
|
|
|
// add content type
|
|
if (reqContentType) {
|
|
access += '\nContent-Type: ' + reqContentType;
|
|
} else {
|
|
access += '\nContent-Type: application/x-www-form-urlencoded';
|
|
}
|
|
|
|
// add headers
|
|
if (reqHeaders) {
|
|
const canonicalHeaders = Object.keys(reqHeaders)
|
|
.reduce(function (acc, k) {
|
|
acc[exports.canonicalMimeHeaderKey(k)] = reqHeaders[k];
|
|
return acc;
|
|
}, {});
|
|
const headerText = Object.keys(canonicalHeaders)
|
|
.filter(function (k) {
|
|
return k.startsWith('X-Qiniu-') && k.length > 'X-Qiniu-'.length;
|
|
})
|
|
.sort()
|
|
.map(function (k) {
|
|
return k + ': ' + canonicalHeaders[k];
|
|
})
|
|
.join('\n');
|
|
if (headerText) {
|
|
access += '\n' + headerText;
|
|
}
|
|
}
|
|
|
|
access += '\n\n';
|
|
|
|
// add reqbody
|
|
if (reqBody && reqContentType !== 'application/octet-stream') {
|
|
access += reqBody;
|
|
}
|
|
|
|
var digest = exports.hmacSha1(access, mac.secretKey);
|
|
var safeDigest = exports.base64ToUrlSafe(digest);
|
|
return 'Qiniu ' + mac.accessKey + ':' + safeDigest;
|
|
};
|
|
|
|
// 校验七牛上传回调的Authorization
|
|
// @param mac AK&SK对象
|
|
// @param requestURI 回调的URL中的requestURI
|
|
// @param reqBody 请求Body,仅当请求的ContentType为
|
|
// application/x-www-form-urlencoded时才需要传入该参数
|
|
// @param callbackAuth 回调时请求的Authorization头部值
|
|
exports.isQiniuCallback = function (mac, requestURI, reqBody, callbackAuth) {
|
|
var auth = exports.generateAccessToken(mac, requestURI, reqBody);
|
|
return auth === callbackAuth;
|
|
};
|
|
|
|
exports.prepareZone = function (ctx, accessKey, bucket, callback) {
|
|
var useCache = false;
|
|
if (ctx.config.zone !== '' && ctx.config.zone != null) {
|
|
if (ctx.config.zoneExpire === -1) {
|
|
useCache = true;
|
|
} else {
|
|
if (!exports.isTimestampExpired(ctx.config.zoneExpire)) {
|
|
useCache = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (useCache) {
|
|
callback(null, ctx);
|
|
} else {
|
|
zone.getZoneInfo(accessKey, bucket, function (err, cZoneInfo,
|
|
cZoneExpire) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
// update object
|
|
ctx.config.zone = cZoneInfo;
|
|
ctx.config.zoneExpire = cZoneExpire + parseInt(Date.now() / 1000);
|
|
callback(null, ctx);
|
|
});
|
|
}
|
|
};
|
|
|