Skip to content

Commit

Permalink
finally, solution for custom masking of http headers / payloads in logs
Browse files Browse the repository at this point in the history
#699

also attempted along with this is more control over [report verbosity] possible from the execution-hook
  • Loading branch information
ptrthomas committed Nov 15, 2019
1 parent dd7c5a5 commit 2dca6af
Show file tree
Hide file tree
Showing 13 changed files with 208 additions and 23 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2005,6 +2005,7 @@ You can adjust configuration settings for the HTTP client used by Karate using t
`retry` | JSON | defaults to `{ count: 3, interval: 3000 }` - see [`retry until`](#retry-until)
`outlineVariablesAuto` | boolean | defaults to `true`, whether each key-value pair in the `Scenario Outline` example-row is automatically injected into the context as a variable (and not just `__row`), see [`Scenario Outline` Enhancements](#scenario-outline-enhancements)
`lowerCaseResponseHeaders` | boolean | Converts every key and value in the [`responseHeaders`](#responseheaders) to lower-case which makes it easier to validate for e.g. using [`match header`](#match-header) (default `false`) [(example)](karate-demo/src/test/java/demo/headers/content-type.feature).
`logModifier` | Java Object | See [Log Masking](#log-masking)
`httpClientClass` | string | See [`karate-mock-servlet`](karate-mock-servlet)
`httpClientInstance` | Java Object | See [`karate-mock-servlet`](karate-mock-servlet)
`userDefined` | JSON | See [`karate-mock-servlet`](karate-mock-servlet)
Expand Down Expand Up @@ -2064,6 +2065,26 @@ And this short-cut is also supported which will disable all logs:
* configure report = false
```

Since you can use `configure` any time within a test, you have control over which requests or steps you want to show / hide.

### Log Masking
In cases where you want to "mask" values which are sensitive from a security point of view from the logs and HTML reports, you can implement the [`HttpLogModifer`](karate-core/src/main/java/com/intuit/karate/http/HttpLogModifier.java) and tell Karate to use it via the [`configure`](#configure) keyword. Here is an [example](karate-demo/src/test/java/demo/headers/DemoLogModifier.java) of an implementation. For performance reasons, you can implement `enableForUri()` so that this "activates" only for some URL patterns.

Instantiating a Java class and using this in a test is easy:

```cucumber
# if this was in karate-config.js, it would apply "globally"
* def LM = Java.type('demo.headers.DemoLogModifier')
* configure logModifier = new LM()
```

Or globally:

```js
var LM = Java.type('demo.headers.DemoLogModifier');
karate.configure('logModifier', new LM());
```

### System Properties for SSL and HTTP proxy
For HTTPS / SSL, you can also specify a custom certificate or trust store by [setting Java system properties](https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#InstallationAndCustomization). And similarly - for [specifying the HTTP proxy](https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
package com.intuit.karate.http.apache;

import com.intuit.karate.http.HttpLogModifier;
import com.intuit.karate.http.HttpRequest;
import com.intuit.karate.http.HttpUtils;
import java.util.ArrayList;
Expand Down Expand Up @@ -52,34 +53,42 @@ private static Collection<String> sortKeys(Header[] headers) {
return keys;
}

private static void logHeaderLine(StringBuilder sb, int id, char prefix, String key, Header[] headers) {
private static void logHeaderLine(HttpLogModifier logModifier, StringBuilder sb, int id, char prefix, String key, Header[] headers) {
sb.append(id).append(' ').append(prefix).append(' ').append(key).append(": ");
if (headers.length == 1) {
sb.append(headers[0].getValue());
if (logModifier == null) {
sb.append(headers[0].getValue());
} else {
sb.append(logModifier.header(key, headers[0].getValue()));
}
} else {
List<String> list = new ArrayList(headers.length);
for (Header header : headers) {
list.add(header.getValue());
if (logModifier == null) {
list.add(header.getValue());
} else {
list.add(logModifier.header(key, header.getValue()));
}
}
sb.append(list);
}
sb.append('\n');
}

public static void logHeaders(StringBuilder sb, int id, char prefix, org.apache.http.HttpRequest request, HttpRequest actual) {
public static void logHeaders(HttpLogModifier logModifier, StringBuilder sb, int id, char prefix, org.apache.http.HttpRequest request, HttpRequest actual) {
for (String key : sortKeys(request.getAllHeaders())) {
Header[] headers = request.getHeaders(key);
logHeaderLine(sb, id, prefix, key, headers);
logHeaderLine(logModifier, sb, id, prefix, key, headers);
for (Header header : headers) {
actual.addHeader(header.getName(), header.getValue());
}
}
}

public static void logHeaders(StringBuilder sb, int id, char prefix, HttpResponse response) {
public static void logHeaders(HttpLogModifier logModifier, StringBuilder sb, int id, char prefix, HttpResponse response) {
for (String key : sortKeys(response.getAllHeaders())) {
Header[] headers = response.getHeaders(key);
logHeaderLine(sb, id, prefix, key, headers);
logHeaderLine(logModifier, sb, id, prefix, key, headers);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import com.intuit.karate.FileUtils;
import com.intuit.karate.core.ScenarioContext;
import com.intuit.karate.http.HttpLogModifier;
import com.intuit.karate.http.HttpRequest;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
Expand All @@ -41,10 +42,12 @@
public class RequestLoggingInterceptor implements HttpRequestInterceptor {

private final ScenarioContext context;
private final AtomicInteger counter = new AtomicInteger();
private final HttpLogModifier logModifier;
private final AtomicInteger counter = new AtomicInteger();

public RequestLoggingInterceptor(ScenarioContext context) {
this.context = context;
logModifier = context.getConfig().getLogModifier();
}

public AtomicInteger getCounter() {
Expand All @@ -58,10 +61,11 @@ public void process(org.apache.http.HttpRequest request, HttpContext httpContext
String uri = (String) httpContext.getAttribute(ApacheHttpClient.URI_CONTEXT_KEY);
String method = request.getRequestLine().getMethod();
actual.setUri(uri);
actual.setMethod(method);
actual.setMethod(method);
StringBuilder sb = new StringBuilder();
sb.append("request:\n").append(id).append(" > ").append(method).append(' ').append(uri).append('\n');
LoggingUtils.logHeaders(sb, id, '>', request, actual);
HttpLogModifier requestModifier = logModifier == null ? null : logModifier.enableForUri(uri) ? logModifier : null;
LoggingUtils.logHeaders(requestModifier, sb, id, '>', request, actual);
if (request instanceof HttpEntityEnclosingRequest) {
HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) request;
HttpEntity entity = entityRequest.getEntity();
Expand All @@ -71,6 +75,9 @@ public void process(org.apache.http.HttpRequest request, HttpContext httpContext
if (context.getConfig().isLogPrettyRequest()) {
buffer = FileUtils.toPrettyString(buffer);
}
if (requestModifier != null) {
buffer = requestModifier.request(uri, buffer);
}
sb.append(buffer).append('\n');
actual.setBody(wrapper.getBytes());
entityRequest.setEntity(wrapper);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import com.intuit.karate.FileUtils;
import com.intuit.karate.core.ScenarioContext;
import com.intuit.karate.http.HttpLogModifier;
import com.intuit.karate.http.HttpRequest;
import java.io.IOException;
import org.apache.http.HttpEntity;
Expand All @@ -40,11 +41,13 @@
public class ResponseLoggingInterceptor implements HttpResponseInterceptor {

private final ScenarioContext context;
private final HttpLogModifier logModifier;
private final RequestLoggingInterceptor requestInterceptor;

public ResponseLoggingInterceptor(RequestLoggingInterceptor requestInterceptor, ScenarioContext context) {
this.requestInterceptor = requestInterceptor;
this.context = context;
logModifier = context.getConfig().getLogModifier();
}

@Override
Expand All @@ -55,14 +58,18 @@ public void process(HttpResponse response, HttpContext httpContext) throws HttpE
StringBuilder sb = new StringBuilder();
sb.append("response time in milliseconds: ").append(actual.getResponseTimeFormatted()).append('\n');
sb.append(id).append(" < ").append(response.getStatusLine().getStatusCode()).append('\n');
LoggingUtils.logHeaders(sb, id, '<', response);
HttpLogModifier responseModifier = logModifier == null ? null : logModifier.enableForUri(actual.getUri()) ? logModifier : null;
LoggingUtils.logHeaders(responseModifier, sb, id, '<', response);
HttpEntity entity = response.getEntity();
if (LoggingUtils.isPrintable(entity)) {
LoggingEntityWrapper wrapper = new LoggingEntityWrapper(entity);
String buffer = FileUtils.toString(wrapper.getContent());
if (context.getConfig().isLogPrettyResponse()) {
buffer = FileUtils.toPrettyString(buffer);
}
if (responseModifier != null) {
buffer = responseModifier.response(actual.getUri(), buffer);
}
sb.append(buffer).append('\n');
response.setEntity(wrapper);
}
Expand Down
10 changes: 10 additions & 0 deletions karate-core/src/main/java/com/intuit/karate/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.intuit.karate.driver.DockerTarget;
import com.intuit.karate.driver.Target;
import com.intuit.karate.http.HttpClient;
import com.intuit.karate.http.HttpLogModifier;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -73,6 +74,7 @@ public class Config {
private Map<String, Object> driverOptions;
private ScriptValue afterScenario = ScriptValue.NULL;
private ScriptValue afterFeature = ScriptValue.NULL;
private HttpLogModifier logModifier;

// retry config
private int retryInterval = DEFAULT_RETRY_INTERVAL;
Expand Down Expand Up @@ -166,6 +168,9 @@ public boolean configure(String key, ScriptValue value) { // TODO use enum
case "httpClientClass":
clientClass = value.getAsString();
return true;
case "logModifier":
logModifier = value.getValue(HttpLogModifier.class);
return true;
case "httpClientInstance":
clientInstance = value.getValue(HttpClient.class);
return true;
Expand Down Expand Up @@ -266,6 +271,7 @@ public Config(Config parent) {
retryInterval = parent.retryInterval;
retryCount = parent.retryCount;
outlineVariablesAuto = parent.outlineVariablesAuto;
logModifier = parent.logModifier;
}

public void setCookies(ScriptValue cookies) {
Expand Down Expand Up @@ -462,4 +468,8 @@ public void setDriverTarget(Target driverTarget) {
this.driverTarget = driverTarget;
}

public HttpLogModifier getLogModifier() {
return logModifier;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ private static void stepHtml(Document doc, DecimalFormat formatter, StepResult s
if (step.getDocString() != null) {
sb.append(step.getDocString());
}
if (stepResult.getStepLog() != null) {
if (stepResult.isShowLog() && stepResult.getStepLog() != null) {
if (sb.length() > 0) {
sb.append('\n');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ private StepResult afterStep(StepResult result) {
return result;
}

// extracted for karate UI
// extracted for debug
public StepResult execute(Step step) {
currentStep = step;
actions.context.setExecutionUnit(this);// just for deriving call stack
Expand All @@ -206,7 +206,9 @@ public StepResult execute(Step step) {
}
boolean hidden = step.isPrefixStar() && !step.isPrint() && !actions.context.getConfig().isShowAllSteps();
if (stopped) {
return afterStep(new StepResult(hidden, step, aborted ? Result.passed(0) : Result.skipped(), null, null, null));
StepResult sr = new StepResult(step, aborted ? Result.passed(0) : Result.skipped(), null, null, null);
sr.setHidden(hidden);
return afterStep(sr);
} else {
Result execResult = Engine.executeStep(step, actions);
List<FeatureResult> callResults = actions.context.getAndClearCallResults();
Expand All @@ -221,7 +223,10 @@ public StepResult execute(Step step) {
// log appender collection for each step happens here
String stepLog = StringUtils.trimToNull(appender.collect());
boolean showLog = actions.context.getConfig().isShowLog();
return afterStep(new StepResult(hidden, step, execResult, showLog ? stepLog : null, embeds, callResults));
StepResult sr = new StepResult(step, execResult, stepLog, embeds, callResults);
sr.setHidden(hidden);
sr.setShowLog(showLog);
return afterStep(sr);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public void addError(String message, Throwable error) {
step.setLine(scenario.getLine());
step.setPrefix("*");
step.setText(message);
StepResult sr = new StepResult(false, step, Result.failed(0, error, step), null, null, null);
StepResult sr = new StepResult(step, Result.failed(0, error, step), null, null, null);
addStepResult(sr);
}

Expand All @@ -136,7 +136,8 @@ private static void recurse(List<Map> list, StepResult stepResult, int depth) {
call.setPrefix(StringUtils.repeat('>', depth));
call.setText(fr.getCallName());
call.setDocString(fr.getCallArgPretty());
StepResult callResult = new StepResult(stepResult.isHidden(), call, Result.passed(0), null, null, null);
StepResult callResult = new StepResult(call, Result.passed(0), null, null, null);
callResult.setHidden(stepResult.isHidden());
list.add(callResult.toMap());
for (StepResult sr : fr.getStepResults()) { // flattened
if (sr.isHidden()) {
Expand Down
26 changes: 20 additions & 6 deletions karate-core/src/main/java/com/intuit/karate/core/StepResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ public class StepResult {
private final Step step;
private final Result result;
private final List<FeatureResult> callResults;
private final boolean hidden;


private boolean hidden;
private boolean showLog = true;
private List<Embed> embeds;
private String stepLog;

Expand All @@ -57,9 +58,12 @@ public String getErrorMessage() {
}

public void appendToStepLog(String log) {
if (log == null || stepLog == null) {
if (log == null) {
return;
}
if (stepLog == null) {
stepLog = "";
}
stepLog = stepLog + log;
}

Expand All @@ -85,7 +89,6 @@ public StepResult(Map<String, Object> map) {
step.setText((String) map.get("name"));
result = new Result((Map) map.get("result"));
callResults = null;
hidden = false;
}

public Map<String, Object> toMap() {
Expand Down Expand Up @@ -121,16 +124,27 @@ public Map<String, Object> toMap() {
return map;
}

public void setHidden(boolean hidden) {
this.hidden = hidden;
}

public boolean isHidden() {
return hidden;
}

public boolean isShowLog() {
return showLog;
}

public void setShowLog(boolean showLog) {
this.showLog = showLog;
}

public boolean isStopped() {
return result.isFailed() || result.isAborted();
}

public StepResult(boolean hidden, Step step, Result result, String stepLog, List<Embed> embeds, List<FeatureResult> callResults) {
this.hidden = hidden;
public StepResult(Step step, Result result, String stepLog, List<Embed> embeds, List<FeatureResult> callResults) {
this.step = step;
this.result = result;
this.stepLog = stepLog;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* The MIT License
*
* Copyright 2019 Intuit Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.intuit.karate.http;

/**
*
* @author pthomas3
*/
public interface HttpLogModifier {

boolean enableForUri(String uri);

String header(String header, String value);

String request(String uri, String request);

String response(String uri, String response);

}
Loading

0 comments on commit 2dca6af

Please sign in to comment.