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

Support SSL client certificate options per request #808

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,10 @@ We add the capability to directly run [curl request](https://curl.haxx.se/) in R
* -b, --cookie(no cookie jar file support)
* -u, --user(Basic auth support only)
* -d, --data, --data-ascii,--data-binary, --data-raw
* -E, --cert
* --key
+ --pass
* --cacert

## Copy Request As cURL
Sometimes you may want to get the curl format of an http request quickly and save it to clipboard, just pressing `F1` and then selecting/typing `Rest Client: Copy Request As cURL` or simply right-click in the editor, and select `Copy Request As cURL`.
Expand Down Expand Up @@ -684,6 +688,10 @@ REST Client Extension also supports request-level settings for each independent
Name | Syntax | Description
-----|-----------|--------------------------------------------------------------
note | `# @note` | Use for request confirmation, especially for critical request
https.cert | `# @https.cert` | Use for per request [SSL Client Certificate](#ssl-client-certificates)
https.key | `# @https.key` | Use for per request [SSL Client Certificate](#ssl-client-certificates)
https.pfx | `# @https.pfx` | Use for per request [SSL Client Certificate](#ssl-client-certificates)
https.passphrase | `# @https.passphrase` | Use for per request [SSL Client Certificate](#ssl-client-certificates)

> All the above leading `#` can be replaced with `//`

Expand Down
16 changes: 16 additions & 0 deletions snippets/http.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,21 @@
"# @note"
],
"description": "Add sending request confirmation"
},
"Request with SSL client certificate": {
"prefix": "mtls",
"body": [
"# @http.cert",
"# @http.key"
],
"description": "Use SSL client certificate"
},
"Request with SSL client certificate (PFX)": {
"prefix": "mtls-pfx",
"body": [
"# @http.pfx",
"# @http.passphrase"
],
"description": "Use SSL client certificate with PKCS#12 file"
}
}
4 changes: 4 additions & 0 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,15 @@ export const CommentIdentifiersRegex: RegExp = /^\s*(#|\/{2})/;

export const FileVariableDefinitionRegex: RegExp = /^\s*@([^\s=]+)\s*=\s*(.*?)\s*$/;

export const RequestOptionRegex: RegExp = /^\s*(?:#{1,}|\/{2,})\s*@(?:note|name|https\.(?:cert|key|pfx|passphrase|ca))\s/;

export const RequestVariableDefinitionWithNameRegexFactory = (name: string, flags?: string): RegExp =>
new RegExp(`^\\s*(?:#{1,}|\\/{2,})\\s+@name\\s+(${name})\\s*$`, flags);

export const RequestVariableDefinitionRegex: RegExp = RequestVariableDefinitionWithNameRegexFactory("\\w+", "m");

export const NoteCommentRegex = /^\s*(?:#{1,}|\/{2,})\s*@note\s*$/m;

export const HttpsCommentRegex = /^\s*(?:#{1,}|\/{2,})\s*@https\.(cert|key|pfx|passphrase|ca)\s+(.+)\s*$/gm;

export const LineSplitterRegex: RegExp = /\r?\n/g;
37 changes: 32 additions & 5 deletions src/controllers/codeSnippetController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { EOL } from 'os';
import * as url from 'url';
import { Clipboard, env, ExtensionContext, QuickInputButtons, window } from 'vscode';
import Logger from '../logger';
import { HARCookie, HARHeader, HARHttpRequest, HARPostData } from '../models/harHttpRequest';
import { HARCookie, HARHeader, HARHttpRequest, HAROption, HARPostData } from '../models/harHttpRequest';
import { HttpRequest } from '../models/httpRequest';
import { RequestParserFactory } from '../models/requestParserFactory';
import { trace } from "../utils/decorator";
Expand Down Expand Up @@ -119,10 +119,10 @@ export class CodeSnippetController {
return;
}

const { text } = selectedRequest;
const { text, https } = selectedRequest;

// parse http request
const httpRequest = await RequestParserFactory.createRequestParser(text).parseHttpRequest();
const httpRequest = await RequestParserFactory.createRequestParser(text).parseHttpRequest(undefined, https);

const harHttpRequest = this.convertToHARHttpRequest(httpRequest);
const addPrefix = !(url.parse(harHttpRequest.url).protocol);
Expand All @@ -135,7 +135,15 @@ export class CodeSnippetController {
if (addPrefix) {
snippet.requests[0].fullUrl = originalUrl;
}
const result = snippet.convert('shell', 'curl', process.platform === 'win32' ? { indent: false } : {});
let result = snippet.convert('shell', 'curl', process.platform === 'win32' ? { indent: false } : {});

// custom options (e.g. SSL client certificate)
if (harHttpRequest._options?.length) {
for (const opt of harHttpRequest._options) {
result += ` --${opt.name} ${opt.value}`;
}
}

await this.clipboard.writeText(result);
}

Expand Down Expand Up @@ -179,7 +187,26 @@ export class CodeSnippetController {
}
}

return new HARHttpRequest(request.method, encodeUrl(request.url), headers, cookies, body);
// convert additional options
const options: HAROption[] = [];
if (request.https) {
if (request.https.pfx) {
options.push(new HAROption("cert", request.https.pfx));
} else if (request.https.cert) {
options.push(new HAROption("cert", request.https.cert));
}
if (request.https.key) {
options.push(new HAROption("key", request.https.key));
}
if (request.https.passphrase) {
options.push(new HAROption("pass", request.https.passphrase));
}
if (request.https.ca) {
options.push(new HAROption("cacert", request.https.ca));
}
}

return new HARHttpRequest(request.method, encodeUrl(request.url), headers, cookies, body, options);
}

public dispose() {
Expand Down
13 changes: 12 additions & 1 deletion src/controllers/historyController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,18 @@ export class HistoryController {

private async createRequestInTempFile(request: HistoricalHttpRequest): Promise<string> {
const file = await this.createTempFile();
let output = `${request.method.toUpperCase()} ${request.url}${EOL}`;

let output = "";
if (request.https) {
for (const key in request.https) {
const val = request.https[key];
if (val) {
output += `# @https.${key} ${val}${EOL}`;
}
}
}

output += `${request.method.toUpperCase()} ${request.url}${EOL}`;
if (request.headers) {
for (const header in request.headers) {
if (request.headers.hasOwnProperty(header)) {
Expand Down
4 changes: 2 additions & 2 deletions src/controllers/requestController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class RequestController {
return;
}

const { text, name, warnBeforeSend } = selectedRequest;
const { text, name, https, warnBeforeSend } = selectedRequest;

if (warnBeforeSend) {
const note = name ? `Are you sure you want to send the request "${name}"?` : 'Are you sure you want to send this request?';
Expand All @@ -54,7 +54,7 @@ export class RequestController {
}

// parse http request
const httpRequest = await RequestParserFactory.createRequestParser(text).parseHttpRequest(name);
const httpRequest = await RequestParserFactory.createRequestParser(text).parseHttpRequest(name, https);

await this.runCore(httpRequest, document);
}
Expand Down
9 changes: 9 additions & 0 deletions src/models/base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import * as http from 'http';

// supported subset of the TLS options => https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options
export type HttpsOptions = {
cert?: string;
key?: string;
ca?: string;
pfx?: string;
passphrase?: string;
};

export type ResponseHeaders = http.IncomingHttpHeaders;

export type ResponseHeaderValue = { [K in keyof ResponseHeaders]: ResponseHeaders[K] }[keyof ResponseHeaders];
Expand Down
9 changes: 2 additions & 7 deletions src/models/configurationSettings.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import { CharacterPair, Event, EventEmitter, languages, ViewColumn, window, workspace } from 'vscode';
import configuration from '../../language-configuration.json';
import { getCurrentTextDocument } from '../utils/workspaceUtility';
import { RequestHeaders } from './base';
import { HttpsOptions, RequestHeaders } from './base';
import { FormParamEncodingStrategy, fromString as ParseFormParamEncodingStr } from './formParamEncodingStrategy';
import { fromString as ParseLogLevelStr, LogLevel } from './logLevel';
import { fromString as ParsePreviewOptionStr, PreviewOption } from './previewOption';

export type HostCertificates = {
[key: string]: {
cert?: string;
key?: string;
pfx?: string;
passphrase?: string;
}
[key: string]: HttpsOptions
};

interface IRestClientSettings {
Expand Down
7 changes: 6 additions & 1 deletion src/models/harHttpRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export class HARParam implements HARNameValue {
}
}

export class HAROption implements HARNameValue {
public constructor(public name: string, public value: string) {
}
}

export class HARPostData {
public params: HARParam[];
public constructor(public mimeType: string, public text: string) {
Expand All @@ -41,7 +46,7 @@ export class HARPostData {
export class HARHttpRequest {
public queryString: HARParam[];

public constructor(public method: string, public url: string, public headers: HARHeader[], public cookies: HARCookie[], public postData?: HARPostData) {
public constructor(public method: string, public url: string, public headers: HARHeader[], public cookies: HARCookie[], public postData?: HARPostData, public _options?: HAROption[]) {
const queryObj = urlParse(url, true).query;
this.queryString = this.flatten(queryObj);
}
Expand Down
7 changes: 5 additions & 2 deletions src/models/httpRequest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Stream } from 'stream';
import { getContentType } from '../utils/misc';
import { RequestHeaders } from './base';
import { HttpsOptions, RequestHeaders } from './base';

export class HttpRequest {
public isCancelled: boolean;
Expand All @@ -10,7 +10,8 @@ export class HttpRequest {
public headers: RequestHeaders,
public body?: string | Stream,
public rawBody?: string,
public name?: string) {
public name?: string,
public https?: HttpsOptions) {
this.method = method.toLocaleUpperCase();
this.isCancelled = false;
}
Expand All @@ -30,6 +31,7 @@ export class HistoricalHttpRequest {
public url: string,
public headers: RequestHeaders,
public body: string | undefined,
public https: HttpsOptions | undefined,
public startTime: number) {
}

Expand All @@ -39,6 +41,7 @@ export class HistoricalHttpRequest {
httpRequest.url,
httpRequest.headers,
httpRequest.rawBody,
httpRequest.https,
startTime
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/models/requestParser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { HttpsOptions } from './base';
import { HttpRequest } from './httpRequest';

export interface RequestParser {
parseHttpRequest(name?: string): Promise<HttpRequest>;
parseHttpRequest(name?: string, https?: HttpsOptions): Promise<HttpRequest>;
}
2 changes: 1 addition & 1 deletion src/providers/customVariableDiagnosticsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export class CustomVariableDiagnosticsProvider {
const lines = document.getText().split(Constants.LineSplitterRegex);
const pattern = /\{\{(\w[^\s\.{}]*)(\.[^\.]*?)*\}\}/g;
lines.forEach((line, lineNumber) => {
if (Selector.isCommentLine(line)) {
if (Selector.isCommentLine(line) && !Selector.isRequestOption(line)) {
return;
}

Expand Down
51 changes: 48 additions & 3 deletions src/utils/curlRequestParser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as fs from 'fs-extra';
import { RequestHeaders } from '../models/base';
import * as path from 'path';
import { HttpsOptions, RequestHeaders } from '../models/base';
import { RestClientSettings } from '../models/configurationSettings';
import { HttpRequest } from '../models/httpRequest';
import { RequestParser } from '../models/requestParser';
Expand All @@ -15,7 +16,7 @@ export class CurlRequestParser implements RequestParser {
public constructor(private readonly requestRawText: string, private readonly settings: RestClientSettings) {
}

public async parseHttpRequest(name?: string): Promise<HttpRequest> {
public async parseHttpRequest(name?: string, https?: HttpsOptions): Promise<HttpRequest> {
let requestText = CurlRequestParser.mergeMultipleSpacesIntoSingle(
CurlRequestParser.mergeIntoSingleLine(this.requestRawText.trim()));
requestText = requestText
Expand Down Expand Up @@ -77,7 +78,51 @@ export class CurlRequestParser implements RequestParser {
method = body ? "POST" : "GET";
}

return new HttpRequest(method, url, headers, body, body, name);
// parse https arguments
let httpsArgs: undefined | HttpsOptions;
if (typeof parsedArguments.E === "string" || typeof parsedArguments.cert === "string") {
const certArg = parsedArguments.E || parsedArguments.cert;
const [cert, passphrase] = certArg.split(":", 2);
const certExt = path.extname(cert).toLowerCase();
if (certExt === ".p12" || certExt === ".pfx") {
httpsArgs = { pfx: cert, passphrase };
} else {
httpsArgs = { cert, passphrase };
}
}

if (typeof parsedArguments.key === "string") {
const key = parsedArguments.key;
if (httpsArgs) {
httpsArgs.key = key;
} else {
httpsArgs = { key };
}
}

if (httpsArgs && typeof parsedArguments.pass === "string") {
httpsArgs.passphrase = parsedArguments.pass;
}

if (typeof parsedArguments.cacert === "string") {
const ca = parsedArguments.cacert;
if (httpsArgs) {
httpsArgs.ca = ca;
} else {
httpsArgs = { ca };
}
}

if (httpsArgs) {
// curl command line arguments override the per request settings
if (https) {
https = Object.assign({}, https, httpsArgs);
} else {
https = httpsArgs;
}
}

return new HttpRequest(method, url, headers, body, body, name, https);
}

private static mergeIntoSingleLine(text: string): string {
Expand Down
Loading