Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public class CopyRequestResponseConfiguration {
private static final boolean USE_NBSP_DEFAULT = false;
private static final String TEMPLATE_LABEL = "Template";
private static final String TEMPLATE_DEFAULT = "{request}\\n\\n{response}";
private static final String TEMPLATE_DESCRIPTION =
"Placeholders: {request}, {response}, {request:indent} (4 spaces), {response:indent=2} (n spaces), "
+ "{request:indent=\\t} (literal prefix). Control characters: \\n, \\r, \\t, \\0 and \\\\.";
private static final String HIDE_REQUEST_HEADERS_LABEL = "Hide Request Headers";
private static final String HIDE_REQUEST_HEADERS_DEFAULT = "";
private static final String HIDE_RESPONSE_HEADERS_LABEL = "Hide Response Headers";
Expand All @@ -31,7 +34,7 @@ public class CopyRequestResponseConfiguration {
SettingsPanelSetting.stringSetting(COPY_FULL_FULL_OR_SELECTION_HOT_KEY_LABEL,
COPY_FULL_FULL_OR_SELECTION_HOT_KEY_DEFAULT),
SettingsPanelSetting.stringSetting(COPY_FULL_HEADER_LABEL, COPY_FULL_HEADER_DEFAULT),
SettingsPanelSetting.stringSetting(TEMPLATE_LABEL, TEMPLATE_DEFAULT),
SettingsPanelSetting.stringSetting(TEMPLATE_DESCRIPTION, TEMPLATE_LABEL, TEMPLATE_DEFAULT),
SettingsPanelSetting.stringSetting(
"Comma-separated, case-insensitive regexes to match header names, e.g., \"sec-.*,accept.*\"",
HIDE_REQUEST_HEADERS_LABEL, HIDE_REQUEST_HEADERS_DEFAULT),
Expand Down
92 changes: 88 additions & 4 deletions src/main/java/ch/csnc/burp/CopyRequestResponseCopyActions.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,21 @@
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class CopyRequestResponseCopyActions {

private static final int DEFAULT_INDENT_WIDTH = 4;
private static final int MAX_INDENT_WIDTH = 64;

/**
* Matches {request}, {response}, {request:indent}, {response:indent=2},
* {request:indent=\t} and alike.
*/
private static final Pattern PLACEHOLDER = Pattern.compile("\\{(request|response)(:indent(?:=([^}]*))?)?\\}");

public static void copyFullFull(List<HttpRequestResponse> requestResponses) {
requestResponses = requestResponses.stream().map(CopyRequestResponseCopyActions::hideHeaders).toList();

Expand Down Expand Up @@ -153,10 +164,83 @@ private static HttpRequestResponse hideHeaders(HttpRequestResponse requestRespon
}

private static String format(String request, String response) {
return CopyRequestResponseConfiguration.template()
.replace("\\n", "\n")
.replace("{request}", request)
.replace("{response}", response);
// Control characters are expanded on the template only, never on the
// request/response payload itself.
var template = unescape(CopyRequestResponseConfiguration.template());

var matcher = PLACEHOLDER.matcher(template);
var text = new StringBuilder();
while (matcher.find()) {
var value = "request".equals(matcher.group(1)) ? request : response;
if (matcher.group(2) != null) {
value = indent(value, indentPrefix(matcher.group(3)));
}
// quoteReplacement, so that a payload containing $ or \ is copied verbatim
// and a payload containing a placeholder is not substituted again.
matcher.appendReplacement(text, Matcher.quoteReplacement(value));
}
matcher.appendTail(text);

return text.toString();
}

/**
* Prefixes every non-empty line with the given prefix. Empty lines are kept
* empty, so that no trailing whitespace is copied.
*/
private static String indent(String text, String prefix) {
if (text.isEmpty()) {
return text;
}
return text.lines()
.map(line -> line.isEmpty() ? line : prefix + line)
.collect(Collectors.joining("\n"));
}

/**
* Resolves the argument of an {@code :indent} placeholder: no argument means
* four spaces, a number means that many spaces, anything else is taken
* literally (control characters such as \t are expanded).
*/
private static String indentPrefix(String argument) {
String prefix;
if (argument == null || argument.isEmpty()) {
prefix = " ".repeat(DEFAULT_INDENT_WIDTH);
} else if (argument.chars().allMatch(Character::isDigit)) {
var width = argument.length() > 2 ? MAX_INDENT_WIDTH : Integer.parseInt(argument);
prefix = " ".repeat(Math.min(width, MAX_INDENT_WIDTH));
} else {
prefix = unescape(argument);
}
if (CopyRequestResponseConfiguration.useNonBreakableSpace()) {
prefix = prefix.replace(' ', '\u00a0');
}
return prefix;
}

/**
* Expands the control characters \n, \r, \t, \0 and the escaped backslash \\.
* Unknown escape sequences are kept as they are.
*/
private static String unescape(String text) {
var unescaped = new StringBuilder(text.length());
for (var index = 0; index < text.length(); index++) {
var character = text.charAt(index);
if (character != '\\' || index + 1 >= text.length()) {
unescaped.append(character);
continue;
}
var escaped = text.charAt(++index);
switch (escaped) {
case 'n' -> unescaped.append('\n');
case 'r' -> unescaped.append('\r');
case 't' -> unescaped.append('\t');
case '0' -> unescaped.append('\0');
case '\\' -> unescaped.append('\\');
default -> unescaped.append('\\').append(escaped);
}
}
return unescaped.toString();
}

private static void toClipboard(String text0) {
Expand Down