Skip to content
Merged
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
4 changes: 2 additions & 2 deletions src/fixtures/requests/headers.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"value": "Bar"
},
{
"name": "x-bar",
"value": "Foo"
"name": "quoted-value",
"value": "\"quoted\" 'string'"
}
]
}
35 changes: 35 additions & 0 deletions src/helpers/escape.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { escapeString } from './escape';

describe('Escape methods', () => {
describe('escapeString', () => {
it('does nothing to a safe string', () => {
expect(
escapeString("hello world")
).toBe("hello world");
});

it('escapes double quotes by default', () => {
expect(
escapeString('"hello world"')
).toBe('\\"hello world\\"');
});

it('escapes newlines by default', () => {
expect(
escapeString('hello\r\nworld')
).toBe('hello\\r\\nworld');
});

it('escapes backslashes', () => {
expect(
escapeString('hello\\world')
).toBe('hello\\\\world');
});

it('escapes unrepresentable characters', () => {
expect(
escapeString('hello \u0000') // 0 = ASCII 'null' character
).toBe('hello \\u0000');
});
});
});
95 changes: 95 additions & 0 deletions src/helpers/escape.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
export interface EscapeOptions {
/**
* The delimiter that will be used to wrap the string (and so must be escaped
* when used within the string).
* Defaults to "
*/
delimiter?: string;

/**
* The char to use to escape the delimiter and other special characters.
* Defaults to \
*/
escapeChar?: string;

/**
* Whether newlines (\n and \r) should be escaped within the string.
* Defaults to true.
*/
escapeNewlines?: boolean;
}

/**
* Escape characters within a value to make it safe to insert directly into a
* snippet. Takes options which define the escape requirements.
*
* This is closely based on the JSON-stringify string serialization algorithm,
* but generalized for other string delimiters (e.g. " or ') and different escape
* characters (e.g. Powershell uses `)
*
* See https://tc39.es/ecma262/multipage/structured-data.html#sec-quotejsonstring
* for the complete original algorithm.
*/
export function escapeString(rawValue: any, options: EscapeOptions = {}) {
const {
delimiter = '"',
escapeChar = '\\',
escapeNewlines = true
} = options;

const stringValue = rawValue.toString();

return [...stringValue].map((c) => {
if (c === '\b') {
return escapeChar + 'b';
} else if (c === '\t') {
return escapeChar + 't';
} else if (c === '\n') {
if (escapeNewlines) {
return escapeChar + 'n';
} else {
return c; // Don't just continue, or this is caught by < \u0020
}
} else if (c === '\f') {
return escapeChar + 'f';
} else if (c === '\r') {
if (escapeNewlines) {
return escapeChar + 'r';
} else {
return c; // Don't just continue, or this is caught by < \u0020
}
} else if (c === escapeChar) {
return escapeChar + escapeChar;
} else if (c === delimiter) {
return escapeChar + delimiter;
} else if (c < '\u0020' || c > '\u007E') {
// Delegate the trickier non-ASCII cases to the normal algorithm. Some of these
// are escaped as \uXXXX, whilst others are represented literally. Since we're
// using this primarily for header values that are generally (though not 100%
// strictly?) ASCII-only, this should almost never happen.
return JSON.stringify(c).slice(1, -1);
} else {
return c;
}
}).join('');
}

/**
* Make a string value safe to insert literally into a snippet within single quotes,
* by escaping problematic characters, including single quotes inside the string,
* backslashes, newlines, and other special characters.
*
* If value is not a string, it will be stringified with .toString() first.
*/
export const escapeForSingleQuotes = (value: any) =>
escapeString(value, { delimiter: "'" });

/**
* Make a string value safe to insert literally into a snippet within double quotes,
* by escaping problematic characters, including double quotes inside the string,
* backslashes, newlines, and other special characters.
*
* If value is not a string, it will be stringified with .toString() first.
*/
export const escapeForDoubleQuotes = (value: any) =>
escapeString(value, { delimiter: '"' });
3 changes: 2 additions & 1 deletion src/targets/c/libcurl/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CodeBuilder } from '../../../helpers/code-builder';
import { escapeForDoubleQuotes } from '../../../helpers/escape';
import { Client } from '../../targets';

export const libcurl: Client = {
Expand All @@ -25,7 +26,7 @@ export const libcurl: Client = {
push('struct curl_slist *headers = NULL;');

headers.forEach(header => {
push(`headers = curl_slist_append(headers, "${header}: ${headersObj[header]}");`);
push(`headers = curl_slist_append(headers, "${header}: ${escapeForDoubleQuotes(headersObj[header])}");`);
});

push('curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, headers);');
Expand Down
2 changes: 1 addition & 1 deletion src/targets/c/libcurl/fixtures/headers.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ curl_easy_setopt(hnd, CURLOPT_URL, "http://mockbin.com/har");
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "accept: application/json");
headers = curl_slist_append(headers, "x-foo: Bar");
headers = curl_slist_append(headers, "x-bar: Foo");
headers = curl_slist_append(headers, "quoted-value: \"quoted\" 'string'");
curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, headers);

CURLcode ret = curl_easy_perform(hnd);
2 changes: 1 addition & 1 deletion src/targets/clojure/clj_http/fixtures/headers.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
(require '[clj-http.client :as client])

(client/get "http://mockbin.com/har" {:headers {:x-foo "Bar"
:x-bar "Foo"}
:quoted-value "\"quoted\" 'string'"}
:accept :json})
3 changes: 2 additions & 1 deletion src/targets/csharp/httpclient/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CodeBuilder } from '../../../helpers/code-builder';
import { escapeForDoubleQuotes } from '../../../helpers/escape';
import { getHeader } from '../../../helpers/headers';
import { Request } from '../../../httpsnippet';
import { Client } from '../../targets';
Expand Down Expand Up @@ -102,7 +103,7 @@ export const httpclient: Client = {
push('Headers =', 1);
push('{', 1);
headers.forEach(key => {
push(`{ "${key}", "${allHeaders[key]}" },`, 2);
push(`{ "${key}", "${escapeForDoubleQuotes(allHeaders[key])}" },`, 2);
});
push('},', 1);
}
Expand Down
2 changes: 1 addition & 1 deletion src/targets/csharp/httpclient/fixtures/headers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
{
{ "accept", "application/json" },
{ "x-foo", "Bar" },
{ "x-bar", "Foo" },
{ "quoted-value", "\"quoted\" 'string'" },
},
};
using (var response = await client.SendAsync(request))
Expand Down
3 changes: 2 additions & 1 deletion src/targets/csharp/restsharp/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CodeBuilder } from '../../../helpers/code-builder';
import { escapeForDoubleQuotes } from '../../../helpers/escape';
import { getHeader } from '../../../helpers/headers';
import { Client } from '../../targets';

Expand All @@ -25,7 +26,7 @@ export const restsharp: Client = {
// Add headers, including the cookies

Object.keys(headersObj).forEach(key => {
push(`request.AddHeader("${key}", "${headersObj[key]}");`);
push(`request.AddHeader("${key}", "${escapeForDoubleQuotes(headersObj[key])}");`);
});

cookies.forEach(({ name, value }) => {
Expand Down
2 changes: 1 addition & 1 deletion src/targets/csharp/restsharp/fixtures/headers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
var request = new RestRequest(Method.GET);
request.AddHeader("accept", "application/json");
request.AddHeader("x-foo", "Bar");
request.AddHeader("x-bar", "Foo");
request.AddHeader("quoted-value", "\"quoted\" 'string'");
IRestResponse response = client.Execute(request);
3 changes: 2 additions & 1 deletion src/targets/go/native/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

import { CodeBuilder } from '../../../helpers/code-builder';
import { escapeForDoubleQuotes } from '../../../helpers/escape';
import { Client } from '../../targets';

export interface GoNativeOptions {
Expand Down Expand Up @@ -125,7 +126,7 @@ export const native: Client<GoNativeOptions> = {
// Add headers
if (Object.keys(allHeaders).length) {
Object.keys(allHeaders).forEach(key => {
push(`req.Header.Add("${key}", "${allHeaders[key]}")`, indent);
push(`req.Header.Add("${key}", "${escapeForDoubleQuotes(allHeaders[key])}")`, indent);
});

blank();
Expand Down
2 changes: 1 addition & 1 deletion src/targets/go/native/fixtures/headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func main() {

req.Header.Add("accept", "application/json")
req.Header.Add("x-foo", "Bar")
req.Header.Add("x-bar", "Foo")
req.Header.Add("quoted-value", "\"quoted\" 'string'")

res, _ := http.DefaultClient.Do(req)

Expand Down
2 changes: 1 addition & 1 deletion src/targets/http/http1.1/fixtures/headers
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
GET /har HTTP/1.1
Accept: application/json
X-Foo: Bar
X-Bar: Foo
Quoted-Value: "quoted" 'string'
Host: mockbin.com

3 changes: 2 additions & 1 deletion src/targets/java/asynchttp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

import { CodeBuilder } from '../../../helpers/code-builder';
import { escapeForDoubleQuotes } from '../../../helpers/escape';
import { Client } from '../../targets';

export const asynchttp: Client = {
Expand All @@ -31,7 +32,7 @@ export const asynchttp: Client = {

// Add headers, including the cookies
Object.keys(allHeaders).forEach(key => {
push(`.setHeader("${key}", "${allHeaders[key]}")`, 1);
push(`.setHeader("${key}", "${escapeForDoubleQuotes(allHeaders[key])}")`, 1);
});

if (postData.text) {
Expand Down
2 changes: 1 addition & 1 deletion src/targets/java/asynchttp/fixtures/headers.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
client.prepare("GET", "http://mockbin.com/har")
.setHeader("accept", "application/json")
.setHeader("x-foo", "Bar")
.setHeader("x-bar", "Foo")
.setHeader("quoted-value", "\"quoted\" 'string'")
.execute()
.toCompletableFuture()
.thenAccept(System.out::println)
Expand Down
3 changes: 2 additions & 1 deletion src/targets/java/nethttp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

import { CodeBuilder } from '../../../helpers/code-builder';
import { escapeForDoubleQuotes } from '../../../helpers/escape';
import { Client } from '../../targets';

export interface NetHttpOptions {
Expand All @@ -34,7 +35,7 @@ export const nethttp: Client = {
push(`.uri(URI.create("${fullUrl}"))`, 2);

Object.keys(allHeaders).forEach(key => {
push(`.header("${key}", "${allHeaders[key]}")`, 2);
push(`.header("${key}", "${escapeForDoubleQuotes(allHeaders[key])}")`, 2);
});

if (postData.text) {
Expand Down
2 changes: 1 addition & 1 deletion src/targets/java/nethttp/fixtures/headers.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
.uri(URI.create("http://mockbin.com/har"))
.header("accept", "application/json")
.header("x-foo", "Bar")
.header("x-bar", "Foo")
.header("quoted-value", "\"quoted\" 'string'")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
Expand Down
3 changes: 2 additions & 1 deletion src/targets/java/okhttp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

import { CodeBuilder } from '../../../helpers/code-builder';
import { escapeForDoubleQuotes } from '../../../helpers/escape';
import { Client } from '../../targets';

export const okhttp: Client = {
Expand Down Expand Up @@ -62,7 +63,7 @@ export const okhttp: Client = {

// Add headers, including the cookies
Object.keys(allHeaders).forEach(key => {
push(`.addHeader("${key}", "${allHeaders[key]}")`, 1);
push(`.addHeader("${key}", "${escapeForDoubleQuotes(allHeaders[key])}")`, 1);
});

push('.build();', 1);
Expand Down
2 changes: 1 addition & 1 deletion src/targets/java/okhttp/fixtures/headers.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
.get()
.addHeader("accept", "application/json")
.addHeader("x-foo", "Bar")
.addHeader("x-bar", "Foo")
.addHeader("quoted-value", "\"quoted\" 'string'")
.build();

Response response = client.newCall(request).execute();
3 changes: 2 additions & 1 deletion src/targets/java/unirest/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

import { CodeBuilder } from '../../../helpers/code-builder';
import { escapeForDoubleQuotes } from '../../../helpers/escape';
import { Client } from '../../targets';

export const unirest: Client = {
Expand Down Expand Up @@ -38,7 +39,7 @@ export const unirest: Client = {

// Add headers, including the cookies
Object.keys(allHeaders).forEach(key => {
push(`.header("${key}", "${allHeaders[key]}")`, 1);
push(`.header("${key}", "${escapeForDoubleQuotes(allHeaders[key])}")`, 1);
});

if (postData.text) {
Expand Down
2 changes: 1 addition & 1 deletion src/targets/java/unirest/fixtures/headers.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
HttpResponse<String> response = Unirest.get("http://mockbin.com/har")
.header("accept", "application/json")
.header("x-foo", "Bar")
.header("x-bar", "Foo")
.header("quoted-value", "\"quoted\" 'string'")
.asString();
6 changes: 5 additions & 1 deletion src/targets/javascript/axios/fixtures/headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import axios from 'axios';
const options = {
method: 'GET',
url: 'http://mockbin.com/har',
headers: {accept: 'application/json', 'x-foo': 'Bar', 'x-bar': 'Foo'}
headers: {
accept: 'application/json',
'x-foo': 'Bar',
'quoted-value': '"quoted" \'string\''
}
};

try {
Expand Down
6 changes: 5 additions & 1 deletion src/targets/javascript/fetch/fixtures/headers.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
const url = 'http://mockbin.com/har';
const options = {
method: 'GET',
headers: {accept: 'application/json', 'x-foo': 'Bar', 'x-bar': 'Foo'}
headers: {
accept: 'application/json',
'x-foo': 'Bar',
'quoted-value': '"quoted" \'string\''
}
};

try {
Expand Down
2 changes: 1 addition & 1 deletion src/targets/javascript/jquery/fixtures/headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const settings = {
headers: {
accept: 'application/json',
'x-foo': 'Bar',
'x-bar': 'Foo'
'quoted-value': '"quoted" \'string\''
}
};

Expand Down
Loading