Skip to content

Commit 14eef1c

Browse files
authored
Fix request bodies, improve censoring (#3)
- Fix HTTP accidentally using HTTPS under the hood - Delay setting request method to the last second to avoid conflict with setting request body - Fix parsing body/error from HTTP response - Fix body not included on requests - Enhance censoring to handle nested data - Throw exception when trying to apply censors to non-JSON data
1 parent cbf2d23 commit 14eef1c

File tree

10 files changed

+320
-118
lines changed

10 files changed

+320
-118
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ Now when tests are run, no real HTTP calls will be made. Instead, the HTTP respo
9090

9191
Censor sensitive data in the request and response bodies and headers, such as API keys and auth tokens.
9292

93+
NOTE: Censors can only be applied to JSON request and response bodies. Attempting to apply censors to non-JSON data will throw an exception.
94+
9395
**Default**: *Disabled*
9496

9597
```java
@@ -106,7 +108,9 @@ public class Example {
106108
Cassette cassette = new Cassette("path/to/cassettes", "my_cassette");
107109

108110
AdvancedSettings advancedSettings = new AdvancedSettings();
109-
advancedSettings.censors = new Censors().hideHeader("Authorization"); // Hide the Authorization header
111+
List<String> headersToCensor = new ArrayList<>();
112+
headersToCensor.add("Authorization"); // Hide the Authorization header
113+
advancedSettings.censors = new Censors().hideHeader(headersToCensor);
110114
// or
111115
advancedSettings.censors = Censors.strict(); // use the built-in strict censoring mode (hides common sensitive data)
112116

@@ -196,7 +200,9 @@ import com.easypost.easyvcr.clients.httpurlconnection.RecordableURL;
196200
public class Example {
197201
public static void main(String[] args) {
198202
AdvancedSettings advancedSettings = new AdvancedSettings();
199-
advancedSettings.censors = new Censors().hideQueryParameter("api_key"); // hide the api_key query parameter
203+
List<String> censoredQueryParams = new ArrayList<String>();
204+
censoredQueryParams.add("api_key"); // hide the api_key query parameter
205+
advancedSettings.censors = new Censors().hideQueryParameter(censoredQueryParams);
200206

201207
// Create a VCR with the advanced settings applied
202208
VCR vcr = new VCR(advancedSettings);

src/main/java/com/easypost/easyvcr/Censors.java

Lines changed: 168 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import com.easypost.easyvcr.internalutilities.Tools;
44
import com.easypost.easyvcr.internalutilities.json.Serialization;
5-
import com.google.gson.JsonSyntaxException;
5+
import com.google.gson.JsonParseException;
66
import org.apache.http.NameValuePair;
77
import org.apache.http.client.utils.URLEncodedUtils;
88

@@ -22,6 +22,10 @@ public final class Censors {
2222
* The body parameters to censor.
2323
*/
2424
private final List<String> bodyParamsToCensor;
25+
/**
26+
* Whether censor keys are case sensitive.
27+
*/
28+
private final boolean caseSensitive;
2529
/**
2630
* The string to replace censored data with.
2731
*/
@@ -48,10 +52,21 @@ public Censors() {
4852
* @param censorString The string to use to censor sensitive information.
4953
*/
5054
public Censors(String censorString) {
51-
queryParamsToCensor = new ArrayList<>();
52-
bodyParamsToCensor = new ArrayList<>();
53-
headersToCensor = new ArrayList<>();
54-
censorText = censorString;
55+
this(censorString, false);
56+
}
57+
58+
/**
59+
* Initialize a new instance of the Censors factory.
60+
*
61+
* @param censorString The string to use to censor sensitive information.
62+
* @param caseSensitive Whether to use case sensitive censoring.
63+
*/
64+
public Censors(String censorString, boolean caseSensitive) {
65+
this.queryParamsToCensor = new ArrayList<>();
66+
this.bodyParamsToCensor = new ArrayList<>();
67+
this.headersToCensor = new ArrayList<>();
68+
this.censorText = censorString;
69+
this.caseSensitive = caseSensitive;
5570
}
5671

5772
/**
@@ -70,48 +85,44 @@ public static Censors regular() {
7085
*/
7186
public static Censors strict() {
7287
Censors censors = new Censors();
73-
for (String key : Statics.DEFAULT_CREDENTIAL_HEADERS_TO_HIDE) {
74-
censors.hideHeader(key);
75-
}
76-
for (String key : Statics.DEFAULT_CREDENTIAL_PARAMETERS_TO_HIDE) {
77-
censors.hideQueryParameter(key);
78-
censors.hideBodyParameter(key);
79-
}
88+
censors.hideHeaders(Statics.DEFAULT_CREDENTIAL_HEADERS_TO_HIDE);
89+
censors.hideBodyParameters(Statics.DEFAULT_CREDENTIAL_PARAMETERS_TO_HIDE);
90+
censors.hideQueryParameters(Statics.DEFAULT_CREDENTIAL_PARAMETERS_TO_HIDE);
8091
return censors;
8192
}
8293

8394
/**
84-
* Add a rule to censor a specified body parameter.
95+
* Add a rule to censor specified body parameters.
8596
* Note: Only top-level pairs can be censored.
8697
*
87-
* @param parameterKey Key of body parameter to censor.
98+
* @param parameterKeys Keys of body parameters to censor.
8899
* @return This Censors factory.
89100
*/
90-
public Censors hideBodyParameter(String parameterKey) {
91-
bodyParamsToCensor.add(parameterKey);
101+
public Censors hideBodyParameters(List<String> parameterKeys) {
102+
bodyParamsToCensor.addAll(parameterKeys);
92103
return this;
93104
}
94105

95106
/**
96-
* Add a rule to censor a specified header key.
97-
* Note: This will censor the header key in both the request and response.
107+
* Add a rule to censor specified header keys.
108+
* Note: This will censor the header keys in both the request and response.
98109
*
99-
* @param headerKey Key of header to censor.
110+
* @param headerKeys Keys of headers to censor.
100111
* @return This Censors factory.
101112
*/
102-
public Censors hideHeader(String headerKey) {
103-
headersToCensor.add(headerKey);
113+
public Censors hideHeaders(List<String> headerKeys) {
114+
headersToCensor.addAll(headerKeys);
104115
return this;
105116
}
106117

107118
/**
108-
* Add a rule to censor a specified query parameter.
119+
* Add a rule to censor specified query parameters.
109120
*
110-
* @param parameterKey Key of query parameter to censor.
121+
* @param parameterKeys Keys of query parameters to censor.
111122
* @return This Censors factory.
112123
*/
113-
public Censors hideQueryParameter(String parameterKey) {
114-
queryParamsToCensor.add(parameterKey);
124+
public Censors hideQueryParameters(List<String> parameterKeys) {
125+
queryParamsToCensor.addAll(parameterKeys);
115126
return this;
116127
}
117128

@@ -121,32 +132,19 @@ public Censors hideQueryParameter(String parameterKey) {
121132
* @param body String representation of request body to apply censors to.
122133
* @return Censored string representation of request body.
123134
*/
124-
public String applyBodyParametersCensors(String body) {
135+
public String censorBodyParameters(String body) {
125136
if (body == null || body.length() == 0) {
126137
// short circuit if body is null or empty
127138
return body;
128139
}
129140

130-
Map<String, Object> bodyParameters;
131-
try {
132-
bodyParameters = Serialization.convertJsonToObject(body, Map.class);
133-
} catch (JsonSyntaxException ignored) {
134-
// short circuit if body is not a JSON dictionary
135-
return body;
136-
}
137-
138-
if (bodyParameters == null || bodyParameters.size() == 0) {
139-
// short circuit if there are no body parameters
141+
if (bodyParamsToCensor.size() == 0) {
142+
// short circuit if there are no censors to apply
140143
return body;
141144
}
142145

143-
for (String parameterKey : bodyParamsToCensor) {
144-
if (bodyParameters.containsKey(parameterKey)) {
145-
bodyParameters.put(parameterKey, censorText);
146-
}
147-
}
148-
149-
return Serialization.convertObjectToJson(bodyParameters);
146+
// TODO: Future different content type support here, only JSON is supported currently
147+
return censorJsonBodyParameters(body);
150148
}
151149

152150
/**
@@ -155,12 +153,17 @@ public String applyBodyParametersCensors(String body) {
155153
* @param headers Map of headers to apply censors to.
156154
* @return Censored map of headers.
157155
*/
158-
public Map<String, List<String>> applyHeadersCensors(Map<String, List<String>> headers) {
156+
public Map<String, List<String>> censorHeaders(Map<String, List<String>> headers) {
159157
if (headers == null || headers.size() == 0) {
160158
// short circuit if there are no headers to censor
161159
return headers;
162160
}
163161

162+
if (headersToCensor.size() == 0) {
163+
// short circuit if there are no censors to apply
164+
return headers;
165+
}
166+
164167
final Map<String, List<String>> headersCopy = new HashMap<>(headers);
165168

166169
for (String headerKey : headersToCensor) {
@@ -177,11 +180,17 @@ public Map<String, List<String>> applyHeadersCensors(Map<String, List<String>> h
177180
* @param url Full URL string to apply censors to.
178181
* @return Censored URL string.
179182
*/
180-
public String applyQueryParametersCensors(String url) {
183+
public String censorQueryParameters(String url) {
181184
if (url == null || url.length() == 0) {
182185
// short circuit if url is null
183186
return url;
184187
}
188+
189+
if (queryParamsToCensor.size() == 0) {
190+
// short circuit if there are no censors to apply
191+
return url;
192+
}
193+
185194
URI uri = URI.create(url);
186195
Map<String, String> queryParameters = Tools.queryParametersToMap(uri);
187196
if (queryParameters.size() == 0) {
@@ -204,4 +213,118 @@ public String applyQueryParametersCensors(String url) {
204213

205214
return uri.getScheme() + "://" + uri.getHost() + uri.getPath() + "?" + formattedQueryParameters;
206215
}
216+
217+
private List<Object> applyBodyCensors(List<Object> list) {
218+
if (list == null || list.size() == 0) {
219+
// short circuit if list is null or empty
220+
return list;
221+
}
222+
223+
List<Object> censoredList = new ArrayList<>();
224+
225+
for (Object object : list) {
226+
Object value = object;
227+
if (Utilities.isDictionary(value)) {
228+
// recursively censor inner dictionaries
229+
try {
230+
// change the value if can be parsed as a dictionary
231+
value = applyBodyCensors((Map<String, Object>) value);
232+
} catch (ClassCastException e) {
233+
// otherwise, skip censoring
234+
}
235+
} else if (Utilities.isList(value)) {
236+
// recursively censor list elements
237+
try {
238+
// change the value if can be parsed as a list
239+
value = applyBodyCensors((List<Object>) value);
240+
} catch (ClassCastException e) {
241+
// otherwise, skip censoring
242+
}
243+
} // either a primitive or null, no censoring needed
244+
245+
censoredList.add(value);
246+
}
247+
248+
return censoredList;
249+
250+
}
251+
252+
private Map<String, Object> applyBodyCensors(Map<String, Object> dictionary) {
253+
if (dictionary == null || dictionary.size() == 0) {
254+
// short circuit if dictionary is null or empty
255+
return dictionary;
256+
}
257+
258+
Map<String, Object> censoredBodyDictionary = new HashMap<>();
259+
260+
for (Map.Entry<String, Object> entry : dictionary.entrySet()) {
261+
String key = entry.getKey();
262+
Object value = entry.getValue();
263+
if (keyShouldBeCensored(key, this.bodyParamsToCensor)) {
264+
if (value == null) {
265+
// don't need to worry about censoring something that's null
266+
// (don't replace null with the censor string)
267+
continue;
268+
} else if (Utilities.isDictionary(value)) {
269+
// replace with empty dictionary
270+
censoredBodyDictionary.put(key, new HashMap<>());
271+
} else if (Utilities.isList(value)) {
272+
// replace with empty array
273+
censoredBodyDictionary.put(key, new ArrayList<>());
274+
} else {
275+
// replace with censor text
276+
censoredBodyDictionary.put(key, this.censorText);
277+
}
278+
} else {
279+
if (Utilities.isDictionary(value)) {
280+
// recursively censor inner dictionaries
281+
try {
282+
// change the value if can be parsed as a dictionary
283+
value = applyBodyCensors((Map<String, Object>) value);
284+
} catch (ClassCastException e) {
285+
// otherwise, skip censoring
286+
}
287+
} else if (Utilities.isList(value)) {
288+
// recursively censor list elements
289+
try {
290+
// change the value if can be parsed as a list
291+
value = applyBodyCensors((List<Object>) value);
292+
} catch (ClassCastException e) {
293+
// otherwise, skip censoring
294+
}
295+
}
296+
297+
censoredBodyDictionary.put(key, value);
298+
}
299+
}
300+
301+
return censoredBodyDictionary;
302+
}
303+
304+
private String censorJsonBodyParameters(String body) {
305+
Map<String, Object> bodyDictionary;
306+
try {
307+
bodyDictionary = Serialization.convertJsonToObject(body, Map.class);
308+
Map<String, Object> censoredBodyDictionary = applyBodyCensors(bodyDictionary);
309+
return censoredBodyDictionary == null ? body : Serialization.convertObjectToJson(censoredBodyDictionary);
310+
} catch (Exception ignored) {
311+
// body is not a JSON dictionary
312+
try {
313+
List<Object> bodyList = Serialization.convertJsonToObject(body, List.class);
314+
List<Object> censoredBodyList = applyBodyCensors(bodyList);
315+
return censoredBodyList == null ? body : Serialization.convertObjectToJson(censoredBodyList);
316+
} catch (Exception notJsonData) {
317+
throw new JsonParseException("Body is not a JSON dictionary or list");
318+
}
319+
}
320+
}
321+
322+
private boolean keyShouldBeCensored(String foundKey, List<String> keysToCensor) {
323+
// keysToCensor are already cased as needed
324+
if (!this.caseSensitive) {
325+
foundKey = foundKey.toLowerCase();
326+
}
327+
328+
return keysToCensor.contains(foundKey);
329+
}
207330
}

src/main/java/com/easypost/easyvcr/Utilities.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,22 @@ public static boolean responseCameFromRecording(HttpURLConnection connection) {
3030
}
3131
return false;
3232
}
33+
34+
/**
35+
* Check if the object is a dictionary.
36+
* @param obj The object to check.
37+
* @return True if the object is a dictionary.
38+
*/
39+
public static boolean isDictionary(Object obj) {
40+
return obj instanceof Map;
41+
}
42+
43+
/**
44+
* Check if the object is a list.
45+
* @param obj The object to check.
46+
* @return True if the object is a list.
47+
*/
48+
public static boolean isList(Object obj) {
49+
return obj instanceof List;
50+
}
3351
}

0 commit comments

Comments
 (0)