oss-client
Version:
Aliyun OSS(Object Storage Service) Node.js Client
187 lines (164 loc) • 5.25 kB
text/typescript
import { debuglog } from 'node:util';
import crypto from 'node:crypto';
import type { IncomingHttpHeaders } from 'urllib';
import type { SignatureUrlOptions } from 'oss-interface';
import type { Request, RequestParameters } from '../type/Request.js';
import { encodeCallback } from './encodeCallback.js';
const debug = debuglog('oss-client:sign');
const OSS_PREFIX = 'x-oss-';
function compareCanonicalizedString(entry1: string, entry2: string) {
if (entry1[0] > entry2[0]) {
return 1;
} else if (entry1[0] < entry2[0]) {
return -1;
}
return 0;
}
/**
* build canonicalized resource
* @see https://help.aliyun.com/zh/oss/developer-reference/include-signatures-in-the-authorization-header#section-rvv-dx2-xdb
*/
function buildCanonicalizedResource(
resourcePath: string,
parameters?: RequestParameters
) {
let canonicalizedResource = `${resourcePath}`;
let separatorString = '?';
if (typeof parameters === 'string') {
if (parameters.trim()) {
canonicalizedResource += separatorString + parameters;
}
} else if (Array.isArray(parameters)) {
parameters.sort();
canonicalizedResource += separatorString + parameters.join('&');
} else if (parameters) {
const processFunc = (key: string) => {
canonicalizedResource += separatorString + key;
if (parameters[key] || parameters[key] === 0) {
canonicalizedResource += `=${parameters[key]}`;
}
separatorString = '&';
};
for (const key of Object.keys(parameters).sort(
compareCanonicalizedString
)) {
processFunc(key);
}
}
debug('canonicalizedResource: %o', canonicalizedResource);
return canonicalizedResource;
}
function lowercaseKeyHeader(headers: IncomingHttpHeaders) {
const lowercaseHeaders: IncomingHttpHeaders = {};
if (headers) {
for (const name in headers) {
lowercaseHeaders[name.toLowerCase()] = headers[name];
}
}
return lowercaseHeaders;
}
export function buildCanonicalString(
method: string,
resourcePath: string,
request: Request,
expiresTimestamp?: string
) {
const headers = lowercaseKeyHeader(request.headers);
const headersToSign: IncomingHttpHeaders = {};
const signContent: string[] = [
method.toUpperCase(),
(headers['content-md5'] as string) ?? '',
headers['content-type'] as string,
expiresTimestamp || (headers['x-oss-date'] as string),
];
for (const key of Object.keys(headers)) {
if (key.startsWith(OSS_PREFIX)) {
headersToSign[key] = String(headers[key]).trim();
}
}
for (const key of Object.keys(headersToSign).sort()) {
signContent.push(`${key}:${headersToSign[key]}`);
}
signContent.push(
buildCanonicalizedResource(resourcePath, request.parameters)
);
return signContent.join('\n');
}
export function computeSignature(
accessKeySecret: string,
canonicalString: string
) {
const signature = crypto.createHmac('sha1', accessKeySecret);
return signature.update(Buffer.from(canonicalString)).digest('base64');
}
export function authorization(
accessKeyId: string,
accessKeySecret: string,
canonicalString: string
) {
// https://help.aliyun.com/zh/oss/developer-reference/include-signatures-in-the-authorization-header
return `OSS ${accessKeyId}:${computeSignature(accessKeySecret, canonicalString)}`;
}
export function signatureForURL(
accessKeySecret: string,
options: SignatureUrlOptions,
resource: string,
expiresTimestamp: number
) {
const headers: Record<string, string> = {};
const subResource = options.subResource ?? {};
if (options.process) {
subResource['x-oss-process'] = options.process;
}
if (options.trafficLimit) {
subResource['x-oss-traffic-limit'] = `${options.trafficLimit}`;
}
if (options.response) {
const customResponseHeaders = options.response as Record<string, string>;
for (const k in customResponseHeaders) {
subResource[`response-${k.toLowerCase()}`] = customResponseHeaders[k];
}
}
if (options['Content-MD5'] && !options['content-md5']) {
options['content-md5'] = options['Content-MD5'];
}
if (options['Content-Md5'] && !options['content-md5']) {
options['content-md5'] = options['Content-Md5'];
}
if (options['content-md5']) {
headers['content-md5'] = options['content-md5'];
}
if (options['Content-Type'] && !options['content-type']) {
options['content-type'] = options['Content-Type'];
}
if (options['content-type']) {
headers['content-type'] = options['content-type'];
}
// copy other x-oss-* headers
for (const key in options) {
const lowerKey = key.toLowerCase();
if (lowerKey.startsWith('x-oss-')) {
headers[lowerKey] = options[key];
}
}
if (options.callback) {
const callbackOptions = encodeCallback(options.callback);
subResource.callback = callbackOptions.callback;
if (callbackOptions.callbackVar) {
subResource['callback-var'] = callbackOptions.callbackVar;
}
}
const canonicalString = buildCanonicalString(
options.method as string,
resource,
{
headers,
parameters: subResource,
},
`${expiresTimestamp}`
);
return {
Signature: computeSignature(accessKeySecret, canonicalString),
subResource,
};
}