From eb61f23c9c3684872893dc9314a02ad18c1526d5 Mon Sep 17 00:00:00 2001 From: clia Date: Mon, 25 Apr 2022 17:46:48 +0800 Subject: [PATCH] Add support for URL Signature function. --- README.md | 8 ++ package.json | 37 ++++++++ src/models/configurationSettings.ts | 41 +++++++++ src/models/urlSignConfiguration.ts | 23 +++++ src/utils/httpClient.ts | 125 ++++++++++++++++++++++++++++ 5 files changed, 234 insertions(+) create mode 100644 src/models/urlSignConfiguration.ts diff --git a/README.md b/README.md index d47760e4..6ad6fac7 100644 --- a/README.md +++ b/README.md @@ -381,6 +381,14 @@ GET https://httpbin.org/aws-auth HTTP/1.1 Authorization: AWS [token:] [region:] [service:] ``` +### URL Signature +URL Signature method like AliYun's: [https://help.aliyun.com/document_detail/30563.htm](https://help.aliyun.com/document_detail/30563.htm). +The signature algorithm is configurable. Configuration contains two parts: +- `Url Sign Configuration`: Configuration for the signature algorithm and names. +- `Url Sign Key Secrets`: Key and secret pairs for use. + +You should enable it in the configuration if you want to use this function, the default is disabled. + ## Generate Code Snippet ![Generate Code Snippet](https://raw.githubusercontent.com/Huachao/vscode-restclient/master/images/code-snippet.gif) Once you’ve finalized your request in REST Client extension, you might want to make the same request from your source code. We allow you to generate snippets of code in various languages and libraries that will help you achieve this. Once you prepared a request as previously, use shortcut `Ctrl+Alt+C`(`Cmd+Alt+C` for macOS), or right-click in the editor and then select `Generate Code Snippet` in the menu, or press `F1` and then select/type `Rest Client: Generate Code Snippet`, it will pop up the language pick list, as well as library list. After you selected the code snippet language/library you want, the generated code snippet will be previewed in a separate panel of Visual Studio Code, you can click the `Copy Code Snippet` icon in the tab title to copy it to clipboard. diff --git a/package.json b/package.json index 5849bbce..9f51c731 100644 --- a/package.json +++ b/package.json @@ -611,6 +611,43 @@ "scope": "resource", "description": "Enable/disable custom variable references CodeLens in request file" }, + "rest-client.urlSignConfiguration": { + "type": "object", + "default": { + "enableUrlSign": false, + "algorithm": { + "step1OrderParams": true, + "step1UrlEncodeParams": true, + "step1PercentEncode": false, + "step1AddEqual": false, + "step1AddAnd": false, + "step2SeparatorAnd": false, + "step2AddHttpMethod": false, + "step2AddPercentEncodeSlash": false, + "step2PercentEncode": false, + "step3ComputeAlgorithm": "md5", + "step3SecretAppend": "", + "step3TextAlgorithm": "hex" + }, + "keyParamName": "appkey", + "signParamName": "sign" + }, + "scope": "resource", + "markdownDescription": "Sets the URL Signature configuration" + }, + "rest-client.urlSignKeySecrets": { + "type": "object", + "default": {}, + "scope": "resource", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + } + ] + }, + "markdownDescription": "Sets the key and secret pairs for URL Signature" + }, "rest-client.useContentDispositionFilename": { "type": "boolean", "default": true, diff --git a/src/models/configurationSettings.ts b/src/models/configurationSettings.ts index a324723d..dd25fd05 100644 --- a/src/models/configurationSettings.ts +++ b/src/models/configurationSettings.ts @@ -6,6 +6,7 @@ import { FormParamEncodingStrategy, fromString as ParseFormParamEncodingStr } fr import { fromString as ParseLogLevelStr, LogLevel } from './logLevel'; import { fromString as ParsePreviewOptionStr, PreviewOption } from './previewOption'; import { RequestMetadata } from './requestMetadata'; +import { UrlSignConfiguration } from './urlSignConfiguration'; export type HostCertificates = { [key: string]: { @@ -47,6 +48,8 @@ export interface IRestClientSettings { readonly logLevel: LogLevel; readonly enableSendRequestCodeLens: boolean; readonly enableCustomVariableReferencesCodeLens: boolean; + readonly urlSignConfiguration: UrlSignConfiguration; + readonly urlSignKeySecrets: { [key: string]: string }; readonly useContentDispositionFilename: boolean; } @@ -81,6 +84,8 @@ export class SystemSettings implements IRestClientSettings { private _logLevel: LogLevel; private _enableSendRequestCodeLens: boolean; private _enableCustomVariableReferencesCodeLens: boolean; + private _urlSignConfiguration: UrlSignConfiguration; + private _urlSignKeySecrets: { [key: string]: string }; private _useContentDispositionFilename: boolean; public get followRedirect() { @@ -203,6 +208,14 @@ export class SystemSettings implements IRestClientSettings { return this._enableCustomVariableReferencesCodeLens; } + public get urlSignConfiguration() { + return this._urlSignConfiguration; + } + + public get urlSignKeySecrets() { + return this._urlSignKeySecrets; + } + public get useContentDispositionFilename() { return this._useContentDispositionFilename; } @@ -280,6 +293,26 @@ export class SystemSettings implements IRestClientSettings { this._logLevel = ParseLogLevelStr(restClientSettings.get('logLevel', 'error')); this._enableSendRequestCodeLens = restClientSettings.get('enableSendRequestCodeLens', true); this._enableCustomVariableReferencesCodeLens = restClientSettings.get('enableCustomVariableReferencesCodeLens', true); + this._urlSignConfiguration = restClientSettings.get('urlSignConfiguration', { + enableUrlSign: false, + algorithm: { + step1OrderParams: true, + step1UrlEncodeParams: true, + step1PercentEncode: false, + step1AddEqual: false, + step1AddAnd: false, + step2SeparatorAnd: false, + step2AddHttpMethod: false, + step2AddPercentEncodeSlash: false, + step2PercentEncode: false, + step3ComputeAlgorithm: 'md5', + step3SecretAppend: '', + step3TextAlgorithm: 'hex', + }, + keyParamName: 'appkey', + signParamName: 'sign', + }); + this._urlSignKeySecrets = restClientSettings.get<{ [key: string]: string }>("urlSignKeySecrets", {}); this._useContentDispositionFilename = restClientSettings.get('useContentDispositionFilename', true); languages.setLanguageConfiguration('http', { brackets: this._addRequestBodyLineIndentationAroundBrackets ? this.brackets : [] }); @@ -445,6 +478,14 @@ export class RestClientSettings implements IRestClientSettings { return this.systemSettings.enableCustomVariableReferencesCodeLens; } + public get urlSignConfiguration() { + return this.systemSettings.urlSignConfiguration; + } + + public get urlSignKeySecrets() { + return this.systemSettings.urlSignKeySecrets; + } + public get useContentDispositionFilename() { return this.systemSettings.useContentDispositionFilename; } diff --git a/src/models/urlSignConfiguration.ts b/src/models/urlSignConfiguration.ts new file mode 100644 index 00000000..e941eaf6 --- /dev/null +++ b/src/models/urlSignConfiguration.ts @@ -0,0 +1,23 @@ +export type UrlSignConfiguration = { + enableUrlSign: boolean, + // See aliyun sign algo: https://help.aliyun.com/document_detail/30563.htm + algorithm: { + // Step 1: Canonicalized Query String + step1OrderParams: boolean; + step1UrlEncodeParams: boolean; + step1PercentEncode: boolean; + step1AddEqual: boolean; + step1AddAnd: boolean; + // Step 2: Construct signature string StringToSign + step2SeparatorAnd: boolean; + step2AddHttpMethod: boolean; + step2AddPercentEncodeSlash: boolean; + step2PercentEncode: boolean; + // Step 3: Compute sign + step3ComputeAlgorithm: string; // md5 | hmacsha1 + step3SecretAppend: string; // like: '&' in aliyun + step3TextAlgorithm: string; // hex | base64 + } + keyParamName: string; // appkey | AccessKeyId | ... + signParamName: string; // sign | Signature | ... +} diff --git a/src/utils/httpClient.ts b/src/utils/httpClient.ts index 6cef3096..d5afbf3e 100644 --- a/src/utils/httpClient.ts +++ b/src/utils/httpClient.ts @@ -1,9 +1,11 @@ import * as fs from 'fs-extra'; import * as iconv from 'iconv-lite'; import * as path from 'path'; +import * as crypto from 'crypto'; import { Cookie, CookieJar, Store } from 'tough-cookie'; import * as url from 'url'; import { Uri, window } from 'vscode'; +import Logger from '../logger'; import { RequestHeaders, ResponseHeaders } from '../models/base'; import { IRestClientSettings, SystemSettings } from '../models/configurationSettings'; import { HttpRequest } from '../models/httpRequest'; @@ -42,6 +44,8 @@ export class HttpClient { public async send(httpRequest: HttpRequest, settings?: IRestClientSettings): Promise { settings = settings || SystemSettings.Instance; + this.processUrlSign(httpRequest, settings); + const options = await this.prepareOptions(httpRequest, settings); let bodySize = 0; @@ -105,6 +109,127 @@ export class HttpClient { )); } + private processUrlSign(httpRequest: HttpRequest, settings: IRestClientSettings) { + const conf = settings.urlSignConfiguration; + const keySecrets = settings.urlSignKeySecrets; + const algo = conf.algorithm; + const httpMethod = httpRequest.method; + + if (!conf.enableUrlSign) { + return; + } + + // Step 1: Canonicalized Query String + let urlObj = new url.URL(httpRequest.url); + let searchParams = urlObj.searchParams; + + let secret = ''; + let pairArr: Array<[string, string]> = []; + + if (algo.step1OrderParams) { + searchParams.sort(); + } + + for (const [key, value] of searchParams) { + if (key === conf.keyParamName) { + for (let k in keySecrets) { + let s = keySecrets[k]; + if (k === value) { + secret = s; + } + } + } + + var encodeValue = value; + if (algo.step1UrlEncodeParams) { + encodeValue = encodeURIComponent(value); + } + if (algo.step1PercentEncode) { + encodeValue = this.percentEncode(encodeValue); + } + + if (key !== conf.signParamName) { + pairArr.push([key, encodeValue]); + } + } + + let joinSeparator = ''; + if (algo.step1AddAnd) { + joinSeparator = '&'; + } + + let canonicalizedQueryString = pairArr.map(x => { + let pairSeparator = ''; + if (algo.step1AddEqual) { + pairSeparator = '='; + } + return x[0] + pairSeparator + x[1]; + }).join(joinSeparator); + + Logger.verbose("canonicalizedQueryString: " + canonicalizedQueryString); + Logger.verbose("secret: " + secret); + + if (secret === '') { + Logger.warn("No secret setted, please set it in plugin configuration: Url Sign Key Secrets!"); + } + + // Step 2: Construct StringToSign + let signArr: Array = []; + if (algo.step2AddHttpMethod) { + signArr.push(httpMethod); + } + if (algo.step2AddPercentEncodeSlash) { + signArr.push(encodeURIComponent('/')); + } + let encodeStr = canonicalizedQueryString; + if (algo.step2PercentEncode) { + encodeStr = this.percentEncode(encodeURIComponent(canonicalizedQueryString)); + } + signArr.push(encodeStr); + + let step2JoinSeparator = ''; + if (algo.step2SeparatorAnd) { + step2JoinSeparator = '&'; + } + + let stringToSign = signArr.join(step2JoinSeparator); + Logger.verbose("stringToSign: " + stringToSign); + + // Step 3: Compute signature + let computeSign: string = ''; + let signSecret = secret + algo.step3SecretAppend; + if (algo.step3ComputeAlgorithm === 'hmacsha1') { + let hmac = crypto.createHmac('sha1', signSecret).update(stringToSign); + + if (algo.step3TextAlgorithm === 'base64') { + computeSign = hmac.digest('base64'); + } else { + // Default hex + computeSign = hmac.digest('hex'); + } + } else { + // Default md5 + let hash = crypto.createHash('md5').update(stringToSign + signSecret); + + if (algo.step3TextAlgorithm === 'base64') { + computeSign = hash.digest('base64'); + } else { + // Default hex + computeSign = hash.digest('hex'); + } + } + Logger.verbose("computeSign: " + computeSign); + + searchParams.set(conf.signParamName, computeSign); + + Logger.info("Request URL: " + urlObj.toString()); + httpRequest.url = urlObj.toString(); + } + + private percentEncode(s: string): string { + return s.replace(/\+/g, "%20").replace(/\*/g, "%2A").replace(/\%7E/g, "~"); + } + private async prepareOptions(httpRequest: HttpRequest, settings: IRestClientSettings): Promise> { const originalRequestBody = httpRequest.body; let requestBody: string | Buffer | undefined;