Skip to content

Commit

Permalink
feat: support non-english http header (#842)
Browse files Browse the repository at this point in the history
* feat: support non-english http header

* test: Add test cases for non-English request header information

* feat: add headerEncoding option

* feat: add encoder types

* fix: remove object spread syntax

Co-authored-by: biejia <wb-bj559483@alibaba-inc.com>
  • Loading branch information
beajer and biejia committed Aug 3, 2020
1 parent 87dd7d6 commit 4b6f511
Show file tree
Hide file tree
Showing 16 changed files with 184 additions and 23 deletions.
7 changes: 4 additions & 3 deletions lib/browser/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const signUtils = require('../common/signUtils');
const { isIP: _isIP } = require('../common/utils/isIP');
const _initOptions = require('../common/client/initOptions');
const { createRequest } = require('../common/utils/createRequest');
const { encoder } = require('../common/utils/encoder');

const globalHttpAgent = new AgentKeepalive();

Expand Down Expand Up @@ -127,7 +128,7 @@ merge(proto, require('../common/parallel'));
proto.signature = function signature(stringToSign) {
this.debug('authorization stringToSign: %s', stringToSign, 'info');

return signUtils.computeSignature(this.options.accessKeySecret, stringToSign);
return signUtils.computeSignature(this.options.accessKeySecret, stringToSign, this.options.headerEncoding);
};

/**
Expand Down Expand Up @@ -157,7 +158,7 @@ proto.authorization = function authorization(method, resource, subres, headers)
parameters: subres
});

return signUtils.authorization(this.options.accessKeyId, this.options.accessKeySecret, stringToSign);
return signUtils.authorization(this.options.accessKeyId, this.options.accessKeySecret, stringToSign, this.options.headerEncoding);
};

/**
Expand Down Expand Up @@ -221,7 +222,7 @@ proto.request = async function request(params) {
proto._getResource = function _getResource(params) {
let resource = '/';
if (params.bucket) resource += `${params.bucket}/`;
if (params.object) resource += params.object;
if (params.object) resource += encoder(params.object, this.options.headerEncoding);

return resource;
};
Expand Down
2 changes: 1 addition & 1 deletion lib/browser/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ proto.signatureUrl = function signatureUrl(name, options) {
options['security-token'] = this.options.stsToken;
}

const signRes = signHelper._signatureForURL(this.options.accessKeySecret, options, resource, expires);
const signRes = signHelper._signatureForURL(this.options.accessKeySecret, options, resource, expires, this.options.headerEncoding);

const url = urlutil.parse(this._getReqUrl(params));
url.query = {
Expand Down
8 changes: 7 additions & 1 deletion lib/bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ function toArray(obj) {
proto.listBuckets = async function listBuckets(query = {}, options = {}) {
// prefix, marker, max-keys

const { subres = {}, ...rest } = query;
const { subres = {} } = query;
const rest = {};
for (const key in query) {
if (key !== 'subres') {
rest[key] = query[key];
}
}
const params = this._bucketRequestParams(
'GET',
'',
Expand Down
7 changes: 4 additions & 3 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const signUtils = require('./common/signUtils');
const { isIP: _isIP } = require('./common/utils/isIP');
const _initOptions = require('./common/client/initOptions');
const { createRequest } = require('./common/utils/createRequest');
const { encoder } = require('./common/utils/encoder');

const globalHttpAgent = new AgentKeepalive();
const globalHttpsAgent = new HttpsAgentKeepalive();
Expand Down Expand Up @@ -112,7 +113,7 @@ Client.STS = require('./sts');
proto.signature = function signature(stringToSign) {
debug('authorization stringToSign: %s', stringToSign);

return signUtils.computeSignature(this.options.accessKeySecret, stringToSign);
return signUtils.computeSignature(this.options.accessKeySecret, stringToSign, this.options.headerEncoding);
};

/**
Expand Down Expand Up @@ -142,7 +143,7 @@ proto.authorization = function authorization(method, resource, subres, headers)
parameters: subres
});

return signUtils.authorization(this.options.accessKeyId, this.options.accessKeySecret, stringToSign);
return signUtils.authorization(this.options.accessKeyId, this.options.accessKeySecret, stringToSign, this.options.headerEncoding);
};

/**
Expand Down Expand Up @@ -199,7 +200,7 @@ proto.request = async function request(params) {
proto._getResource = function _getResource(params) {
let resource = '/';
if (params.bucket) resource += `${params.bucket}/`;
if (params.object) resource += params.object;
if (params.object) resource += encoder(params.object, this.options.headerEncoding);

return resource;
};
Expand Down
3 changes: 2 additions & 1 deletion lib/common/client/initOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ module.exports = function (options) {
endpoint: null,
cname: false,
isRequestPay: false,
sldEnable: false
sldEnable: false,
headerEncoding: 'utf-8'
}, options);

opts.accessKeyId = opts.accessKeyId.trim();
Expand Down
12 changes: 6 additions & 6 deletions lib/common/signUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,18 +82,18 @@ exports.buildCanonicalString = function canonicalString(method, resourcePath, re
* @param {String} accessKeySecret
* @param {String} canonicalString
*/
exports.computeSignature = function computeSignature(accessKeySecret, canonicalString) {
exports.computeSignature = function computeSignature(accessKeySecret, canonicalString, headerEncoding = 'utf-8') {
const signature = crypto.createHmac('sha1', accessKeySecret);
return signature.update(Buffer.from(canonicalString, 'utf8')).digest('base64');
return signature.update(Buffer.from(canonicalString, headerEncoding)).digest('base64');
};

/**
* @param {String} accessKeyId
* @param {String} accessKeySecret
* @param {String} canonicalString
*/
exports.authorization = function authorization(accessKeyId, accessKeySecret, canonicalString) {
return `OSS ${accessKeyId}:${this.computeSignature(accessKeySecret, canonicalString)}`;
exports.authorization = function authorization(accessKeyId, accessKeySecret, canonicalString, headerEncoding) {
return `OSS ${accessKeyId}:${this.computeSignature(accessKeySecret, canonicalString, headerEncoding)}`;
};

/**
Expand All @@ -103,7 +103,7 @@ exports.authorization = function authorization(accessKeyId, accessKeySecret, can
* @param {String} resource
* @param {Number} expires
*/
exports._signatureForURL = function _signatureForURL(accessKeySecret, options = {}, resource, expires) {
exports._signatureForURL = function _signatureForURL(accessKeySecret, options = {}, resource, expires, headerEncoding) {
const headers = {};
const { subResource = {} } = options;

Expand Down Expand Up @@ -168,7 +168,7 @@ exports._signatureForURL = function _signatureForURL(accessKeySecret, options =
}, expires.toString());

return {
Signature: this.computeSignature(accessKeySecret, canonicalString),
Signature: this.computeSignature(accessKeySecret, canonicalString, headerEncoding),
subResource
};
};
9 changes: 8 additions & 1 deletion lib/common/utils/createRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const mime = require('mime');
const dateFormat = require('dateformat');
const copy = require('copy-to');
const path = require('path');
const { encoder } = require('./encoder');
function getHeader(headers, name) {
return headers[name] || headers[name.toLowerCase()];
}
Expand Down Expand Up @@ -53,8 +54,14 @@ function createRequest(params) {
headers['Content-Length'] = params.content.length;
}
}
const { hasOwnProperty } = Object.prototype;
for (const k in headers) {
if (headers[k] && hasOwnProperty.call(headers, k)) {
headers[k] = encoder(String(headers[k]), this.options.headerEncoding);
}
}
const authResource = this._getResource(params);
headers.authorization = this.authorization(params.method, authResource, params.subres, headers);
headers.authorization = this.authorization(params.method, authResource, params.subres, headers, this.options.headerEncoding);
const url = this._getReqUrl(params);
debug('request %s %s, with headers %j, !!stream: %s', params.method, url, headers, !!params.stream);
const timeout = params.timeout || this.options.timeout;
Expand Down
10 changes: 9 additions & 1 deletion lib/common/utils/createRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const mime = require('mime');
const dateFormat = require('dateformat');
const copy = require('copy-to');
const path = require('path');
const { encoder } = require('./encoder')

interface Headers {
[propName: string]: any
Expand Down Expand Up @@ -70,8 +71,15 @@ export function createRequest(this: any, params) {
}
}

const { hasOwnProperty } = Object.prototype;
for (const k in headers) {
if (headers[k] && hasOwnProperty.call(headers, k)) {
headers[k] = encoder(String(headers[k]), this.options.headerEncoding);
}
}

const authResource = this._getResource(params);
headers.authorization = this.authorization(params.method, authResource, params.subres, headers);
headers.authorization = this.authorization(params.method, authResource, params.subres, headers, this.options.headerEncoding);

const url = this._getReqUrl(params);
debug('request %s %s, with headers %j, !!stream: %s', params.method, url, headers, !!params.stream);
Expand Down
1 change: 1 addition & 0 deletions lib/common/utils/encoder.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export declare function encoder(str: string, encoding?: string): string;
9 changes: 9 additions & 0 deletions lib/common/utils/encoder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.encoder = void 0;
function encoder(str, encoding = 'utf-8') {
if (encoding === 'utf-8')
return str;
return Buffer.from(str).toString('latin1');
}
exports.encoder = encoder;
6 changes: 6 additions & 0 deletions lib/common/utils/encoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { THeaderEncoding } from '../../types/experimental';

export function encoder(str: string, encoding: THeaderEncoding = 'utf-8') {
if (encoding === 'utf-8') return str;
return Buffer.from(str).toString('latin1');
}
2 changes: 1 addition & 1 deletion lib/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ proto.signatureUrl = function signatureUrl(name, options) {
options['security-token'] = this.options.stsToken;
}

const signRes = signHelper._signatureForURL(this.options.accessKeySecret, options, resource, expires);
const signRes = signHelper._signatureForURL(this.options.accessKeySecret, options, resource, expires, this.options.headerEncoding);

const url = urlutil.parse(this._getReqUrl(params));
url.query = {
Expand Down
1 change: 1 addition & 0 deletions lib/types/experimental.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type THeaderEncoding = 'utf-8' | 'latin1';
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"prepublish": "npm run snyk-protect",
"lint-staged": "lint-staged",
"detect-secrets": "node task/detect-secrets",
"tsc": "npm run tsc:clean && tsc:build",
"tsc": "npm run tsc:clean && npm run tsc:build",
"tsc:build": "tsc -b tsconfig.json tsconfig-cjs.json",
"tsc:watch": "tsc -b tsconfig.json tsconfig-cjs.json --watch",
"tsc:clean": "tsc -b tsconfig.json tsconfig-cjs.json --clean "
Expand Down
56 changes: 56 additions & 0 deletions test/browser/browser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1608,4 +1608,60 @@ describe('browser', () => {
store.urllib.request.restore();
});
});

describe('options.headerEncoding', () => {
let store;
const utf8_content = '阿达的大多';
const latin1_content = Buffer.from(utf8_content).toString('latin1');
let name;
before(async () => {
store = oss(Object.assign({}, ossConfig, { headerEncoding: 'latin1' }));
name = `${prefix}ali-sdk/oss/put-new-latin1.js`;
const result = await store.put(name, Buffer.from('123'), {
meta: {
a: utf8_content
}
});
assert.equal(result.res.status, 200);
const info = await store.head(name);
assert.equal(info.status, 200);
assert.equal(info.meta.a, latin1_content);
});

it('copy() should return 200 when set zh-cn meta', async () => {
const originname = `${prefix}ali-sdk/oss/copy-new-latin1.js`;
const result = await store.copy(originname, name, {
meta: {
a: utf8_content
}
});
assert.equal(result.res.status, 200);
const info = await store.head(originname);
assert.equal(info.status, 200);
assert.equal(info.meta.a, latin1_content);
});

it('copy() should return 200 when set zh-cn meta with zh-cn object name', async () => {
const originname = `${prefix}ali-sdk/oss/copy-new-latin1-中文.js`;
const result = await store.copy(originname, name, {
meta: {
a: utf8_content
}
});
assert.equal(result.res.status, 200);
const info = await store.head(originname);
assert.equal(info.status, 200);
assert.equal(info.meta.a, latin1_content);
});

it('putMeta() should return 200', async () => {
const result = await store.putMeta(name, {
b: utf8_content
});
assert.equal(result.res.status, 200);
const info = await store.head(name);
assert.equal(info.status, 200);
assert.equal(info.meta.b, latin1_content);
});
});
});
Loading

0 comments on commit 4b6f511

Please sign in to comment.