Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for URL Signature function. #997

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,14 @@ GET https://httpbin.org/aws-auth HTTP/1.1
Authorization: AWS <accessId> <accessKey> [token:<sessionToken>] [region:<regionName>] [service:<serviceName>]
```

### 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.
Expand Down
37 changes: 37 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
41 changes: 41 additions & 0 deletions src/models/configurationSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]: {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -280,6 +293,26 @@ export class SystemSettings implements IRestClientSettings {
this._logLevel = ParseLogLevelStr(restClientSettings.get<string>('logLevel', 'error'));
this._enableSendRequestCodeLens = restClientSettings.get<boolean>('enableSendRequestCodeLens', true);
this._enableCustomVariableReferencesCodeLens = restClientSettings.get<boolean>('enableCustomVariableReferencesCodeLens', true);
this._urlSignConfiguration = restClientSettings.get<UrlSignConfiguration>('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<boolean>('useContentDispositionFilename', true);
languages.setLanguageConfiguration('http', { brackets: this._addRequestBodyLineIndentationAroundBrackets ? this.brackets : [] });

Expand Down Expand Up @@ -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;
}
Expand Down
23 changes: 23 additions & 0 deletions src/models/urlSignConfiguration.ts
Original file line number Diff line number Diff line change
@@ -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 | ...
}
125 changes: 125 additions & 0 deletions src/utils/httpClient.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -42,6 +44,8 @@ export class HttpClient {
public async send(httpRequest: HttpRequest, settings?: IRestClientSettings): Promise<HttpResponse> {
settings = settings || SystemSettings.Instance;

this.processUrlSign(httpRequest, settings);

const options = await this.prepareOptions(httpRequest, settings);

let bodySize = 0;
Expand Down Expand Up @@ -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<string> = [];
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<got.GotBodyOptions<null>> {
const originalRequestBody = httpRequest.body;
let requestBody: string | Buffer | undefined;
Expand Down